diff --git a/server/src/main/java/org/elasticsearch/common/io/stream/StreamInput.java b/server/src/main/java/org/elasticsearch/common/io/stream/StreamInput.java index bc5ea517556a2..b5ad65c41c6a6 100644 --- a/server/src/main/java/org/elasticsearch/common/io/stream/StreamInput.java +++ b/server/src/main/java/org/elasticsearch/common/io/stream/StreamInput.java @@ -59,6 +59,7 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.EnumSet; @@ -937,8 +938,23 @@ public List readStreamableList(Supplier constructor * Reads a list of objects */ public List readList(Writeable.Reader reader) throws IOException { + return readCollection(reader, ArrayList::new); + } + + /** + * Reads a set of objects + */ + public Set readSet(Writeable.Reader reader) throws IOException { + return readCollection(reader, HashSet::new); + } + + /** + * Reads a collection of objects + */ + private > C readCollection(Writeable.Reader reader, + IntFunction constructor) throws IOException { int count = readArraySize(); - List builder = new ArrayList<>(count); + C builder = constructor.apply(count); for (int i=0; i list) throws IOException { } } + /** + * Writes a collection of generic objects via a {@link Writer} + */ + public void writeCollection(Collection collection, Writer writer) throws IOException { + writeVInt(collection.size()); + for (T val: collection) { + writer.write(this, val); + } + } + /** * Writes a list of strings */ diff --git a/server/src/test/java/org/elasticsearch/common/io/stream/StreamTests.java b/server/src/test/java/org/elasticsearch/common/io/stream/StreamTests.java index d64dece7867aa..6431a3469b6b0 100644 --- a/server/src/test/java/org/elasticsearch/common/io/stream/StreamTests.java +++ b/server/src/test/java/org/elasticsearch/common/io/stream/StreamTests.java @@ -31,6 +31,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; @@ -42,6 +43,7 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasToString; +import static org.hamcrest.Matchers.iterableWithSize; public class StreamTests extends ESTestCase { @@ -65,7 +67,7 @@ public void testBooleanSerialization() throws IOException { final Set set = IntStream.range(Byte.MIN_VALUE, Byte.MAX_VALUE).mapToObj(v -> (byte) v).collect(Collectors.toSet()); set.remove((byte) 0); set.remove((byte) 1); - final byte[] corruptBytes = new byte[] { randomFrom(set) }; + final byte[] corruptBytes = new byte[]{randomFrom(set)}; final BytesReference corrupt = new BytesArray(corruptBytes); final IllegalStateException e = expectThrows(IllegalStateException.class, () -> corrupt.streamInput().readBoolean()); final String message = String.format(Locale.ROOT, "unexpected byte [0x%02x]", corruptBytes[0]); @@ -100,7 +102,7 @@ public void testOptionalBooleanSerialization() throws IOException { set.remove((byte) 0); set.remove((byte) 1); set.remove((byte) 2); - final byte[] corruptBytes = new byte[] { randomFrom(set) }; + final byte[] corruptBytes = new byte[]{randomFrom(set)}; final BytesReference corrupt = new BytesArray(corruptBytes); final IllegalStateException e = expectThrows(IllegalStateException.class, () -> corrupt.streamInput().readOptionalBoolean()); final String message = String.format(Locale.ROOT, "unexpected byte [0x%02x]", corruptBytes[0]); @@ -119,22 +121,22 @@ public void testRandomVLongSerialization() throws IOException { public void testSpecificVLongSerialization() throws IOException { List> values = - Arrays.asList( - new Tuple<>(0L, new byte[]{0}), - new Tuple<>(-1L, new byte[]{1}), - new Tuple<>(1L, new byte[]{2}), - new Tuple<>(-2L, new byte[]{3}), - new Tuple<>(2L, new byte[]{4}), - new Tuple<>(Long.MIN_VALUE, new byte[]{-1, -1, -1, -1, -1, -1, -1, -1, -1, 1}), - new Tuple<>(Long.MAX_VALUE, new byte[]{-2, -1, -1, -1, -1, -1, -1, -1, -1, 1}) - - ); + Arrays.asList( + new Tuple<>(0L, new byte[]{0}), + new Tuple<>(-1L, new byte[]{1}), + new Tuple<>(1L, new byte[]{2}), + new Tuple<>(-2L, new byte[]{3}), + new Tuple<>(2L, new byte[]{4}), + new Tuple<>(Long.MIN_VALUE, new byte[]{-1, -1, -1, -1, -1, -1, -1, -1, -1, 1}), + new Tuple<>(Long.MAX_VALUE, new byte[]{-2, -1, -1, -1, -1, -1, -1, -1, -1, 1}) + + ); for (Tuple value : values) { BytesStreamOutput out = new BytesStreamOutput(); out.writeZLong(value.v1()); assertArrayEquals(Long.toString(value.v1()), value.v2(), BytesReference.toBytes(out.bytes())); BytesReference bytes = new BytesArray(value.v2()); - assertEquals(Arrays.toString(value.v2()), (long)value.v1(), bytes.streamInput().readZLong()); + assertEquals(Arrays.toString(value.v2()), (long) value.v1(), bytes.streamInput().readZLong()); } } @@ -158,7 +160,7 @@ public void testLinkedHashMap() throws IOException { } BytesStreamOutput out = new BytesStreamOutput(); out.writeGenericValue(write); - LinkedHashMap read = (LinkedHashMap)out.bytes().streamInput().readGenericValue(); + LinkedHashMap read = (LinkedHashMap) out.bytes().streamInput().readGenericValue(); assertEquals(size, read.size()); int index = 0; for (Map.Entry entry : read.entrySet()) { @@ -172,7 +174,8 @@ public void testFilterStreamInputDelegatesAvailable() throws IOException { final int length = randomIntBetween(1, 1024); StreamInput delegate = StreamInput.wrap(new byte[length]); - FilterStreamInput filterInputStream = new FilterStreamInput(delegate) {}; + FilterStreamInput filterInputStream = new FilterStreamInput(delegate) { + }; assertEquals(filterInputStream.available(), length); // read some bytes @@ -201,7 +204,7 @@ public void testReadArraySize() throws IOException { } stream.writeByteArray(array); InputStreamStreamInput streamInput = new InputStreamStreamInput(StreamInput.wrap(BytesReference.toBytes(stream.bytes())), array - .length-1); + .length - 1); expectThrows(EOFException.class, streamInput::readByteArray); streamInput = new InputStreamStreamInput(StreamInput.wrap(BytesReference.toBytes(stream.bytes())), BytesReference.toBytes(stream .bytes()).length); @@ -230,6 +233,21 @@ public void testWritableArrays() throws IOException { assertThat(targetArray, equalTo(sourceArray)); } + public void testSetOfLongs() throws IOException { + final int size = randomIntBetween(0, 6); + final Set sourceSet = new HashSet<>(size); + for (int i = 0; i < size; i++) { + sourceSet.add(randomLongBetween(i * 1000, (i + 1) * 1000 - 1)); + } + assertThat(sourceSet, iterableWithSize(size)); + + final BytesStreamOutput out = new BytesStreamOutput(); + out.writeCollection(sourceSet, StreamOutput::writeLong); + + final Set targetSet = out.bytes().streamInput().readSet(StreamInput::readLong); + assertThat(targetSet, equalTo(sourceSet)); + } + static final class WriteableString implements Writeable { final String string; diff --git a/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java index 57cd39746f6d2..41be702d1432a 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java @@ -148,6 +148,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BooleanSupplier; import java.util.function.Consumer; +import java.util.function.IntFunction; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -716,6 +717,20 @@ public static String[] generateRandomStringArray(int maxArraySize, int stringSiz return generateRandomStringArray(maxArraySize, stringSize, allowNull, true); } + public static T[] randomArray(int maxArraySize, IntFunction arrayConstructor, Supplier valueConstructor) { + return randomArray(0, maxArraySize, arrayConstructor, valueConstructor); + } + + public static T[] randomArray(int minArraySize, int maxArraySize, IntFunction arrayConstructor, Supplier valueConstructor) { + final int size = randomIntBetween(minArraySize, maxArraySize); + final T[] array = arrayConstructor.apply(size); + for (int i = 0; i < array.length; i++) { + array[i] = valueConstructor.get(); + } + return array; + } + + private static final String[] TIME_SUFFIXES = new String[]{"d", "h", "ms", "s", "m", "micros", "nanos"}; public static String randomTimeValue(int lower, int upper, String... suffixes) { diff --git a/x-pack/docs/en/rest-api/security/privileges.asciidoc b/x-pack/docs/en/rest-api/security/privileges.asciidoc index 4ec192d633b12..adaf27e97073e 100644 --- a/x-pack/docs/en/rest-api/security/privileges.asciidoc +++ b/x-pack/docs/en/rest-api/security/privileges.asciidoc @@ -84,7 +84,8 @@ The following example output indicates which privileges the "rdeniro" user has: "read" : true, "write" : false } - } + }, + "application" : {} } -------------------------------------------------- // TESTRESPONSE[s/"rdeniro"/"$body.username"/] diff --git a/x-pack/docs/en/rest-api/security/roles.asciidoc b/x-pack/docs/en/rest-api/security/roles.asciidoc index d82c260006237..38ff774099ea5 100644 --- a/x-pack/docs/en/rest-api/security/roles.asciidoc +++ b/x-pack/docs/en/rest-api/security/roles.asciidoc @@ -138,6 +138,7 @@ role. If the role is not defined in the `native` realm, the request 404s. }, "query" : "{\"match\": {\"title\": \"foo\"}}" } ], + "applications" : [ ], "run_as" : [ "other_user" ], "metadata" : { "version" : 1 diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java index 94d81613ee8be..13b0ff91c88ca 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java @@ -131,6 +131,8 @@ import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.ExceptExpression; import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.FieldExpression; import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.RoleMapperExpression; +import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivileges; +import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege; import org.elasticsearch.xpack.core.security.transport.netty4.SecurityNetty4Transport; import org.elasticsearch.xpack.core.ssl.SSLService; import org.elasticsearch.xpack.core.ssl.action.GetCertificateInfoAction; @@ -339,6 +341,11 @@ public List getNamedWriteables() { new NamedWriteableRegistry.Entry(ClusterState.Custom.class, TokenMetaData.TYPE, TokenMetaData::new), new NamedWriteableRegistry.Entry(NamedDiff.class, TokenMetaData.TYPE, TokenMetaData::readDiffFrom), new NamedWriteableRegistry.Entry(XPackFeatureSet.Usage.class, XPackField.SECURITY, SecurityFeatureSetUsage::new), + // security : conditional privileges + new NamedWriteableRegistry.Entry(ConditionalClusterPrivilege.class, + ConditionalClusterPrivileges.ManageApplicationPrivileges.WRITEABLE_NAME, + ConditionalClusterPrivileges.ManageApplicationPrivileges::createFrom), + // security : role-mappings new NamedWriteableRegistry.Entry(RoleMapperExpression.class, AllExpression.NAME, AllExpression::new), new NamedWriteableRegistry.Entry(RoleMapperExpression.class, AnyExpression.NAME, AnyExpression::new), new NamedWriteableRegistry.Entry(RoleMapperExpression.class, FieldExpression.NAME, FieldExpression::new), diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/ApplicationPrivilegesRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/ApplicationPrivilegesRequest.java new file mode 100644 index 0000000000000..3d7c765936112 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/ApplicationPrivilegesRequest.java @@ -0,0 +1,17 @@ +/* + * 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.action.privilege; + +import java.util.Collection; + +/** + * Interface implemented by all Requests that manage application privileges + */ +public interface ApplicationPrivilegesRequest { + + Collection getApplicationNames(); +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/DeletePrivilegesAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/DeletePrivilegesAction.java new file mode 100644 index 0000000000000..fd423b8a0d857 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/DeletePrivilegesAction.java @@ -0,0 +1,33 @@ +/* + * 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.action.privilege; + +import org.elasticsearch.action.Action; +import org.elasticsearch.client.ElasticsearchClient; + +/** + * Action for deleting application privileges. + */ +public final class DeletePrivilegesAction + extends Action { + + public static final DeletePrivilegesAction INSTANCE = new DeletePrivilegesAction(); + public static final String NAME = "cluster:admin/xpack/security/privilege/delete"; + + private DeletePrivilegesAction() { + super(NAME); + } + + @Override + public DeletePrivilegesResponse newResponse() { + return new DeletePrivilegesResponse(); + } + + @Override + public DeletePrivilegesRequestBuilder newRequestBuilder(ElasticsearchClient client) { + return new DeletePrivilegesRequestBuilder(client, INSTANCE); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/DeletePrivilegesRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/DeletePrivilegesRequest.java new file mode 100644 index 0000000000000..d5ed78a482315 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/DeletePrivilegesRequest.java @@ -0,0 +1,101 @@ +/* + * 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.action.privilege; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.support.WriteRequest; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import static org.elasticsearch.action.ValidateActions.addValidationError; + +/** + * A request to delete an application privilege. + */ +public final class DeletePrivilegesRequest extends ActionRequest + implements ApplicationPrivilegesRequest, WriteRequest { + + private String application; + private String[] privileges; + private RefreshPolicy refreshPolicy = RefreshPolicy.IMMEDIATE; + + public DeletePrivilegesRequest() { + this(null, Strings.EMPTY_ARRAY); + } + + public DeletePrivilegesRequest(String application, String[] privileges) { + this.application = application; + this.privileges = privileges; + } + + @Override + public DeletePrivilegesRequest setRefreshPolicy(RefreshPolicy refreshPolicy) { + this.refreshPolicy = refreshPolicy; + return this; + } + + @Override + public RefreshPolicy getRefreshPolicy() { + return refreshPolicy; + } + + @Override + public ActionRequestValidationException validate() { + ActionRequestValidationException validationException = null; + if (Strings.isNullOrEmpty(application)) { + validationException = addValidationError("application name is missing", validationException); + } + if (privileges == null || privileges.length == 0 || Arrays.stream(privileges).allMatch(Strings::isNullOrEmpty)) { + validationException = addValidationError("privileges are missing", validationException); + } + return validationException; + } + + public void application(String application) { + this.application = application; + } + + public String application() { + return application; + } + + @Override + public Collection getApplicationNames() { + return Collections.singleton(application); + } + + public String[] privileges() { + return this.privileges; + } + + public void privileges(String[] privileges) { + this.privileges = privileges; + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + application = in.readString(); + privileges = in.readStringArray(); + refreshPolicy = RefreshPolicy.readFrom(in); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(application); + out.writeStringArray(privileges); + refreshPolicy.writeTo(out); + } + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/DeletePrivilegesRequestBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/DeletePrivilegesRequestBuilder.java new file mode 100644 index 0000000000000..0658792986127 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/DeletePrivilegesRequestBuilder.java @@ -0,0 +1,32 @@ +/* + * 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.action.privilege; + +import org.elasticsearch.action.ActionRequestBuilder; +import org.elasticsearch.action.support.WriteRequestBuilder; +import org.elasticsearch.client.ElasticsearchClient; + +/** + * Builder for {@link DeletePrivilegesRequest} + */ +public final class DeletePrivilegesRequestBuilder + extends ActionRequestBuilder + implements WriteRequestBuilder { + + public DeletePrivilegesRequestBuilder(ElasticsearchClient client, DeletePrivilegesAction action) { + super(client, action, new DeletePrivilegesRequest()); + } + + public DeletePrivilegesRequestBuilder privileges(String[] privileges) { + request.privileges(privileges); + return this; + } + + public DeletePrivilegesRequestBuilder application(String applicationName) { + request.application(applicationName); + return this; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/DeletePrivilegesResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/DeletePrivilegesResponse.java new file mode 100644 index 0000000000000..18efb2ac5fac3 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/DeletePrivilegesResponse.java @@ -0,0 +1,57 @@ +/* + * 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.action.privilege; + +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Response when deleting application privileges. + * Returns a collection of privileges that were successfully found and deleted. + */ +public final class DeletePrivilegesResponse extends ActionResponse implements ToXContentObject { + + private Set found; + + public DeletePrivilegesResponse() { + } + + public DeletePrivilegesResponse(Collection found) { + this.found = Collections.unmodifiableSet(new HashSet<>(found)); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject().field("found", found).endObject(); + return builder; + } + + public Set found() { + return this.found; + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + this.found = Collections.unmodifiableSet(in.readSet(StreamInput::readString)); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeCollection(found, StreamOutput::writeString); + } + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetPrivilegesAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetPrivilegesAction.java new file mode 100644 index 0000000000000..8bf68af47b23f --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetPrivilegesAction.java @@ -0,0 +1,32 @@ +/* + * 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.action.privilege; + +import org.elasticsearch.action.Action; +import org.elasticsearch.client.ElasticsearchClient; + +/** + * Action for retrieving one or more application privileges from the security index + */ +public final class GetPrivilegesAction extends Action { + + public static final GetPrivilegesAction INSTANCE = new GetPrivilegesAction(); + public static final String NAME = "cluster:admin/xpack/security/privilege/get"; + + private GetPrivilegesAction() { + super(NAME); + } + + @Override + public GetPrivilegesResponse newResponse() { + return new GetPrivilegesResponse(); + } + + @Override + public GetPrivilegesRequestBuilder newRequestBuilder(ElasticsearchClient client) { + return new GetPrivilegesRequestBuilder(client, INSTANCE); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetPrivilegesRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetPrivilegesRequest.java new file mode 100644 index 0000000000000..559e0ab8d9877 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetPrivilegesRequest.java @@ -0,0 +1,78 @@ +/* + * 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.action.privilege; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; + +import static org.elasticsearch.action.ValidateActions.addValidationError; + +/** + * Request to retrieve one or more application privileges. + */ +public final class GetPrivilegesRequest extends ActionRequest implements ApplicationPrivilegesRequest { + + @Nullable + private String application; + private String[] privileges; + + public GetPrivilegesRequest() { + privileges = Strings.EMPTY_ARRAY; + } + + @Override + public ActionRequestValidationException validate() { + ActionRequestValidationException validationException = null; + if (privileges == null) { + validationException = addValidationError("privileges cannot be null", validationException); + } + return validationException; + } + + public void application(String application) { + this.application = application; + } + + public String application() { + return this.application; + } + + @Override + public Collection getApplicationNames() { + return Collections.singleton(application); + } + + public void privileges(String... privileges) { + this.privileges = privileges; + } + + public String[] privileges() { + return this.privileges; + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + application = in.readOptionalString(); + privileges = in.readStringArray(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeOptionalString(application); + out.writeStringArray(privileges); + } + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetPrivilegesRequestBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetPrivilegesRequestBuilder.java new file mode 100644 index 0000000000000..dcde839e40786 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetPrivilegesRequestBuilder.java @@ -0,0 +1,30 @@ +/* + * 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.action.privilege; + +import org.elasticsearch.action.ActionRequestBuilder; +import org.elasticsearch.client.ElasticsearchClient; + +/** + * Builder for {@link GetPrivilegesRequest} + */ +public final class GetPrivilegesRequestBuilder + extends ActionRequestBuilder { + + public GetPrivilegesRequestBuilder(ElasticsearchClient client, GetPrivilegesAction action) { + super(client, action, new GetPrivilegesRequest()); + } + + public GetPrivilegesRequestBuilder privileges(String... privileges) { + request.privileges(privileges); + return this; + } + + public GetPrivilegesRequestBuilder application(String applicationName) { + request.application(applicationName); + return this; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetPrivilegesResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetPrivilegesResponse.java new file mode 100644 index 0000000000000..664673aa97e41 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetPrivilegesResponse.java @@ -0,0 +1,47 @@ +/* + * 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.action.privilege; + +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; + +import java.io.IOException; +import java.util.Collection; + +/** + * Response containing one or more application privileges retrieved from the security index + */ +public final class GetPrivilegesResponse extends ActionResponse { + + private ApplicationPrivilegeDescriptor[] privileges; + + public GetPrivilegesResponse(ApplicationPrivilegeDescriptor... privileges) { + this.privileges = privileges; + } + + public GetPrivilegesResponse(Collection privileges) { + this(privileges.toArray(new ApplicationPrivilegeDescriptor[privileges.size()])); + } + + public ApplicationPrivilegeDescriptor[] privileges() { + return privileges; + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + this.privileges = in.readArray(ApplicationPrivilegeDescriptor::new, ApplicationPrivilegeDescriptor[]::new); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeArray(privileges); + } + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesAction.java new file mode 100644 index 0000000000000..6dbf4eaa831d2 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesAction.java @@ -0,0 +1,32 @@ +/* + * 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.action.privilege; + +import org.elasticsearch.action.Action; +import org.elasticsearch.client.ElasticsearchClient; + +/** + * Action for putting (adding/updating) one or more application privileges. + */ +public final class PutPrivilegesAction extends Action { + + public static final PutPrivilegesAction INSTANCE = new PutPrivilegesAction(); + public static final String NAME = "cluster:admin/xpack/security/privilege/put"; + + private PutPrivilegesAction() { + super(NAME); + } + + @Override + public PutPrivilegesResponse newResponse() { + return new PutPrivilegesResponse(); + } + + @Override + public PutPrivilegesRequestBuilder newRequestBuilder(ElasticsearchClient client) { + return new PutPrivilegesRequestBuilder(client, INSTANCE); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesRequest.java new file mode 100644 index 0000000000000..beba805f6df2f --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesRequest.java @@ -0,0 +1,124 @@ +/* + * 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.action.privilege; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.support.WriteRequest; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; +import org.elasticsearch.xpack.core.security.support.MetadataUtils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static org.elasticsearch.action.ValidateActions.addValidationError; + +/** + * Request object to put a one or more application privileges. + */ +public final class PutPrivilegesRequest extends ActionRequest implements ApplicationPrivilegesRequest, WriteRequest { + + private List privileges; + private RefreshPolicy refreshPolicy = RefreshPolicy.IMMEDIATE; + + public PutPrivilegesRequest() { + privileges = Collections.emptyList(); + } + + @Override + public ActionRequestValidationException validate() { + ActionRequestValidationException validationException = null; + for (ApplicationPrivilegeDescriptor privilege : privileges) { + try { + ApplicationPrivilege.validateApplicationName(privilege.getApplication()); + } catch (IllegalArgumentException e) { + validationException = addValidationError(e.getMessage(), validationException); + } + try { + ApplicationPrivilege.validatePrivilegeName(privilege.getName()); + } catch (IllegalArgumentException e) { + validationException = addValidationError(e.getMessage(), validationException); + } + if (privilege.getActions().isEmpty()) { + validationException = addValidationError("Application privileges must have at least one action", validationException); + } + for (String action : privilege.getActions()) { + if (action.indexOf('/') == -1 && action.indexOf('*') == -1 && action.indexOf(':') == -1) { + validationException = addValidationError("action [" + action + "] must contain one of [ '/' , '*' , ':' ]", + validationException); + } + try { + ApplicationPrivilege.validatePrivilegeOrActionName(action); + } catch (IllegalArgumentException e) { + validationException = addValidationError(e.getMessage(), validationException); + } + } + if (MetadataUtils.containsReservedMetadata(privilege.getMetadata())) { + validationException = addValidationError("metadata keys may not start with [" + MetadataUtils.RESERVED_PREFIX + + "] (in privilege " + privilege.getApplication() + ' ' + privilege.getName() + ")", validationException); + } + } + return validationException; + } + + /** + * Should this request trigger a refresh ({@linkplain RefreshPolicy#IMMEDIATE}, the default), wait for a refresh ( + * {@linkplain RefreshPolicy#WAIT_UNTIL}), or proceed ignore refreshes entirely ({@linkplain RefreshPolicy#NONE}). + */ + @Override + public RefreshPolicy getRefreshPolicy() { + return refreshPolicy; + } + + @Override + public PutPrivilegesRequest setRefreshPolicy(RefreshPolicy refreshPolicy) { + this.refreshPolicy = refreshPolicy; + return this; + } + + public List getPrivileges() { + return privileges; + } + + public void setPrivileges(Collection privileges) { + this.privileges = Collections.unmodifiableList(new ArrayList<>(privileges)); + } + + @Override + public Collection getApplicationNames() { + return Collections.unmodifiableSet(privileges.stream() + .map(ApplicationPrivilegeDescriptor::getApplication) + .collect(Collectors.toSet())); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{[" + privileges.stream().map(Strings::toString).collect(Collectors.joining(",")) + + "];" + refreshPolicy + "}"; + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + privileges = Collections.unmodifiableList(in.readList(ApplicationPrivilegeDescriptor::new)); + refreshPolicy = RefreshPolicy.readFrom(in); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeList(privileges); + refreshPolicy.writeTo(out); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesRequestBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesRequestBuilder.java new file mode 100644 index 0000000000000..d52a4dd2bb6ed --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesRequestBuilder.java @@ -0,0 +1,132 @@ +/* + * 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.action.privilege; + +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.action.ActionRequestBuilder; +import org.elasticsearch.action.support.WriteRequestBuilder; +import org.elasticsearch.client.ElasticsearchClient; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * Request builder for {@link PutPrivilegesRequest} + */ +public final class PutPrivilegesRequestBuilder + extends ActionRequestBuilder + implements WriteRequestBuilder { + + public PutPrivilegesRequestBuilder(ElasticsearchClient client, PutPrivilegesAction action) { + super(client, action, new PutPrivilegesRequest()); + } + + /** + * Populate the put privileges request using the given source, application name and privilege name + * The source must contain a single privilege object which matches the application and privilege names. + */ + public PutPrivilegesRequestBuilder source(String applicationName, String expectedName, + BytesReference source, XContentType xContentType) + throws IOException { + Objects.requireNonNull(xContentType); + // EMPTY is ok here because we never call namedObject + try (InputStream stream = source.streamInput(); + XContentParser parser = xContentType.xContent() + .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, stream)) { + XContentParser.Token token = parser.currentToken(); + if (token == null) { + token = parser.nextToken(); + } + if (token == XContentParser.Token.START_OBJECT) { + final ApplicationPrivilegeDescriptor privilege = parsePrivilege(parser, applicationName, expectedName); + this.request.setPrivileges(Collections.singleton(privilege)); + } else { + throw new ElasticsearchParseException("expected an object but found {} instead", token); + } + } + return this; + } + + ApplicationPrivilegeDescriptor parsePrivilege(XContentParser parser, String applicationName, String privilegeName) throws IOException { + ApplicationPrivilegeDescriptor privilege = ApplicationPrivilegeDescriptor.parse(parser, applicationName, privilegeName, false); + checkPrivilegeName(privilege, applicationName, privilegeName); + return privilege; + } + + /** + * Populate the put privileges request using the given source, application name and privilege name + * The source must contain a top-level object, keyed by application name. + * The value for each application-name, is an object keyed by privilege name. + * The value for each privilege-name is a privilege object which much match the application and privilege names in which it is nested. + */ + public PutPrivilegesRequestBuilder source(BytesReference source, XContentType xContentType) + throws IOException { + Objects.requireNonNull(xContentType); + // EMPTY is ok here because we never call namedObject + try (InputStream stream = source.streamInput(); + XContentParser parser = xContentType.xContent() + .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, stream)) { + XContentParser.Token token = parser.currentToken(); + if (token == null) { + token = parser.nextToken(); + } + if (token != XContentParser.Token.START_OBJECT) { + throw new ElasticsearchParseException("expected object but found {} instead", token); + } + + List privileges = new ArrayList<>(); + while (parser.nextToken() != XContentParser.Token.END_OBJECT) { + token = parser.currentToken(); + assert token == XContentParser.Token.FIELD_NAME : "Invalid token " + token; + final String applicationName = parser.currentName(); + + token = parser.nextToken(); + if (token != XContentParser.Token.START_OBJECT) { + throw new ElasticsearchParseException("expected the value for {} to be an object, but found {} instead", + applicationName, token); + } + + while (parser.nextToken() != XContentParser.Token.END_OBJECT) { + token = parser.currentToken(); + assert (token == XContentParser.Token.FIELD_NAME); + final String privilegeName = parser.currentName(); + + token = parser.nextToken(); + if (token != XContentParser.Token.START_OBJECT) { + throw new ElasticsearchParseException("expected the value for {} to be an object, but found {} instead", + applicationName, token); + } + privileges.add(parsePrivilege(parser, applicationName, privilegeName)); + } + } + request.setPrivileges(privileges); + } + return this; + } + + private void checkPrivilegeName(ApplicationPrivilegeDescriptor privilege, String applicationName, String providedName) { + final String privilegeName = privilege.getName(); + if (Strings.isNullOrEmpty(applicationName) == false && applicationName.equals(privilege.getApplication()) == false) { + throw new IllegalArgumentException("privilege application [" + privilege.getApplication() + + "] in source does not match the provided application [" + applicationName + "]"); + } + if (Strings.isNullOrEmpty(providedName) == false && providedName.equals(privilegeName) == false) { + throw new IllegalArgumentException("privilege name [" + privilegeName + + "] in source does not match the provided name [" + providedName + "]"); + } + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesResponse.java new file mode 100644 index 0000000000000..6d4a3f1ad44d0 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesResponse.java @@ -0,0 +1,60 @@ +/* + * 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.action.privilege; + +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Response when adding one or more application privileges to the security index. + * Returns a collection of the privileges that were created (by implication, any other privileges were updated). + */ +public final class PutPrivilegesResponse extends ActionResponse implements ToXContentObject { + + private Map> created; + + PutPrivilegesResponse() { + this(Collections.emptyMap()); + } + + public PutPrivilegesResponse(Map> created) { + this.created = Collections.unmodifiableMap(created); + } + + /** + * Get a list of privileges that were created (as opposed to updated) + * @return A map from Application Name to a {@code List} of privilege names + */ + public Map> created() { + return created; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject().field("created", created).endObject(); + return builder; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeMap(created, StreamOutput::writeString, StreamOutput::writeStringList); + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + this.created = Collections.unmodifiableMap(in.readMap(StreamInput::readString, si -> si.readList(StreamInput::readString))); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequest.java index d0f3423fdcfe0..82863a6e8d155 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequest.java @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.core.security.action.role; +import org.elasticsearch.Version; import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.support.WriteRequest; @@ -14,11 +15,15 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivileges; import org.elasticsearch.xpack.core.security.support.MetadataUtils; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -31,11 +36,13 @@ public class PutRoleRequest extends ActionRequest implements WriteRequest indicesPrivileges = new ArrayList<>(); + private List applicationPrivileges = new ArrayList<>(); private String[] runAs = Strings.EMPTY_ARRAY; private RefreshPolicy refreshPolicy = RefreshPolicy.IMMEDIATE; private Map metadata; - + public PutRoleRequest() { } @@ -45,9 +52,25 @@ public ActionRequestValidationException validate() { if (name == null) { validationException = addValidationError("role name is missing", validationException); } + if(applicationPrivileges != null) { + for (RoleDescriptor.ApplicationResourcePrivileges privilege : applicationPrivileges) { + try { + ApplicationPrivilege.validateApplicationNameOrWildcard(privilege.getApplication()); + } catch (IllegalArgumentException e) { + validationException = addValidationError(e.getMessage(), validationException); + } + for (String name : privilege.getPrivileges()) { + try { + ApplicationPrivilege.validatePrivilegeOrActionName(name); + } catch (IllegalArgumentException e) { + validationException = addValidationError(e.getMessage(), validationException); + } + } + } + } if (metadata != null && MetadataUtils.containsReservedMetadata(metadata)) { validationException = - addValidationError("metadata keys may not start with [" + MetadataUtils.RESERVED_PREFIX + "]", validationException); + addValidationError("metadata keys may not start with [" + MetadataUtils.RESERVED_PREFIX + "]", validationException); } return validationException; } @@ -60,6 +83,10 @@ public void cluster(String... clusterPrivileges) { this.clusterPrivileges = clusterPrivileges; } + void conditionalCluster(ConditionalClusterPrivilege... conditionalClusterPrivileges) { + this.conditionalClusterPrivileges = conditionalClusterPrivileges; + } + void addIndex(RoleDescriptor.IndicesPrivileges... privileges) { this.indicesPrivileges.addAll(Arrays.asList(privileges)); } @@ -75,6 +102,10 @@ public void addIndex(String[] indices, String[] privileges, String[] grantedFiel .build()); } + void addApplicationPrivileges(RoleDescriptor.ApplicationResourcePrivileges... privileges) { + this.applicationPrivileges.addAll(Arrays.asList(privileges)); + } + public void runAs(String... usernames) { this.runAs = usernames; } @@ -110,6 +141,14 @@ public RoleDescriptor.IndicesPrivileges[] indices() { return indicesPrivileges.toArray(new RoleDescriptor.IndicesPrivileges[indicesPrivileges.size()]); } + public List applicationPrivileges() { + return Collections.unmodifiableList(applicationPrivileges); + } + + public ConditionalClusterPrivilege[] conditionalClusterPrivileges() { + return conditionalClusterPrivileges; + } + public String[] runAs() { return runAs; } @@ -128,6 +167,10 @@ public void readFrom(StreamInput in) throws IOException { for (int i = 0; i < indicesSize; i++) { indicesPrivileges.add(RoleDescriptor.IndicesPrivileges.createFrom(in)); } + if (in.getVersion().onOrAfter(Version.V_6_4_0)) { + applicationPrivileges = in.readList(RoleDescriptor.ApplicationResourcePrivileges::createFrom); + conditionalClusterPrivileges = ConditionalClusterPrivileges.readArray(in); + } runAs = in.readStringArray(); refreshPolicy = RefreshPolicy.readFrom(in); metadata = in.readMap(); @@ -142,6 +185,10 @@ public void writeTo(StreamOutput out) throws IOException { for (RoleDescriptor.IndicesPrivileges index : indicesPrivileges) { index.writeTo(out); } + if (out.getVersion().onOrAfter(Version.V_6_4_0)) { + out.writeStreamableList(applicationPrivileges); + ConditionalClusterPrivileges.writeArray(out, this.conditionalClusterPrivileges); + } out.writeStringArray(runAs); refreshPolicy.writeTo(out); out.writeMap(metadata); @@ -151,7 +198,11 @@ public RoleDescriptor roleDescriptor() { return new RoleDescriptor(name, clusterPrivileges, indicesPrivileges.toArray(new RoleDescriptor.IndicesPrivileges[indicesPrivileges.size()]), + applicationPrivileges.toArray(new RoleDescriptor.ApplicationResourcePrivileges[applicationPrivileges.size()]), + conditionalClusterPrivileges, runAs, - metadata); + metadata, + Collections.emptyMap()); } -} \ No newline at end of file + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequestBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequestBuilder.java index 79142a7b5758a..a00fff95ec947 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequestBuilder.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequestBuilder.java @@ -40,7 +40,9 @@ public PutRoleRequestBuilder source(String name, BytesReference source, XContent assert name.equals(descriptor.getName()); request.name(name); request.cluster(descriptor.getClusterPrivileges()); + request.conditionalCluster(descriptor.getConditionalClusterPrivileges()); request.addIndex(descriptor.getIndicesPrivileges()); + request.addApplicationPrivileges(descriptor.getApplicationPrivileges()); request.runAs(descriptor.getRunAs()); request.metadata(descriptor.getMetadata()); return this; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesRequest.java index 101ae00d635fc..4f5aed012cb11 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesRequest.java @@ -5,11 +5,14 @@ */ package org.elasticsearch.xpack.core.security.action.user; +import org.elasticsearch.Version; import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.ApplicationResourcePrivileges; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; import java.io.IOException; @@ -23,6 +26,7 @@ public class HasPrivilegesRequest extends ActionRequest implements UserRequest { private String username; private String[] clusterPrivileges; private RoleDescriptor.IndicesPrivileges[] indexPrivileges; + private ApplicationResourcePrivileges[] applicationPrivileges; @Override public ActionRequestValidationException validate() { @@ -33,9 +37,21 @@ public ActionRequestValidationException validate() { if (indexPrivileges == null) { validationException = addValidationError("indexPrivileges must not be null", validationException); } - if (clusterPrivileges != null && clusterPrivileges.length == 0 && indexPrivileges != null && indexPrivileges.length == 0) { - validationException = addValidationError("clusterPrivileges and indexPrivileges cannot both be empty", - validationException); + if (applicationPrivileges == null) { + validationException = addValidationError("applicationPrivileges must not be null", validationException); + } else { + for (ApplicationResourcePrivileges applicationPrivilege : applicationPrivileges) { + try { + ApplicationPrivilege.validateApplicationName(applicationPrivilege.getApplication()); + } catch (IllegalArgumentException e) { + validationException = addValidationError(e.getMessage(), validationException); + } + } + } + if (clusterPrivileges != null && clusterPrivileges.length == 0 + && indexPrivileges != null && indexPrivileges.length == 0 + && applicationPrivileges != null && applicationPrivileges.length == 0) { + validationException = addValidationError("must specify at least one privilege", validationException); } return validationException; } @@ -67,6 +83,10 @@ public String[] clusterPrivileges() { return clusterPrivileges; } + public ApplicationResourcePrivileges[] applicationPrivileges() { + return applicationPrivileges; + } + public void indexPrivileges(RoleDescriptor.IndicesPrivileges... privileges) { this.indexPrivileges = privileges; } @@ -75,6 +95,10 @@ public void clusterPrivileges(String... privileges) { this.clusterPrivileges = privileges; } + public void applicationPrivileges(ApplicationResourcePrivileges... appPrivileges) { + this.applicationPrivileges = appPrivileges; + } + @Override public void readFrom(StreamInput in) throws IOException { super.readFrom(in); @@ -85,6 +109,9 @@ public void readFrom(StreamInput in) throws IOException { for (int i = 0; i < indexSize; i++) { indexPrivileges[i] = RoleDescriptor.IndicesPrivileges.createFrom(in); } + if (in.getVersion().onOrAfter(Version.V_6_4_0)) { + applicationPrivileges = in.readArray(ApplicationResourcePrivileges::createFrom, ApplicationResourcePrivileges[]::new); + } } @Override @@ -96,6 +123,9 @@ public void writeTo(StreamOutput out) throws IOException { for (RoleDescriptor.IndicesPrivileges priv : indexPrivileges) { priv.writeTo(out); } + if (out.getVersion().onOrAfter(Version.V_6_4_0)) { + out.writeArray(ApplicationResourcePrivileges::write, applicationPrivileges); + } } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesRequestBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesRequestBuilder.java index 2bf2bdb4d876e..af28777abd374 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesRequestBuilder.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesRequestBuilder.java @@ -39,6 +39,7 @@ public HasPrivilegesRequestBuilder source(String username, BytesReference source request.username(username); request.indexPrivileges(role.getIndicesPrivileges()); request.clusterPrivileges(role.getClusterPrivileges()); + request.applicationPrivileges(role.getApplicationPrivileges()); return this; } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponse.java index dcc34d75ddbaf..8cd8b510c6499 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponse.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponse.java @@ -5,6 +5,11 @@ */ package org.elasticsearch.xpack.core.security.action.user; +import org.elasticsearch.Version; +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + import java.io.IOException; import java.util.ArrayList; import java.util.Collection; @@ -14,27 +19,27 @@ import java.util.Map; import java.util.Objects; -import org.elasticsearch.action.ActionResponse; -import org.elasticsearch.common.io.stream.StreamInput; -import org.elasticsearch.common.io.stream.StreamOutput; - /** * Response for a {@link HasPrivilegesRequest} */ public class HasPrivilegesResponse extends ActionResponse { private boolean completeMatch; private Map cluster; - private List index; + private List index; + private Map> application; public HasPrivilegesResponse() { - this(true, Collections.emptyMap(), Collections.emptyList()); + this(true, Collections.emptyMap(), Collections.emptyList(), Collections.emptyMap()); } - public HasPrivilegesResponse(boolean completeMatch, Map cluster, Collection index) { + public HasPrivilegesResponse(boolean completeMatch, Map cluster, Collection index, + Map> application) { super(); this.completeMatch = completeMatch; this.cluster = new HashMap<>(cluster); this.index = new ArrayList<>(index); + this.application = new HashMap<>(); + application.forEach((key, val) -> this.application.put(key, Collections.unmodifiableList(new ArrayList<>(val)))); } public boolean isCompleteMatch() { @@ -45,44 +50,67 @@ public Map getClusterPrivileges() { return Collections.unmodifiableMap(cluster); } - public List getIndexPrivileges() { + public List getIndexPrivileges() { return Collections.unmodifiableList(index); } + /** + * Retrieves the results from checking application privileges, + * @return A {@code Map} keyed by application-name + */ + public Map> getApplicationPrivileges() { + return Collections.unmodifiableMap(application); + } + public void readFrom(StreamInput in) throws IOException { super.readFrom(in); completeMatch = in.readBoolean(); - int count = in.readVInt(); - index = new ArrayList<>(count); + index = readResourcePrivileges(in); + if (in.getVersion().onOrAfter(Version.V_6_4_0)) { + application = in.readMap(StreamInput::readString, HasPrivilegesResponse::readResourcePrivileges); + } + } + + private static List readResourcePrivileges(StreamInput in) throws IOException { + final int count = in.readVInt(); + final List list = new ArrayList<>(count); for (int i = 0; i < count; i++) { final String index = in.readString(); final Map privileges = in.readMap(StreamInput::readString, StreamInput::readBoolean); - this.index.add(new IndexPrivileges(index, privileges)); + list.add(new ResourcePrivileges(index, privileges)); } + return list; } @Override public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); out.writeBoolean(completeMatch); - out.writeVInt(index.size()); - for (IndexPrivileges index : index) { - out.writeString(index.index); - out.writeMap(index.privileges, StreamOutput::writeString, StreamOutput::writeBoolean); + writeResourcePrivileges(out, index); + if (out.getVersion().onOrAfter(Version.V_6_4_0)) { + out.writeMap(application, StreamOutput::writeString, HasPrivilegesResponse::writeResourcePrivileges); + } + } + + private static void writeResourcePrivileges(StreamOutput out, List privileges) throws IOException { + out.writeVInt(privileges.size()); + for (ResourcePrivileges priv : privileges) { + out.writeString(priv.resource); + out.writeMap(priv.privileges, StreamOutput::writeString, StreamOutput::writeBoolean); } } - public static class IndexPrivileges { - private final String index; + public static class ResourcePrivileges { + private final String resource; private final Map privileges; - public IndexPrivileges(String index, Map privileges) { - this.index = Objects.requireNonNull(index); + public ResourcePrivileges(String resource, Map privileges) { + this.resource = Objects.requireNonNull(resource); this.privileges = Collections.unmodifiableMap(privileges); } - public String getIndex() { - return index; + public String getResource() { + return resource; } public Map getPrivileges() { @@ -92,14 +120,14 @@ public Map getPrivileges() { @Override public String toString() { return getClass().getSimpleName() + "{" + - "index='" + index + '\'' + + "resource='" + resource + '\'' + ", privileges=" + privileges + '}'; } @Override public int hashCode() { - int result = index.hashCode(); + int result = resource.hashCode(); result = 31 * result + privileges.hashCode(); return result; } @@ -113,8 +141,8 @@ public boolean equals(Object o) { return false; } - final IndexPrivileges other = (IndexPrivileges) o; - return this.index.equals(other.index) && this.privileges.equals(other.privileges); + final ResourcePrivileges other = (ResourcePrivileges) o; + return this.resource.equals(other.resource) && this.privileges.equals(other.privileges); } } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java index 65bde9bd1dfe5..a3e24fd6dd3f0 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java @@ -18,11 +18,14 @@ import org.elasticsearch.common.io.stream.Streamable; import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivileges; import org.elasticsearch.xpack.core.security.support.MetadataUtils; import org.elasticsearch.xpack.core.security.support.Validation; import org.elasticsearch.xpack.core.security.xcontent.XContentUtils; @@ -31,9 +34,11 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Objects; /** * A holder for a Role that contains user-readable information about the Role @@ -45,7 +50,9 @@ public class RoleDescriptor implements ToXContentObject { private final String name; private final String[] clusterPrivileges; + private final ConditionalClusterPrivilege[] conditionalClusterPrivileges; private final IndicesPrivileges[] indicesPrivileges; + private final ApplicationResourcePrivileges[] applicationPrivileges; private final String[] runAs; private final Map metadata; private final Map transientMetadata; @@ -57,6 +64,11 @@ public RoleDescriptor(String name, this(name, clusterPrivileges, indicesPrivileges, runAs, null); } + /** + * @deprecated Use {@link #RoleDescriptor(String, String[], IndicesPrivileges[], ApplicationResourcePrivileges[], + * ConditionalClusterPrivilege[], String[], Map, Map)} + */ + @Deprecated public RoleDescriptor(String name, @Nullable String[] clusterPrivileges, @Nullable IndicesPrivileges[] indicesPrivileges, @@ -65,16 +77,34 @@ public RoleDescriptor(String name, this(name, clusterPrivileges, indicesPrivileges, runAs, metadata, null); } + /** + * @deprecated Use {@link #RoleDescriptor(String, String[], IndicesPrivileges[], ApplicationResourcePrivileges[], + * ConditionalClusterPrivilege[], String[], Map, Map)} + */ + @Deprecated + public RoleDescriptor(String name, + @Nullable String[] clusterPrivileges, + @Nullable IndicesPrivileges[] indicesPrivileges, + @Nullable String[] runAs, + @Nullable Map metadata, + @Nullable Map transientMetadata) { + this(name, clusterPrivileges, indicesPrivileges, null, null, runAs, metadata, transientMetadata); + } public RoleDescriptor(String name, @Nullable String[] clusterPrivileges, @Nullable IndicesPrivileges[] indicesPrivileges, + @Nullable ApplicationResourcePrivileges[] applicationPrivileges, + @Nullable ConditionalClusterPrivilege[] conditionalClusterPrivileges, @Nullable String[] runAs, @Nullable Map metadata, @Nullable Map transientMetadata) { this.name = name; this.clusterPrivileges = clusterPrivileges != null ? clusterPrivileges : Strings.EMPTY_ARRAY; + this.conditionalClusterPrivileges = conditionalClusterPrivileges != null + ? conditionalClusterPrivileges : ConditionalClusterPrivileges.EMPTY_ARRAY; this.indicesPrivileges = indicesPrivileges != null ? indicesPrivileges : IndicesPrivileges.NONE; + this.applicationPrivileges = applicationPrivileges != null ? applicationPrivileges : ApplicationResourcePrivileges.NONE; this.runAs = runAs != null ? runAs : Strings.EMPTY_ARRAY; this.metadata = metadata != null ? Collections.unmodifiableMap(metadata) : Collections.emptyMap(); this.transientMetadata = transientMetadata != null ? Collections.unmodifiableMap(transientMetadata) : @@ -89,10 +119,18 @@ public String[] getClusterPrivileges() { return this.clusterPrivileges; } + public ConditionalClusterPrivilege[] getConditionalClusterPrivileges() { + return this.conditionalClusterPrivileges; + } + public IndicesPrivileges[] getIndicesPrivileges() { return this.indicesPrivileges; } + public ApplicationResourcePrivileges[] getApplicationPrivileges() { + return this.applicationPrivileges; + } + public String[] getRunAs() { return this.runAs; } @@ -114,10 +152,15 @@ public String toString() { StringBuilder sb = new StringBuilder("Role["); sb.append("name=").append(name); sb.append(", cluster=[").append(Strings.arrayToCommaDelimitedString(clusterPrivileges)); + sb.append("], global=[").append(Strings.arrayToCommaDelimitedString(conditionalClusterPrivileges)); sb.append("], indicesPrivileges=["); for (IndicesPrivileges group : indicesPrivileges) { sb.append(group.toString()).append(","); } + sb.append("], applicationPrivileges=["); + for (ApplicationResourcePrivileges privilege : applicationPrivileges) { + sb.append(privilege.toString()).append(","); + } sb.append("], runAs=[").append(Strings.arrayToCommaDelimitedString(runAs)); sb.append("], metadata=["); MetadataUtils.writeValue(sb, metadata); @@ -134,7 +177,9 @@ public boolean equals(Object o) { if (!name.equals(that.name)) return false; if (!Arrays.equals(clusterPrivileges, that.clusterPrivileges)) return false; + if (!Arrays.equals(conditionalClusterPrivileges, that.conditionalClusterPrivileges)) return false; if (!Arrays.equals(indicesPrivileges, that.indicesPrivileges)) return false; + if (!Arrays.equals(applicationPrivileges, that.applicationPrivileges)) return false; if (!metadata.equals(that.getMetadata())) return false; return Arrays.equals(runAs, that.runAs); } @@ -143,7 +188,9 @@ public boolean equals(Object o) { public int hashCode() { int result = name.hashCode(); result = 31 * result + Arrays.hashCode(clusterPrivileges); + result = 31 * result + Arrays.hashCode(conditionalClusterPrivileges); result = 31 * result + Arrays.hashCode(indicesPrivileges); + result = 31 * result + Arrays.hashCode(applicationPrivileges); result = 31 * result + Arrays.hashCode(runAs); result = 31 * result + metadata.hashCode(); return result; @@ -157,8 +204,8 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws /** * Generates x-content for this {@link RoleDescriptor} instance. * - * @param builder the x-content builder - * @param params the parameters for x-content generation directives + * @param builder the x-content builder + * @param params the parameters for x-content generation directives * @param docCreation {@code true} if the x-content is being generated for creating a document * in the security index, {@code false} if the x-content being generated * is for API display purposes @@ -168,7 +215,12 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws public XContentBuilder toXContent(XContentBuilder builder, Params params, boolean docCreation) throws IOException { builder.startObject(); builder.array(Fields.CLUSTER.getPreferredName(), clusterPrivileges); + if (conditionalClusterPrivileges.length != 0) { + builder.field(Fields.GLOBAL.getPreferredName()); + ConditionalClusterPrivileges.toXContent(builder, params, Arrays.asList(conditionalClusterPrivileges)); + } builder.array(Fields.INDICES.getPreferredName(), (Object[]) indicesPrivileges); + builder.array(Fields.APPLICATIONS.getPreferredName(), (Object[]) applicationPrivileges); if (runAs != null) { builder.array(Fields.RUN_AS.getPreferredName(), runAs); } @@ -198,7 +250,19 @@ public static RoleDescriptor readFrom(StreamInput in) throws IOException { } else { transientMetadata = Collections.emptyMap(); } - return new RoleDescriptor(name, clusterPrivileges, indicesPrivileges, runAs, metadata, transientMetadata); + + final ApplicationResourcePrivileges[] applicationPrivileges; + final ConditionalClusterPrivilege[] conditionalClusterPrivileges; + if (in.getVersion().onOrAfter(Version.V_6_4_0)) { + applicationPrivileges = in.readArray(ApplicationResourcePrivileges::createFrom, ApplicationResourcePrivileges[]::new); + conditionalClusterPrivileges = ConditionalClusterPrivileges.readArray(in); + } else { + applicationPrivileges = ApplicationResourcePrivileges.NONE; + conditionalClusterPrivileges = ConditionalClusterPrivileges.EMPTY_ARRAY; + } + + return new RoleDescriptor(name, clusterPrivileges, indicesPrivileges, applicationPrivileges, conditionalClusterPrivileges, + runAs, metadata, transientMetadata); } public static void writeTo(RoleDescriptor descriptor, StreamOutput out) throws IOException { @@ -213,6 +277,10 @@ public static void writeTo(RoleDescriptor descriptor, StreamOutput out) throws I if (out.getVersion().onOrAfter(Version.V_5_2_0)) { out.writeMap(descriptor.transientMetadata); } + if (out.getVersion().onOrAfter(Version.V_6_4_0)) { + out.writeArray(ApplicationResourcePrivileges::write, descriptor.applicationPrivileges); + ConditionalClusterPrivileges.writeArray(out, descriptor.getConditionalClusterPrivileges()); + } } public static RoleDescriptor parse(String name, BytesReference source, boolean allow2xFormat, XContentType xContentType) @@ -221,7 +289,7 @@ public static RoleDescriptor parse(String name, BytesReference source, boolean a // EMPTY is safe here because we never use namedObject try (InputStream stream = source.streamInput(); XContentParser parser = xContentType.xContent() - .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, stream)) { + .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, stream)) { return parse(name, parser, allow2xFormat); } } @@ -243,6 +311,8 @@ public static RoleDescriptor parse(String name, XContentParser parser, boolean a String currentFieldName = null; IndicesPrivileges[] indicesPrivileges = null; String[] clusterPrivileges = null; + List conditionalClusterPrivileges = Collections.emptyList(); + ApplicationResourcePrivileges[] applicationPrivileges = null; String[] runAsUsers = null; Map metadata = null; while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { @@ -255,6 +325,11 @@ public static RoleDescriptor parse(String name, XContentParser parser, boolean a runAsUsers = readStringArray(name, parser, true); } else if (Fields.CLUSTER.match(currentFieldName, parser.getDeprecationHandler())) { clusterPrivileges = readStringArray(name, parser, true); + } else if (Fields.APPLICATIONS.match(currentFieldName, parser.getDeprecationHandler()) + || Fields.APPLICATION.match(currentFieldName, parser.getDeprecationHandler())) { + applicationPrivileges = parseApplicationPrivileges(name, parser); + } else if (Fields.GLOBAL.match(currentFieldName, parser.getDeprecationHandler())) { + conditionalClusterPrivileges = ConditionalClusterPrivileges.parse(parser); } else if (Fields.METADATA.match(currentFieldName, parser.getDeprecationHandler())) { if (token != XContentParser.Token.START_OBJECT) { throw new ElasticsearchParseException( @@ -266,8 +341,7 @@ public static RoleDescriptor parse(String name, XContentParser parser, boolean a // consume object but just drop parser.map(); } else { - throw new ElasticsearchParseException("expected field [{}] to be an object, but found [{}] instead", - currentFieldName, token); + throw new ElasticsearchParseException("failed to parse role [{}]. unexpected field [{}]", name, currentFieldName); } } else if (Fields.TYPE.match(currentFieldName, parser.getDeprecationHandler())) { // don't need it @@ -275,7 +349,9 @@ public static RoleDescriptor parse(String name, XContentParser parser, boolean a throw new ElasticsearchParseException("failed to parse role [{}]. unexpected field [{}]", name, currentFieldName); } } - return new RoleDescriptor(name, clusterPrivileges, indicesPrivileges, runAsUsers, metadata); + return new RoleDescriptor(name, clusterPrivileges, indicesPrivileges, applicationPrivileges, + conditionalClusterPrivileges.toArray(new ConditionalClusterPrivilege[conditionalClusterPrivileges.size()]), runAsUsers, + metadata, null); } private static String[] readStringArray(String roleName, XContentParser parser, boolean allowNull) throws IOException { @@ -291,7 +367,7 @@ public static RoleDescriptor parsePrivilegesCheck(String description, BytesRefer throws IOException { try (InputStream stream = source.streamInput(); XContentParser parser = xContentType.xContent() - .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, stream)) { + .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, stream)) { // advance to the START_OBJECT token XContentParser.Token token = parser.nextToken(); if (token != XContentParser.Token.START_OBJECT) { @@ -301,6 +377,7 @@ public static RoleDescriptor parsePrivilegesCheck(String description, BytesRefer String currentFieldName = null; IndicesPrivileges[] indexPrivileges = null; String[] clusterPrivileges = null; + ApplicationResourcePrivileges[] applicationPrivileges = null; while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { if (token == XContentParser.Token.FIELD_NAME) { currentFieldName = parser.currentName(); @@ -308,14 +385,17 @@ public static RoleDescriptor parsePrivilegesCheck(String description, BytesRefer indexPrivileges = parseIndices(description, parser, false); } else if (Fields.CLUSTER.match(currentFieldName, parser.getDeprecationHandler())) { clusterPrivileges = readStringArray(description, parser, true); + } else if (Fields.APPLICATIONS.match(currentFieldName, parser.getDeprecationHandler()) + || Fields.APPLICATION.match(currentFieldName, parser.getDeprecationHandler())) { + applicationPrivileges = parseApplicationPrivileges(description, parser); } else { throw new ElasticsearchParseException("failed to parse privileges check [{}]. unexpected field [{}]", description, currentFieldName); } } - if (indexPrivileges == null && clusterPrivileges == null) { - throw new ElasticsearchParseException("failed to parse privileges check [{}]. fields [{}] and [{}] are both missing", - description, Fields.INDEX, Fields.CLUSTER); + if (indexPrivileges == null && clusterPrivileges == null && applicationPrivileges == null) { + throw new ElasticsearchParseException("failed to parse privileges check [{}]. All privilege fields [{},{},{}] are missing", + description, Fields.CLUSTER, Fields.INDEX, Fields.APPLICATIONS); } if (indexPrivileges != null) { if (Arrays.stream(indexPrivileges).anyMatch(IndicesPrivileges::isUsingFieldLevelSecurity)) { @@ -326,7 +406,7 @@ public static RoleDescriptor parsePrivilegesCheck(String description, BytesRefer throw new ElasticsearchParseException("Field [{}] is not supported in a has_privileges request", Fields.QUERY); } } - return new RoleDescriptor(description, clusterPrivileges, indexPrivileges, null); + return new RoleDescriptor(description, clusterPrivileges, indexPrivileges, applicationPrivileges, null, null, null, null); } } @@ -361,7 +441,7 @@ private static RoleDescriptor.IndicesPrivileges parseIndex(String roleName, XCon currentFieldName = parser.currentName(); } else if (Fields.NAMES.match(currentFieldName, parser.getDeprecationHandler())) { if (token == XContentParser.Token.VALUE_STRING) { - names = new String[] { parser.text() }; + names = new String[]{parser.text()}; } else if (token == XContentParser.Token.START_ARRAY) { names = readStringArray(roleName, parser, false); if (names.length == 0) { @@ -474,6 +554,37 @@ private static RoleDescriptor.IndicesPrivileges parseIndex(String roleName, XCon .build(); } + private static ApplicationResourcePrivileges[] parseApplicationPrivileges(String roleName, XContentParser parser) + throws IOException { + if (parser.currentToken() != XContentParser.Token.START_ARRAY) { + throw new ElasticsearchParseException("failed to parse application privileges for role [{}]. expected field [{}] value " + + "to be an array, but found [{}] instead", roleName, parser.currentName(), parser.currentToken()); + } + List privileges = new ArrayList<>(); + while (parser.nextToken() != XContentParser.Token.END_ARRAY) { + privileges.add(parseApplicationPrivilege(roleName, parser)); + } + return privileges.toArray(new ApplicationResourcePrivileges[privileges.size()]); + } + + private static ApplicationResourcePrivileges parseApplicationPrivilege(String roleName, XContentParser parser) throws IOException { + XContentParser.Token token = parser.currentToken(); + if (token != XContentParser.Token.START_OBJECT) { + throw new ElasticsearchParseException("failed to parse application privileges for role [{}]. expected field [{}] value to " + + "be an array of objects, but found an array element of type [{}]", roleName, parser.currentName(), token); + } + final ApplicationResourcePrivileges.Builder builder = ApplicationResourcePrivileges.PARSER.parse(parser, null); + if (builder.hasResources() == false) { + throw new ElasticsearchParseException("failed to parse application privileges for role [{}]. missing required [{}] field", + roleName, Fields.RESOURCES.getPreferredName()); + } + if (builder.hasPrivileges() == false) { + throw new ElasticsearchParseException("failed to parse application privileges for role [{}]. missing required [{}] field", + roleName, Fields.PRIVILEGES.getPreferredName()); + } + return builder.build(); + } + /** * A class representing permissions for a group of indices mapped to * privileges, field permissions, and a query. @@ -695,14 +806,176 @@ public IndicesPrivileges build() { } } + public static class ApplicationResourcePrivileges implements ToXContentObject, Streamable { + + private static final ApplicationResourcePrivileges[] NONE = new ApplicationResourcePrivileges[0]; + private static final ObjectParser PARSER = new ObjectParser<>("application", + ApplicationResourcePrivileges::builder); + + static { + PARSER.declareString(Builder::application, Fields.APPLICATION); + PARSER.declareStringArray(Builder::privileges, Fields.PRIVILEGES); + PARSER.declareStringArray(Builder::resources, Fields.RESOURCES); + } + + private String application; + private String[] privileges; + private String[] resources; + + private ApplicationResourcePrivileges() { + } + + public static Builder builder() { + return new Builder(); + } + + public String getApplication() { + return application; + } + + public String[] getResources() { + return this.resources; + } + + public String[] getPrivileges() { + return this.privileges; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(getClass().getSimpleName()) + .append("[application=") + .append(application) + .append(", privileges=[") + .append(Strings.arrayToCommaDelimitedString(privileges)) + .append("], resources=[") + .append(Strings.arrayToCommaDelimitedString(resources)) + .append("]]"); + return sb.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || this.getClass() != o.getClass()) { + return false; + } + + ApplicationResourcePrivileges that = (ApplicationResourcePrivileges) o; + + return Objects.equals(this.application, that.application) + && Arrays.equals(this.resources, that.resources) + && Arrays.equals(this.privileges, that.privileges); + } + + @Override + public int hashCode() { + int result = Arrays.hashCode(resources); + result = 31 * result + Arrays.hashCode(privileges); + return result; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(Fields.APPLICATION.getPreferredName(), application); + builder.array(Fields.PRIVILEGES.getPreferredName(), privileges); + builder.array(Fields.RESOURCES.getPreferredName(), resources); + return builder.endObject(); + } + + public static ApplicationResourcePrivileges createFrom(StreamInput in) throws IOException { + ApplicationResourcePrivileges ip = new ApplicationResourcePrivileges(); + ip.readFrom(in); + return ip; + } + + @Override + public void readFrom(StreamInput in) throws IOException { + this.application = in.readString(); + this.privileges = in.readStringArray(); + this.resources = in.readStringArray(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(application); + out.writeStringArray(privileges); + out.writeStringArray(resources); + } + + public static void write(StreamOutput out, ApplicationResourcePrivileges privileges) throws IOException { + privileges.writeTo(out); + } + + public static class Builder { + + private ApplicationResourcePrivileges applicationPrivileges = new ApplicationResourcePrivileges(); + + private Builder() { + } + + public Builder application(String appName) { + applicationPrivileges.application = appName; + return this; + } + + public Builder resources(String... resources) { + applicationPrivileges.resources = resources; + return this; + } + + public Builder resources(List resources) { + return resources(resources.toArray(new String[resources.size()])); + } + + public Builder privileges(String... privileges) { + applicationPrivileges.privileges = privileges; + return this; + } + + public Builder privileges(Collection privileges) { + return privileges(privileges.toArray(new String[privileges.size()])); + } + + public boolean hasResources() { + return applicationPrivileges.resources != null; + } + + public boolean hasPrivileges() { + return applicationPrivileges.privileges != null; + } + + public ApplicationResourcePrivileges build() { + if (Strings.isNullOrEmpty(applicationPrivileges.application)) { + throw new IllegalArgumentException("application privileges must have an application name"); + } + if (applicationPrivileges.privileges == null || applicationPrivileges.privileges.length == 0) { + throw new IllegalArgumentException("application privileges must define at least one privilege"); + } + if (applicationPrivileges.resources == null || applicationPrivileges.resources.length == 0) { + throw new IllegalArgumentException("application privileges must refer to at least one resource"); + } + return applicationPrivileges; + } + + } + } + public interface Fields { ParseField CLUSTER = new ParseField("cluster"); + ParseField GLOBAL = new ParseField("global"); ParseField INDEX = new ParseField("index"); ParseField INDICES = new ParseField("indices"); + ParseField APPLICATIONS = new ParseField("applications"); ParseField RUN_AS = new ParseField("run_as"); ParseField NAMES = new ParseField("names"); + ParseField RESOURCES = new ParseField("resources"); ParseField QUERY = new ParseField("query"); ParseField PRIVILEGES = new ParseField("privileges"); + ParseField APPLICATION = new ParseField("application"); ParseField FIELD_PERMISSIONS = new ParseField("field_security"); ParseField FIELD_PERMISSIONS_2X = new ParseField("fields"); ParseField GRANT_FIELDS = new ParseField("grant"); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ApplicationPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ApplicationPermission.java new file mode 100644 index 0000000000000..8f1e78a4663e4 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ApplicationPermission.java @@ -0,0 +1,110 @@ +/* + * 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.authz.permission; + +import org.apache.logging.log4j.Logger; +import org.apache.lucene.util.automaton.Automaton; +import org.apache.lucene.util.automaton.Operations; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.logging.Loggers; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; +import org.elasticsearch.xpack.core.security.support.Automatons; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; + +/** + * A permission that is based on privileges for application (non elasticsearch) capabilities + */ +public final class ApplicationPermission { + + public static final ApplicationPermission NONE = new ApplicationPermission(Collections.emptyList()); + + private final Logger logger; + private final List permissions; + + /** + * @param privilegesAndResources A list of (privilege, resources). Each element in the {@link List} is a {@link Tuple} containing + * a single {@link ApplicationPrivilege} and the {@link Set} of resources to which that privilege is + * applied. The resources are treated as a wildcard {@link Automatons#pattern}. + */ + ApplicationPermission(List>> privilegesAndResources) { + this.logger = Loggers.getLogger(getClass()); + Map permissionsByPrivilege = new HashMap<>(); + privilegesAndResources.forEach(tup -> permissionsByPrivilege.compute(tup.v1(), (k, existing) -> { + final Automaton patterns = Automatons.patterns(tup.v2()); + if (existing == null) { + return new PermissionEntry(k, patterns); + } else { + return new PermissionEntry(k, Automatons.unionAndMinimize(Arrays.asList(existing.resources, patterns))); + } + })); + this.permissions = Collections.unmodifiableList(new ArrayList<>(permissionsByPrivilege.values())); + } + + /** + * Determines whether this permission grants the specified privilege on the given resource. + *

+ * An {@link ApplicationPermission} consists of a sequence of permission entries, where each entry contains a single + * {@link ApplicationPrivilege} and one or more resource patterns. + *

+ *

+ * This method returns {@code true} if, one or more of those entries meet the following criteria + *

+ *
    + *
  • The entry's application, when interpreted as an {@link Automaton} {@link Automatons#pattern(String) pattern} matches the + * application given in the argument (interpreted as a raw string) + *
  • + *
  • The {@link ApplicationPrivilege#getAutomaton automaton that defines the entry's actions} entirely covers the + * automaton given in the argument (that is, the argument is a subset of the entry's automaton) + *
  • + *
  • The entry's resources, when interpreted as an {@link Automaton} {@link Automatons#patterns(String...)} set of patterns} entirely + * covers the resource given in the argument (also interpreted as an {@link Automaton} {@link Automatons#pattern(String) pattern}. + *
  • + *
+ */ + public boolean grants(ApplicationPrivilege other, String resource) { + Automaton resourceAutomaton = Automatons.patterns(resource); + final boolean matched = permissions.stream().anyMatch(e -> e.grants(other, resourceAutomaton)); + logger.trace("Permission [{}] {} grant [{} , {}]", this, matched ? "does" : "does not", other, resource); + return matched; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{privileges=" + permissions + "}"; + } + + private static class PermissionEntry { + private final ApplicationPrivilege privilege; + private final Predicate application; + private final Automaton resources; + + private PermissionEntry(ApplicationPrivilege privilege, Automaton resources) { + this.privilege = privilege; + this.application = Automatons.predicate(privilege.getApplication()); + this.resources = resources; + } + + private boolean grants(ApplicationPrivilege other, Automaton resource) { + return this.application.test(other.getApplication()) + && Operations.isEmpty(privilege.getAutomaton()) == false + && Operations.subsetOf(other.getAutomaton(), privilege.getAutomaton()) + && Operations.subsetOf(resource, this.resources); + } + + @Override + public String toString() { + return privilege.toString(); + } + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermission.java index 7c990bd735a41..370fd70b169e8 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermission.java @@ -5,30 +5,97 @@ */ package org.elasticsearch.xpack.core.security.authz.permission; +import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege; +import java.util.Collection; +import java.util.Set; import java.util.function.Predicate; +import java.util.stream.Collectors; /** - * A permission that is based on privileges for cluster wide actions + * A permission that is based on privileges for cluster wide actions, with the optional ability to inspect the request object */ -public final class ClusterPermission { - - public static final ClusterPermission NONE = new ClusterPermission(ClusterPrivilege.NONE); - +public abstract class ClusterPermission { private final ClusterPrivilege privilege; - private final Predicate predicate; ClusterPermission(ClusterPrivilege privilege) { this.privilege = privilege; - this.predicate = privilege.predicate(); } public ClusterPrivilege privilege() { return privilege; } - public boolean check(String action) { - return predicate.test(action); + public abstract boolean check(String action, TransportRequest request); + + /** + * A permission that is based solely on cluster privileges and does not consider request state + */ + public static class SimpleClusterPermission extends ClusterPermission { + + public static final SimpleClusterPermission NONE = new SimpleClusterPermission(ClusterPrivilege.NONE); + + private final Predicate predicate; + + SimpleClusterPermission(ClusterPrivilege privilege) { + super(privilege); + this.predicate = privilege.predicate(); + } + + @Override + public boolean check(String action, TransportRequest request) { + return predicate.test(action); + } + } + + /** + * A permission that makes use of both cluster privileges and request inspection + */ + public static class ConditionalClusterPermission extends ClusterPermission { + private final Predicate actionPredicate; + private final Predicate requestPredicate; + + public ConditionalClusterPermission(ConditionalClusterPrivilege conditionalPrivilege) { + this(conditionalPrivilege.getPrivilege(), conditionalPrivilege.getRequestPredicate()); + } + + public ConditionalClusterPermission(ClusterPrivilege privilege, Predicate requestPredicate) { + super(privilege); + this.actionPredicate = privilege.predicate(); + this.requestPredicate = requestPredicate; + } + + @Override + public boolean check(String action, TransportRequest request) { + return actionPredicate.test(action) && requestPredicate.test(request); + } + } + + /** + * A permission that composes a number of other cluster permissions + */ + public static class CompositeClusterPermission extends ClusterPermission { + private final Collection children; + + public CompositeClusterPermission(Collection children) { + super(buildPrivilege(children)); + this.children = children; + } + + private static ClusterPrivilege buildPrivilege(Collection children) { + final Set names = children.stream() + .map(ClusterPermission::privilege) + .map(ClusterPrivilege::name) + .flatMap(Set::stream) + .collect(Collectors.toSet()); + return ClusterPrivilege.get(names); + } + + @Override + public boolean check(String action, TransportRequest request) { + return children.stream().anyMatch(p -> p.check(action, request)); + } } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java index 8fed501ece2c9..2994592b5e92b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java @@ -8,14 +8,18 @@ import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.Privilege; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; @@ -29,12 +33,14 @@ public final class Role { private final String[] names; private final ClusterPermission cluster; private final IndicesPermission indices; + private final ApplicationPermission application; private final RunAsPermission runAs; - Role(String[] names, ClusterPermission cluster, IndicesPermission indices, RunAsPermission runAs) { + Role(String[] names, ClusterPermission cluster, IndicesPermission indices, ApplicationPermission application, RunAsPermission runAs) { this.names = names; this.cluster = Objects.requireNonNull(cluster); this.indices = Objects.requireNonNull(indices); + this.application = Objects.requireNonNull(application); this.runAs = Objects.requireNonNull(runAs); } @@ -50,6 +56,10 @@ public IndicesPermission indices() { return indices; } + public ApplicationPermission application() { + return application; + } + public RunAsPermission runAs() { return runAs; } @@ -74,7 +84,7 @@ public static Builder builder(RoleDescriptor rd, FieldPermissionsCache fieldPerm public IndicesAccessControl authorize(String action, Set requestedIndicesOrAliases, MetaData metaData, FieldPermissionsCache fieldPermissionsCache) { Map indexPermissions = indices.authorize( - action, requestedIndicesOrAliases, metaData, fieldPermissionsCache + action, requestedIndicesOrAliases, metaData, fieldPermissionsCache ); // At least one role / indices permission set need to match with all the requested indices/aliases: @@ -91,10 +101,11 @@ public IndicesAccessControl authorize(String action, Set requestedIndice public static class Builder { private final String[] names; - private ClusterPermission cluster = ClusterPermission.NONE; + private ClusterPermission cluster = ClusterPermission.SimpleClusterPermission.NONE; private RunAsPermission runAs = RunAsPermission.NONE; private List groups = new ArrayList<>(); private FieldPermissionsCache fieldPermissionsCache = null; + private List>> applicationPrivs = new ArrayList<>(); private Builder(String[] names, FieldPermissionsCache fieldPermissionsCache) { this.names = names; @@ -104,20 +115,44 @@ private Builder(String[] names, FieldPermissionsCache fieldPermissionsCache) { private Builder(RoleDescriptor rd, @Nullable FieldPermissionsCache fieldPermissionsCache) { this.names = new String[] { rd.getName() }; this.fieldPermissionsCache = fieldPermissionsCache; - if (rd.getClusterPrivileges().length == 0) { - cluster = ClusterPermission.NONE; - } else { - this.cluster(ClusterPrivilege.get(Sets.newHashSet(rd.getClusterPrivileges()))); - } + cluster(Sets.newHashSet(rd.getClusterPrivileges()), Arrays.asList(rd.getConditionalClusterPrivileges())); groups.addAll(convertFromIndicesPrivileges(rd.getIndicesPrivileges(), fieldPermissionsCache)); + + final RoleDescriptor.ApplicationResourcePrivileges[] applicationPrivileges = rd.getApplicationPrivileges(); + for (int i = 0; i < applicationPrivileges.length; i++) { + applicationPrivs.add(convertApplicationPrivilege(rd.getName(), i, applicationPrivileges[i])); + } + String[] rdRunAs = rd.getRunAs(); if (rdRunAs != null && rdRunAs.length > 0) { this.runAs(new Privilege(Sets.newHashSet(rdRunAs), rdRunAs)); } } + public Builder cluster(Set privilegeNames, Iterable conditionalClusterPrivileges) { + List clusterPermissions = new ArrayList<>(); + if (privilegeNames.isEmpty() == false) { + clusterPermissions.add(new ClusterPermission.SimpleClusterPermission(ClusterPrivilege.get(privilegeNames))); + } + for (ConditionalClusterPrivilege ccp : conditionalClusterPrivileges) { + clusterPermissions.add(new ClusterPermission.ConditionalClusterPermission(ccp)); + } + if (clusterPermissions.isEmpty()) { + this.cluster = ClusterPermission.SimpleClusterPermission.NONE; + } else if (clusterPermissions.size() == 1) { + this.cluster = clusterPermissions.get(0); + } else { + this.cluster = new ClusterPermission.CompositeClusterPermission(clusterPermissions); + } + return this; + } + + /** + * @deprecated Use {@link #cluster(Set, Iterable)} + */ + @Deprecated public Builder cluster(ClusterPrivilege privilege) { - cluster = new ClusterPermission(privilege); + cluster = new ClusterPermission.SimpleClusterPermission(privilege); return this; } @@ -136,10 +171,17 @@ public Builder add(FieldPermissions fieldPermissions, Set query, return this; } + public Builder addApplicationPrivilege(ApplicationPrivilege privilege, Set resources) { + applicationPrivs.add(new Tuple<>(privilege, resources)); + return this; + } + public Role build() { IndicesPermission indices = groups.isEmpty() ? IndicesPermission.NONE : - new IndicesPermission(groups.toArray(new IndicesPermission.Group[groups.size()])); - return new Role(names, cluster, indices, runAs); + new IndicesPermission(groups.toArray(new IndicesPermission.Group[groups.size()])); + final ApplicationPermission applicationPermission + = applicationPrivs.isEmpty() ? ApplicationPermission.NONE : new ApplicationPermission(applicationPrivs); + return new Role(names, cluster, indices, applicationPermission, runAs); } static List convertFromIndicesPrivileges(RoleDescriptor.IndicesPrivileges[] indicesPrivileges, @@ -151,16 +193,24 @@ static List convertFromIndicesPrivileges(RoleDescriptor fieldPermissions = fieldPermissionsCache.getFieldPermissions(privilege.getGrantedFields(), privilege.getDeniedFields()); } else { fieldPermissions = new FieldPermissions( - new FieldPermissionsDefinition(privilege.getGrantedFields(), privilege.getDeniedFields())); + new FieldPermissionsDefinition(privilege.getGrantedFields(), privilege.getDeniedFields())); } final Set query = privilege.getQuery() == null ? null : Collections.singleton(privilege.getQuery()); list.add(new IndicesPermission.Group(IndexPrivilege.get(Sets.newHashSet(privilege.getPrivileges())), - fieldPermissions, - query, - privilege.getIndices())); + fieldPermissions, + query, + privilege.getIndices())); } return list; } + + static Tuple> convertApplicationPrivilege(String role, int index, + RoleDescriptor.ApplicationResourcePrivileges arp) { + return new Tuple<>(new ApplicationPrivilege(arp.getApplication(), + "role." + role.replaceAll("[^a-zA-Z0-9]", "") + "." + index, + arp.getPrivileges() + ), Sets.newHashSet(arp.getResources())); + } } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ApplicationPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ApplicationPrivilege.java new file mode 100644 index 0000000000000..13db17a63bb0d --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ApplicationPrivilege.java @@ -0,0 +1,225 @@ +/* + * 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.authz.privilege; + +import org.elasticsearch.common.Strings; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * An application privilege has an application name (e.g. {@code "my-app"}) that identifies an application (that exists + * outside of elasticsearch), a privilege name (e.g. {@code "admin}) that is meaningful to that application, and one or + * more "action patterns" (e.g {@code "admin/user/*", "admin/team/*"}). + * Action patterns must contain at least one special character from ({@code /}, {@code :}, {@code *}) to distinguish them + * from privilege names. + * The action patterns are entirely optional - many application will find that simple "privilege names" are sufficient, but + * they allow applications to define high level abstract privileges that map to multiple low level capabilities. + */ +public final class ApplicationPrivilege extends Privilege { + + private static final Pattern VALID_APPLICATION_PREFIX = Pattern.compile("^[a-z][A-Za-z0-9]*$"); + private static final Pattern WHITESPACE = Pattern.compile("[\\v\\h]"); + private static final Pattern VALID_NAME = Pattern.compile("^[a-z][a-zA-Z0-9_.-]*$"); + + /** + * A name or action must be composed of printable, visible ASCII characters. + * That is: letters, numbers & symbols, but no whitespace. + */ + private static final Pattern VALID_NAME_OR_ACTION = Pattern.compile("^\\p{Graph}*$"); + + public static final Function NONE = app -> new ApplicationPrivilege(app, "none", new String[0]); + + private final String application; + private final String[] patterns; + + public ApplicationPrivilege(String application, String privilegeName, String... patterns) { + this(application, Collections.singleton(privilegeName), patterns); + } + + public ApplicationPrivilege(String application, Set name, String... patterns) { + super(name, patterns); + this.application = application; + this.patterns = patterns; + } + + public String getApplication() { + return application; + } + + // Package level for testing + String[] getPatterns() { + return patterns; + } + + /** + * Validate that the provided application name is valid, and throws an exception otherwise + * + * @throws IllegalArgumentException if the name is not valid + */ + public static void validateApplicationName(String application) { + validateApplicationName(application, false); + } + + /** + * Validate that the provided name is a valid application, or a wildcard pattern for an application and throws an exception otherwise + * + * @throws IllegalArgumentException if the name is not valid + */ + public static void validateApplicationNameOrWildcard(String application) { + validateApplicationName(application, true); + } + + /** + * Validates that an application name matches the following rules: + * - consist of a "prefix", optionally followed by either "-" or "_" and a suffix + * - the prefix must begin with a lowercase ASCII letter + * - the prefix only contain ASCII letter or digits + * - the prefix must be at least 3 characters long + * - the suffix must only contain {@link Strings#validFileName valid filename} characters + * - no part of the name may contain whitespace + * If {@code allowWildcard} is true, then the names that end with a '*', and would match a valid + * application name are also accepted. + */ + private static void validateApplicationName(String application, boolean allowWildcard) { + if (Strings.isEmpty(application)) { + throw new IllegalArgumentException("Application names cannot be blank"); + } + final int asterisk = application.indexOf('*'); + if (asterisk != -1) { + if (allowWildcard == false) { + throw new IllegalArgumentException("Application names may not contain '*' (found '" + application + "')"); + } + if(application.equals("*")) { + // this is allowed and short-circuiting here makes the later validation simpler + return; + } + if (asterisk != application.length() - 1) { + throw new IllegalArgumentException("Application name patterns only support trailing wildcards (found '" + application + + "')"); + } + } + if (WHITESPACE.matcher(application).find()) { + throw new IllegalArgumentException("Application names may not contain whitespace (found '" + application + "')"); + } + + final String[] parts = application.split("[_-]", 2); + String prefix = parts[0]; + if (prefix.endsWith("*")) { + prefix = prefix.substring(0, prefix.length() - 1); + } + if (VALID_APPLICATION_PREFIX.matcher(prefix).matches() == false) { + throw new IllegalArgumentException("An application name prefix must match the pattern " + VALID_APPLICATION_PREFIX.pattern() + + " (found '" + prefix + "')"); + } + if (prefix.length() < 3 && asterisk == -1) { + throw new IllegalArgumentException("An application name prefix must be at least 3 characters long (found '" + prefix + "')"); + } + + if (parts.length > 1) { + final String suffix = parts[1]; + if (Strings.validFileName(suffix) == false) { + throw new IllegalArgumentException("An application name suffix may not contain any of the characters '" + + Strings.collectionToDelimitedString(Strings.INVALID_FILENAME_CHARS, "") + "' (found '" + suffix + "')"); + } + } + } + + /** + * Validate that the provided privilege name is valid, and throws an exception otherwise + * + * @throws IllegalArgumentException if the name is not valid + */ + public static void validatePrivilegeName(String name) { + if (isValidPrivilegeName(name) == false) { + throw new IllegalArgumentException("Application privilege names must match the pattern " + VALID_NAME.pattern() + + " (found '" + name + "')"); + } + } + + private static boolean isValidPrivilegeName(String name) { + return VALID_NAME.matcher(name).matches(); + } + + /** + * Validate that the provided name is a valid privilege name or action name, and throws an exception otherwise + * + * @throws IllegalArgumentException if the name is not valid + */ + public static void validatePrivilegeOrActionName(String name) { + if (VALID_NAME_OR_ACTION.matcher(name).matches() == false) { + throw new IllegalArgumentException("Application privilege names and actions must match the pattern " + + VALID_NAME_OR_ACTION.pattern() + " (found '" + name + "')"); + } + } + + /** + * Finds or creates an application privileges with the provided names. + * Each element in {@code name} may be the name of a stored privilege (to be resolved from {@code stored}, or a bespoke action pattern. + */ + public static ApplicationPrivilege get(String application, Set name, Collection stored) { + if (name.isEmpty()) { + return NONE.apply(application); + } else { + Map lookup = stored.stream() + .filter(apd -> apd.getApplication().equals(application)) + .collect(Collectors.toMap(ApplicationPrivilegeDescriptor::getName, Function.identity())); + return resolve(application, name, lookup); + } + } + + private static ApplicationPrivilege resolve(String application, Set names, Map lookup) { + final int size = names.size(); + if (size == 0) { + throw new IllegalArgumentException("empty set should not be used"); + } + + Set actions = new HashSet<>(); + Set patterns = new HashSet<>(); + for (String name : names) { + if (isValidPrivilegeName(name)) { + ApplicationPrivilegeDescriptor descriptor = lookup.get(name); + if (descriptor != null) { + patterns.addAll(descriptor.getActions()); + } + } else { + actions.add(name); + } + } + + patterns.addAll(actions); + return new ApplicationPrivilege(application, names, patterns.toArray(new String[patterns.size()])); + } + + @Override + public String toString() { + return application + ":" + super.toString() + "(" + Strings.arrayToCommaDelimitedString(patterns) + ")"; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + Objects.hashCode(application); + result = 31 * result + Arrays.hashCode(patterns); + return result; + } + + @Override + public boolean equals(Object o) { + return super.equals(o) + && Objects.equals(this.application, ((ApplicationPrivilege) o).application) + && Arrays.equals(this.patterns, ((ApplicationPrivilege) o).patterns); + } + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ApplicationPrivilegeDescriptor.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ApplicationPrivilegeDescriptor.java new file mode 100644 index 0000000000000..85d6aad3e3560 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ApplicationPrivilegeDescriptor.java @@ -0,0 +1,194 @@ +/* + * 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.authz.privilege; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * An {@code ApplicationPrivilegeDescriptor} is a representation of a stored {@link ApplicationPrivilege}. + * A user (via a role) can be granted an application privilege by name (e.g. ("myapp", "read"). + * In general, this privilege name will correspond to a pre-defined {@link ApplicationPrivilegeDescriptor}, which then + * is used to determine the set of actions granted by the privilege. + */ +public class ApplicationPrivilegeDescriptor implements ToXContentObject, Writeable { + + public static final String DOC_TYPE_VALUE = "application-privilege"; + + private static final ObjectParser PARSER = new ObjectParser<>(DOC_TYPE_VALUE, Builder::new); + + static { + PARSER.declareString(Builder::applicationName, Fields.APPLICATION); + PARSER.declareString(Builder::privilegeName, Fields.NAME); + PARSER.declareStringArray(Builder::actions, Fields.ACTIONS); + PARSER.declareObject(Builder::metadata, (parser, context) -> parser.map(), Fields.METADATA); + PARSER.declareField((parser, builder, allowType) -> builder.type(parser.text(), allowType), Fields.TYPE, + ObjectParser.ValueType.STRING); + } + + private String application; + private String name; + private Set actions; + private Map metadata; + + public ApplicationPrivilegeDescriptor(String application, String name, Set actions, Map metadata) { + this.application = Objects.requireNonNull(application); + this.name = Objects.requireNonNull(name); + this.actions = Collections.unmodifiableSet(actions); + this.metadata = Collections.unmodifiableMap(metadata); + } + + public ApplicationPrivilegeDescriptor(StreamInput input) throws IOException { + this.application = input.readString(); + this.name = input.readString(); + this.actions = Collections.unmodifiableSet(input.readSet(StreamInput::readString)); + this.metadata = Collections.unmodifiableMap(input.readMap()); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(application); + out.writeString(name); + out.writeCollection(actions, StreamOutput::writeString); + out.writeMap(metadata); + } + + public String getApplication() { + return application; + } + + public String getName() { + return name; + } + + public Set getActions() { + return actions; + } + + public Map getMetadata() { + return metadata; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return toXContent(builder, false); + } + + public XContentBuilder toXContent(XContentBuilder builder, boolean includeTypeField) throws IOException { + builder.startObject() + .field(Fields.APPLICATION.getPreferredName(), application) + .field(Fields.NAME.getPreferredName(), name) + .field(Fields.ACTIONS.getPreferredName(), actions) + .field(Fields.METADATA.getPreferredName(), metadata); + if (includeTypeField) { + builder.field(Fields.TYPE.getPreferredName(), DOC_TYPE_VALUE); + } + return builder.endObject(); + } + + /** + * Construct a new {@link ApplicationPrivilegeDescriptor} from XContent. + * + * @param defaultApplication The application name to use if none is specified in the XContent body + * @param defaultName The privilege name to use if none is specified in the XContent body + * @param allowType If true, accept a "type" field (for which the value must match {@link #DOC_TYPE_VALUE}); + */ + public static ApplicationPrivilegeDescriptor parse(XContentParser parser, String defaultApplication, String defaultName, + boolean allowType) throws IOException { + final Builder builder = PARSER.parse(parser, allowType); + if (builder.applicationName == null) { + builder.applicationName(defaultApplication); + } + if (builder.privilegeName == null) { + builder.privilegeName(defaultName); + } + return builder.build(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final ApplicationPrivilegeDescriptor that = (ApplicationPrivilegeDescriptor) o; + return Objects.equals(this.application, that.application) && + Objects.equals(this.name, that.name) && + Objects.equals(this.actions, that.actions) && + Objects.equals(this.metadata, that.metadata); + } + + @Override + public int hashCode() { + return Objects.hash(application, name, actions, metadata); + } + + private static final class Builder { + private String applicationName; + private String privilegeName; + private Set actions = Collections.emptySet(); + private Map metadata = Collections.emptyMap(); + + private Builder applicationName(String applicationName) { + this.applicationName = applicationName; + return this; + } + + private Builder privilegeName(String privilegeName) { + this.privilegeName = privilegeName; + return this; + } + + private Builder actions(Collection actions) { + this.actions = new HashSet<>(actions); + return this; + } + + private Builder metadata(Map metadata) { + this.metadata = metadata; + return this; + } + + private Builder type(String type, boolean allowed) { + if (allowed == false) { + throw new IllegalStateException("Field " + Fields.TYPE.getPreferredName() + " cannot be specified here"); + } + if (ApplicationPrivilegeDescriptor.DOC_TYPE_VALUE.equals(type) == false) { + throw new IllegalStateException("XContent has wrong " + Fields.TYPE.getPreferredName() + " field " + type); + } + return this; + } + + private ApplicationPrivilegeDescriptor build() { + return new ApplicationPrivilegeDescriptor(applicationName, privilegeName, actions, metadata); + } + } + + public interface Fields { + ParseField APPLICATION = new ParseField("application"); + ParseField NAME = new ParseField("name"); + ParseField ACTIONS = new ParseField("actions"); + ParseField METADATA = new ParseField("metadata"); + ParseField TYPE = new ParseField("type"); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConditionalClusterPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConditionalClusterPrivilege.java new file mode 100644 index 0000000000000..dd89c2bda705d --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConditionalClusterPrivilege.java @@ -0,0 +1,63 @@ +/* + * 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.authz.privilege; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.io.stream.NamedWriteable; +import org.elasticsearch.common.xcontent.ToXContentFragment; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.transport.TransportRequest; + +import java.io.IOException; +import java.util.Collection; +import java.util.function.Predicate; + +/** + * A ConditionalClusterPrivilege is a composition of a {@link ClusterPrivilege} (that determines which actions may be executed) + * with a {@link Predicate} for a {@link TransportRequest} (that determines which requests may be executed). + * The a given execution of an action is considered to be permitted if both the action and the request are permitted. + */ +public interface ConditionalClusterPrivilege extends NamedWriteable, ToXContentFragment { + + /** + * The category under which this privilege should be rendered when output as XContent. + */ + Category getCategory(); + + /** + * The action-level privilege that is required by this conditional privilege. + */ + ClusterPrivilege getPrivilege(); + + /** + * The request-level privilege (as a {@link Predicate}) that is required by this conditional privilege. + */ + Predicate getRequestPredicate(); + + /** + * A {@link ConditionalClusterPrivilege} should generate a fragment of {@code XContent}, which consists of + * a single field name, followed by its value (which may be an object, an array, or a simple value). + */ + @Override + XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException; + + /** + * Categories exist for to segment privileges for the purposes of rendering to XContent. + * {@link ConditionalClusterPrivileges#toXContent(XContentBuilder, Params, Collection)} builds one XContent + * object for a collection of {@link ConditionalClusterPrivilege} instances, with the top level fields built + * from the categories. + */ + enum Category { + APPLICATION(new ParseField("application")); + + public final ParseField field; + + Category(ParseField field) { + this.field = field; + } + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConditionalClusterPrivileges.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConditionalClusterPrivileges.java new file mode 100644 index 0000000000000..c068c77781b14 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConditionalClusterPrivileges.java @@ -0,0 +1,225 @@ +/* + * 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.authz.privilege; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParseException; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.transport.TransportRequest; +import org.elasticsearch.xpack.core.security.action.privilege.ApplicationPrivilegesRequest; +import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege.Category; +import org.elasticsearch.xpack.core.security.support.Automatons; +import org.elasticsearch.xpack.core.security.xcontent.XContentUtils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; + +/** + * Static utility class for working with {@link ConditionalClusterPrivilege} instances + */ +public final class ConditionalClusterPrivileges { + + public static final ConditionalClusterPrivilege[] EMPTY_ARRAY = new ConditionalClusterPrivilege[0]; + + private ConditionalClusterPrivileges() { + } + + /** + * Utility method to read an array of {@link ConditionalClusterPrivilege} objects from a {@link StreamInput} + */ + public static ConditionalClusterPrivilege[] readArray(StreamInput in) throws IOException { + return in.readArray(in1 -> + in1.readNamedWriteable(ConditionalClusterPrivilege.class), ConditionalClusterPrivilege[]::new); + } + + /** + * Utility method to write an array of {@link ConditionalClusterPrivilege} objects to a {@link StreamOutput} + */ + public static void writeArray(StreamOutput out, ConditionalClusterPrivilege[] privileges) throws IOException { + out.writeArray((out1, value) -> out1.writeNamedWriteable(value), privileges); + } + + /** + * Writes a single object value to the {@code builder} that contains each of the provided privileges. + * The privileges are grouped according to their {@link ConditionalClusterPrivilege#getCategory() categories} + */ + public static XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params, + Collection privileges) throws IOException { + builder.startObject(); + for (Category category : Category.values()) { + builder.startObject(category.field.getPreferredName()); + for (ConditionalClusterPrivilege privilege : privileges) { + if (category == privilege.getCategory()) { + privilege.toXContent(builder, params); + } + } + builder.endObject(); + } + return builder.endObject(); + } + + /** + * Read a list of privileges from the parser. The parser should be positioned at the + * {@link XContentParser.Token#START_OBJECT} token for the privileges value + */ + public static List parse(XContentParser parser) throws IOException { + List privileges = new ArrayList<>(); + + expectedToken(parser.currentToken(), parser, XContentParser.Token.START_OBJECT); + while (parser.nextToken() != XContentParser.Token.END_OBJECT) { + expectedToken(parser.currentToken(), parser, XContentParser.Token.FIELD_NAME); + + expectFieldName(parser, Category.APPLICATION.field); + expectedToken(parser.nextToken(), parser, XContentParser.Token.START_OBJECT); + expectedToken(parser.nextToken(), parser, XContentParser.Token.FIELD_NAME); + + expectFieldName(parser, ManageApplicationPrivileges.Fields.MANAGE); + privileges.add(ManageApplicationPrivileges.parse(parser)); + expectedToken(parser.nextToken(), parser, XContentParser.Token.END_OBJECT); + } + + return privileges; + } + + private static void expectedToken(XContentParser.Token read, XContentParser parser, XContentParser.Token expected) { + if (read != expected) { + throw new XContentParseException(parser.getTokenLocation(), + "failed to parse privilege. expected [" + expected + "] but found [" + read + "] instead"); + } + } + + private static void expectFieldName(XContentParser parser, ParseField... fields) throws IOException { + final String fieldName = parser.currentName(); + if (Arrays.stream(fields).anyMatch(pf -> pf.match(fieldName, parser.getDeprecationHandler())) == false) { + throw new XContentParseException(parser.getTokenLocation(), + "failed to parse privilege. expected " + (fields.length == 1 ? "field name" : "one of") + " [" + + Strings.arrayToCommaDelimitedString(fields) + "] but found [" + fieldName + "] instead"); + } + } + + /** + * The {@code ManageApplicationPrivileges} privilege is a {@link ConditionalClusterPrivilege} that grants the + * ability to execute actions related to the management of application privileges (Get, Put, Delete) for a subset + * of applications (identified by a wildcard-aware application-name). + */ + public static class ManageApplicationPrivileges implements ConditionalClusterPrivilege { + + private static final ClusterPrivilege PRIVILEGE = ClusterPrivilege.get( + Collections.singleton("cluster:admin/xpack/security/privilege/*") + ); + public static final String WRITEABLE_NAME = "manage-application-privileges"; + + private final Set applicationNames; + private final Predicate applicationPredicate; + private final Predicate requestPredicate; + + public ManageApplicationPrivileges(Set applicationNames) { + this.applicationNames = Collections.unmodifiableSet(applicationNames); + this.applicationPredicate = Automatons.predicate(applicationNames); + this.requestPredicate = request -> { + if (request instanceof ApplicationPrivilegesRequest) { + final ApplicationPrivilegesRequest privRequest = (ApplicationPrivilegesRequest) request; + return privRequest.getApplicationNames().stream().allMatch(application -> applicationPredicate.test(application)); + } + return false; + }; + } + + @Override + public Category getCategory() { + return Category.APPLICATION; + } + + @Override + public ClusterPrivilege getPrivilege() { + return PRIVILEGE; + } + + @Override + public Predicate getRequestPredicate() { + return this.requestPredicate; + } + + public Collection getApplicationNames() { + return Collections.unmodifiableCollection(this.applicationNames); + } + + @Override + public String getWriteableName() { + return WRITEABLE_NAME; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeCollection(this.applicationNames, StreamOutput::writeString); + } + + public static ManageApplicationPrivileges createFrom(StreamInput in) throws IOException { + final Set applications = in.readSet(StreamInput::readString); + return new ManageApplicationPrivileges(applications); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.field(Fields.MANAGE.getPreferredName(), + Collections.singletonMap(Fields.APPLICATIONS.getPreferredName(), applicationNames) + ); + } + + public static ManageApplicationPrivileges parse(XContentParser parser) throws IOException { + expectedToken(parser.currentToken(), parser, XContentParser.Token.FIELD_NAME); + expectFieldName(parser, Fields.MANAGE); + expectedToken(parser.nextToken(), parser, XContentParser.Token.START_OBJECT); + expectedToken(parser.nextToken(), parser, XContentParser.Token.FIELD_NAME); + expectFieldName(parser, Fields.APPLICATIONS); + expectedToken(parser.nextToken(), parser, XContentParser.Token.START_ARRAY); + final String[] applications = XContentUtils.readStringArray(parser, false); + expectedToken(parser.nextToken(), parser, XContentParser.Token.END_OBJECT); + return new ManageApplicationPrivileges(new LinkedHashSet<>(Arrays.asList(applications))); + } + + @Override + public String toString() { + return "{" + getCategory() + ":" + Fields.MANAGE.getPreferredName() + ":" + Fields.APPLICATIONS.getPreferredName() + "=" + + Strings.collectionToDelimitedString(applicationNames, ",") + "}"; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final ManageApplicationPrivileges that = (ManageApplicationPrivileges) o; + return this.applicationNames.equals(that.applicationNames); + } + + @Override + public int hashCode() { + return applicationNames.hashCode(); + } + + private interface Fields { + ParseField MANAGE = new ParseField("manage"); + ParseField APPLICATIONS = new ParseField("applications"); + } + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java index 821c659fa4023..edd85eb3ba20c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java @@ -9,6 +9,8 @@ import org.elasticsearch.xpack.core.monitoring.action.MonitoringBulkAction; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.permission.Role; +import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivileges.ManageApplicationPrivileges; import org.elasticsearch.xpack.core.security.support.MetadataUtils; import org.elasticsearch.xpack.core.security.user.KibanaUser; import org.elasticsearch.xpack.core.security.user.UsernamesField; @@ -27,8 +29,11 @@ public class ReservedRolesStore { new String[] { "all" }, new RoleDescriptor.IndicesPrivileges[] { RoleDescriptor.IndicesPrivileges.builder().indices("*").privileges("all").build()}, - new String[] { "*" }, - MetadataUtils.DEFAULT_RESERVED_METADATA); + new RoleDescriptor.ApplicationResourcePrivileges[] { + RoleDescriptor.ApplicationResourcePrivileges.builder().application("*").privileges("*").resources("*").build() + }, + null, new String[] { "*" }, + MetadataUtils.DEFAULT_RESERVED_METADATA, Collections.emptyMap()); public static final Role SUPERUSER_ROLE = Role.builder(SUPERUSER_ROLE_DESCRIPTOR, null).build(); private static final Map RESERVED_ROLES = initializeReservedRoles(); @@ -43,7 +48,11 @@ private static Map initializeReservedRoles() { MetadataUtils.DEFAULT_RESERVED_METADATA)) .put("kibana_user", new RoleDescriptor("kibana_user", null, new RoleDescriptor.IndicesPrivileges[] { RoleDescriptor.IndicesPrivileges.builder().indices(".kibana*").privileges("manage", "read", "index", "delete") - .build() }, null, MetadataUtils.DEFAULT_RESERVED_METADATA)) + .build() }, new RoleDescriptor.ApplicationResourcePrivileges[] { + RoleDescriptor.ApplicationResourcePrivileges.builder() + .application("kibana-.kibana").resources("*").privileges("all").build() }, + null, null, + MetadataUtils.DEFAULT_RESERVED_METADATA, null)) .put("monitoring_user", new RoleDescriptor("monitoring_user", new String[] { "cluster:monitor/main" }, new RoleDescriptor.IndicesPrivileges[] { @@ -70,19 +79,27 @@ private static Map initializeReservedRoles() { "kibana_dashboard_only_user", null, new RoleDescriptor.IndicesPrivileges[] { - RoleDescriptor.IndicesPrivileges.builder() - .indices(".kibana*").privileges("read", "view_index_metadata").build() + RoleDescriptor.IndicesPrivileges.builder() + .indices(".kibana*").privileges("read", "view_index_metadata").build() }, - null, - MetadataUtils.DEFAULT_RESERVED_METADATA)) + new RoleDescriptor.ApplicationResourcePrivileges[] { + RoleDescriptor.ApplicationResourcePrivileges.builder() + .application("kibana-.kibana").resources("*").privileges("read").build() }, + null, null, + MetadataUtils.DEFAULT_RESERVED_METADATA, + null)) .put(KibanaUser.ROLE_NAME, new RoleDescriptor(KibanaUser.ROLE_NAME, - new String[] { "monitor", "manage_index_templates", MonitoringBulkAction.NAME, "manage_saml" }, + new String[] { + "monitor", "manage_index_templates", MonitoringBulkAction.NAME, "manage_saml", + }, new RoleDescriptor.IndicesPrivileges[] { RoleDescriptor.IndicesPrivileges.builder().indices(".kibana*", ".reporting-*").privileges("all").build(), RoleDescriptor.IndicesPrivileges.builder() .indices(".monitoring-*").privileges("read", "read_cross_cluster").build() }, - null, MetadataUtils.DEFAULT_RESERVED_METADATA)) + null, + new ConditionalClusterPrivilege[] { new ManageApplicationPrivileges(Collections.singleton("kibana-*")) }, + null, MetadataUtils.DEFAULT_RESERVED_METADATA, null)) .put("logstash_system", new RoleDescriptor("logstash_system", new String[] { "monitor", MonitoringBulkAction.NAME}, null, null, MetadataUtils.DEFAULT_RESERVED_METADATA)) .put(UsernamesField.BEATS_ROLE, new RoleDescriptor(UsernamesField.BEATS_ROLE, diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/client/SecurityClient.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/client/SecurityClient.java index 26c35db1fc92b..3e4129b54e688 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/client/SecurityClient.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/client/SecurityClient.java @@ -10,6 +10,12 @@ import org.elasticsearch.client.ElasticsearchClient; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.xpack.core.security.action.privilege.DeletePrivilegesAction; +import org.elasticsearch.xpack.core.security.action.privilege.DeletePrivilegesRequestBuilder; +import org.elasticsearch.xpack.core.security.action.privilege.GetPrivilegesAction; +import org.elasticsearch.xpack.core.security.action.privilege.GetPrivilegesRequestBuilder; +import org.elasticsearch.xpack.core.security.action.privilege.PutPrivilegesAction; +import org.elasticsearch.xpack.core.security.action.privilege.PutPrivilegesRequestBuilder; import org.elasticsearch.xpack.core.security.action.realm.ClearRealmCacheAction; import org.elasticsearch.xpack.core.security.action.realm.ClearRealmCacheRequest; import org.elasticsearch.xpack.core.security.action.realm.ClearRealmCacheRequestBuilder; @@ -170,7 +176,9 @@ public void hasPrivileges(HasPrivilegesRequest request, ActionListener list client.execute(PutRoleAction.INSTANCE, request, listener); } - /** Role Mappings */ + /** + * Role Mappings + */ public GetRoleMappingsRequestBuilder prepareGetRoleMappings(String... names) { return new GetRoleMappingsRequestBuilder(client, GetRoleMappingsAction.INSTANCE) @@ -278,6 +290,27 @@ public DeleteRoleMappingRequestBuilder prepareDeleteRoleMapping(String name) { .name(name); } + /* -- Application Privileges -- */ + public GetPrivilegesRequestBuilder prepareGetPrivileges(String applicationName, String[] privileges) { + return new GetPrivilegesRequestBuilder(client, GetPrivilegesAction.INSTANCE).application(applicationName).privileges(privileges); + } + + public PutPrivilegesRequestBuilder preparePutPrivilege(String applicationName, String privilegeName, + BytesReference bytesReference, XContentType xContentType) throws IOException { + return new PutPrivilegesRequestBuilder(client, PutPrivilegesAction.INSTANCE) + .source(applicationName, privilegeName, bytesReference, xContentType); + } + + public PutPrivilegesRequestBuilder preparePutPrivileges(BytesReference bytesReference, XContentType xContentType) throws IOException { + return new PutPrivilegesRequestBuilder(client, PutPrivilegesAction.INSTANCE).source(bytesReference, xContentType); + } + + public DeletePrivilegesRequestBuilder prepareDeletePrivileges(String applicationName, String[] privileges) { + return new DeletePrivilegesRequestBuilder(client, DeletePrivilegesAction.INSTANCE) + .application(applicationName) + .privileges(privileges); + } + public CreateTokenRequestBuilder prepareCreateToken() { return new CreateTokenRequestBuilder(client, CreateTokenAction.INSTANCE); } @@ -301,7 +334,7 @@ public SamlAuthenticateRequestBuilder prepareSamlAuthenticate(byte[] xmlContent, return builder; } - public void samlAuthenticate(SamlAuthenticateRequest request, ActionListener< SamlAuthenticateResponse> listener) { + public void samlAuthenticate(SamlAuthenticateRequest request, ActionListener listener) { client.execute(SamlAuthenticateAction.INSTANCE, request, listener); } diff --git a/x-pack/plugin/core/src/main/resources/security-index-template.json b/x-pack/plugin/core/src/main/resources/security-index-template.json index 778f44a93bf3a..dd17baf04740f 100644 --- a/x-pack/plugin/core/src/main/resources/security-index-template.json +++ b/x-pack/plugin/core/src/main/resources/security-index-template.json @@ -91,6 +91,41 @@ } } }, + "applications": { + "type": "object", + "properties": { + "application": { + "type": "keyword" + }, + "privileges": { + "type": "keyword" + }, + "resources": { + "type": "keyword" + } + } + }, + "application" : { + "type" : "keyword" + }, + "global": { + "type": "object", + "properties": { + "application": { + "type": "object", + "properties": { + "manage": { + "type": "object", + "properties": { + "applications": { + "type": "keyword" + } + } + } + } + } + } + }, "name" : { "type" : "keyword" }, @@ -103,6 +138,9 @@ "type" : { "type" : "keyword" }, + "actions" : { + "type" : "keyword" + }, "expiration_time" : { "type" : "date", "format" : "epoch_millis" diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/test/TestMatchers.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/test/TestMatchers.java index 9fd1d64323eb5..2a9041575df2b 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/test/TestMatchers.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/test/TestMatchers.java @@ -5,16 +5,16 @@ */ package org.elasticsearch.test; +import org.hamcrest.CustomMatcher; +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; + import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import java.util.function.Predicate; import java.util.regex.Pattern; -import org.hamcrest.CustomMatcher; -import org.hamcrest.Matcher; -import org.hamcrest.Matchers; - public class TestMatchers extends Matchers { public static Matcher pathExists(Path path, LinkOption... options) { @@ -26,6 +26,19 @@ public boolean matches(Object item) { }; } + public static Matcher> predicateMatches(T value) { + return new CustomMatcher>("Matches " + value) { + @Override + public boolean matches(Object item) { + if (Predicate.class.isInstance(item)) { + return ((Predicate) item).test(value); + } else { + return false; + } + } + }; + } + public static Matcher matchesPattern(String regex) { return matchesPattern(Pattern.compile(regex)); } @@ -34,16 +47,17 @@ public static Matcher matchesPattern(Pattern pattern) { return predicate("Matches " + pattern.pattern(), String.class, pattern.asPredicate()); } - private static Matcher predicate(String description, Class type, Predicate stringPredicate) { + private static Matcher predicate(String description, Class type, Predicate predicate) { return new CustomMatcher(description) { @Override public boolean matches(Object item) { if (type.isInstance(item)) { - return stringPredicate.test(type.cast(item)); + return predicate.test(type.cast(item)); } else { return false; } } }; } + } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/DeletePrivilegesRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/DeletePrivilegesRequestTests.java new file mode 100644 index 0000000000000..03232181f930e --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/DeletePrivilegesRequestTests.java @@ -0,0 +1,62 @@ +/* + * 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.action.privilege; + +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.support.WriteRequest; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.test.ESTestCase; +import org.hamcrest.Matchers; + +import java.io.IOException; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +public class DeletePrivilegesRequestTests extends ESTestCase { + + public void testSerialization() throws IOException { + final DeletePrivilegesRequest original = new DeletePrivilegesRequest( + randomAlphaOfLengthBetween(3, 8), generateRandomStringArray(5, randomIntBetween(3, 8), false, false)); + original.setRefreshPolicy(randomFrom(WriteRequest.RefreshPolicy.values())); + + final BytesStreamOutput output = new BytesStreamOutput(); + original.writeTo(output); + output.flush(); + final DeletePrivilegesRequest copy = new DeletePrivilegesRequest(); + copy.readFrom(output.bytes().streamInput()); + assertThat(copy.application(), equalTo(original.application())); + assertThat(copy.privileges(), equalTo(original.privileges())); + assertThat(copy.getRefreshPolicy(), equalTo(original.getRefreshPolicy())); + } + + public void testValidation() { + assertValidationFailure(new DeletePrivilegesRequest(null, null), "application name", "privileges"); + assertValidationFailure(new DeletePrivilegesRequest("", null), "application name", "privileges"); + assertValidationFailure(new DeletePrivilegesRequest(null, new String[0]), "application name", "privileges"); + assertValidationFailure(new DeletePrivilegesRequest("", new String[0]), "application name", "privileges"); + assertValidationFailure(new DeletePrivilegesRequest(null, new String[]{"all"}), "application name"); + assertValidationFailure(new DeletePrivilegesRequest("", new String[]{"all"}), "application name"); + assertValidationFailure(new DeletePrivilegesRequest("app", null), "privileges"); + assertValidationFailure(new DeletePrivilegesRequest("app", new String[0]), "privileges"); + assertValidationFailure(new DeletePrivilegesRequest("app", new String[]{""}), "privileges"); + + assertThat(new DeletePrivilegesRequest("app", new String[]{"all"}).validate(), nullValue()); + assertThat(new DeletePrivilegesRequest("app", new String[]{"all", "some"}).validate(), nullValue()); + } + + private void assertValidationFailure(DeletePrivilegesRequest request, String... messages) { + final ActionRequestValidationException exception = request.validate(); + assertThat(exception, notNullValue()); + for (String message : messages) { + assertThat(exception.validationErrors(), Matchers.hasItem(containsString(message))); + } + } + +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/DeletePrivilegesResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/DeletePrivilegesResponseTests.java new file mode 100644 index 0000000000000..d490177c0cec4 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/DeletePrivilegesResponseTests.java @@ -0,0 +1,30 @@ +/* + * 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.action.privilege; + +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.util.Arrays; + +import static org.hamcrest.Matchers.equalTo; + +public class DeletePrivilegesResponseTests extends ESTestCase { + + public void testSerialization() throws IOException { + final DeletePrivilegesResponse original = new DeletePrivilegesResponse( + Arrays.asList(generateRandomStringArray(5, randomIntBetween(3, 8), false, true))); + + final BytesStreamOutput output = new BytesStreamOutput(); + original.writeTo(output); + output.flush(); + final DeletePrivilegesResponse copy = new DeletePrivilegesResponse(); + copy.readFrom(output.bytes().streamInput()); + assertThat(copy.found(), equalTo(original.found())); + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/GetPrivilegesRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/GetPrivilegesRequestTests.java new file mode 100644 index 0000000000000..4d67b82dfd4c0 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/GetPrivilegesRequestTests.java @@ -0,0 +1,58 @@ +/* + * 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.action.privilege; + +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.test.ESTestCase; +import org.hamcrest.Matchers; + +import java.io.IOException; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +public class GetPrivilegesRequestTests extends ESTestCase { + + public void testSerialization() throws IOException { + final GetPrivilegesRequest original = new GetPrivilegesRequest(); + if (randomBoolean()) { + original.application(randomAlphaOfLengthBetween(3, 8)); + } + original.privileges(generateRandomStringArray(3, 5, false, true)); + + final BytesStreamOutput out = new BytesStreamOutput(); + original.writeTo(out); + + final GetPrivilegesRequest copy = new GetPrivilegesRequest(); + copy.readFrom(out.bytes().streamInput()); + + assertThat(original.application(), Matchers.equalTo(copy.application())); + assertThat(original.privileges(), Matchers.equalTo(copy.privileges())); + } + + public void testValidation() { + assertThat(request(null).validate(), nullValue()); + assertThat(request(null, "all").validate(), nullValue()); + assertThat(request(null, "read", "write").validate(), nullValue()); + assertThat(request("my_app").validate(), nullValue()); + assertThat(request("my_app", "all").validate(), nullValue()); + assertThat(request("my_app", "read", "write").validate(), nullValue()); + final ActionRequestValidationException exception = request("my_app", ((String[]) null)).validate(); + assertThat(exception, notNullValue()); + assertThat(exception.validationErrors(), containsInAnyOrder("privileges cannot be null")); + } + + private GetPrivilegesRequest request(String application, String... privileges) { + final GetPrivilegesRequest request = new GetPrivilegesRequest(); + request.application(application); + request.privileges(privileges); + return request; + } + +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/GetPrivilegesResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/GetPrivilegesResponseTests.java new file mode 100644 index 0000000000000..50c247967a780 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/GetPrivilegesResponseTests.java @@ -0,0 +1,41 @@ +/* + * 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.action.privilege; + +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; +import org.hamcrest.Matchers; + +import java.io.IOException; +import java.util.Collections; +import java.util.Locale; + +public class GetPrivilegesResponseTests extends ESTestCase { + + public void testSerialization() throws IOException { + ApplicationPrivilegeDescriptor[] privileges = randomArray(6, ApplicationPrivilegeDescriptor[]::new, () -> + new ApplicationPrivilegeDescriptor( + randomAlphaOfLengthBetween(3, 8).toLowerCase(Locale.ROOT), + randomAlphaOfLengthBetween(3, 8).toLowerCase(Locale.ROOT), + Sets.newHashSet(randomArray(3, String[]::new, () -> randomAlphaOfLength(3).toLowerCase(Locale.ROOT) + "/*")), + Collections.emptyMap() + ) + ); + final GetPrivilegesResponse original = new GetPrivilegesResponse(privileges); + + final BytesStreamOutput out = new BytesStreamOutput(); + original.writeTo(out); + + final GetPrivilegesResponse copy = new GetPrivilegesResponse(); + copy.readFrom(out.bytes().streamInput()); + + assertThat(copy.privileges(), Matchers.equalTo(original.privileges())); + } + +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesRequestTests.java new file mode 100644 index 0000000000000..e258efd04c5ec --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesRequestTests.java @@ -0,0 +1,97 @@ +/* + * 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.action.privilege; + +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.support.WriteRequest; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; +import org.hamcrest.Matchers; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.Locale; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.notNullValue; + +public class PutPrivilegesRequestTests extends ESTestCase { + + public void testSerialization() throws IOException { + final PutPrivilegesRequest original = request(randomArray(8, ApplicationPrivilegeDescriptor[]::new, + () -> new ApplicationPrivilegeDescriptor( + randomAlphaOfLengthBetween(3, 8).toLowerCase(Locale.ROOT), + randomAlphaOfLengthBetween(3, 8).toLowerCase(Locale.ROOT), + Sets.newHashSet(randomArray(3, String[]::new, () -> randomAlphaOfLength(3).toLowerCase(Locale.ROOT) + "/*")), + Collections.emptyMap() + ) + )); + original.setRefreshPolicy(randomFrom(WriteRequest.RefreshPolicy.values())); + + final BytesStreamOutput out = new BytesStreamOutput(); + original.writeTo(out); + + final PutPrivilegesRequest copy = new PutPrivilegesRequest(); + copy.readFrom(out.bytes().streamInput()); + + assertThat(original.getPrivileges(), Matchers.equalTo(copy.getPrivileges())); + assertThat(original.getRefreshPolicy(), Matchers.equalTo(copy.getRefreshPolicy())); + } + + public void testValidation() { + // wildcard app name + final ApplicationPrivilegeDescriptor wildcardApp = descriptor("*", "all", "*"); + assertValidationFailure(request(wildcardApp), "Application names may not contain"); + + // invalid priv names + final ApplicationPrivilegeDescriptor spaceName = descriptor("app", "r e a d", "read/*"); + final ApplicationPrivilegeDescriptor numericName = descriptor("app", "7346", "read/*"); + assertValidationFailure(request(spaceName), "Application privilege names must match"); + assertValidationFailure(request(numericName), "Application privilege names must match"); + + // no actions + final ApplicationPrivilegeDescriptor nothing = descriptor("*", "nothing"); + assertValidationFailure(request(nothing), "Application privileges must have at least one action"); + + // reserved metadata + final ApplicationPrivilegeDescriptor reservedMetadata = new ApplicationPrivilegeDescriptor("app", "all", + Collections.emptySet(), Collections.singletonMap("_notAllowed", true) + ); + assertValidationFailure(request(reservedMetadata), "metadata keys may not start"); + + ApplicationPrivilegeDescriptor badAction = descriptor("app", "foo", randomFrom("data.read", "data_read", "data+read", "read")); + assertValidationFailure(request(badAction), "must contain one of"); + + // mixed + assertValidationFailure(request(wildcardApp, numericName, reservedMetadata, badAction), + "Application names may not contain", "Application privilege names must match", "metadata keys may not start", + "must contain one of"); + } + + private ApplicationPrivilegeDescriptor descriptor(String application, String name, String... actions) { + return new ApplicationPrivilegeDescriptor(application, name, Sets.newHashSet(actions), Collections.emptyMap()); + } + + private void assertValidationFailure(PutPrivilegesRequest request, String... messages) { + final ActionRequestValidationException exception = request.validate(); + assertThat(exception, notNullValue()); + for (String message : messages) { + assertThat(exception.validationErrors(), hasItem(containsString(message))); + } + } + + private PutPrivilegesRequest request(ApplicationPrivilegeDescriptor... privileges) { + final PutPrivilegesRequest original = new PutPrivilegesRequest(); + + original.setPrivileges(Arrays.asList(privileges)); + return original; + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesResponseTests.java new file mode 100644 index 0000000000000..431d7f326ee88 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesResponseTests.java @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.security.action.privilege; + +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.hamcrest.Matchers.equalTo; + +public class PutPrivilegesResponseTests extends ESTestCase { + + public void testSerialization() throws IOException { + final int applicationCount = randomInt(3); + final Map> map = new HashMap<>(applicationCount); + for (int i = 0; i < applicationCount; i++) { + map.put(randomAlphaOfLengthBetween(3, 8), + Arrays.asList(generateRandomStringArray(5, 6, false, true)) + ); + } + final PutPrivilegesResponse original = new PutPrivilegesResponse(map); + + final BytesStreamOutput output = new BytesStreamOutput(); + original.writeTo(output); + output.flush(); + final PutPrivilegesResponse copy = new PutPrivilegesResponse(); + copy.readFrom(output.bytes().streamInput()); + assertThat(copy.created(), equalTo(original.created())); + assertThat(Strings.toString(copy), equalTo(Strings.toString(original))); + } + +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequestTests.java new file mode 100644 index 0000000000000..ae458cbb2f5ed --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequestTests.java @@ -0,0 +1,164 @@ +/* + * 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.action.role; + +import org.elasticsearch.Version; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.support.WriteRequest; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.io.stream.ByteBufferStreamInput; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.NamedWriteableAwareStreamInput; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.VersionUtils; +import org.elasticsearch.xpack.core.XPackClientPlugin; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.ApplicationResourcePrivileges; +import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivileges; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.function.Supplier; + +import static org.hamcrest.Matchers.arrayWithSize; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.iterableWithSize; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +public class PutRoleRequestTests extends ESTestCase { + + public void testValidationOfApplicationPrivileges() { + assertSuccessfulValidation(buildRequestWithApplicationPrivilege("app", new String[]{"read"}, new String[]{"*"})); + assertSuccessfulValidation(buildRequestWithApplicationPrivilege("app", new String[]{"action:login"}, new String[]{"/"})); + assertSuccessfulValidation(buildRequestWithApplicationPrivilege("*", new String[]{"data/read:user"}, new String[]{"user/123"})); + + // Fail + assertValidationError("privilege names and actions must match the pattern", + buildRequestWithApplicationPrivilege("app", new String[]{"in valid"}, new String[]{"*"})); + assertValidationError("An application name prefix must match the pattern", + buildRequestWithApplicationPrivilege("000", new String[]{"all"}, new String[]{"*"})); + assertValidationError("An application name prefix must match the pattern", + buildRequestWithApplicationPrivilege("%*", new String[]{"all"}, new String[]{"*"})); + } + + public void testSerialization() throws IOException { + final PutRoleRequest original = buildRandomRequest(); + + final BytesStreamOutput out = new BytesStreamOutput(); + original.writeTo(out); + + final PutRoleRequest copy = new PutRoleRequest(); + final NamedWriteableRegistry registry = new NamedWriteableRegistry(new XPackClientPlugin(Settings.EMPTY).getNamedWriteables()); + StreamInput in = new NamedWriteableAwareStreamInput(ByteBufferStreamInput.wrap(BytesReference.toBytes(out.bytes())), registry); + copy.readFrom(in); + + assertThat(copy.roleDescriptor(), equalTo(original.roleDescriptor())); + } + + public void testSerializationV63AndBefore() throws IOException { + final PutRoleRequest original = buildRandomRequest(); + + final BytesStreamOutput out = new BytesStreamOutput(); + final Version version = VersionUtils.randomVersionBetween(random(), Version.V_5_6_0, Version.V_6_3_2); + out.setVersion(version); + original.writeTo(out); + + final PutRoleRequest copy = new PutRoleRequest(); + final StreamInput in = out.bytes().streamInput(); + in.setVersion(version); + copy.readFrom(in); + + assertThat(copy.name(), equalTo(original.name())); + assertThat(copy.cluster(), equalTo(original.cluster())); + assertThat(copy.indices(), equalTo(original.indices())); + assertThat(copy.runAs(), equalTo(original.runAs())); + assertThat(copy.metadata(), equalTo(original.metadata())); + assertThat(copy.getRefreshPolicy(), equalTo(original.getRefreshPolicy())); + + assertThat(copy.applicationPrivileges(), iterableWithSize(0)); + assertThat(copy.conditionalClusterPrivileges(), arrayWithSize(0)); + } + + private void assertSuccessfulValidation(PutRoleRequest request) { + final ActionRequestValidationException exception = request.validate(); + assertThat(exception, nullValue()); + } + + private void assertValidationError(String message, PutRoleRequest request) { + final ActionRequestValidationException exception = request.validate(); + assertThat(exception, notNullValue()); + assertThat(exception.validationErrors(), hasItem(containsString(message))); + } + + private PutRoleRequest buildRequestWithApplicationPrivilege(String appName, String[] privileges, String[] resources) { + final PutRoleRequest request = new PutRoleRequest(); + request.name("test"); + final ApplicationResourcePrivileges privilege = ApplicationResourcePrivileges.builder() + .application(appName) + .privileges(privileges) + .resources(resources) + .build(); + request.addApplicationPrivileges(new ApplicationResourcePrivileges[]{privilege}); + return request; + } + + private PutRoleRequest buildRandomRequest() { + + final PutRoleRequest request = new PutRoleRequest(); + request.name(randomAlphaOfLengthBetween(4, 9)); + + request.cluster(randomSubsetOf(Arrays.asList("monitor", "manage", "all", "manage_security", "manage_ml", "monitor_watcher")) + .toArray(Strings.EMPTY_ARRAY)); + + for (int i = randomIntBetween(0, 4); i > 0; i--) { + request.addIndex( + generateRandomStringArray(randomIntBetween(1, 3), randomIntBetween(3, 8), false, false), + randomSubsetOf(randomIntBetween(1, 2), "read", "write", "index", "all").toArray(Strings.EMPTY_ARRAY), + generateRandomStringArray(randomIntBetween(1, 3), randomIntBetween(3, 8), true), + generateRandomStringArray(randomIntBetween(1, 3), randomIntBetween(3, 8), true), + null + ); + } + + final Supplier stringWithInitialLowercase = () + -> randomAlphaOfLength(1).toLowerCase(Locale.ROOT) + randomAlphaOfLengthBetween(3, 12); + final ApplicationResourcePrivileges[] applicationPrivileges = new ApplicationResourcePrivileges[randomIntBetween(0, 5)]; + for (int i = 0; i < applicationPrivileges.length; i++) { + applicationPrivileges[i] = ApplicationResourcePrivileges.builder() + .application(stringWithInitialLowercase.get()) + .privileges(randomArray(1, 3, String[]::new, stringWithInitialLowercase)) + .resources(generateRandomStringArray(5, randomIntBetween(3, 8), false, false)) + .build(); + } + request.addApplicationPrivileges(applicationPrivileges); + + if (randomBoolean()) { + final String[] appNames = randomArray(1, 4, String[]::new, stringWithInitialLowercase); + request.conditionalCluster(new ConditionalClusterPrivileges.ManageApplicationPrivileges(Sets.newHashSet(appNames))); + } + + request.runAs(generateRandomStringArray(4, 3, false, true)); + + final Map metadata = new HashMap<>(); + for (String key : generateRandomStringArray(3, 5, false, true)) { + metadata.put(key, randomFrom(Boolean.TRUE, Boolean.FALSE, 1, 2, randomAlphaOfLengthBetween(2, 9))); + } + request.metadata(metadata); + + request.setRefreshPolicy(randomFrom(WriteRequest.RefreshPolicy.values())); + return request; + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesRequestTests.java new file mode 100644 index 0000000000000..9e712e615f62f --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesRequestTests.java @@ -0,0 +1,125 @@ +/* + * 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.action.user; + +import org.elasticsearch.Version; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.ApplicationResourcePrivileges; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.IndicesPrivileges; +import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +public class HasPrivilegesRequestTests extends ESTestCase { + + public void testSerializationV7() throws IOException { + final HasPrivilegesRequest original = randomRequest(); + final HasPrivilegesRequest copy = serializeAndDeserialize(original, Version.V_6_4_0); + + assertThat(copy.username(), equalTo(original.username())); + assertThat(copy.clusterPrivileges(), equalTo(original.clusterPrivileges())); + assertThat(copy.indexPrivileges(), equalTo(original.indexPrivileges())); + assertThat(copy.applicationPrivileges(), equalTo(original.applicationPrivileges())); + } + + public void testSerializationV63() throws IOException { + final HasPrivilegesRequest original = randomRequest(); + final HasPrivilegesRequest copy = serializeAndDeserialize(original, Version.V_6_3_0); + + assertThat(copy.username(), equalTo(original.username())); + assertThat(copy.clusterPrivileges(), equalTo(original.clusterPrivileges())); + assertThat(copy.indexPrivileges(), equalTo(original.indexPrivileges())); + assertThat(copy.applicationPrivileges(), nullValue()); + } + + public void testValidateNullPrivileges() { + final HasPrivilegesRequest request = new HasPrivilegesRequest(); + final ActionRequestValidationException exception = request.validate(); + assertThat(exception, notNullValue()); + assertThat(exception.validationErrors(), hasItem("clusterPrivileges must not be null")); + assertThat(exception.validationErrors(), hasItem("indexPrivileges must not be null")); + assertThat(exception.validationErrors(), hasItem("applicationPrivileges must not be null")); + } + + public void testValidateEmptyPrivileges() { + final HasPrivilegesRequest request = new HasPrivilegesRequest(); + request.clusterPrivileges(new String[0]); + request.indexPrivileges(new IndicesPrivileges[0]); + request.applicationPrivileges(new ApplicationResourcePrivileges[0]); + final ActionRequestValidationException exception = request.validate(); + assertThat(exception, notNullValue()); + assertThat(exception.validationErrors(), hasItem("must specify at least one privilege")); + } + + public void testValidateNoWildcardApplicationPrivileges() { + final HasPrivilegesRequest request = new HasPrivilegesRequest(); + request.clusterPrivileges(new String[0]); + request.indexPrivileges(new IndicesPrivileges[0]); + request.applicationPrivileges(new ApplicationResourcePrivileges[] { + ApplicationResourcePrivileges.builder().privileges("read").application("*").resources("item/1").build() + }); + final ActionRequestValidationException exception = request.validate(); + assertThat(exception, notNullValue()); + assertThat(exception.validationErrors(), hasItem("Application names may not contain '*' (found '*')")); + } + + private HasPrivilegesRequest serializeAndDeserialize(HasPrivilegesRequest original, Version version) throws IOException { + final BytesStreamOutput out = new BytesStreamOutput(); + out.setVersion(version); + original.writeTo(out); + + final HasPrivilegesRequest copy = new HasPrivilegesRequest(); + final StreamInput in = out.bytes().streamInput(); + in.setVersion(version); + copy.readFrom(in); + assertThat(in.read(), equalTo(-1)); + return copy; + } + + private HasPrivilegesRequest randomRequest() { + final HasPrivilegesRequest request = new HasPrivilegesRequest(); + request.username(randomAlphaOfLength(8)); + + final List clusterPrivileges = randomSubsetOf(Arrays.asList(ClusterPrivilege.MONITOR, ClusterPrivilege.MANAGE, + ClusterPrivilege.MANAGE_ML, ClusterPrivilege.MANAGE_SECURITY, ClusterPrivilege.MANAGE_PIPELINE, ClusterPrivilege.ALL)) + .stream().flatMap(p -> p.name().stream()).collect(Collectors.toList()); + request.clusterPrivileges(clusterPrivileges.toArray(Strings.EMPTY_ARRAY)); + + IndicesPrivileges[] indicesPrivileges = new IndicesPrivileges[randomInt(5)]; + for (int i = 0; i < indicesPrivileges.length; i++) { + indicesPrivileges[i] = IndicesPrivileges.builder() + .privileges(randomFrom("read", "write", "create", "delete", "all")) + .indices(randomAlphaOfLengthBetween(2, 8) + (randomBoolean() ? "*" : "")) + .build(); + } + request.indexPrivileges(indicesPrivileges); + + final ApplicationResourcePrivileges[] appPrivileges = new ApplicationResourcePrivileges[randomInt(5)]; + for (int i = 0; i < appPrivileges.length; i++) { + appPrivileges[i] = ApplicationResourcePrivileges.builder() + .application(randomAlphaOfLengthBetween(3, 8)) + .resources(randomAlphaOfLengthBetween(5, 7) + (randomBoolean() ? "*" : "")) + .privileges(generateRandomStringArray(6, 7, false, false)) + .build(); + } + request.applicationPrivileges(appPrivileges); + return request; + } + +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/ApplicationPermissionTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/ApplicationPermissionTests.java new file mode 100644 index 0000000000000..47a189b41f12d --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/ApplicationPermissionTests.java @@ -0,0 +1,124 @@ +/* + * 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.authz.permission; + +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.hamcrest.Matchers.equalTo; + +public class ApplicationPermissionTests extends ESTestCase { + + private List store = new ArrayList<>(); + + private ApplicationPrivilege app1All = storePrivilege("app1", "all", "*"); + private ApplicationPrivilege app1Empty = storePrivilege("app1", "empty"); + private ApplicationPrivilege app1Read = storePrivilege("app1", "read", "read/*"); + private ApplicationPrivilege app1Write = storePrivilege("app1", "write", "write/*"); + private ApplicationPrivilege app1Delete = storePrivilege("app1", "delete", "write/delete"); + private ApplicationPrivilege app1Create = storePrivilege("app1", "create", "write/create"); + private ApplicationPrivilege app2Read = storePrivilege("app2", "read", "read/*"); + + private ApplicationPrivilege storePrivilege(String app, String name, String... patterns) { + store.add(new ApplicationPrivilegeDescriptor(app, name, Sets.newHashSet(patterns), Collections.emptyMap())); + return new ApplicationPrivilege(app, name, patterns); + } + + public void testCheckSimplePermission() { + final ApplicationPermission hasPermission = buildPermission(app1Write, "*"); + assertThat(hasPermission.grants(app1Write, "*"), equalTo(true)); + assertThat(hasPermission.grants(app1Write, "foo"), equalTo(true)); + assertThat(hasPermission.grants(app1Delete, "*"), equalTo(true)); + assertThat(hasPermission.grants(app1Create, "foo"), equalTo(true)); + + assertThat(hasPermission.grants(app1Read, "*"), equalTo(false)); + assertThat(hasPermission.grants(app1Read, "foo"), equalTo(false)); + assertThat(hasPermission.grants(app1All, "*"), equalTo(false)); + assertThat(hasPermission.grants(app1All, "foo"), equalTo(false)); + } + + public void testNonePermission() { + final ApplicationPermission hasPermission = buildPermission(ApplicationPrivilege.NONE.apply("app1"), "*"); + for (ApplicationPrivilege privilege : Arrays.asList(app1All, app1Empty, app1Create, app1Delete, app1Read, app1Write, app2Read)) { + assertThat("Privilege " + privilege + " on *", hasPermission.grants(privilege, "*"), equalTo(false)); + final String resource = randomAlphaOfLengthBetween(1, 6); + assertThat("Privilege " + privilege + " on " + resource, hasPermission.grants(privilege, resource), equalTo(false)); + } + } + + public void testResourceMatching() { + final ApplicationPermission hasPermission = buildPermission(app1All, "dashboard/*", "audit/*", "user/12345"); + + assertThat(hasPermission.grants(app1Write, "*"), equalTo(false)); + assertThat(hasPermission.grants(app1Write, "dashboard"), equalTo(false)); + assertThat(hasPermission.grants(app1Write, "dashboard/999"), equalTo(true)); + + assertThat(hasPermission.grants(app1Create, "audit/2018-02-21"), equalTo(true)); + assertThat(hasPermission.grants(app1Create, "report/2018-02-21"), equalTo(false)); + + assertThat(hasPermission.grants(app1Read, "user/12345"), equalTo(true)); + assertThat(hasPermission.grants(app1Read, "user/67890"), equalTo(false)); + + assertThat(hasPermission.grants(app1All, "dashboard/999"), equalTo(true)); + assertThat(hasPermission.grants(app1All, "audit/2018-02-21"), equalTo(true)); + assertThat(hasPermission.grants(app1All, "user/12345"), equalTo(true)); + } + + public void testActionMatching() { + final ApplicationPermission hasPermission = buildPermission(app1Write, "allow/*"); + + final ApplicationPrivilege update = actionPrivilege("app1", "write/update"); + assertThat(hasPermission.grants(update, "allow/1"), equalTo(true)); + assertThat(hasPermission.grants(update, "deny/1"), equalTo(false)); + + final ApplicationPrivilege updateCreate = actionPrivilege("app1", "write/update", "write/create"); + assertThat(hasPermission.grants(updateCreate, "allow/1"), equalTo(true)); + assertThat(hasPermission.grants(updateCreate, "deny/1"), equalTo(false)); + + final ApplicationPrivilege manage = actionPrivilege("app1", "admin/manage"); + assertThat(hasPermission.grants(manage, "allow/1"), equalTo(false)); + assertThat(hasPermission.grants(manage, "deny/1"), equalTo(false)); + } + + public void testDoesNotMatchAcrossApplications() { + assertThat(buildPermission(app1Read, "*").grants(app1Read, "123"), equalTo(true)); + assertThat(buildPermission(app1All, "*").grants(app1Read, "123"), equalTo(true)); + + assertThat(buildPermission(app1Read, "*").grants(app2Read, "123"), equalTo(false)); + assertThat(buildPermission(app1All, "*").grants(app2Read, "123"), equalTo(false)); + } + + public void testMergedPermissionChecking() { + final ApplicationPrivilege app1ReadWrite = ApplicationPrivilege.get("app1", Sets.union(app1Read.name(), app1Write.name()), store); + final ApplicationPermission hasPermission = buildPermission(app1ReadWrite, "allow/*"); + + assertThat(hasPermission.grants(app1Read, "allow/1"), equalTo(true)); + assertThat(hasPermission.grants(app1Write, "allow/1"), equalTo(true)); + + assertThat(hasPermission.grants(app1Read, "deny/1"), equalTo(false)); + assertThat(hasPermission.grants(app1Write, "deny/1"), equalTo(false)); + + assertThat(hasPermission.grants(app1All, "allow/1"), equalTo(false)); + assertThat(hasPermission.grants(app2Read, "allow/1"), equalTo(false)); + } + + private ApplicationPrivilege actionPrivilege(String appName, String... actions) { + return ApplicationPrivilege.get(appName, Sets.newHashSet(actions), Collections.emptyList()); + } + + private ApplicationPermission buildPermission(ApplicationPrivilege privilege, String... resources) { + return new ApplicationPermission(singletonList(new Tuple<>(privilege, Sets.newHashSet(resources)))); + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/ApplicationPrivilegeDescriptorTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/ApplicationPrivilegeDescriptorTests.java new file mode 100644 index 0000000000000..9db998bd2d1d2 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/ApplicationPrivilegeDescriptorTests.java @@ -0,0 +1,143 @@ +/* + * 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.authz.privilege; + +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.EqualsHashCodeTestUtils; +import org.hamcrest.Matchers; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import static org.elasticsearch.common.xcontent.DeprecationHandler.THROW_UNSUPPORTED_OPERATION; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.iterableWithSize; + +public class ApplicationPrivilegeDescriptorTests extends ESTestCase { + + public void testEqualsAndHashCode() { + final ApplicationPrivilegeDescriptor privilege = randomPrivilege(); + final EqualsHashCodeTestUtils.MutateFunction mutate = randomFrom( + orig -> new ApplicationPrivilegeDescriptor( + "x" + orig.getApplication(), orig.getName(), orig.getActions(), orig.getMetadata()), + orig -> new ApplicationPrivilegeDescriptor( + orig.getApplication(), "x" + orig.getName(), orig.getActions(), orig.getMetadata()), + orig -> new ApplicationPrivilegeDescriptor( + orig.getApplication(), orig.getName(), Collections.singleton("*"), orig.getMetadata()), + orig -> new ApplicationPrivilegeDescriptor( + orig.getApplication(), orig.getName(), orig.getActions(), Collections.singletonMap("mutate", -1L)) + ); + EqualsHashCodeTestUtils.checkEqualsAndHashCode(privilege, + original -> new ApplicationPrivilegeDescriptor( + original.getApplication(), original.getName(), original.getActions(), original.getMetadata()), + mutate + ); + } + + public void testSerialization() throws IOException { + try (BytesStreamOutput out = new BytesStreamOutput()) { + final ApplicationPrivilegeDescriptor original = randomPrivilege(); + original.writeTo(out); + final ApplicationPrivilegeDescriptor clone = new ApplicationPrivilegeDescriptor(out.bytes().streamInput()); + assertThat(clone, Matchers.equalTo(original)); + assertThat(original, Matchers.equalTo(clone)); + } + } + + public void testXContentGenerationAndParsing() throws IOException { + final boolean includeTypeField = randomBoolean(); + + final XContent xContent = randomFrom(XContentType.values()).xContent(); + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + final XContentBuilder builder = new XContentBuilder(xContent, out); + + final ApplicationPrivilegeDescriptor original = randomPrivilege(); + if (includeTypeField) { + original.toXContent(builder, true); + } else if (randomBoolean()) { + original.toXContent(builder, false); + } else { + original.toXContent(builder, ToXContent.EMPTY_PARAMS); + } + builder.flush(); + + final byte[] bytes = out.toByteArray(); + try (XContentParser parser = xContent.createParser(NamedXContentRegistry.EMPTY, THROW_UNSUPPORTED_OPERATION, bytes)) { + final ApplicationPrivilegeDescriptor clone = ApplicationPrivilegeDescriptor.parse(parser, + randomBoolean() ? randomAlphaOfLength(3) : null, + randomBoolean() ? randomAlphaOfLength(3) : null, + includeTypeField); + assertThat(clone, Matchers.equalTo(original)); + assertThat(original, Matchers.equalTo(clone)); + } + } + } + + public void testParseXContentWithDefaultNames() throws IOException { + final String json = "{ \"actions\": [ \"data:read\" ], \"metadata\" : { \"num\": 1, \"bool\":false } }"; + final XContent xContent = XContentType.JSON.xContent(); + try (XContentParser parser = xContent.createParser(NamedXContentRegistry.EMPTY, THROW_UNSUPPORTED_OPERATION, json)) { + final ApplicationPrivilegeDescriptor privilege = ApplicationPrivilegeDescriptor.parse(parser, "my_app", "read", false); + assertThat(privilege.getApplication(), equalTo("my_app")); + assertThat(privilege.getName(), equalTo("read")); + assertThat(privilege.getActions(), contains("data:read")); + assertThat(privilege.getMetadata().entrySet(), iterableWithSize(2)); + assertThat(privilege.getMetadata().get("num"), equalTo(1)); + assertThat(privilege.getMetadata().get("bool"), equalTo(false)); + } + } + + public void testParseXContentWithoutUsingDefaultNames() throws IOException { + final String json = "{" + + " \"application\": \"your_app\"," + + " \"name\": \"write\"," + + " \"actions\": [ \"data:write\" ]" + + "}"; + final XContent xContent = XContentType.JSON.xContent(); + try (XContentParser parser = xContent.createParser(NamedXContentRegistry.EMPTY, THROW_UNSUPPORTED_OPERATION, json)) { + final ApplicationPrivilegeDescriptor privilege = ApplicationPrivilegeDescriptor.parse(parser, "my_app", "read", false); + assertThat(privilege.getApplication(), equalTo("your_app")); + assertThat(privilege.getName(), equalTo("write")); + assertThat(privilege.getActions(), contains("data:write")); + assertThat(privilege.getMetadata().entrySet(), iterableWithSize(0)); + } + } + + private ApplicationPrivilegeDescriptor randomPrivilege() { + final String applicationName; + if (randomBoolean()) { + applicationName = "*"; + } else { + applicationName = randomAlphaOfLength(1).toLowerCase(Locale.ROOT) + randomAlphaOfLengthBetween(2, 10); + } + final String privilegeName = randomAlphaOfLength(1).toLowerCase(Locale.ROOT) + randomAlphaOfLengthBetween(2, 8); + final String[] patterns = new String[randomIntBetween(0, 5)]; + for (int i = 0; i < patterns.length; i++) { + final String suffix = randomBoolean() ? "*" : randomAlphaOfLengthBetween(4, 9); + patterns[i] = randomAlphaOfLengthBetween(2, 5) + "/" + suffix; + } + + final Map metadata = new HashMap<>(); + for (int i = randomInt(3); i > 0; i--) { + metadata.put(randomAlphaOfLengthBetween(2, 5), randomFrom(randomBoolean(), randomInt(10), randomAlphaOfLength(5))); + } + return new ApplicationPrivilegeDescriptor(applicationName, privilegeName, Sets.newHashSet(patterns), metadata); + } + +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/ApplicationPrivilegeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/ApplicationPrivilegeTests.java new file mode 100644 index 0000000000000..c65f06f05f957 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/ApplicationPrivilegeTests.java @@ -0,0 +1,202 @@ +/* + * 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.authz.privilege; + +import junit.framework.AssertionFailedError; +import org.apache.lucene.util.automaton.CharacterRunAutomaton; +import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.EqualsHashCodeTestUtils; +import org.junit.Assert; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; + +import static org.elasticsearch.common.Strings.collectionToCommaDelimitedString; +import static org.hamcrest.Matchers.arrayContainingInAnyOrder; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; + +public class ApplicationPrivilegeTests extends ESTestCase { + + public void testValidationOfApplicationName() { + final String specialCharacters = ":;$#%()+='.{}[]!@^&'"; + final Supplier specialCharacter = () -> specialCharacters.charAt(randomInt(specialCharacters.length() - 1)); + + assertValidationFailure("a p p", "application name", () -> ApplicationPrivilege.validateApplicationName("a p p")); + assertValidationFailure("ap", "application name", () -> ApplicationPrivilege.validateApplicationName("ap")); + for (String app : Arrays.asList( + "App",// must start with lowercase + "1app", // must start with letter + "app" + specialCharacter.get() // cannot contain special characters unless preceded by a "-" or "_" + )) { + assertValidationFailure(app, "application name", () -> ApplicationPrivilege.validateApplicationName(app)); + assertValidationFailure(app, "application name", () -> ApplicationPrivilege.validateApplicationNameOrWildcard(app)); + } + + // no wildcards + assertValidationFailure("app*", "application names", () -> ApplicationPrivilege.validateApplicationName("app*")); + // no special characters with wildcards + final String appNameWithSpecialCharAndWildcard = "app" + specialCharacter.get() + "*"; + assertValidationFailure(appNameWithSpecialCharAndWildcard, "application name", + () -> ApplicationPrivilege.validateApplicationNameOrWildcard(appNameWithSpecialCharAndWildcard)); + + String appNameWithSpecialChars = "myapp" + randomFrom('-', '_'); + for (int i = randomIntBetween(1, 12); i > 0; i--) { + appNameWithSpecialChars = appNameWithSpecialChars + specialCharacter.get(); + } + // these should all be OK + for (String app : Arrays.asList("app", "app1", "myApp", "myApp-:;$#%()+='.", "myApp_:;$#%()+='.", appNameWithSpecialChars)) { + assertNoException(app, () -> ApplicationPrivilege.validateApplicationName(app)); + assertNoException(app, () -> ApplicationPrivilege.validateApplicationNameOrWildcard(app)); + } + } + + public void testValidationOfPrivilegeName() { + // must start with lowercase + assertValidationFailure("Read", "privilege names", () -> ApplicationPrivilege.validatePrivilegeName("Read")); + // must start with letter + assertValidationFailure("1read", "privilege names", () -> ApplicationPrivilege.validatePrivilegeName("1read")); + // cannot contain special characters + final String specialChars = ":;$#%()+=/',"; + final String withSpecialChar = "read" + specialChars.charAt(randomInt(specialChars.length()-1)); + assertValidationFailure(withSpecialChar, "privilege names", () -> ApplicationPrivilege.validatePrivilegeName(withSpecialChar)); + + // these should all be OK + for (String priv : Arrays.asList("read", "read1", "readData", "read-data", "read.data", "read_data")) { + assertNoException(priv, () -> ApplicationPrivilege.validatePrivilegeName(priv)); + assertNoException(priv, () -> ApplicationPrivilege.validatePrivilegeOrActionName(priv)); + } + + for (String priv : Arrays.asList("r e a d", "read\n", "copy®")) { + assertValidationFailure(priv, "privilege names and action", () -> ApplicationPrivilege.validatePrivilegeOrActionName(priv)); + } + + for (String priv : Arrays.asList("read:*", "read/*", "read/a_b.c-d+e%f#(g)")) { + assertNoException(priv, () -> ApplicationPrivilege.validatePrivilegeOrActionName(priv)); + } + } + + public void testNonePrivilege() { + final ApplicationPrivilege none = ApplicationPrivilege.NONE.apply("super-mega-app"); + CharacterRunAutomaton run = new CharacterRunAutomaton(none.getAutomaton()); + for (int i = randomIntBetween(5, 10); i > 0; i--) { + final String action; + if (randomBoolean()) { + action = randomAlphaOfLengthBetween(3, 12); + } else { + action = randomAlphaOfLengthBetween(3, 6) + randomFrom(":", "/") + randomAlphaOfLengthBetween(3, 8); + } + assertFalse("NONE should not grant " + action, run.run(action)); + } + } + + public void testGetPrivilegeByName() { + final ApplicationPrivilegeDescriptor descriptor = descriptor("my-app", "read", "data:read/*", "action:login"); + final ApplicationPrivilegeDescriptor myWrite = descriptor("my-app", "write", "data:write/*", "action:login"); + final ApplicationPrivilegeDescriptor myAdmin = descriptor("my-app", "admin", "data:read/*", "action:*"); + final ApplicationPrivilegeDescriptor yourRead = descriptor("your-app", "read", "data:read/*", "action:login"); + final Set stored = Sets.newHashSet(descriptor, myWrite, myAdmin, yourRead); + + assertEqual(ApplicationPrivilege.get("my-app", Collections.singleton("read"), stored), descriptor); + assertEqual(ApplicationPrivilege.get("my-app", Collections.singleton("write"), stored), myWrite); + + final ApplicationPrivilege readWrite = ApplicationPrivilege.get("my-app", Sets.newHashSet("read", "write"), stored); + assertThat(readWrite.getApplication(), equalTo("my-app")); + assertThat(readWrite.name(), containsInAnyOrder("read", "write")); + assertThat(readWrite.getPatterns(), arrayContainingInAnyOrder("data:read/*", "data:write/*", "action:login")); + + CharacterRunAutomaton run = new CharacterRunAutomaton(readWrite.getAutomaton()); + for (String action : Arrays.asList("data:read/settings", "data:write/user/kimchy", "action:login")) { + assertTrue(run.run(action)); + } + for (String action : Arrays.asList("data:delete/user/kimchy", "action:shutdown")) { + assertFalse(run.run(action)); + } + } + + private void assertEqual(ApplicationPrivilege myReadPriv, ApplicationPrivilegeDescriptor myRead) { + assertThat(myReadPriv.getApplication(), equalTo(myRead.getApplication())); + assertThat(getPrivilegeName(myReadPriv), equalTo(myRead.getName())); + assertThat(Sets.newHashSet(myReadPriv.getPatterns()), equalTo(myRead.getActions())); + } + + private ApplicationPrivilegeDescriptor descriptor(String application, String name, String... actions) { + return new ApplicationPrivilegeDescriptor(application, name, Sets.newHashSet(actions), Collections.emptyMap()); + } + + public void testEqualsAndHashCode() { + final ApplicationPrivilege privilege = randomPrivilege(); + final EqualsHashCodeTestUtils.MutateFunction mutate = randomFrom( + orig -> createPrivilege("x" + orig.getApplication(), getPrivilegeName(orig), orig.getPatterns()), + orig -> createPrivilege(orig.getApplication(), "x" + getPrivilegeName(orig), orig.getPatterns()), + orig -> new ApplicationPrivilege(orig.getApplication(), getPrivilegeName(orig), "*") + ); + EqualsHashCodeTestUtils.checkEqualsAndHashCode(privilege, + original -> createPrivilege(original.getApplication(), getPrivilegeName(original), original.getPatterns()), + mutate + ); + } + + private ApplicationPrivilege createPrivilege(String applicationName, String privilegeName, String... patterns) { + return new ApplicationPrivilege(applicationName, privilegeName, patterns); + } + + private String getPrivilegeName(ApplicationPrivilege privilege) { + if (privilege.name.size() == 1) { + return privilege.name.iterator().next(); + } else { + throw new IllegalStateException(privilege + " has a multivariate name: " + collectionToCommaDelimitedString(privilege.name)); + } + } + + private void assertValidationFailure(String reason,String messageContent, ThrowingRunnable body) { + final IllegalArgumentException exception; + try { + exception = expectThrows(IllegalArgumentException.class, body); + assertThat(exception.getMessage().toLowerCase(Locale.ROOT), containsString(messageContent.toLowerCase(Locale.ROOT))); + } catch (AssertionFailedError e) { + fail(reason + " - " + e.getMessage()); + } + } + + private void assertNoException(String reason, ThrowingRunnable body) { + try { + body.run(); + // pass + } catch (Throwable e) { + Assert.fail(reason + " - Expected no exception, but got: " + e); + } + } + + private ApplicationPrivilege randomPrivilege() { + final String applicationName; + if (randomBoolean()) { + applicationName = "*"; + } else { + applicationName = randomAlphaOfLength(1).toLowerCase(Locale.ROOT) + randomAlphaOfLengthBetween(2, 10); + } + final String privilegeName = randomAlphaOfLength(1).toLowerCase(Locale.ROOT) + randomAlphaOfLengthBetween(2, 8); + final String[] patterns = new String[randomIntBetween(0, 5)]; + for (int i = 0; i < patterns.length; i++) { + final String suffix = randomBoolean() ? "*" : randomAlphaOfLengthBetween(4, 9); + patterns[i] = randomAlphaOfLengthBetween(2, 5) + "/" + suffix; + } + + final Map metadata = new HashMap<>(); + for (int i = randomInt(3); i > 0; i--) { + metadata.put(randomAlphaOfLengthBetween(2, 5), randomFrom(randomBoolean(), randomInt(10), randomAlphaOfLength(5))); + } + return createPrivilege(applicationName, privilegeName, patterns); + } + +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/ConditionalClusterPrivilegesTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/ConditionalClusterPrivilegesTests.java new file mode 100644 index 0000000000000..ebcd70869cb02 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/ConditionalClusterPrivilegesTests.java @@ -0,0 +1,73 @@ +/* + * 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.authz.privilege; + +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.NamedWriteableAwareStreamInput; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.XPackClientPlugin; + +import java.io.ByteArrayOutputStream; +import java.util.Arrays; +import java.util.List; + +import static org.elasticsearch.common.xcontent.DeprecationHandler.THROW_UNSUPPORTED_OPERATION; +import static org.hamcrest.Matchers.equalTo; + +public class ConditionalClusterPrivilegesTests extends ESTestCase { + + public void testSerialization() throws Exception { + final ConditionalClusterPrivilege[] original = buildSecurityPrivileges(); + try (BytesStreamOutput out = new BytesStreamOutput()) { + ConditionalClusterPrivileges.writeArray(out, original); + final NamedWriteableRegistry registry = new NamedWriteableRegistry(new XPackClientPlugin(Settings.EMPTY).getNamedWriteables()); + try (StreamInput in = new NamedWriteableAwareStreamInput(out.bytes().streamInput(), registry)) { + final ConditionalClusterPrivilege[] copy = ConditionalClusterPrivileges.readArray(in); + assertThat(copy, equalTo(original)); + assertThat(original, equalTo(copy)); + } + } + } + + public void testGenerateAndParseXContent() throws Exception { + final XContent xContent = randomFrom(XContentType.values()).xContent(); + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + final XContentBuilder builder = new XContentBuilder(xContent, out); + + final List original = Arrays.asList(buildSecurityPrivileges()); + ConditionalClusterPrivileges.toXContent(builder, ToXContent.EMPTY_PARAMS, original); + builder.flush(); + + final byte[] bytes = out.toByteArray(); + try (XContentParser parser = xContent.createParser(NamedXContentRegistry.EMPTY, THROW_UNSUPPORTED_OPERATION, bytes)) { + assertThat(parser.nextToken(), equalTo(XContentParser.Token.START_OBJECT)); + final List clone = ConditionalClusterPrivileges.parse(parser); + assertThat(clone, equalTo(original)); + assertThat(original, equalTo(clone)); + } + } + } + + private ConditionalClusterPrivilege[] buildSecurityPrivileges() { + return buildSecurityPrivileges(randomIntBetween(4, 7)); + } + + private ConditionalClusterPrivilege[] buildSecurityPrivileges(int applicationNameLength) { + return new ConditionalClusterPrivilege[] { + ManageApplicationPrivilegesTests.buildPrivileges(applicationNameLength) + }; + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/ManageApplicationPrivilegesTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/ManageApplicationPrivilegesTests.java new file mode 100644 index 0000000000000..a5c1bbc98d1ba --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/ManageApplicationPrivilegesTests.java @@ -0,0 +1,156 @@ +/* + * 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.authz.privilege; + +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.NamedWriteableAwareStreamInput; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.EqualsHashCodeTestUtils; +import org.elasticsearch.transport.TransportRequest; +import org.elasticsearch.xpack.core.XPackClientPlugin; +import org.elasticsearch.xpack.core.security.action.privilege.DeletePrivilegesAction; +import org.elasticsearch.xpack.core.security.action.privilege.DeletePrivilegesRequest; +import org.elasticsearch.xpack.core.security.action.privilege.GetPrivilegesAction; +import org.elasticsearch.xpack.core.security.action.privilege.GetPrivilegesRequest; +import org.elasticsearch.xpack.core.security.action.privilege.PutPrivilegesAction; +import org.elasticsearch.xpack.core.security.action.privilege.PutPrivilegesRequest; +import org.elasticsearch.xpack.core.security.action.role.PutRoleAction; +import org.elasticsearch.xpack.core.security.action.rolemapping.DeleteRoleMappingAction; +import org.elasticsearch.xpack.core.security.action.user.GetUsersAction; +import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesAction; +import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivileges.ManageApplicationPrivileges; + +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.function.Predicate; + +import static org.elasticsearch.common.xcontent.DeprecationHandler.THROW_UNSUPPORTED_OPERATION; +import static org.elasticsearch.test.TestMatchers.predicateMatches; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.notNullValue; + +public class ManageApplicationPrivilegesTests extends ESTestCase { + + public void testSerialization() throws Exception { + final ManageApplicationPrivileges original = buildPrivileges(); + try (BytesStreamOutput out = new BytesStreamOutput()) { + original.writeTo(out); + final NamedWriteableRegistry registry = new NamedWriteableRegistry(new XPackClientPlugin(Settings.EMPTY).getNamedWriteables()); + try (StreamInput in = new NamedWriteableAwareStreamInput(out.bytes().streamInput(), registry)) { + final ManageApplicationPrivileges copy = ManageApplicationPrivileges.createFrom(in); + assertThat(copy, equalTo(original)); + assertThat(original, equalTo(copy)); + } + } + } + + public void testGenerateAndParseXContent() throws Exception { + final XContent xContent = randomFrom(XContentType.values()).xContent(); + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + final XContentBuilder builder = new XContentBuilder(xContent, out); + + final ManageApplicationPrivileges original = buildPrivileges(); + builder.startObject(); + original.toXContent(builder, ToXContent.EMPTY_PARAMS); + builder.endObject(); + builder.flush(); + + final byte[] bytes = out.toByteArray(); + try (XContentParser parser = xContent.createParser(NamedXContentRegistry.EMPTY, THROW_UNSUPPORTED_OPERATION, bytes)) { + assertThat(parser.nextToken(), equalTo(XContentParser.Token.START_OBJECT)); + // ManageApplicationPrivileges.parse requires that the parser be positioned on the "manage" field. + assertThat(parser.nextToken(), equalTo(XContentParser.Token.FIELD_NAME)); + final ManageApplicationPrivileges clone = ManageApplicationPrivileges.parse(parser); + assertThat(parser.nextToken(), equalTo(XContentParser.Token.END_OBJECT)); + + assertThat(clone, equalTo(original)); + assertThat(original, equalTo(clone)); + } + } + } + + public void testEqualsAndHashCode() { + final int applicationNameLength = randomIntBetween(4, 7); + final ManageApplicationPrivileges privileges = buildPrivileges(applicationNameLength); + final EqualsHashCodeTestUtils.MutateFunction mutate + = orig -> buildPrivileges(applicationNameLength + randomIntBetween(1, 3)); + EqualsHashCodeTestUtils.checkEqualsAndHashCode(privileges, this::clone, mutate); + } + + public void testPrivilege() { + final ManageApplicationPrivileges privileges = buildPrivileges(); + assertThat(privileges.getPrivilege(), instanceOf(ClusterPrivilege.class)); + for (String actionName : Arrays.asList(GetPrivilegesAction.NAME, PutPrivilegesAction.NAME, DeletePrivilegesAction.NAME)) { + assertThat(privileges.getPrivilege().predicate(), predicateMatches(actionName)); + } + for (String actionName : Arrays.asList(GetUsersAction.NAME, PutRoleAction.NAME, DeleteRoleMappingAction.NAME, + HasPrivilegesAction.NAME)) { + assertThat(privileges.getPrivilege().predicate(), not(predicateMatches(actionName))); + } + } + + public void testRequestPredicate() { + final ManageApplicationPrivileges kibanaAndLogstash = new ManageApplicationPrivileges(Sets.newHashSet("kibana-*", "logstash")); + final ManageApplicationPrivileges cloudAndSwiftype = new ManageApplicationPrivileges(Sets.newHashSet("cloud-*", "swiftype")); + final Predicate kibanaAndLogstashPredicate = kibanaAndLogstash.getRequestPredicate(); + final Predicate cloudAndSwiftypePredicate = cloudAndSwiftype.getRequestPredicate(); + assertThat(kibanaAndLogstashPredicate, notNullValue()); + assertThat(cloudAndSwiftypePredicate, notNullValue()); + + final GetPrivilegesRequest getKibana1 = new GetPrivilegesRequest(); + getKibana1.application("kibana-1"); + assertThat(kibanaAndLogstashPredicate, predicateMatches(getKibana1)); + assertThat(cloudAndSwiftypePredicate, not(predicateMatches(getKibana1))); + + final DeletePrivilegesRequest deleteLogstash = new DeletePrivilegesRequest("logstash", new String[]{"all"}); + assertThat(kibanaAndLogstashPredicate, predicateMatches(deleteLogstash)); + assertThat(cloudAndSwiftypePredicate, not(predicateMatches(deleteLogstash))); + + final PutPrivilegesRequest putKibana = new PutPrivilegesRequest(); + + final List kibanaPrivileges = new ArrayList<>(); + for (int i = randomIntBetween(2, 6); i > 0; i--) { + kibanaPrivileges.add(new ApplicationPrivilegeDescriptor("kibana-" + i, + randomAlphaOfLengthBetween(3, 6).toLowerCase(Locale.ROOT), Collections.emptySet(), Collections.emptyMap())); + } + putKibana.setPrivileges(kibanaPrivileges); + assertThat(kibanaAndLogstashPredicate, predicateMatches(putKibana)); + assertThat(cloudAndSwiftypePredicate, not(predicateMatches(putKibana))); + } + + + private ManageApplicationPrivileges clone(ManageApplicationPrivileges original) { + return new ManageApplicationPrivileges(new LinkedHashSet<>(original.getApplicationNames())); + } + + private ManageApplicationPrivileges buildPrivileges() { + return buildPrivileges(randomIntBetween(4, 7)); + } + + static ManageApplicationPrivileges buildPrivileges(int applicationNameLength) { + Set applicationNames = Sets.newHashSet(Arrays.asList(generateRandomStringArray(5, applicationNameLength, false, false))); + return new ManageApplicationPrivileges(applicationNames); + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java index b25f3f374b389..d78a87c19d71b 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java @@ -35,6 +35,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.xpack.core.ml.MlMetaIndex; import org.elasticsearch.xpack.core.ml.action.CloseJobAction; import org.elasticsearch.xpack.core.ml.action.DeleteDatafeedAction; @@ -75,6 +76,12 @@ import org.elasticsearch.xpack.core.ml.job.persistence.AnomalyDetectorsIndexFields; import org.elasticsearch.xpack.core.ml.notifications.AuditorField; import org.elasticsearch.xpack.core.monitoring.action.MonitoringBulkAction; +import org.elasticsearch.xpack.core.security.action.privilege.DeletePrivilegesAction; +import org.elasticsearch.xpack.core.security.action.privilege.DeletePrivilegesRequest; +import org.elasticsearch.xpack.core.security.action.privilege.GetPrivilegesAction; +import org.elasticsearch.xpack.core.security.action.privilege.GetPrivilegesRequest; +import org.elasticsearch.xpack.core.security.action.privilege.PutPrivilegesAction; +import org.elasticsearch.xpack.core.security.action.privilege.PutPrivilegesRequest; import org.elasticsearch.xpack.core.security.action.role.PutRoleAction; import org.elasticsearch.xpack.core.security.action.saml.SamlAuthenticateAction; import org.elasticsearch.xpack.core.security.action.saml.SamlPrepareAuthenticationAction; @@ -85,6 +92,8 @@ import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl.IndexAccessControl; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsCache; import org.elasticsearch.xpack.core.security.authz.permission.Role; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; import org.elasticsearch.xpack.core.security.user.BeatsSystemUser; import org.elasticsearch.xpack.core.security.user.LogstashSystemUser; import org.elasticsearch.xpack.core.security.user.SystemUser; @@ -104,10 +113,12 @@ import org.joda.time.DateTimeZone; import java.util.Arrays; +import java.util.Collections; import java.util.Map; import static org.hamcrest.Matchers.hasEntry; import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.mock; /** * Unit tests for the {@link ReservedRolesStore} @@ -138,21 +149,23 @@ public void testIsReserved() { } public void testIngestAdminRole() { + final TransportRequest request = mock(TransportRequest.class); + RoleDescriptor roleDescriptor = new ReservedRolesStore().roleDescriptor("ingest_admin"); assertNotNull(roleDescriptor); assertThat(roleDescriptor.getMetadata(), hasEntry("_reserved", true)); Role ingestAdminRole = Role.builder(roleDescriptor, null).build(); - assertThat(ingestAdminRole.cluster().check(PutIndexTemplateAction.NAME), is(true)); - assertThat(ingestAdminRole.cluster().check(GetIndexTemplatesAction.NAME), is(true)); - assertThat(ingestAdminRole.cluster().check(DeleteIndexTemplateAction.NAME), is(true)); - assertThat(ingestAdminRole.cluster().check(PutPipelineAction.NAME), is(true)); - assertThat(ingestAdminRole.cluster().check(GetPipelineAction.NAME), is(true)); - assertThat(ingestAdminRole.cluster().check(DeletePipelineAction.NAME), is(true)); + assertThat(ingestAdminRole.cluster().check(PutIndexTemplateAction.NAME, request), is(true)); + assertThat(ingestAdminRole.cluster().check(GetIndexTemplatesAction.NAME, request), is(true)); + assertThat(ingestAdminRole.cluster().check(DeleteIndexTemplateAction.NAME, request), is(true)); + assertThat(ingestAdminRole.cluster().check(PutPipelineAction.NAME, request), is(true)); + assertThat(ingestAdminRole.cluster().check(GetPipelineAction.NAME, request), is(true)); + assertThat(ingestAdminRole.cluster().check(DeletePipelineAction.NAME, request), is(true)); - assertThat(ingestAdminRole.cluster().check(ClusterRerouteAction.NAME), is(false)); - assertThat(ingestAdminRole.cluster().check(ClusterUpdateSettingsAction.NAME), is(false)); - assertThat(ingestAdminRole.cluster().check(MonitoringBulkAction.NAME), is(false)); + assertThat(ingestAdminRole.cluster().check(ClusterRerouteAction.NAME, request), is(false)); + assertThat(ingestAdminRole.cluster().check(ClusterUpdateSettingsAction.NAME, request), is(false)); + assertThat(ingestAdminRole.cluster().check(MonitoringBulkAction.NAME, request), is(false)); assertThat(ingestAdminRole.indices().allowedIndicesMatcher(IndexAction.NAME).test("foo"), is(false)); assertThat(ingestAdminRole.indices().allowedIndicesMatcher("indices:foo").test(randomAlphaOfLengthBetween(8, 24)), @@ -162,25 +175,49 @@ public void testIngestAdminRole() { } public void testKibanaSystemRole() { + final TransportRequest request = mock(TransportRequest.class); + RoleDescriptor roleDescriptor = new ReservedRolesStore().roleDescriptor("kibana_system"); assertNotNull(roleDescriptor); assertThat(roleDescriptor.getMetadata(), hasEntry("_reserved", true)); Role kibanaRole = Role.builder(roleDescriptor, null).build(); - assertThat(kibanaRole.cluster().check(ClusterHealthAction.NAME), is(true)); - assertThat(kibanaRole.cluster().check(ClusterStateAction.NAME), is(true)); - assertThat(kibanaRole.cluster().check(ClusterStatsAction.NAME), is(true)); - assertThat(kibanaRole.cluster().check(PutIndexTemplateAction.NAME), is(true)); - assertThat(kibanaRole.cluster().check(GetIndexTemplatesAction.NAME), is(true)); - assertThat(kibanaRole.cluster().check(ClusterRerouteAction.NAME), is(false)); - assertThat(kibanaRole.cluster().check(ClusterUpdateSettingsAction.NAME), is(false)); - assertThat(kibanaRole.cluster().check(MonitoringBulkAction.NAME), is(true)); + assertThat(kibanaRole.cluster().check(ClusterHealthAction.NAME, request), is(true)); + assertThat(kibanaRole.cluster().check(ClusterStateAction.NAME, request), is(true)); + assertThat(kibanaRole.cluster().check(ClusterStatsAction.NAME, request), is(true)); + assertThat(kibanaRole.cluster().check(PutIndexTemplateAction.NAME, request), is(true)); + assertThat(kibanaRole.cluster().check(GetIndexTemplatesAction.NAME, request), is(true)); + assertThat(kibanaRole.cluster().check(ClusterRerouteAction.NAME, request), is(false)); + assertThat(kibanaRole.cluster().check(ClusterUpdateSettingsAction.NAME, request), is(false)); + assertThat(kibanaRole.cluster().check(MonitoringBulkAction.NAME, request), is(true)); // SAML - assertThat(kibanaRole.cluster().check(SamlPrepareAuthenticationAction.NAME), is(true)); - assertThat(kibanaRole.cluster().check(SamlAuthenticateAction.NAME), is(true)); - assertThat(kibanaRole.cluster().check(InvalidateTokenAction.NAME), is(true)); - assertThat(kibanaRole.cluster().check(CreateTokenAction.NAME), is(false)); + assertThat(kibanaRole.cluster().check(SamlPrepareAuthenticationAction.NAME, request), is(true)); + assertThat(kibanaRole.cluster().check(SamlAuthenticateAction.NAME, request), is(true)); + assertThat(kibanaRole.cluster().check(InvalidateTokenAction.NAME, request), is(true)); + assertThat(kibanaRole.cluster().check(CreateTokenAction.NAME, request), is(false)); + + // Application Privileges + DeletePrivilegesRequest deleteKibanaPrivileges = new DeletePrivilegesRequest("kibana-.kibana", new String[]{ "all", "read" }); + DeletePrivilegesRequest deleteLogstashPrivileges = new DeletePrivilegesRequest("logstash", new String[]{ "all", "read" }); + assertThat(kibanaRole.cluster().check(DeletePrivilegesAction.NAME, deleteKibanaPrivileges), is(true)); + assertThat(kibanaRole.cluster().check(DeletePrivilegesAction.NAME, deleteLogstashPrivileges), is(false)); + + GetPrivilegesRequest getKibanaPrivileges = new GetPrivilegesRequest(); + getKibanaPrivileges.application("kibana-.kibana-sales"); + GetPrivilegesRequest getApmPrivileges = new GetPrivilegesRequest(); + getApmPrivileges.application("apm"); + assertThat(kibanaRole.cluster().check(GetPrivilegesAction.NAME, getKibanaPrivileges), is(true)); + assertThat(kibanaRole.cluster().check(GetPrivilegesAction.NAME, getApmPrivileges), is(false)); + + PutPrivilegesRequest putKibanaPrivileges = new PutPrivilegesRequest(); + putKibanaPrivileges.setPrivileges(Collections.singletonList(new ApplicationPrivilegeDescriptor( + "kibana-.kibana-" + randomAlphaOfLengthBetween(2,6), "all", Collections.emptySet(), Collections.emptyMap()))); + PutPrivilegesRequest putSwiftypePrivileges = new PutPrivilegesRequest(); + putSwiftypePrivileges.setPrivileges(Collections.singletonList(new ApplicationPrivilegeDescriptor( + "swiftype-kibana" , "all", Collections.emptySet(), Collections.emptyMap()))); + assertThat(kibanaRole.cluster().check(PutPrivilegesAction.NAME, putKibanaPrivileges), is(true)); + assertThat(kibanaRole.cluster().check(PutPrivilegesAction.NAME, putSwiftypePrivileges), is(false)); // Everything else assertThat(kibanaRole.runAs().check(randomAlphaOfLengthBetween(1, 12)), is(false)); @@ -223,18 +260,20 @@ public void testKibanaSystemRole() { } public void testKibanaUserRole() { + final TransportRequest request = mock(TransportRequest.class); + RoleDescriptor roleDescriptor = new ReservedRolesStore().roleDescriptor("kibana_user"); assertNotNull(roleDescriptor); assertThat(roleDescriptor.getMetadata(), hasEntry("_reserved", true)); Role kibanaUserRole = Role.builder(roleDescriptor, null).build(); - assertThat(kibanaUserRole.cluster().check(ClusterHealthAction.NAME), is(false)); - assertThat(kibanaUserRole.cluster().check(ClusterStateAction.NAME), is(false)); - assertThat(kibanaUserRole.cluster().check(ClusterStatsAction.NAME), is(false)); - assertThat(kibanaUserRole.cluster().check(PutIndexTemplateAction.NAME), is(false)); - assertThat(kibanaUserRole.cluster().check(ClusterRerouteAction.NAME), is(false)); - assertThat(kibanaUserRole.cluster().check(ClusterUpdateSettingsAction.NAME), is(false)); - assertThat(kibanaUserRole.cluster().check(MonitoringBulkAction.NAME), is(false)); + assertThat(kibanaUserRole.cluster().check(ClusterHealthAction.NAME, request), is(false)); + assertThat(kibanaUserRole.cluster().check(ClusterStateAction.NAME, request), is(false)); + assertThat(kibanaUserRole.cluster().check(ClusterStatsAction.NAME, request), is(false)); + assertThat(kibanaUserRole.cluster().check(PutIndexTemplateAction.NAME, request), is(false)); + assertThat(kibanaUserRole.cluster().check(ClusterRerouteAction.NAME, request), is(false)); + assertThat(kibanaUserRole.cluster().check(ClusterUpdateSettingsAction.NAME, request), is(false)); + assertThat(kibanaUserRole.cluster().check(MonitoringBulkAction.NAME, request), is(false)); assertThat(kibanaUserRole.runAs().check(randomAlphaOfLengthBetween(1, 12)), is(false)); @@ -256,22 +295,35 @@ public void testKibanaUserRole() { assertThat(kibanaUserRole.indices().allowedIndicesMatcher(MultiSearchAction.NAME).test(index), is(true)); assertThat(kibanaUserRole.indices().allowedIndicesMatcher(UpdateSettingsAction.NAME).test(index), is(true)); }); + + final String randomApplication = "kibana-" + randomAlphaOfLengthBetween(8, 24); + assertThat(kibanaUserRole.application().grants(new ApplicationPrivilege(randomApplication, "app-random", "all"), "*"), is(false)); + + final String application = "kibana-.kibana"; + assertThat(kibanaUserRole.application().grants(new ApplicationPrivilege(application, "app-foo", "foo"), "*"), is(false)); + assertThat(kibanaUserRole.application().grants(new ApplicationPrivilege(application, "app-all", "all"), "*"), is(true)); + + final String applicationWithRandomIndex = "kibana-.kibana_" + randomAlphaOfLengthBetween(8, 24); + assertThat(kibanaUserRole.application().grants(new ApplicationPrivilege(applicationWithRandomIndex, "app-random-index", "all"), + "*"), is(false)); } public void testMonitoringUserRole() { + final TransportRequest request = mock(TransportRequest.class); + RoleDescriptor roleDescriptor = new ReservedRolesStore().roleDescriptor("monitoring_user"); assertNotNull(roleDescriptor); assertThat(roleDescriptor.getMetadata(), hasEntry("_reserved", true)); Role monitoringUserRole = Role.builder(roleDescriptor, null).build(); - assertThat(monitoringUserRole.cluster().check(MainAction.NAME), is(true)); - assertThat(monitoringUserRole.cluster().check(ClusterHealthAction.NAME), is(false)); - assertThat(monitoringUserRole.cluster().check(ClusterStateAction.NAME), is(false)); - assertThat(monitoringUserRole.cluster().check(ClusterStatsAction.NAME), is(false)); - assertThat(monitoringUserRole.cluster().check(PutIndexTemplateAction.NAME), is(false)); - assertThat(monitoringUserRole.cluster().check(ClusterRerouteAction.NAME), is(false)); - assertThat(monitoringUserRole.cluster().check(ClusterUpdateSettingsAction.NAME), is(false)); - assertThat(monitoringUserRole.cluster().check(MonitoringBulkAction.NAME), is(false)); + assertThat(monitoringUserRole.cluster().check(MainAction.NAME, request), is(true)); + assertThat(monitoringUserRole.cluster().check(ClusterHealthAction.NAME, request), is(false)); + assertThat(monitoringUserRole.cluster().check(ClusterStateAction.NAME, request), is(false)); + assertThat(monitoringUserRole.cluster().check(ClusterStatsAction.NAME, request), is(false)); + assertThat(monitoringUserRole.cluster().check(PutIndexTemplateAction.NAME, request), is(false)); + assertThat(monitoringUserRole.cluster().check(ClusterRerouteAction.NAME, request), is(false)); + assertThat(monitoringUserRole.cluster().check(ClusterUpdateSettingsAction.NAME, request), is(false)); + assertThat(monitoringUserRole.cluster().check(MonitoringBulkAction.NAME, request), is(false)); assertThat(monitoringUserRole.runAs().check(randomAlphaOfLengthBetween(1, 12)), is(false)); @@ -298,27 +350,29 @@ public void testMonitoringUserRole() { } public void testRemoteMonitoringAgentRole() { + final TransportRequest request = mock(TransportRequest.class); + RoleDescriptor roleDescriptor = new ReservedRolesStore().roleDescriptor("remote_monitoring_agent"); assertNotNull(roleDescriptor); assertThat(roleDescriptor.getMetadata(), hasEntry("_reserved", true)); Role remoteMonitoringAgentRole = Role.builder(roleDescriptor, null).build(); - assertThat(remoteMonitoringAgentRole.cluster().check(ClusterHealthAction.NAME), is(true)); - assertThat(remoteMonitoringAgentRole.cluster().check(ClusterStateAction.NAME), is(true)); - assertThat(remoteMonitoringAgentRole.cluster().check(ClusterStatsAction.NAME), is(true)); - assertThat(remoteMonitoringAgentRole.cluster().check(PutIndexTemplateAction.NAME), is(true)); - assertThat(remoteMonitoringAgentRole.cluster().check(ClusterRerouteAction.NAME), is(false)); - assertThat(remoteMonitoringAgentRole.cluster().check(ClusterUpdateSettingsAction.NAME), is(false)); - assertThat(remoteMonitoringAgentRole.cluster().check(MonitoringBulkAction.NAME), is(false)); - assertThat(remoteMonitoringAgentRole.cluster().check(GetWatchAction.NAME), is(true)); - assertThat(remoteMonitoringAgentRole.cluster().check(PutWatchAction.NAME), is(true)); - assertThat(remoteMonitoringAgentRole.cluster().check(DeleteWatchAction.NAME), is(true)); - assertThat(remoteMonitoringAgentRole.cluster().check(ExecuteWatchAction.NAME), is(false)); - assertThat(remoteMonitoringAgentRole.cluster().check(AckWatchAction.NAME), is(false)); - assertThat(remoteMonitoringAgentRole.cluster().check(ActivateWatchAction.NAME), is(false)); - assertThat(remoteMonitoringAgentRole.cluster().check(WatcherServiceAction.NAME), is(false)); + assertThat(remoteMonitoringAgentRole.cluster().check(ClusterHealthAction.NAME, request), is(true)); + assertThat(remoteMonitoringAgentRole.cluster().check(ClusterStateAction.NAME, request), is(true)); + assertThat(remoteMonitoringAgentRole.cluster().check(ClusterStatsAction.NAME, request), is(true)); + assertThat(remoteMonitoringAgentRole.cluster().check(PutIndexTemplateAction.NAME, request), is(true)); + assertThat(remoteMonitoringAgentRole.cluster().check(ClusterRerouteAction.NAME, request), is(false)); + assertThat(remoteMonitoringAgentRole.cluster().check(ClusterUpdateSettingsAction.NAME, request), is(false)); + assertThat(remoteMonitoringAgentRole.cluster().check(MonitoringBulkAction.NAME, request), is(false)); + assertThat(remoteMonitoringAgentRole.cluster().check(GetWatchAction.NAME, request), is(true)); + assertThat(remoteMonitoringAgentRole.cluster().check(PutWatchAction.NAME, request), is(true)); + assertThat(remoteMonitoringAgentRole.cluster().check(DeleteWatchAction.NAME, request), is(true)); + assertThat(remoteMonitoringAgentRole.cluster().check(ExecuteWatchAction.NAME, request), is(false)); + assertThat(remoteMonitoringAgentRole.cluster().check(AckWatchAction.NAME, request), is(false)); + assertThat(remoteMonitoringAgentRole.cluster().check(ActivateWatchAction.NAME, request), is(false)); + assertThat(remoteMonitoringAgentRole.cluster().check(WatcherServiceAction.NAME, request), is(false)); // we get this from the cluster:monitor privilege - assertThat(remoteMonitoringAgentRole.cluster().check(WatcherStatsAction.NAME), is(true)); + assertThat(remoteMonitoringAgentRole.cluster().check(WatcherStatsAction.NAME, request), is(true)); assertThat(remoteMonitoringAgentRole.runAs().check(randomAlphaOfLengthBetween(1, 12)), is(false)); @@ -342,18 +396,20 @@ public void testRemoteMonitoringAgentRole() { } public void testReportingUserRole() { + final TransportRequest request = mock(TransportRequest.class); + RoleDescriptor roleDescriptor = new ReservedRolesStore().roleDescriptor("reporting_user"); assertNotNull(roleDescriptor); assertThat(roleDescriptor.getMetadata(), hasEntry("_reserved", true)); Role reportingUserRole = Role.builder(roleDescriptor, null).build(); - assertThat(reportingUserRole.cluster().check(ClusterHealthAction.NAME), is(false)); - assertThat(reportingUserRole.cluster().check(ClusterStateAction.NAME), is(false)); - assertThat(reportingUserRole.cluster().check(ClusterStatsAction.NAME), is(false)); - assertThat(reportingUserRole.cluster().check(PutIndexTemplateAction.NAME), is(false)); - assertThat(reportingUserRole.cluster().check(ClusterRerouteAction.NAME), is(false)); - assertThat(reportingUserRole.cluster().check(ClusterUpdateSettingsAction.NAME), is(false)); - assertThat(reportingUserRole.cluster().check(MonitoringBulkAction.NAME), is(false)); + assertThat(reportingUserRole.cluster().check(ClusterHealthAction.NAME, request), is(false)); + assertThat(reportingUserRole.cluster().check(ClusterStateAction.NAME, request), is(false)); + assertThat(reportingUserRole.cluster().check(ClusterStatsAction.NAME, request), is(false)); + assertThat(reportingUserRole.cluster().check(PutIndexTemplateAction.NAME, request), is(false)); + assertThat(reportingUserRole.cluster().check(ClusterRerouteAction.NAME, request), is(false)); + assertThat(reportingUserRole.cluster().check(ClusterUpdateSettingsAction.NAME, request), is(false)); + assertThat(reportingUserRole.cluster().check(MonitoringBulkAction.NAME, request), is(false)); assertThat(reportingUserRole.runAs().check(randomAlphaOfLengthBetween(1, 12)), is(false)); @@ -378,18 +434,20 @@ public void testReportingUserRole() { } public void testKibanaDashboardOnlyUserRole() { + final TransportRequest request = mock(TransportRequest.class); + RoleDescriptor roleDescriptor = new ReservedRolesStore().roleDescriptor("kibana_dashboard_only_user"); assertNotNull(roleDescriptor); assertThat(roleDescriptor.getMetadata(), hasEntry("_reserved", true)); Role dashboardsOnlyUserRole = Role.builder(roleDescriptor, null).build(); - assertThat(dashboardsOnlyUserRole.cluster().check(ClusterHealthAction.NAME), is(false)); - assertThat(dashboardsOnlyUserRole.cluster().check(ClusterStateAction.NAME), is(false)); - assertThat(dashboardsOnlyUserRole.cluster().check(ClusterStatsAction.NAME), is(false)); - assertThat(dashboardsOnlyUserRole.cluster().check(PutIndexTemplateAction.NAME), is(false)); - assertThat(dashboardsOnlyUserRole.cluster().check(ClusterRerouteAction.NAME), is(false)); - assertThat(dashboardsOnlyUserRole.cluster().check(ClusterUpdateSettingsAction.NAME), is(false)); - assertThat(dashboardsOnlyUserRole.cluster().check(MonitoringBulkAction.NAME), is(false)); + assertThat(dashboardsOnlyUserRole.cluster().check(ClusterHealthAction.NAME, request), is(false)); + assertThat(dashboardsOnlyUserRole.cluster().check(ClusterStateAction.NAME, request), is(false)); + assertThat(dashboardsOnlyUserRole.cluster().check(ClusterStatsAction.NAME, request), is(false)); + assertThat(dashboardsOnlyUserRole.cluster().check(PutIndexTemplateAction.NAME, request), is(false)); + assertThat(dashboardsOnlyUserRole.cluster().check(ClusterRerouteAction.NAME, request), is(false)); + assertThat(dashboardsOnlyUserRole.cluster().check(ClusterUpdateSettingsAction.NAME, request), is(false)); + assertThat(dashboardsOnlyUserRole.cluster().check(MonitoringBulkAction.NAME, request), is(false)); assertThat(dashboardsOnlyUserRole.runAs().check(randomAlphaOfLengthBetween(1, 12)), is(false)); @@ -405,20 +463,35 @@ public void testKibanaDashboardOnlyUserRole() { assertThat(dashboardsOnlyUserRole.indices().allowedIndicesMatcher(GetIndexAction.NAME).test(index), is(true)); assertThat(dashboardsOnlyUserRole.indices().allowedIndicesMatcher(SearchAction.NAME).test(index), is(true)); assertThat(dashboardsOnlyUserRole.indices().allowedIndicesMatcher(MultiSearchAction.NAME).test(index), is(true)); + + final String randomApplication = "kibana-" + randomAlphaOfLengthBetween(8, 24); + assertThat(dashboardsOnlyUserRole.application().grants(new ApplicationPrivilege(randomApplication, "app-random", "all"), "*"), + is(false)); + + final String application = "kibana-.kibana"; + assertThat(dashboardsOnlyUserRole.application().grants(new ApplicationPrivilege(application, "app-foo", "foo"), "*"), is(false)); + assertThat(dashboardsOnlyUserRole.application().grants(new ApplicationPrivilege(application, "app-all", "all"), "*"), is(false)); + assertThat(dashboardsOnlyUserRole.application().grants(new ApplicationPrivilege(application, "app-read", "read"), "*"), is(true)); + + final String applicationWithRandomIndex = "kibana-.kibana_" + randomAlphaOfLengthBetween(8, 24); + assertThat(dashboardsOnlyUserRole.application().grants( + new ApplicationPrivilege(applicationWithRandomIndex, "app-random-index", "all"), "*"), is(false)); } public void testSuperuserRole() { + final TransportRequest request = mock(TransportRequest.class); + RoleDescriptor roleDescriptor = new ReservedRolesStore().roleDescriptor("superuser"); assertNotNull(roleDescriptor); assertThat(roleDescriptor.getMetadata(), hasEntry("_reserved", true)); Role superuserRole = Role.builder(roleDescriptor, null).build(); - assertThat(superuserRole.cluster().check(ClusterHealthAction.NAME), is(true)); - assertThat(superuserRole.cluster().check(ClusterUpdateSettingsAction.NAME), is(true)); - assertThat(superuserRole.cluster().check(PutUserAction.NAME), is(true)); - assertThat(superuserRole.cluster().check(PutRoleAction.NAME), is(true)); - assertThat(superuserRole.cluster().check(PutIndexTemplateAction.NAME), is(true)); - assertThat(superuserRole.cluster().check("internal:admin/foo"), is(false)); + assertThat(superuserRole.cluster().check(ClusterHealthAction.NAME, request), is(true)); + assertThat(superuserRole.cluster().check(ClusterUpdateSettingsAction.NAME, request), is(true)); + assertThat(superuserRole.cluster().check(PutUserAction.NAME, request), is(true)); + assertThat(superuserRole.cluster().check(PutRoleAction.NAME, request), is(true)); + assertThat(superuserRole.cluster().check(PutIndexTemplateAction.NAME, request), is(true)); + assertThat(superuserRole.cluster().check("internal:admin/foo", request), is(false)); final Settings indexSettings = Settings.builder().put("index.version.created", Version.CURRENT).build(); final MetaData metaData = new MetaData.Builder() @@ -457,18 +530,20 @@ public void testSuperuserRole() { } public void testLogstashSystemRole() { + final TransportRequest request = mock(TransportRequest.class); + RoleDescriptor roleDescriptor = new ReservedRolesStore().roleDescriptor("logstash_system"); assertNotNull(roleDescriptor); assertThat(roleDescriptor.getMetadata(), hasEntry("_reserved", true)); Role logstashSystemRole = Role.builder(roleDescriptor, null).build(); - assertThat(logstashSystemRole.cluster().check(ClusterHealthAction.NAME), is(true)); - assertThat(logstashSystemRole.cluster().check(ClusterStateAction.NAME), is(true)); - assertThat(logstashSystemRole.cluster().check(ClusterStatsAction.NAME), is(true)); - assertThat(logstashSystemRole.cluster().check(PutIndexTemplateAction.NAME), is(false)); - assertThat(logstashSystemRole.cluster().check(ClusterRerouteAction.NAME), is(false)); - assertThat(logstashSystemRole.cluster().check(ClusterUpdateSettingsAction.NAME), is(false)); - assertThat(logstashSystemRole.cluster().check(MonitoringBulkAction.NAME), is(true)); + assertThat(logstashSystemRole.cluster().check(ClusterHealthAction.NAME, request), is(true)); + assertThat(logstashSystemRole.cluster().check(ClusterStateAction.NAME, request), is(true)); + assertThat(logstashSystemRole.cluster().check(ClusterStatsAction.NAME, request), is(true)); + assertThat(logstashSystemRole.cluster().check(PutIndexTemplateAction.NAME, request), is(false)); + assertThat(logstashSystemRole.cluster().check(ClusterRerouteAction.NAME, request), is(false)); + assertThat(logstashSystemRole.cluster().check(ClusterUpdateSettingsAction.NAME, request), is(false)); + assertThat(logstashSystemRole.cluster().check(MonitoringBulkAction.NAME, request), is(true)); assertThat(logstashSystemRole.runAs().check(randomAlphaOfLengthBetween(1, 30)), is(false)); @@ -479,18 +554,20 @@ public void testLogstashSystemRole() { } public void testBeatsSystemRole() { + final TransportRequest request = mock(TransportRequest.class); + RoleDescriptor roleDescriptor = new ReservedRolesStore().roleDescriptor(BeatsSystemUser.ROLE_NAME); assertNotNull(roleDescriptor); assertThat(roleDescriptor.getMetadata(), hasEntry("_reserved", true)); Role logstashSystemRole = Role.builder(roleDescriptor, null).build(); - assertThat(logstashSystemRole.cluster().check(ClusterHealthAction.NAME), is(true)); - assertThat(logstashSystemRole.cluster().check(ClusterStateAction.NAME), is(true)); - assertThat(logstashSystemRole.cluster().check(ClusterStatsAction.NAME), is(true)); - assertThat(logstashSystemRole.cluster().check(PutIndexTemplateAction.NAME), is(false)); - assertThat(logstashSystemRole.cluster().check(ClusterRerouteAction.NAME), is(false)); - assertThat(logstashSystemRole.cluster().check(ClusterUpdateSettingsAction.NAME), is(false)); - assertThat(logstashSystemRole.cluster().check(MonitoringBulkAction.NAME), is(true)); + assertThat(logstashSystemRole.cluster().check(ClusterHealthAction.NAME, request), is(true)); + assertThat(logstashSystemRole.cluster().check(ClusterStateAction.NAME, request), is(true)); + assertThat(logstashSystemRole.cluster().check(ClusterStatsAction.NAME, request), is(true)); + assertThat(logstashSystemRole.cluster().check(PutIndexTemplateAction.NAME, request), is(false)); + assertThat(logstashSystemRole.cluster().check(ClusterRerouteAction.NAME, request), is(false)); + assertThat(logstashSystemRole.cluster().check(ClusterUpdateSettingsAction.NAME, request), is(false)); + assertThat(logstashSystemRole.cluster().check(MonitoringBulkAction.NAME, request), is(true)); assertThat(logstashSystemRole.runAs().check(randomAlphaOfLengthBetween(1, 30)), is(false)); @@ -501,46 +578,48 @@ public void testBeatsSystemRole() { } public void testMachineLearningAdminRole() { + final TransportRequest request = mock(TransportRequest.class); + RoleDescriptor roleDescriptor = new ReservedRolesStore().roleDescriptor("machine_learning_admin"); assertNotNull(roleDescriptor); assertThat(roleDescriptor.getMetadata(), hasEntry("_reserved", true)); Role role = Role.builder(roleDescriptor, null).build(); - assertThat(role.cluster().check(CloseJobAction.NAME), is(true)); - assertThat(role.cluster().check(DeleteDatafeedAction.NAME), is(true)); - assertThat(role.cluster().check(DeleteExpiredDataAction.NAME), is(true)); - assertThat(role.cluster().check(DeleteFilterAction.NAME), is(true)); - assertThat(role.cluster().check(DeleteJobAction.NAME), is(true)); - assertThat(role.cluster().check(DeleteModelSnapshotAction.NAME), is(true)); - assertThat(role.cluster().check(FinalizeJobExecutionAction.NAME), is(false)); // internal use only - assertThat(role.cluster().check(FlushJobAction.NAME), is(true)); - assertThat(role.cluster().check(GetBucketsAction.NAME), is(true)); - assertThat(role.cluster().check(GetCategoriesAction.NAME), is(true)); - assertThat(role.cluster().check(GetDatafeedsAction.NAME), is(true)); - assertThat(role.cluster().check(GetDatafeedsStatsAction.NAME), is(true)); - assertThat(role.cluster().check(GetFiltersAction.NAME), is(true)); - assertThat(role.cluster().check(GetInfluencersAction.NAME), is(true)); - assertThat(role.cluster().check(GetJobsAction.NAME), is(true)); - assertThat(role.cluster().check(GetJobsStatsAction.NAME), is(true)); - assertThat(role.cluster().check(GetModelSnapshotsAction.NAME), is(true)); - assertThat(role.cluster().check(GetRecordsAction.NAME), is(true)); - assertThat(role.cluster().check(IsolateDatafeedAction.NAME), is(false)); // internal use only - assertThat(role.cluster().check(KillProcessAction.NAME), is(false)); // internal use only - assertThat(role.cluster().check(OpenJobAction.NAME), is(true)); - assertThat(role.cluster().check(PostDataAction.NAME), is(true)); - assertThat(role.cluster().check(PreviewDatafeedAction.NAME), is(true)); - assertThat(role.cluster().check(PutDatafeedAction.NAME), is(true)); - assertThat(role.cluster().check(PutFilterAction.NAME), is(true)); - assertThat(role.cluster().check(PutJobAction.NAME), is(true)); - assertThat(role.cluster().check(RevertModelSnapshotAction.NAME), is(true)); - assertThat(role.cluster().check(StartDatafeedAction.NAME), is(true)); - assertThat(role.cluster().check(StopDatafeedAction.NAME), is(true)); - assertThat(role.cluster().check(UpdateDatafeedAction.NAME), is(true)); - assertThat(role.cluster().check(UpdateJobAction.NAME), is(true)); - assertThat(role.cluster().check(UpdateModelSnapshotAction.NAME), is(true)); - assertThat(role.cluster().check(UpdateProcessAction.NAME), is(false)); // internal use only - assertThat(role.cluster().check(ValidateDetectorAction.NAME), is(true)); - assertThat(role.cluster().check(ValidateJobConfigAction.NAME), is(true)); + assertThat(role.cluster().check(CloseJobAction.NAME, request), is(true)); + assertThat(role.cluster().check(DeleteDatafeedAction.NAME, request), is(true)); + assertThat(role.cluster().check(DeleteExpiredDataAction.NAME, request), is(true)); + assertThat(role.cluster().check(DeleteFilterAction.NAME, request), is(true)); + assertThat(role.cluster().check(DeleteJobAction.NAME, request), is(true)); + assertThat(role.cluster().check(DeleteModelSnapshotAction.NAME, request), is(true)); + assertThat(role.cluster().check(FinalizeJobExecutionAction.NAME, request), is(false)); // internal use only + assertThat(role.cluster().check(FlushJobAction.NAME, request), is(true)); + assertThat(role.cluster().check(GetBucketsAction.NAME, request), is(true)); + assertThat(role.cluster().check(GetCategoriesAction.NAME, request), is(true)); + assertThat(role.cluster().check(GetDatafeedsAction.NAME, request), is(true)); + assertThat(role.cluster().check(GetDatafeedsStatsAction.NAME, request), is(true)); + assertThat(role.cluster().check(GetFiltersAction.NAME, request), is(true)); + assertThat(role.cluster().check(GetInfluencersAction.NAME, request), is(true)); + assertThat(role.cluster().check(GetJobsAction.NAME, request), is(true)); + assertThat(role.cluster().check(GetJobsStatsAction.NAME, request), is(true)); + assertThat(role.cluster().check(GetModelSnapshotsAction.NAME, request), is(true)); + assertThat(role.cluster().check(GetRecordsAction.NAME, request), is(true)); + assertThat(role.cluster().check(IsolateDatafeedAction.NAME, request), is(false)); // internal use only + assertThat(role.cluster().check(KillProcessAction.NAME, request), is(false)); // internal use only + assertThat(role.cluster().check(OpenJobAction.NAME, request), is(true)); + assertThat(role.cluster().check(PostDataAction.NAME, request), is(true)); + assertThat(role.cluster().check(PreviewDatafeedAction.NAME, request), is(true)); + assertThat(role.cluster().check(PutDatafeedAction.NAME, request), is(true)); + assertThat(role.cluster().check(PutFilterAction.NAME, request), is(true)); + assertThat(role.cluster().check(PutJobAction.NAME, request), is(true)); + assertThat(role.cluster().check(RevertModelSnapshotAction.NAME, request), is(true)); + assertThat(role.cluster().check(StartDatafeedAction.NAME, request), is(true)); + assertThat(role.cluster().check(StopDatafeedAction.NAME, request), is(true)); + assertThat(role.cluster().check(UpdateDatafeedAction.NAME, request), is(true)); + assertThat(role.cluster().check(UpdateJobAction.NAME, request), is(true)); + assertThat(role.cluster().check(UpdateModelSnapshotAction.NAME, request), is(true)); + assertThat(role.cluster().check(UpdateProcessAction.NAME, request), is(false)); // internal use only + assertThat(role.cluster().check(ValidateDetectorAction.NAME, request), is(true)); + assertThat(role.cluster().check(ValidateJobConfigAction.NAME, request), is(true)); assertThat(role.runAs().check(randomAlphaOfLengthBetween(1, 30)), is(false)); assertNoAccessAllowed(role, "foo"); @@ -551,46 +630,48 @@ public void testMachineLearningAdminRole() { } public void testMachineLearningUserRole() { + final TransportRequest request = mock(TransportRequest.class); + RoleDescriptor roleDescriptor = new ReservedRolesStore().roleDescriptor("machine_learning_user"); assertNotNull(roleDescriptor); assertThat(roleDescriptor.getMetadata(), hasEntry("_reserved", true)); Role role = Role.builder(roleDescriptor, null).build(); - assertThat(role.cluster().check(CloseJobAction.NAME), is(false)); - assertThat(role.cluster().check(DeleteDatafeedAction.NAME), is(false)); - assertThat(role.cluster().check(DeleteExpiredDataAction.NAME), is(false)); - assertThat(role.cluster().check(DeleteFilterAction.NAME), is(false)); - assertThat(role.cluster().check(DeleteJobAction.NAME), is(false)); - assertThat(role.cluster().check(DeleteModelSnapshotAction.NAME), is(false)); - assertThat(role.cluster().check(FinalizeJobExecutionAction.NAME), is(false)); - assertThat(role.cluster().check(FlushJobAction.NAME), is(false)); - assertThat(role.cluster().check(GetBucketsAction.NAME), is(true)); - assertThat(role.cluster().check(GetCategoriesAction.NAME), is(true)); - assertThat(role.cluster().check(GetDatafeedsAction.NAME), is(true)); - assertThat(role.cluster().check(GetDatafeedsStatsAction.NAME), is(true)); - assertThat(role.cluster().check(GetFiltersAction.NAME), is(false)); - assertThat(role.cluster().check(GetInfluencersAction.NAME), is(true)); - assertThat(role.cluster().check(GetJobsAction.NAME), is(true)); - assertThat(role.cluster().check(GetJobsStatsAction.NAME), is(true)); - assertThat(role.cluster().check(GetModelSnapshotsAction.NAME), is(true)); - assertThat(role.cluster().check(GetRecordsAction.NAME), is(true)); - assertThat(role.cluster().check(IsolateDatafeedAction.NAME), is(false)); - assertThat(role.cluster().check(KillProcessAction.NAME), is(false)); - assertThat(role.cluster().check(OpenJobAction.NAME), is(false)); - assertThat(role.cluster().check(PostDataAction.NAME), is(false)); - assertThat(role.cluster().check(PreviewDatafeedAction.NAME), is(false)); - assertThat(role.cluster().check(PutDatafeedAction.NAME), is(false)); - assertThat(role.cluster().check(PutFilterAction.NAME), is(false)); - assertThat(role.cluster().check(PutJobAction.NAME), is(false)); - assertThat(role.cluster().check(RevertModelSnapshotAction.NAME), is(false)); - assertThat(role.cluster().check(StartDatafeedAction.NAME), is(false)); - assertThat(role.cluster().check(StopDatafeedAction.NAME), is(false)); - assertThat(role.cluster().check(UpdateDatafeedAction.NAME), is(false)); - assertThat(role.cluster().check(UpdateJobAction.NAME), is(false)); - assertThat(role.cluster().check(UpdateModelSnapshotAction.NAME), is(false)); - assertThat(role.cluster().check(UpdateProcessAction.NAME), is(false)); - assertThat(role.cluster().check(ValidateDetectorAction.NAME), is(false)); - assertThat(role.cluster().check(ValidateJobConfigAction.NAME), is(false)); + assertThat(role.cluster().check(CloseJobAction.NAME, request), is(false)); + assertThat(role.cluster().check(DeleteDatafeedAction.NAME, request), is(false)); + assertThat(role.cluster().check(DeleteExpiredDataAction.NAME, request), is(false)); + assertThat(role.cluster().check(DeleteFilterAction.NAME, request), is(false)); + assertThat(role.cluster().check(DeleteJobAction.NAME, request), is(false)); + assertThat(role.cluster().check(DeleteModelSnapshotAction.NAME, request), is(false)); + assertThat(role.cluster().check(FinalizeJobExecutionAction.NAME, request), is(false)); + assertThat(role.cluster().check(FlushJobAction.NAME, request), is(false)); + assertThat(role.cluster().check(GetBucketsAction.NAME, request), is(true)); + assertThat(role.cluster().check(GetCategoriesAction.NAME, request), is(true)); + assertThat(role.cluster().check(GetDatafeedsAction.NAME, request), is(true)); + assertThat(role.cluster().check(GetDatafeedsStatsAction.NAME, request), is(true)); + assertThat(role.cluster().check(GetFiltersAction.NAME, request), is(false)); + assertThat(role.cluster().check(GetInfluencersAction.NAME, request), is(true)); + assertThat(role.cluster().check(GetJobsAction.NAME, request), is(true)); + assertThat(role.cluster().check(GetJobsStatsAction.NAME, request), is(true)); + assertThat(role.cluster().check(GetModelSnapshotsAction.NAME, request), is(true)); + assertThat(role.cluster().check(GetRecordsAction.NAME, request), is(true)); + assertThat(role.cluster().check(IsolateDatafeedAction.NAME, request), is(false)); + assertThat(role.cluster().check(KillProcessAction.NAME, request), is(false)); + assertThat(role.cluster().check(OpenJobAction.NAME, request), is(false)); + assertThat(role.cluster().check(PostDataAction.NAME, request), is(false)); + assertThat(role.cluster().check(PreviewDatafeedAction.NAME, request), is(false)); + assertThat(role.cluster().check(PutDatafeedAction.NAME, request), is(false)); + assertThat(role.cluster().check(PutFilterAction.NAME, request), is(false)); + assertThat(role.cluster().check(PutJobAction.NAME, request), is(false)); + assertThat(role.cluster().check(RevertModelSnapshotAction.NAME, request), is(false)); + assertThat(role.cluster().check(StartDatafeedAction.NAME, request), is(false)); + assertThat(role.cluster().check(StopDatafeedAction.NAME, request), is(false)); + assertThat(role.cluster().check(UpdateDatafeedAction.NAME, request), is(false)); + assertThat(role.cluster().check(UpdateJobAction.NAME, request), is(false)); + assertThat(role.cluster().check(UpdateModelSnapshotAction.NAME, request), is(false)); + assertThat(role.cluster().check(UpdateProcessAction.NAME, request), is(false)); + assertThat(role.cluster().check(ValidateDetectorAction.NAME, request), is(false)); + assertThat(role.cluster().check(ValidateJobConfigAction.NAME, request), is(false)); assertThat(role.runAs().check(randomAlphaOfLengthBetween(1, 30)), is(false)); assertNoAccessAllowed(role, "foo"); @@ -601,19 +682,21 @@ public void testMachineLearningUserRole() { } public void testWatcherAdminRole() { + final TransportRequest request = mock(TransportRequest.class); + RoleDescriptor roleDescriptor = new ReservedRolesStore().roleDescriptor("watcher_admin"); assertNotNull(roleDescriptor); assertThat(roleDescriptor.getMetadata(), hasEntry("_reserved", true)); Role role = Role.builder(roleDescriptor, null).build(); - assertThat(role.cluster().check(PutWatchAction.NAME), is(true)); - assertThat(role.cluster().check(GetWatchAction.NAME), is(true)); - assertThat(role.cluster().check(DeleteWatchAction.NAME), is(true)); - assertThat(role.cluster().check(ExecuteWatchAction.NAME), is(true)); - assertThat(role.cluster().check(AckWatchAction.NAME), is(true)); - assertThat(role.cluster().check(ActivateWatchAction.NAME), is(true)); - assertThat(role.cluster().check(WatcherServiceAction.NAME), is(true)); - assertThat(role.cluster().check(WatcherStatsAction.NAME), is(true)); + assertThat(role.cluster().check(PutWatchAction.NAME, request), is(true)); + assertThat(role.cluster().check(GetWatchAction.NAME, request), is(true)); + assertThat(role.cluster().check(DeleteWatchAction.NAME, request), is(true)); + assertThat(role.cluster().check(ExecuteWatchAction.NAME, request), is(true)); + assertThat(role.cluster().check(AckWatchAction.NAME, request), is(true)); + assertThat(role.cluster().check(ActivateWatchAction.NAME, request), is(true)); + assertThat(role.cluster().check(WatcherServiceAction.NAME, request), is(true)); + assertThat(role.cluster().check(WatcherStatsAction.NAME, request), is(true)); assertThat(role.runAs().check(randomAlphaOfLengthBetween(1, 30)), is(false)); assertThat(role.indices().allowedIndicesMatcher(IndexAction.NAME).test("foo"), is(false)); @@ -626,19 +709,21 @@ public void testWatcherAdminRole() { } public void testWatcherUserRole() { + final TransportRequest request = mock(TransportRequest.class); + RoleDescriptor roleDescriptor = new ReservedRolesStore().roleDescriptor("watcher_user"); assertNotNull(roleDescriptor); assertThat(roleDescriptor.getMetadata(), hasEntry("_reserved", true)); Role role = Role.builder(roleDescriptor, null).build(); - assertThat(role.cluster().check(PutWatchAction.NAME), is(false)); - assertThat(role.cluster().check(GetWatchAction.NAME), is(true)); - assertThat(role.cluster().check(DeleteWatchAction.NAME), is(false)); - assertThat(role.cluster().check(ExecuteWatchAction.NAME), is(false)); - assertThat(role.cluster().check(AckWatchAction.NAME), is(false)); - assertThat(role.cluster().check(ActivateWatchAction.NAME), is(false)); - assertThat(role.cluster().check(WatcherServiceAction.NAME), is(false)); - assertThat(role.cluster().check(WatcherStatsAction.NAME), is(true)); + assertThat(role.cluster().check(PutWatchAction.NAME, request), is(false)); + assertThat(role.cluster().check(GetWatchAction.NAME, request), is(true)); + assertThat(role.cluster().check(DeleteWatchAction.NAME, request), is(false)); + assertThat(role.cluster().check(ExecuteWatchAction.NAME, request), is(false)); + assertThat(role.cluster().check(AckWatchAction.NAME, request), is(false)); + assertThat(role.cluster().check(ActivateWatchAction.NAME, request), is(false)); + assertThat(role.cluster().check(WatcherServiceAction.NAME, request), is(false)); + assertThat(role.cluster().check(WatcherStatsAction.NAME, request), is(true)); assertThat(role.runAs().check(randomAlphaOfLengthBetween(1, 30)), is(false)); assertThat(role.indices().allowedIndicesMatcher(IndexAction.NAME).test("foo"), is(false)); @@ -676,15 +761,17 @@ private void assertNoAccessAllowed(Role role, String index) { } public void testLogstashAdminRole() { + final TransportRequest request = mock(TransportRequest.class); + RoleDescriptor roleDescriptor = new ReservedRolesStore().roleDescriptor("logstash_admin"); assertNotNull(roleDescriptor); assertThat(roleDescriptor.getMetadata(), hasEntry("_reserved", true)); Role logstashAdminRole = Role.builder(roleDescriptor, null).build(); - assertThat(logstashAdminRole.cluster().check(ClusterHealthAction.NAME), is(false)); - assertThat(logstashAdminRole.cluster().check(PutIndexTemplateAction.NAME), is(false)); - assertThat(logstashAdminRole.cluster().check(ClusterRerouteAction.NAME), is(false)); - assertThat(logstashAdminRole.cluster().check(ClusterUpdateSettingsAction.NAME), is(false)); + assertThat(logstashAdminRole.cluster().check(ClusterHealthAction.NAME, request), is(false)); + assertThat(logstashAdminRole.cluster().check(PutIndexTemplateAction.NAME, request), is(false)); + assertThat(logstashAdminRole.cluster().check(ClusterRerouteAction.NAME, request), is(false)); + assertThat(logstashAdminRole.cluster().check(ClusterUpdateSettingsAction.NAME, request), is(false)); assertThat(logstashAdminRole.runAs().check(randomAlphaOfLengthBetween(1, 30)), is(false)); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportPutDatafeedAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportPutDatafeedAction.java index dbbaf7b027058..7a7deac0136a7 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportPutDatafeedAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportPutDatafeedAction.java @@ -92,6 +92,7 @@ protected void masterOperation(PutDatafeedAction.Request request, ClusterState s .indices(request.getDatafeed().getIndices().toArray(new String[0])) .privileges(SearchAction.NAME) .build()); + privRequest.applicationPrivileges(new RoleDescriptor.ApplicationResourcePrivileges[0]); client.execute(HasPrivilegesAction.INSTANCE, privRequest, privResponseListener); } else { @@ -107,8 +108,8 @@ private void handlePrivsResponse(String username, PutDatafeedAction.Request requ } else { XContentBuilder builder = JsonXContent.contentBuilder(); builder.startObject(); - for (HasPrivilegesResponse.IndexPrivileges index : response.getIndexPrivileges()) { - builder.field(index.getIndex()); + for (HasPrivilegesResponse.ResourcePrivileges index : response.getIndexPrivileges()) { + builder.field(index.getResource()); builder.map(index.getPrivileges()); } builder.endObject(); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index bbb4b323172d9..f9b8cb1fd0b18 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -85,6 +85,9 @@ import org.elasticsearch.xpack.core.security.SecurityExtension; import org.elasticsearch.xpack.core.security.SecurityField; import org.elasticsearch.xpack.core.security.SecuritySettings; +import org.elasticsearch.xpack.core.security.action.privilege.DeletePrivilegesAction; +import org.elasticsearch.xpack.core.security.action.privilege.GetPrivilegesAction; +import org.elasticsearch.xpack.core.security.action.privilege.PutPrivilegesAction; import org.elasticsearch.xpack.core.security.action.realm.ClearRealmCacheAction; import org.elasticsearch.xpack.core.security.action.role.ClearRolesCacheAction; import org.elasticsearch.xpack.core.security.action.role.DeleteRoleAction; @@ -120,6 +123,9 @@ import org.elasticsearch.xpack.core.security.authz.accesscontrol.SecurityIndexSearcherWrapper; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsCache; +import org.elasticsearch.xpack.security.action.privilege.TransportDeletePrivilegesAction; +import org.elasticsearch.xpack.security.action.privilege.TransportGetPrivilegesAction; +import org.elasticsearch.xpack.security.action.privilege.TransportPutPrivilegesAction; import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; import org.elasticsearch.xpack.core.security.index.IndexAuditTrailField; import org.elasticsearch.xpack.core.security.user.AnonymousUser; @@ -177,6 +183,7 @@ import org.elasticsearch.xpack.security.authz.SecuritySearchOperationListener; import org.elasticsearch.xpack.security.authz.accesscontrol.OptOutQueryCache; import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; +import org.elasticsearch.xpack.security.authz.store.NativePrivilegeStore; import org.elasticsearch.xpack.security.authz.store.FileRolesStore; import org.elasticsearch.xpack.security.authz.store.NativeRolesStore; import org.elasticsearch.xpack.security.ingest.SetSecurityUserProcessor; @@ -184,6 +191,10 @@ import org.elasticsearch.xpack.security.rest.action.RestAuthenticateAction; import org.elasticsearch.xpack.security.rest.action.oauth2.RestGetTokenAction; import org.elasticsearch.xpack.security.rest.action.oauth2.RestInvalidateTokenAction; +import org.elasticsearch.xpack.security.rest.action.privilege.RestDeletePrivilegesAction; +import org.elasticsearch.xpack.security.rest.action.privilege.RestGetPrivilegesAction; +import org.elasticsearch.xpack.security.rest.action.privilege.RestPutPrivilegeAction; +import org.elasticsearch.xpack.security.rest.action.privilege.RestPutPrivilegesAction; import org.elasticsearch.xpack.security.rest.action.realm.RestClearRealmCacheAction; import org.elasticsearch.xpack.security.rest.action.role.RestClearRolesCacheAction; import org.elasticsearch.xpack.security.rest.action.role.RestDeleteRoleAction; @@ -454,6 +465,9 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste authcService.set(new AuthenticationService(settings, realms, auditTrailService, failureHandler, threadPool, anonymousUser, tokenService)); components.add(authcService.get()); + final NativePrivilegeStore privilegeStore = new NativePrivilegeStore(settings, client, securityIndex.get()); + components.add(privilegeStore); + final FileRolesStore fileRolesStore = new FileRolesStore(settings, env, resourceWatcherService, getLicenseState()); final NativeRolesStore nativeRolesStore = new NativeRolesStore(settings, client, getLicenseState(), securityIndex.get()); final ReservedRolesStore reservedRolesStore = new ReservedRolesStore(); @@ -462,7 +476,7 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste rolesProviders.addAll(extension.getRolesProviders(settings, resourceWatcherService)); } final CompositeRolesStore allRolesStore = new CompositeRolesStore(settings, fileRolesStore, nativeRolesStore, - reservedRolesStore, rolesProviders, threadPool.getThreadContext(), getLicenseState()); + reservedRolesStore, privilegeStore, rolesProviders, threadPool.getThreadContext(), getLicenseState()); securityIndex.get().addIndexStateListener(allRolesStore::onSecurityIndexStateChange); // to keep things simple, just invalidate all cached entries on license change. this happens so rarely that the impact should be // minimal @@ -721,7 +735,10 @@ public void onIndexModule(IndexModule module) { new ActionHandler<>(SamlPrepareAuthenticationAction.INSTANCE, TransportSamlPrepareAuthenticationAction.class), new ActionHandler<>(SamlAuthenticateAction.INSTANCE, TransportSamlAuthenticateAction.class), new ActionHandler<>(SamlLogoutAction.INSTANCE, TransportSamlLogoutAction.class), - new ActionHandler<>(SamlInvalidateSessionAction.INSTANCE, TransportSamlInvalidateSessionAction.class) + new ActionHandler<>(SamlInvalidateSessionAction.INSTANCE, TransportSamlInvalidateSessionAction.class), + new ActionHandler<>(GetPrivilegesAction.INSTANCE, TransportGetPrivilegesAction.class), + new ActionHandler<>(PutPrivilegesAction.INSTANCE, TransportPutPrivilegesAction.class), + new ActionHandler<>(DeletePrivilegesAction.INSTANCE, TransportDeletePrivilegesAction.class) ); } @@ -766,7 +783,11 @@ public List getRestHandlers(Settings settings, RestController restC new RestSamlPrepareAuthenticationAction(settings, restController, getLicenseState()), new RestSamlAuthenticateAction(settings, restController, getLicenseState()), new RestSamlLogoutAction(settings, restController, getLicenseState()), - new RestSamlInvalidateSessionAction(settings, restController, getLicenseState()) + new RestSamlInvalidateSessionAction(settings, restController, getLicenseState()), + new RestGetPrivilegesAction(settings, restController, getLicenseState()), + new RestPutPrivilegesAction(settings, restController, getLicenseState()), + new RestPutPrivilegeAction(settings, restController, getLicenseState()), + new RestDeletePrivilegesAction(settings, restController, getLicenseState()) ); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/privilege/TransportDeletePrivilegesAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/privilege/TransportDeletePrivilegesAction.java new file mode 100644 index 0000000000000..527647bf7416e --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/privilege/TransportDeletePrivilegesAction.java @@ -0,0 +1,53 @@ +/* + * 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.action.privilege; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.security.action.privilege.DeletePrivilegesAction; +import org.elasticsearch.xpack.core.security.action.privilege.DeletePrivilegesRequest; +import org.elasticsearch.xpack.core.security.action.privilege.DeletePrivilegesResponse; +import org.elasticsearch.xpack.security.authz.store.NativePrivilegeStore; + +import java.util.Collections; +import java.util.Set; + +/** + * Transport action to retrieve one or more application privileges from the security index + */ +public class TransportDeletePrivilegesAction extends HandledTransportAction { + + private final NativePrivilegeStore privilegeStore; + + @Inject + public TransportDeletePrivilegesAction(Settings settings, ThreadPool threadPool, ActionFilters actionFilters, + IndexNameExpressionResolver resolver, NativePrivilegeStore privilegeStore, + TransportService transportService) { + super(settings, DeletePrivilegesAction.NAME, threadPool, transportService, actionFilters, resolver, DeletePrivilegesRequest::new); + this.privilegeStore = privilegeStore; + } + + @Override + protected void doExecute(final DeletePrivilegesRequest request, final ActionListener listener) { + if (request.privileges() == null || request.privileges().length == 0) { + listener.onResponse(new DeletePrivilegesResponse(Collections.emptyList())); + return; + } + final Set names = Sets.newHashSet(request.privileges()); + this.privilegeStore.deletePrivileges(request.application(), names, request.getRefreshPolicy(), ActionListener.wrap( + privileges -> listener.onResponse( + new DeletePrivilegesResponse(privileges.getOrDefault(request.application(), Collections.emptyList())) + ), listener::onFailure + )); + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/privilege/TransportGetPrivilegesAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/privilege/TransportGetPrivilegesAction.java new file mode 100644 index 0000000000000..1afff52617eb3 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/privilege/TransportGetPrivilegesAction.java @@ -0,0 +1,58 @@ +/* + * 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.action.privilege; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.security.action.privilege.GetPrivilegesAction; +import org.elasticsearch.xpack.core.security.action.privilege.GetPrivilegesRequest; +import org.elasticsearch.xpack.core.security.action.privilege.GetPrivilegesResponse; +import org.elasticsearch.xpack.security.authz.store.NativePrivilegeStore; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import static org.elasticsearch.common.Strings.isNullOrEmpty; + +/** + * Transport action to retrieve one or more application privileges from the security index + */ +public class TransportGetPrivilegesAction extends HandledTransportAction { + + private final NativePrivilegeStore privilegeStore; + + @Inject + public TransportGetPrivilegesAction(Settings settings, ThreadPool threadPool, ActionFilters actionFilters, + IndexNameExpressionResolver resolver, + NativePrivilegeStore privilegeStore, TransportService transportService) { + super(settings, GetPrivilegesAction.NAME, threadPool, transportService, actionFilters, resolver, GetPrivilegesRequest::new); + this.privilegeStore = privilegeStore; + } + + @Override + protected void doExecute(final GetPrivilegesRequest request, final ActionListener listener) { + final Set names; + if (request.privileges() == null || request.privileges().length == 0) { + names = null; + } else { + names = new HashSet<>(Arrays.asList(request.privileges())); + } + final Collection applications = isNullOrEmpty(request.application()) ? null : Collections.singleton(request.application()); + this.privilegeStore.getPrivileges(applications, names, ActionListener.wrap( + privileges -> listener.onResponse(new GetPrivilegesResponse(privileges)), + listener::onFailure + )); + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/privilege/TransportPutPrivilegesAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/privilege/TransportPutPrivilegesAction.java new file mode 100644 index 0000000000000..d068e225af041 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/privilege/TransportPutPrivilegesAction.java @@ -0,0 +1,49 @@ +/* + * 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.action.privilege; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.security.action.privilege.PutPrivilegesAction; +import org.elasticsearch.xpack.core.security.action.privilege.PutPrivilegesRequest; +import org.elasticsearch.xpack.core.security.action.privilege.PutPrivilegesResponse; +import org.elasticsearch.xpack.security.authz.store.NativePrivilegeStore; + +import java.util.Collections; + +/** + * Transport action to retrieve one or more application privileges from the security index + */ +public class TransportPutPrivilegesAction extends HandledTransportAction { + + private final NativePrivilegeStore privilegeStore; + + @Inject + public TransportPutPrivilegesAction(Settings settings, ThreadPool threadPool, ActionFilters actionFilters, + IndexNameExpressionResolver resolver, + NativePrivilegeStore privilegeStore, TransportService transportService) { + super(settings, PutPrivilegesAction.NAME, threadPool, transportService, actionFilters, resolver, PutPrivilegesRequest::new); + this.privilegeStore = privilegeStore; + } + + @Override + protected void doExecute(final PutPrivilegesRequest request, final ActionListener listener) { + if (request.getPrivileges() == null || request.getPrivileges().size() == 0) { + listener.onResponse(new PutPrivilegesResponse(Collections.emptyMap())); + } else { + this.privilegeStore.putPrivileges(request.getPrivileges(), request.getRefreshPolicy(), ActionListener.wrap( + created -> listener.onResponse(new PutPrivilegesResponse(created)), + listener::onFailure + )); + } + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesAction.java index dbc2d8f82bd94..e97735d9ed2b0 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesAction.java @@ -24,19 +24,26 @@ import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.permission.IndicesPermission; import org.elasticsearch.xpack.core.security.authz.permission.Role; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.Privilege; import org.elasticsearch.xpack.core.security.support.Automatons; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authz.AuthorizationService; +import org.elasticsearch.xpack.security.authz.store.NativePrivilegeStore; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; /** * Transport action that tests whether a user has the specified @@ -45,14 +52,16 @@ public class TransportHasPrivilegesAction extends HandledTransportAction { private final AuthorizationService authorizationService; + private final NativePrivilegeStore privilegeStore; @Inject public TransportHasPrivilegesAction(Settings settings, ThreadPool threadPool, TransportService transportService, ActionFilters actionFilters, IndexNameExpressionResolver indexNameExpressionResolver, - AuthorizationService authorizationService) { + AuthorizationService authorizationService, NativePrivilegeStore privilegeStore) { super(settings, HasPrivilegesAction.NAME, threadPool, transportService, actionFilters, indexNameExpressionResolver, HasPrivilegesRequest::new); this.authorizationService = authorizationService; + this.privilegeStore = privilegeStore; } @Override @@ -66,15 +75,33 @@ protected void doExecute(HasPrivilegesRequest request, ActionListener checkPrivileges(request, role, listener), - listener::onFailure)); + role -> resolveApplicationPrivileges(request, ActionListener.wrap( + applicationPrivilegeLookup -> checkPrivileges(request, role, applicationPrivilegeLookup, listener), + listener::onFailure)), + listener::onFailure)); + } + + private void resolveApplicationPrivileges(HasPrivilegesRequest request, + ActionListener> listener) { + final Set applications = getApplicationNames(request); + privilegeStore.getPrivileges(applications, null, listener); + } + + private Set getApplicationNames(HasPrivilegesRequest request) { + return Arrays.stream(request.applicationPrivileges()) + .map(RoleDescriptor.ApplicationResourcePrivileges::getApplication) + .collect(Collectors.toSet()); } private void checkPrivileges(HasPrivilegesRequest request, Role userRole, + Collection applicationPrivileges, ActionListener listener) { - logger.debug(() -> new ParameterizedMessage("Check whether role [{}] has privileges cluster=[{}] index=[{}]", - Strings.arrayToCommaDelimitedString(userRole.names()), Strings.arrayToCommaDelimitedString(request.clusterPrivileges()), - Strings.arrayToCommaDelimitedString(request.indexPrivileges()))); + logger.trace(() -> new ParameterizedMessage("Check whether role [{}] has privileges cluster=[{}] index=[{}] application=[{}]", + Strings.arrayToCommaDelimitedString(userRole.names()), + Strings.arrayToCommaDelimitedString(request.clusterPrivileges()), + Strings.arrayToCommaDelimitedString(request.indexPrivileges()), + Strings.arrayToCommaDelimitedString(request.applicationPrivileges()) + )); Map cluster = new HashMap<>(); for (String checkAction : request.clusterPrivileges()) { @@ -86,30 +113,62 @@ private void checkPrivileges(HasPrivilegesRequest request, Role userRole, final Map predicateCache = new HashMap<>(); - final Map indices = new LinkedHashMap<>(); + final Map indices = new LinkedHashMap<>(); for (RoleDescriptor.IndicesPrivileges check : request.indexPrivileges()) { for (String index : check.getIndices()) { final Map privileges = new HashMap<>(); - final HasPrivilegesResponse.IndexPrivileges existing = indices.get(index); + final HasPrivilegesResponse.ResourcePrivileges existing = indices.get(index); if (existing != null) { privileges.putAll(existing.getPrivileges()); } for (String privilege : check.getPrivileges()) { if (testIndexMatch(index, privilege, userRole, predicateCache)) { - logger.debug(() -> new ParameterizedMessage("Role [{}] has [{}] on [{}]", - Strings.arrayToCommaDelimitedString(userRole.names()), privilege, index)); + logger.debug(() -> new ParameterizedMessage("Role [{}] has [{}] on index [{}]", + Strings.arrayToCommaDelimitedString(userRole.names()), privilege, index)); privileges.put(privilege, true); } else { - logger.debug(() -> new ParameterizedMessage("Role [{}] does not have [{}] on [{}]", - Strings.arrayToCommaDelimitedString(userRole.names()), privilege, index)); + logger.debug(() -> new ParameterizedMessage("Role [{}] does not have [{}] on index [{}]", + Strings.arrayToCommaDelimitedString(userRole.names()), privilege, index)); privileges.put(privilege, false); allMatch = false; } } - indices.put(index, new HasPrivilegesResponse.IndexPrivileges(index, privileges)); + indices.put(index, new HasPrivilegesResponse.ResourcePrivileges(index, privileges)); + } + } + + final Map> privilegesByApplication = new HashMap<>(); + for (String applicationName : getApplicationNames(request)) { + logger.debug("Checking privileges for application {}", applicationName); + final Map appPrivilegesByResource = new LinkedHashMap<>(); + for (RoleDescriptor.ApplicationResourcePrivileges p : request.applicationPrivileges()) { + if (applicationName.equals(p.getApplication())) { + for (String resource : p.getResources()) { + final Map privileges = new HashMap<>(); + final HasPrivilegesResponse.ResourcePrivileges existing = appPrivilegesByResource.get(resource); + if (existing != null) { + privileges.putAll(existing.getPrivileges()); + } + for (String privilege : p.getPrivileges()) { + if (testResourceMatch(applicationName, resource, privilege, userRole, applicationPrivileges)) { + logger.debug(() -> new ParameterizedMessage("Role [{}] has [{} {}] on resource [{}]", + Strings.arrayToCommaDelimitedString(userRole.names()), applicationName, privilege, resource)); + privileges.put(privilege, true); + } else { + logger.debug(() -> new ParameterizedMessage("Role [{}] does not have [{} {}] on resource [{}]", + Strings.arrayToCommaDelimitedString(userRole.names()), applicationName, privilege, resource)); + privileges.put(privilege, false); + allMatch = false; + } + } + appPrivilegesByResource.put(resource, new HasPrivilegesResponse.ResourcePrivileges(resource, privileges)); + } + } } + privilegesByApplication.put(applicationName, appPrivilegesByResource.values()); } - listener.onResponse(new HasPrivilegesResponse(allMatch, cluster, indices.values())); + + listener.onResponse(new HasPrivilegesResponse(allMatch, cluster, indices.values(), privilegesByApplication)); } private boolean testIndexMatch(String checkIndex, String checkPrivilegeName, Role userRole, @@ -139,4 +198,17 @@ private static boolean testIndex(Automaton checkIndex, Automaton roleIndex) { private static boolean testPrivilege(Privilege checkPrivilege, Automaton roleAutomaton) { return Operations.subsetOf(checkPrivilege.getAutomaton(), roleAutomaton); } + + private boolean testResourceMatch(String application, String checkResource, String checkPrivilegeName, Role userRole, + Collection privileges) { + final Set nameSet = Collections.singleton(checkPrivilegeName); + final ApplicationPrivilege checkPrivilege = ApplicationPrivilege.get(application, nameSet, privileges); + assert checkPrivilege.getApplication().equals(application) + : "Privilege " + checkPrivilege + " should have application " + application; + assert checkPrivilege.name().equals(nameSet) + : "Privilege " + checkPrivilege + " should have name " + nameSet; + + return userRole.application().grants(checkPrivilege, checkResource); + } + } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java index 19760ccab0202..9b62037c6fa92 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java @@ -178,8 +178,8 @@ public void authorize(Authentication authentication, String action, TransportReq // first, we'll check if the action is a cluster action. If it is, we'll only check it against the cluster permissions if (ClusterPrivilege.ACTION_MATCHER.test(action)) { - ClusterPermission cluster = permission.cluster(); - if (cluster.check(action) || checkSameUserPermissions(action, request, authentication)) { + final ClusterPermission cluster = permission.cluster(); + if (cluster.check(action, request) || checkSameUserPermissions(action, request, authentication) ) { putTransientIfNonExisting(AuthorizationServiceField.INDICES_PERMISSIONS_KEY, IndicesAccessControl.ALLOW_ALL); auditTrail.accessGranted(authentication, action, request, permission.names()); return; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java index c31c7ea7d578c..35d163e148134 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java @@ -13,6 +13,7 @@ import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.cache.Cache; import org.elasticsearch.common.cache.CacheBuilder; +import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.component.AbstractComponent; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Setting.Property; @@ -29,7 +30,8 @@ import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsDefinition; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsDefinition.FieldGrantExcludeGroup; import org.elasticsearch.xpack.core.security.authz.permission.Role; -import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.Privilege; import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; @@ -52,6 +54,7 @@ import java.util.function.BiConsumer; import java.util.stream.Collectors; +import static org.elasticsearch.common.util.set.Sets.newHashSet; import static org.elasticsearch.xpack.core.security.SecurityField.setting; import static org.elasticsearch.xpack.security.support.SecurityIndexManager.isIndexDeleted; import static org.elasticsearch.xpack.security.support.SecurityIndexManager.isMoveFromRedToNonRed; @@ -81,6 +84,7 @@ public class CompositeRolesStore extends AbstractComponent { private final FileRolesStore fileRolesStore; private final NativeRolesStore nativeRolesStore; private final ReservedRolesStore reservedRolesStore; + private final NativePrivilegeStore privilegeStore; private final XPackLicenseState licenseState; private final Cache, Role> roleCache; private final Set negativeLookupCache; @@ -89,7 +93,7 @@ public class CompositeRolesStore extends AbstractComponent { private final List, ActionListener>>> customRolesProviders; public CompositeRolesStore(Settings settings, FileRolesStore fileRolesStore, NativeRolesStore nativeRolesStore, - ReservedRolesStore reservedRolesStore, + ReservedRolesStore reservedRolesStore, NativePrivilegeStore privilegeStore, List, ActionListener>>> rolesProviders, ThreadContext threadContext, XPackLicenseState licenseState) { super(settings); @@ -99,6 +103,7 @@ public CompositeRolesStore(Settings settings, FileRolesStore fileRolesStore, Nat fileRolesStore.addListener(this::invalidateAll); this.nativeRolesStore = nativeRolesStore; this.reservedRolesStore = reservedRolesStore; + this.privilegeStore = privilegeStore; this.licenseState = licenseState; CacheBuilder, Role> builder = CacheBuilder.builder(); final int cacheSize = CACHE_SIZE_SETTING.get(settings); @@ -118,31 +123,33 @@ public void roles(Set roleNames, FieldPermissionsCache fieldPermissionsC } else { final long invalidationCounter = numInvalidation.get(); roleDescriptors(roleNames, ActionListener.wrap( - (descriptors) -> { - final Role role; + descriptors -> { + final Set effectiveDescriptors; if (licenseState.isDocumentAndFieldLevelSecurityAllowed()) { - role = buildRoleFromDescriptors(descriptors, fieldPermissionsCache); + effectiveDescriptors = descriptors; } else { - final Set filtered = descriptors.stream() + effectiveDescriptors = descriptors.stream() .filter((rd) -> rd.isUsingDocumentOrFieldLevelSecurity() == false) .collect(Collectors.toSet()); - role = buildRoleFromDescriptors(filtered, fieldPermissionsCache); } - - if (role != null) { - try (ReleasableLock ignored = readLock.acquire()) { - /* this is kinda spooky. We use a read/write lock to ensure we don't modify the cache if we hold the write - * lock (fetching stats for instance - which is kinda overkill?) but since we fetching stuff in an async - * fashion we need to make sure that if the cache got invalidated since we started the request we don't - * put a potential stale result in the cache, hence the numInvalidation.get() comparison to the number of - * invalidation when we started. we just try to be on the safe side and don't cache potentially stale - * results*/ - if (invalidationCounter == numInvalidation.get()) { - roleCache.computeIfAbsent(roleNames, (s) -> role); + logger.trace("Building role from descriptors [{}] for names [{}]", effectiveDescriptors, roleNames); + buildRoleFromDescriptors(effectiveDescriptors, fieldPermissionsCache, privilegeStore, ActionListener.wrap(role -> { + if (role != null) { + try (ReleasableLock ignored = readLock.acquire()) { + /* this is kinda spooky. We use a read/write lock to ensure we don't modify the cache if we hold + * the write lock (fetching stats for instance - which is kinda overkill?) but since we fetching + * stuff in an async fashion we need to make sure that if the cache got invalidated since we + * started the request we don't put a potential stale result in the cache, hence the + * numInvalidation.get() comparison to the number of invalidation when we started. we just try to + * be on the safe side and don't cache potentially stale results + */ + if (invalidationCounter == numInvalidation.get()) { + roleCache.computeIfAbsent(roleNames, (s) -> role); + } } } - } - roleActionListener.onResponse(role); + roleActionListener.onResponse(role); + }, roleActionListener::onFailure)); }, roleActionListener::onFailure)); } @@ -239,25 +246,36 @@ private Set difference(Set roleNames, Set descri return Sets.difference(roleNames, foundNames); } - public static Role buildRoleFromDescriptors(Set roleDescriptors, FieldPermissionsCache fieldPermissionsCache) { + public static void buildRoleFromDescriptors(Collection roleDescriptors, FieldPermissionsCache fieldPermissionsCache, + NativePrivilegeStore privilegeStore, ActionListener listener) { if (roleDescriptors.isEmpty()) { - return Role.EMPTY; + listener.onResponse(Role.EMPTY); + return; } + Set clusterPrivileges = new HashSet<>(); + final List conditionalClusterPrivileges = new ArrayList<>(); Set runAs = new HashSet<>(); Map, MergeableIndicesPrivilege> indicesPrivilegesMap = new HashMap<>(); + + // Keyed by application + resource + Map>, Set> applicationPrivilegesMap = new HashMap<>(); + List roleNames = new ArrayList<>(roleDescriptors.size()); for (RoleDescriptor descriptor : roleDescriptors) { roleNames.add(descriptor.getName()); if (descriptor.getClusterPrivileges() != null) { clusterPrivileges.addAll(Arrays.asList(descriptor.getClusterPrivileges())); } + if (descriptor.getConditionalClusterPrivileges() != null) { + conditionalClusterPrivileges.addAll(Arrays.asList(descriptor.getConditionalClusterPrivileges())); + } if (descriptor.getRunAs() != null) { runAs.addAll(Arrays.asList(descriptor.getRunAs())); } IndicesPrivileges[] indicesPrivileges = descriptor.getIndicesPrivileges(); for (IndicesPrivileges indicesPrivilege : indicesPrivileges) { - Set key = Sets.newHashSet(indicesPrivilege.getIndices()); + Set key = newHashSet(indicesPrivilege.getIndices()); // if a index privilege is an explicit denial, then we treat it as non-existent since we skipped these in the past when // merging final boolean isExplicitDenial = @@ -275,19 +293,44 @@ public static Role buildRoleFromDescriptors(Set roleDescriptors, }); } } + for (RoleDescriptor.ApplicationResourcePrivileges appPrivilege : descriptor.getApplicationPrivileges()) { + Tuple> key = new Tuple<>(appPrivilege.getApplication(), newHashSet(appPrivilege.getResources())); + applicationPrivilegesMap.compute(key, (k, v) -> { + if (v == null) { + return newHashSet(appPrivilege.getPrivileges()); + } else { + v.addAll(Arrays.asList(appPrivilege.getPrivileges())); + return v; + } + }); + } } - final Set clusterPrivs = clusterPrivileges.isEmpty() ? null : clusterPrivileges; final Privilege runAsPrivilege = runAs.isEmpty() ? Privilege.NONE : new Privilege(runAs, runAs.toArray(Strings.EMPTY_ARRAY)); - Role.Builder builder = Role.builder(roleNames.toArray(new String[roleNames.size()]), fieldPermissionsCache) - .cluster(ClusterPrivilege.get(clusterPrivs)) + final Role.Builder builder = Role.builder(roleNames.toArray(new String[roleNames.size()]), fieldPermissionsCache) + .cluster(clusterPrivileges, conditionalClusterPrivileges) .runAs(runAsPrivilege); indicesPrivilegesMap.entrySet().forEach((entry) -> { MergeableIndicesPrivilege privilege = entry.getValue(); builder.add(fieldPermissionsCache.getFieldPermissions(privilege.fieldPermissionsDefinition), privilege.query, IndexPrivilege.get(privilege.privileges), privilege.indices.toArray(Strings.EMPTY_ARRAY)); }); - return builder.build(); + + if (applicationPrivilegesMap.isEmpty()) { + listener.onResponse(builder.build()); + } else { + final Set applicationNames = applicationPrivilegesMap.keySet().stream() + .map(Tuple::v1) + .collect(Collectors.toSet()); + final Set applicationPrivilegeNames = applicationPrivilegesMap.values().stream() + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + privilegeStore.getPrivileges(applicationNames, applicationPrivilegeNames, ActionListener.wrap(appPrivileges -> { + applicationPrivilegesMap.forEach((key, names) -> + builder.addApplicationPrivilege(ApplicationPrivilege.get(key.v1(), names, appPrivileges), key.v2())); + listener.onResponse(builder.build()); + }, listener::onFailure)); + } } public void invalidateAll() { @@ -341,11 +384,11 @@ private static class MergeableIndicesPrivilege { MergeableIndicesPrivilege(String[] indices, String[] privileges, @Nullable String[] grantedFields, @Nullable String[] deniedFields, @Nullable BytesReference query) { - this.indices = Sets.newHashSet(Objects.requireNonNull(indices)); - this.privileges = Sets.newHashSet(Objects.requireNonNull(privileges)); + this.indices = newHashSet(Objects.requireNonNull(indices)); + this.privileges = newHashSet(Objects.requireNonNull(privileges)); this.fieldPermissionsDefinition = new FieldPermissionsDefinition(grantedFields, deniedFields); if (query != null) { - this.query = Sets.newHashSet(query); + this.query = newHashSet(query); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativePrivilegeStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativePrivilegeStore.java new file mode 100644 index 0000000000000..807cfff6c2c19 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativePrivilegeStore.java @@ -0,0 +1,278 @@ +/* + * 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.authz.store; + +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.DocWriteResponse; +import org.elasticsearch.action.delete.DeleteResponse; +import org.elasticsearch.action.get.GetResponse; +import org.elasticsearch.action.index.IndexResponse; +import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.action.support.ContextPreservingActionListener; +import org.elasticsearch.action.support.GroupedActionListener; +import org.elasticsearch.action.support.TransportActions; +import org.elasticsearch.action.support.WriteRequest; +import org.elasticsearch.client.Client; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.component.AbstractComponent; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.util.CollectionUtils; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.util.iterable.Iterables; +import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParseException; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.index.query.TermQueryBuilder; +import org.elasticsearch.xpack.core.ClientHelper; +import org.elasticsearch.xpack.core.security.ScrollHelper; +import org.elasticsearch.xpack.core.security.action.role.ClearRolesCacheRequest; +import org.elasticsearch.xpack.core.security.action.role.ClearRolesCacheResponse; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; +import org.elasticsearch.xpack.core.security.client.SecurityClient; +import org.elasticsearch.xpack.security.support.SecurityIndexManager; + +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.Supplier; +import java.util.stream.Collector; +import java.util.stream.Collectors; + +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.elasticsearch.xpack.core.ClientHelper.SECURITY_ORIGIN; +import static org.elasticsearch.xpack.core.ClientHelper.executeAsyncWithOrigin; +import static org.elasticsearch.xpack.core.ClientHelper.stashWithOrigin; +import static org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor.DOC_TYPE_VALUE; +import static org.elasticsearch.xpack.security.support.SecurityIndexManager.SECURITY_INDEX_NAME; + +/** + * {@code NativePrivilegeStore} is a store that reads/writes {@link ApplicationPrivilegeDescriptor} objects, + * from an Elasticsearch index. + */ +public class NativePrivilegeStore extends AbstractComponent { + + private static final Collector, ?, Map>> TUPLES_TO_MAP = Collectors.toMap( + Tuple::v1, + t -> CollectionUtils.newSingletonArrayList(t.v2()), (a, b) -> { + a.addAll(b); + return a; + }); + + private final Client client; + private final SecurityClient securityClient; + private final SecurityIndexManager securityIndexManager; + + public NativePrivilegeStore(Settings settings, Client client, SecurityIndexManager securityIndexManager) { + super(settings); + this.client = client; + this.securityClient = new SecurityClient(client); + this.securityIndexManager = securityIndexManager; + } + + public void getPrivileges(Collection applications, Collection names, + ActionListener> listener) { + if (applications != null && applications.size() == 1 && names != null && names.size() == 1) { + getPrivilege(Objects.requireNonNull(Iterables.get(applications, 0)), Objects.requireNonNull(Iterables.get(names, 0)), + ActionListener.wrap(privilege -> + listener.onResponse(privilege == null ? Collections.emptyList() : Collections.singletonList(privilege)), + listener::onFailure)); + } else { + securityIndexManager.prepareIndexIfNeededThenExecute(listener::onFailure, () -> { + final QueryBuilder query; + final TermQueryBuilder typeQuery = QueryBuilders + .termQuery(ApplicationPrivilegeDescriptor.Fields.TYPE.getPreferredName(), DOC_TYPE_VALUE); + if (isEmpty(applications) && isEmpty(names)) { + query = typeQuery; + } else if (isEmpty(names)) { + query = QueryBuilders.boolQuery().filter(typeQuery).filter( + QueryBuilders.termsQuery(ApplicationPrivilegeDescriptor.Fields.APPLICATION.getPreferredName(), applications)); + } else if (isEmpty(applications)) { + query = QueryBuilders.boolQuery().filter(typeQuery) + .filter(QueryBuilders.termsQuery(ApplicationPrivilegeDescriptor.Fields.NAME.getPreferredName(), names)); + } else { + final String[] docIds = applications.stream() + .flatMap(a -> names.stream().map(n -> toDocId(a, n))) + .toArray(String[]::new); + query = QueryBuilders.boolQuery().filter(typeQuery).filter(QueryBuilders.idsQuery("doc").addIds(docIds)); + } + final Supplier supplier = client.threadPool().getThreadContext().newRestorableContext(false); + try (ThreadContext.StoredContext ignore = stashWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN)) { + SearchRequest request = client.prepareSearch(SECURITY_INDEX_NAME) + .setScroll(TimeValue.timeValueSeconds(10L)) + .setQuery(query) + .setSize(1000) + .setFetchSource(true) + .request(); + logger.trace(() -> + new ParameterizedMessage("Searching for privileges [{}] with query [{}]", names, Strings.toString(query))); + request.indicesOptions().ignoreUnavailable(); + ScrollHelper.fetchAllByEntity(client, request, new ContextPreservingActionListener<>(supplier, listener), + hit -> buildPrivilege(hit.getId(), hit.getSourceRef())); + } + }); + } + } + + private static boolean isEmpty(Collection collection) { + return collection == null || collection.isEmpty(); + } + + public void getPrivilege(String application, String name, ActionListener listener) { + securityIndexManager.prepareIndexIfNeededThenExecute(listener::onFailure, + () -> executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN, + client.prepareGet(SECURITY_INDEX_NAME, "doc", toDocId(application, name)).request(), + new ActionListener() { + @Override + public void onResponse(GetResponse response) { + if (response.isExists()) { + listener.onResponse(buildPrivilege(response.getId(), response.getSourceAsBytesRef())); + } else { + listener.onResponse(null); + } + } + + @Override + public void onFailure(Exception e) { + // if the index or the shard is not there / available we just claim the privilege is not there + if (TransportActions.isShardNotAvailableException(e)) { + logger.warn(new ParameterizedMessage("failed to load privilege [{}] index not available", name), e); + listener.onResponse(null); + } else { + logger.error(new ParameterizedMessage("failed to load privilege [{}]", name), e); + listener.onFailure(e); + } + } + }, + client::get)); + } + + public void putPrivileges(Collection privileges, WriteRequest.RefreshPolicy refreshPolicy, + ActionListener>> listener) { + securityIndexManager.prepareIndexIfNeededThenExecute(listener::onFailure, () -> { + ActionListener groupListener = new GroupedActionListener<>( + ActionListener.wrap((Collection responses) -> { + final Map> createdNames = responses.stream() + .filter(r -> r.getResult() == DocWriteResponse.Result.CREATED) + .map(r -> r.getId()) + .map(NativePrivilegeStore::nameFromDocId) + .collect(TUPLES_TO_MAP); + clearRolesCache(listener, createdNames); + }, listener::onFailure), privileges.size(), Collections.emptyList()); + for (ApplicationPrivilegeDescriptor privilege : privileges) { + innerPutPrivilege(privilege, refreshPolicy, groupListener); + } + }); + } + + private void innerPutPrivilege(ApplicationPrivilegeDescriptor privilege, WriteRequest.RefreshPolicy refreshPolicy, + ActionListener listener) { + try { + final String name = privilege.getName(); + final XContentBuilder xContentBuilder = privilege.toXContent(jsonBuilder(), true); + ClientHelper.executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN, + client.prepareIndex(SECURITY_INDEX_NAME, "doc", toDocId(privilege.getApplication(), name)) + .setSource(xContentBuilder) + .setRefreshPolicy(refreshPolicy) + .request(), listener, client::index); + } catch (Exception e) { + logger.warn("Failed to put privilege {} - {}", Strings.toString(privilege), e.toString()); + listener.onFailure(e); + } + + } + + public void deletePrivileges(String application, Collection names, WriteRequest.RefreshPolicy refreshPolicy, + ActionListener>> listener) { + securityIndexManager.prepareIndexIfNeededThenExecute(listener::onFailure, () -> { + ActionListener groupListener = new GroupedActionListener<>( + ActionListener.wrap(responses -> { + final Map> deletedNames = responses.stream() + .filter(r -> r.getResult() == DocWriteResponse.Result.DELETED) + .map(r -> r.getId()) + .map(NativePrivilegeStore::nameFromDocId) + .collect(TUPLES_TO_MAP); + clearRolesCache(listener, deletedNames); + }, listener::onFailure), names.size(), Collections.emptyList()); + for (String name : names) { + ClientHelper.executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN, + client.prepareDelete(SECURITY_INDEX_NAME, "doc", toDocId(application, name)) + .setRefreshPolicy(refreshPolicy) + .request(), groupListener, client::delete); + } + }); + } + + private void clearRolesCache(ActionListener listener, T value) { + // This currently clears _all_ roles, but could be improved to clear only those roles that reference the affected application + ClearRolesCacheRequest request = new ClearRolesCacheRequest(); + executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN, request, + new ActionListener() { + @Override + public void onResponse(ClearRolesCacheResponse nodes) { + listener.onResponse(value); + } + + @Override + public void onFailure(Exception e) { + logger.error("unable to clear role cache", e); + listener.onFailure( + new ElasticsearchException("clearing the role cache failed. please clear the role cache manually", e)); + } + }, securityClient::clearRolesCache); + } + + private ApplicationPrivilegeDescriptor buildPrivilege(String docId, BytesReference source) { + logger.trace("Building privilege from [{}] [{}]", docId, source == null ? "<>" : source.utf8ToString()); + if (source == null) { + return null; + } + final Tuple name = nameFromDocId(docId); + try { + // EMPTY is safe here because we never use namedObject + + try (StreamInput input = source.streamInput(); + XContentParser parser = XContentType.JSON.xContent().createParser(NamedXContentRegistry.EMPTY, + LoggingDeprecationHandler.INSTANCE, input)) { + final ApplicationPrivilegeDescriptor privilege = ApplicationPrivilegeDescriptor.parse(parser, null, null, true); + assert privilege.getApplication().equals(name.v1()) + : "Incorrect application name for privilege. Expected [" + name.v1() + "] but was " + privilege.getApplication(); + assert privilege.getName().equals(name.v2()) + : "Incorrect name for application privilege. Expected [" + name.v2() + "] but was " + privilege.getName(); + return privilege; + } + } catch (IOException | XContentParseException e) { + logger.error(new ParameterizedMessage("cannot parse application privilege [{}]", name), e); + return null; + } + } + + private static Tuple nameFromDocId(String docId) { + final String name = docId.substring(DOC_TYPE_VALUE.length() + 1); + assert name != null && name.length() > 0 : "Invalid name '" + name + "'"; + final int colon = name.indexOf(':'); + assert colon > 0 : "Invalid name '" + name + "' (missing colon)"; + return new Tuple<>(name.substring(0, colon), name.substring(colon + 1)); + } + + private static String toDocId(String application, String name) { + return DOC_TYPE_VALUE + "_" + application + ":" + name; + } + +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativeRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativeRolesStore.java index a58c7db2ea825..7eec459c7f7db 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativeRolesStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativeRolesStore.java @@ -13,6 +13,7 @@ import org.elasticsearch.action.delete.DeleteRequest; import org.elasticsearch.action.delete.DeleteResponse; import org.elasticsearch.action.get.GetResponse; +import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.index.IndexResponse; import org.elasticsearch.action.search.MultiSearchResponse; import org.elasticsearch.action.search.MultiSearchResponse.Item; @@ -64,6 +65,7 @@ import static org.elasticsearch.xpack.core.ClientHelper.stashWithOrigin; import static org.elasticsearch.xpack.core.security.SecurityField.setting; import static org.elasticsearch.xpack.core.security.authz.RoleDescriptor.ROLE_TYPE; +import static org.elasticsearch.xpack.security.support.SecurityIndexManager.SECURITY_INDEX_NAME; /** * NativeRolesStore is a {@code RolesStore} that, instead of reading from a @@ -182,15 +184,17 @@ void innerPutRole(final PutRoleRequest request, final RoleDescriptor role, final listener.onFailure(e); return; } + final IndexRequest indexRequest = client.prepareIndex(SECURITY_INDEX_NAME, ROLE_DOC_TYPE, getIdForUser(role.getName())) + .setSource(xContentBuilder) + .setRefreshPolicy(request.getRefreshPolicy()) + .request(); executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN, - client.prepareIndex(SecurityIndexManager.SECURITY_INDEX_NAME, ROLE_DOC_TYPE, getIdForUser(role.getName())) - .setSource(xContentBuilder) - .setRefreshPolicy(request.getRefreshPolicy()) - .request(), + indexRequest, new ActionListener() { @Override public void onResponse(IndexResponse indexResponse) { final boolean created = indexResponse.getResult() == DocWriteResponse.Result.CREATED; + logger.trace("Created role: [{}]", indexRequest); clearRoleCache(role.getName(), listener, created); } @@ -243,7 +247,6 @@ public void onResponse(MultiSearchResponse items) { } else { usageStats.put("size", responses[0].getResponse().getHits().getTotalHits()); } - if (responses[1].isFailure()) { usageStats.put("fls", false); } else { @@ -298,7 +301,7 @@ public void onFailure(Exception e) { private void executeGetRoleRequest(String role, ActionListener listener) { securityIndex.prepareIndexIfNeededThenExecute(listener::onFailure, () -> executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN, - client.prepareGet(SecurityIndexManager.SECURITY_INDEX_NAME, + client.prepareGet(SECURITY_INDEX_NAME, ROLE_DOC_TYPE, getIdForUser(role)).request(), listener, client::get)); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/SecurityBaseRestHandler.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/SecurityBaseRestHandler.java index 0b2642ae5bec4..9006ec620b543 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/SecurityBaseRestHandler.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/SecurityBaseRestHandler.java @@ -56,7 +56,7 @@ protected final RestChannelConsumer prepareRequest(RestRequest request, NodeClie /** * Check whether the given request is allowed within the current license state and setup, * and return the name of any unlicensed feature. - * By default this returns an exception is security is not available by the current license or + * By default this returns an exception if security is not available by the current license or * security is not enabled. * Sub-classes can override this method if they have additional requirements. * diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestDeletePrivilegesAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestDeletePrivilegesAction.java new file mode 100644 index 0000000000000..d0cee0dd6b902 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestDeletePrivilegesAction.java @@ -0,0 +1,66 @@ +/* + * 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.rest.action.privilege; + +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.rest.BytesRestResponse; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestResponse; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.rest.action.RestBuilderListener; +import org.elasticsearch.xpack.core.security.action.privilege.DeletePrivilegesResponse; +import org.elasticsearch.xpack.core.security.client.SecurityClient; +import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; + +import static org.elasticsearch.rest.RestRequest.Method.DELETE; + +/** + * Rest action to delete one or more privileges from the security index + */ +public class RestDeletePrivilegesAction extends SecurityBaseRestHandler { + + public RestDeletePrivilegesAction(Settings settings, RestController controller, XPackLicenseState licenseState) { + super(settings, licenseState); + controller.registerHandler(DELETE, "/_xpack/security/privilege/{application}/{privilege}", this); + } + + @Override + public String getName() { + return "xpack_security_delete_privilege_action"; + } + + @Override + public RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException { + final String application = request.param("application"); + final String[] privileges = request.paramAsStringArray("privilege", null); + final String refresh = request.param("refresh"); + return channel -> new SecurityClient(client).prepareDeletePrivileges(application, privileges) + .setRefreshPolicy(refresh) + .execute(new RestBuilderListener(channel) { + @Override + public RestResponse buildResponse(DeletePrivilegesResponse response, XContentBuilder builder) throws Exception { + builder.startObject(); + builder.startObject(application); + for (String privilege : new HashSet<>(Arrays.asList(privileges))) { + builder.field(privilege, Collections.singletonMap("found", response.found().contains(privilege))); + } + builder.endObject(); + builder.endObject(); + return new BytesRestResponse(response.found().isEmpty() ? RestStatus.NOT_FOUND : RestStatus.OK, builder); + } + }); + } + +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetPrivilegesAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetPrivilegesAction.java new file mode 100644 index 0000000000000..8e3c3bbb87e6e --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetPrivilegesAction.java @@ -0,0 +1,91 @@ +/* + * 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.rest.action.privilege; + +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.rest.BytesRestResponse; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestResponse; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.rest.action.RestBuilderListener; +import org.elasticsearch.xpack.core.security.action.privilege.GetPrivilegesResponse; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; +import org.elasticsearch.xpack.core.security.client.SecurityClient; +import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.elasticsearch.rest.RestRequest.Method.GET; + +/** + * Rest action to retrieve an application privilege from the security index + */ +public class RestGetPrivilegesAction extends SecurityBaseRestHandler { + + public RestGetPrivilegesAction(Settings settings, RestController controller, XPackLicenseState licenseState) { + super(settings, licenseState); + controller.registerHandler(GET, "/_xpack/security/privilege/", this); + controller.registerHandler(GET, "/_xpack/security/privilege/{application}", this); + controller.registerHandler(GET, "/_xpack/security/privilege/{application}/{privilege}", this); + } + + @Override + public String getName() { + return "xpack_security_get_privileges_action"; + } + + @Override + public RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException { + final String application = request.param("application"); + final String[] privileges = request.paramAsStringArray("privilege", Strings.EMPTY_ARRAY); + + return channel -> new SecurityClient(client).prepareGetPrivileges(application, privileges) + .execute(new RestBuilderListener(channel) { + @Override + public RestResponse buildResponse(GetPrivilegesResponse response, XContentBuilder builder) throws Exception { + final Map> privsByApp = groupByApplicationName(response.privileges()); + builder.startObject(); + for (String app : privsByApp.keySet()) { + builder.startObject(app); + for (ApplicationPrivilegeDescriptor privilege : privsByApp.get(app)) { + builder.field(privilege.getName(), privilege); + } + builder.endObject(); + } + builder.endObject(); + + // if the user asked for specific privileges, but none of them were found + // we'll return an empty result and 404 status code + if (privileges.length != 0 && response.privileges().length == 0) { + return new BytesRestResponse(RestStatus.NOT_FOUND, builder); + } + + // either the user asked for all privileges, or at least one of the privileges + // was found + return new BytesRestResponse(RestStatus.OK, builder); + } + }); + } + + static Map> groupByApplicationName(ApplicationPrivilegeDescriptor[] privileges) { + return Arrays.stream(privileges).collect(Collectors.toMap( + ApplicationPrivilegeDescriptor::getApplication, + Collections::singleton, + Sets::union + )); + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestPutPrivilegeAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestPutPrivilegeAction.java new file mode 100644 index 0000000000000..6c3ef8e70fabf --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestPutPrivilegeAction.java @@ -0,0 +1,49 @@ +/* + * 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.rest.action.privilege; + +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.xpack.core.security.action.privilege.PutPrivilegesRequestBuilder; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; +import org.elasticsearch.xpack.core.security.client.SecurityClient; +import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler; + +import java.io.IOException; + +import static org.elasticsearch.rest.RestRequest.Method.POST; +import static org.elasticsearch.rest.RestRequest.Method.PUT; + +/** + * Rest endpoint to add one or more {@link ApplicationPrivilege} objects to the security index + */ +public class RestPutPrivilegeAction extends SecurityBaseRestHandler { + + public RestPutPrivilegeAction(Settings settings, RestController controller, XPackLicenseState licenseState) { + super(settings, licenseState); + controller.registerHandler(PUT, "/_xpack/security/privilege/{application}/{privilege}", this); + controller.registerHandler(POST, "/_xpack/security/privilege/{application}/{privilege}", this); + } + + @Override + public String getName() { + return "xpack_security_put_privilege_action"; + } + + @Override + public RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException { + final String application = request.param("application"); + final String privilege = request.param("privilege"); + PutPrivilegesRequestBuilder requestBuilder = new SecurityClient(client) + .preparePutPrivilege(application, privilege, request.requiredContent(), request.getXContentType()) + .setRefreshPolicy(request.param("refresh")); + + return RestPutPrivilegesAction.execute(requestBuilder); + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestPutPrivilegesAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestPutPrivilegesAction.java new file mode 100644 index 0000000000000..eb1104c9bc036 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestPutPrivilegesAction.java @@ -0,0 +1,78 @@ +/* + * 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.rest.action.privilege; + +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.rest.BytesRestResponse; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestResponse; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.rest.action.RestBuilderListener; +import org.elasticsearch.xpack.core.security.action.privilege.PutPrivilegesRequestBuilder; +import org.elasticsearch.xpack.core.security.action.privilege.PutPrivilegesResponse; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; +import org.elasticsearch.xpack.core.security.client.SecurityClient; +import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.elasticsearch.rest.RestRequest.Method.POST; + +/** + * Rest endpoint to add one or more {@link ApplicationPrivilege} objects to the security index + */ +public class RestPutPrivilegesAction extends SecurityBaseRestHandler { + + public RestPutPrivilegesAction(Settings settings, RestController controller, XPackLicenseState licenseState) { + super(settings, licenseState); + controller.registerHandler(POST, "/_xpack/security/privilege/", this); + } + + @Override + public String getName() { + return "xpack_security_put_privileges_action"; + } + + @Override + public RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException { + PutPrivilegesRequestBuilder requestBuilder = new SecurityClient(client) + .preparePutPrivileges(request.requiredContent(), request.getXContentType()) + .setRefreshPolicy(request.param("refresh")); + + return execute(requestBuilder); + } + + static RestChannelConsumer execute(PutPrivilegesRequestBuilder requestBuilder) { + return channel -> requestBuilder.execute(new RestBuilderListener(channel) { + @Override + public RestResponse buildResponse(PutPrivilegesResponse response, XContentBuilder builder) throws Exception { + final List privileges = requestBuilder.request().getPrivileges(); + Map>> result = new HashMap<>(); + privileges.stream() + .map(ApplicationPrivilegeDescriptor::getApplication) + .distinct() + .forEach(a -> result.put(a, new HashMap<>())); + privileges.forEach(privilege -> { + String name = privilege.getName(); + boolean created = response.created().getOrDefault(privilege.getApplication(), Collections.emptyList()).contains(name); + result.get(privilege.getApplication()).put(name, Collections.singletonMap("created", created)); + }); + builder.map(result); + return new BytesRestResponse(RestStatus.OK, builder); + } + }); + } + +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestHasPrivilegesAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestHasPrivilegesAction.java index cc566c212cfb8..4949b18366df9 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestHasPrivilegesAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestHasPrivilegesAction.java @@ -6,8 +6,11 @@ package org.elasticsearch.xpack.security.rest.action.user; import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.rest.BytesRestResponse; import org.elasticsearch.rest.RestChannel; @@ -24,6 +27,8 @@ import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler; import java.io.IOException; +import java.util.List; +import java.util.Map; import static org.elasticsearch.rest.RestRequest.Method.GET; import static org.elasticsearch.rest.RestRequest.Method.POST; @@ -54,8 +59,8 @@ public String getName() { @Override public RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException { final String username = getUsername(request); - HasPrivilegesRequestBuilder requestBuilder = new SecurityClient(client) - .prepareHasPrivileges(username, request.requiredContent(), request.getXContentType()); + final Tuple content = request.contentOrSourceParam(); + HasPrivilegesRequestBuilder requestBuilder = new SecurityClient(client).prepareHasPrivileges(username, content.v2(), content.v1()); return channel -> requestBuilder.execute(new HasPrivilegesRestResponseBuilder(username, channel)); } @@ -84,10 +89,12 @@ public RestResponse buildResponse(HasPrivilegesResponse response, XContentBuilde builder.field("cluster"); builder.map(response.getClusterPrivileges()); - builder.startObject("index"); - for (HasPrivilegesResponse.IndexPrivileges index : response.getIndexPrivileges()) { - builder.field(index.getIndex()); - builder.map(index.getPrivileges()); + appendResources(builder, "index", response.getIndexPrivileges()); + + builder.startObject("application"); + final Map> appPrivileges = response.getApplicationPrivileges(); + for (String app : appPrivileges.keySet()) { + appendResources(builder, app, appPrivileges.get(app)); } builder.endObject(); @@ -95,5 +102,15 @@ public RestResponse buildResponse(HasPrivilegesResponse response, XContentBuilde return new BytesRestResponse(RestStatus.OK, builder); } + private void appendResources(XContentBuilder builder, String field, List privileges) + throws IOException { + builder.startObject(field); + for (HasPrivilegesResponse.ResourcePrivileges privilege : privileges) { + builder.field(privilege.getResource()); + builder.map(privilege.getPrivileges()); + } + builder.endObject(); + } + } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/KibanaSystemRoleIntegTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/KibanaSystemRoleIntegTests.java new file mode 100644 index 0000000000000..65fa6027c627b --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/KibanaSystemRoleIntegTests.java @@ -0,0 +1,66 @@ +/* + * 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.integration; + +import org.elasticsearch.action.DocWriteResponse; +import org.elasticsearch.action.admin.indices.create.CreateIndexResponse; +import org.elasticsearch.action.delete.DeleteResponse; +import org.elasticsearch.action.index.IndexResponse; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.test.SecurityIntegTestCase; +import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; + +import java.util.Locale; + +import static java.util.Collections.singletonMap; +import static org.elasticsearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE; +import static org.hamcrest.Matchers.is; + +public class KibanaSystemRoleIntegTests extends SecurityIntegTestCase { + + protected static final SecureString USERS_PASSWD = new SecureString("change_me".toCharArray()); + + @Override + public String configUsers() { + final String usersPasswdHashed = new String(getFastStoredHashAlgoForTests().hash(USERS_PASSWD)); + return super.configUsers() + + "kibana_system:" + usersPasswdHashed; + } + + @Override + public String configUsersRoles() { + return super.configUsersRoles() + + "kibana_system:kibana_system"; + } + + + public void testCreateIndexDeleteInKibanaIndex() throws Exception { + final String index = randomBoolean()? ".kibana" : ".kibana-" + randomAlphaOfLengthBetween(1, 10).toLowerCase(Locale.ENGLISH); + + if (randomBoolean()) { + CreateIndexResponse createIndexResponse = client().filterWithHeader(singletonMap("Authorization", + UsernamePasswordToken.basicAuthHeaderValue("kibana_system", USERS_PASSWD))) + .admin().indices().prepareCreate(index).get(); + assertThat(createIndexResponse.isAcknowledged(), is(true)); + } + + IndexResponse response = client() + .filterWithHeader(singletonMap("Authorization", UsernamePasswordToken.basicAuthHeaderValue("kibana_system", USERS_PASSWD))) + .prepareIndex() + .setIndex(index) + .setType("dashboard") + .setSource("foo", "bar") + .setRefreshPolicy(IMMEDIATE) + .get(); + assertEquals(DocWriteResponse.Result.CREATED, response.getResult()); + + DeleteResponse deleteResponse = client() + .filterWithHeader(singletonMap("Authorization", UsernamePasswordToken.basicAuthHeaderValue("kibana_system", USERS_PASSWD))) + .prepareDelete(index, "dashboard", response.getId()) + .get(); + assertEquals(DocWriteResponse.Result.DELETED, deleteResponse.getResult()); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/KibanaUserRoleIntegTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/KibanaUserRoleIntegTests.java index cc080a846fae3..b5b939e174410 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/KibanaUserRoleIntegTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/KibanaUserRoleIntegTests.java @@ -20,7 +20,7 @@ import org.elasticsearch.common.collect.ImmutableOpenMap; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.index.query.QueryBuilders; -import org.elasticsearch.test.SecurityIntegTestCase; +import org.elasticsearch.test.NativeRealmIntegTestCase; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; import java.util.Locale; @@ -36,7 +36,7 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; -public class KibanaUserRoleIntegTests extends SecurityIntegTestCase { +public class KibanaUserRoleIntegTests extends NativeRealmIntegTestCase { protected static final SecureString USERS_PASSWD = new SecureString("change_me".toCharArray()); @@ -154,25 +154,25 @@ public void testCreateIndexDeleteInKibanaIndex() throws Exception { if (randomBoolean()) { CreateIndexResponse createIndexResponse = client().filterWithHeader(singletonMap("Authorization", - UsernamePasswordToken.basicAuthHeaderValue("kibana_user", USERS_PASSWD))) - .admin().indices().prepareCreate(index).get(); + UsernamePasswordToken.basicAuthHeaderValue("kibana_user", USERS_PASSWD))) + .admin().indices().prepareCreate(index).get(); assertThat(createIndexResponse.isAcknowledged(), is(true)); } IndexResponse response = client() - .filterWithHeader(singletonMap("Authorization", UsernamePasswordToken.basicAuthHeaderValue("kibana_user", USERS_PASSWD))) - .prepareIndex() - .setIndex(index) - .setType("dashboard") - .setSource("foo", "bar") - .setRefreshPolicy(IMMEDIATE) - .get(); + .filterWithHeader(singletonMap("Authorization", UsernamePasswordToken.basicAuthHeaderValue("kibana_user", USERS_PASSWD))) + .prepareIndex() + .setIndex(index) + .setType("dashboard") + .setSource("foo", "bar") + .setRefreshPolicy(IMMEDIATE) + .get(); assertEquals(DocWriteResponse.Result.CREATED, response.getResult()); DeleteResponse deleteResponse = client() - .filterWithHeader(singletonMap("Authorization", UsernamePasswordToken.basicAuthHeaderValue("kibana_user", USERS_PASSWD))) - .prepareDelete(index, "dashboard", response.getId()) - .get(); + .filterWithHeader(singletonMap("Authorization", UsernamePasswordToken.basicAuthHeaderValue("kibana_user", USERS_PASSWD))) + .prepareDelete(index, "dashboard", response.getId()) + .get(); assertEquals(DocWriteResponse.Result.DELETED, deleteResponse.getResult()); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesRequestBuilderTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesRequestBuilderTests.java new file mode 100644 index 0000000000000..db0548c03ef30 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesRequestBuilderTests.java @@ -0,0 +1,127 @@ +/* + * 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.action.privilege; + +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; + +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.iterableWithSize; +import static org.hamcrest.Matchers.notNullValue; + +public class PutPrivilegesRequestBuilderTests extends ESTestCase { + + public void testBuildRequestWithMultipleElements() throws Exception { + final PutPrivilegesRequestBuilder builder = new PutPrivilegesRequestBuilder(null, PutPrivilegesAction.INSTANCE); + builder.source(new BytesArray("{ " + + "\"foo\":{" + + " \"read\":{ \"application\":\"foo\", \"name\":\"read\", \"actions\":[ \"data:/read/*\", \"admin:/read/*\" ] }," + + " \"write\":{ \"application\":\"foo\", \"name\":\"write\", \"actions\":[ \"data:/write/*\", \"admin:*\" ] }," + + " \"all\":{ \"application\":\"foo\", \"name\":\"all\", \"actions\":[ \"*\" ] }" + + " }, " + + "\"bar\":{" + + " \"read\":{ \"application\":\"bar\", \"name\":\"read\", \"actions\":[ \"read/*\" ] }," + + " \"write\":{ \"application\":\"bar\", \"name\":\"write\", \"actions\":[ \"write/*\" ] }," + + " \"all\":{ \"application\":\"bar\", \"name\":\"all\", \"actions\":[ \"*\" ] }" + + " } " + + "}"), XContentType.JSON); + final List privileges = builder.request().getPrivileges(); + assertThat(privileges, iterableWithSize(6)); + assertThat(privileges, contains( + descriptor("foo", "read", "data:/read/*", "admin:/read/*"), + descriptor("foo", "write", "data:/write/*", "admin:*"), + descriptor("foo", "all", "*"), + descriptor("bar", "read", "read/*"), + descriptor("bar", "write", "write/*"), + descriptor("bar", "all", "*") + )); + } + + private ApplicationPrivilegeDescriptor descriptor(String app, String name, String ... actions) { + return new ApplicationPrivilegeDescriptor(app, name, Sets.newHashSet(actions), Collections.emptyMap()); + } + + public void testBuildRequestFromJsonObject() throws Exception { + final PutPrivilegesRequestBuilder builder = new PutPrivilegesRequestBuilder(null, PutPrivilegesAction.INSTANCE); + builder.source("foo", "read", new BytesArray( + "{ \"application\":\"foo\", \"name\":\"read\", \"actions\":[ \"data:/read/*\", \"admin:/read/*\" ] }" + ), XContentType.JSON); + final List privileges = builder.request().getPrivileges(); + assertThat(privileges, iterableWithSize(1)); + assertThat(privileges, contains(descriptor("foo", "read", "data:/read/*", "admin:/read/*"))); + } + + public void testPrivilegeNameValidationOfSingleElement() throws Exception { + final PutPrivilegesRequestBuilder builder = new PutPrivilegesRequestBuilder(null, PutPrivilegesAction.INSTANCE); + final IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> + builder.source("foo", "write", new BytesArray( + "{ \"application\":\"foo\", \"name\":\"read\", \"actions\":[ \"data:/read/*\", \"admin:/read/*\" ] }" + ), XContentType.JSON)); + assertThat(exception.getMessage(), containsString("write")); + assertThat(exception.getMessage(), containsString("read")); + } + + public void testApplicationNameValidationOfSingleElement() throws Exception { + final PutPrivilegesRequestBuilder builder = new PutPrivilegesRequestBuilder(null, PutPrivilegesAction.INSTANCE); + final IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> + builder.source("bar", "read", new BytesArray( + "{ \"application\":\"foo\", \"name\":\"read\", \"actions\":[ \"data:/read/*\", \"admin:/read/*\" ] }" + ), XContentType.JSON)); + assertThat(exception.getMessage(), containsString("foo")); + assertThat(exception.getMessage(), containsString("bar")); + } + + public void testPrivilegeNameValidationOfMultipleElement() throws Exception { + final PutPrivilegesRequestBuilder builder = new PutPrivilegesRequestBuilder(null, PutPrivilegesAction.INSTANCE); + final IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> + builder.source(new BytesArray("{ \"foo\":{" + + "\"write\":{ \"application\":\"foo\", \"name\":\"read\", \"actions\":[\"data:/read/*\",\"admin:/read/*\"] }," + + "\"all\":{ \"application\":\"foo\", \"name\":\"all\", \"actions\":[ \"/*\" ] }" + + "} }"), XContentType.JSON) + ); + assertThat(exception.getMessage(), containsString("write")); + assertThat(exception.getMessage(), containsString("read")); + } + + public void testApplicationNameValidationOfMultipleElement() throws Exception { + final PutPrivilegesRequestBuilder builder = new PutPrivilegesRequestBuilder(null, PutPrivilegesAction.INSTANCE); + final IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> + builder.source(new BytesArray("{ \"bar\":{" + + "\"read\":{ \"application\":\"foo\", \"name\":\"read\", \"actions\":[ \"data:/read/*\", \"admin:/read/*\" ] }," + + "\"write\":{ \"application\":\"foo\", \"name\":\"write\", \"actions\":[ \"data:/write/*\", \"admin:/*\" ] }," + + "\"all\":{ \"application\":\"foo\", \"name\":\"all\", \"actions\":[ \"/*\" ] }" + + "} }"), XContentType.JSON) + ); + assertThat(exception.getMessage(), containsString("bar")); + assertThat(exception.getMessage(), containsString("foo")); + } + + public void testInferApplicationNameAndPrivilegeName() throws Exception { + final PutPrivilegesRequestBuilder builder = new PutPrivilegesRequestBuilder(null, PutPrivilegesAction.INSTANCE); + builder.source(new BytesArray("{ \"foo\":{" + + "\"read\":{ \"actions\":[ \"data:/read/*\", \"admin:/read/*\" ] }," + + "\"write\":{ \"actions\":[ \"data:/write/*\", \"admin:/*\" ] }," + + "\"all\":{ \"actions\":[ \"*\" ] }" + + "} }"), XContentType.JSON); + assertThat(builder.request().getPrivileges(), iterableWithSize(3)); + for (ApplicationPrivilegeDescriptor p : builder.request().getPrivileges()) { + assertThat(p.getApplication(), equalTo("foo")); + assertThat(p.getName(), notNullValue()); + } + assertThat(builder.request().getPrivileges().get(0).getName(), equalTo("read")); + assertThat(builder.request().getPrivileges().get(1).getName(), equalTo("write")); + assertThat(builder.request().getPrivileges().get(2).getName(), equalTo("all")); + } + +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/HasPrivilegesRequestBuilderTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/HasPrivilegesRequestBuilderTests.java index 2d53a3e6e8615..0b9de2da33288 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/HasPrivilegesRequestBuilderTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/HasPrivilegesRequestBuilderTests.java @@ -114,6 +114,6 @@ public void testMissingPrivilegesThrowsException() throws Exception { final ElasticsearchParseException parseException = expectThrows(ElasticsearchParseException.class, () -> builder.source("elastic", new BytesArray(json.getBytes(StandardCharsets.UTF_8)), XContentType.JSON) ); - assertThat(parseException.getMessage(), containsString("[index] and [cluster] are both missing")); + assertThat(parseException.getMessage(), containsString("[cluster,index,applications] are missing")); } } \ No newline at end of file diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesActionTests.java index cba30cd33e88b..883e961c0b3c0 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesActionTests.java @@ -16,42 +16,59 @@ import org.elasticsearch.common.collect.MapBuilder; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.mock.orig.Mockito; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.junit.annotations.TestLogging; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.Transport; import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesRequest; import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesResponse; -import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesResponse.IndexPrivileges; +import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesResponse.ResourcePrivileges; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.AuthenticationField; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.permission.Role; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authz.AuthorizationService; +import org.elasticsearch.xpack.security.authz.store.NativePrivilegeStore; import org.hamcrest.Matchers; import org.junit.Before; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import static java.util.Collections.emptyMap; +import static org.elasticsearch.common.util.set.Sets.newHashSet; +import static org.hamcrest.Matchers.arrayWithSize; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.iterableWithSize; import static org.hamcrest.Matchers.notNullValue; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +@TestLogging("org.elasticsearch.xpack.security.action.user.TransportHasPrivilegesAction:TRACE," + + "org.elasticsearch.xpack.core.security.authz.permission.ApplicationPermission:DEBUG") public class TransportHasPrivilegesActionTests extends ESTestCase { private User user; private Role role; private TransportHasPrivilegesAction action; + private List applicationPrivileges; @Before public void setup() { @@ -75,8 +92,19 @@ public void setup() { return null; }).when(authorizationService).roles(eq(user), any(ActionListener.class)); - action = new TransportHasPrivilegesAction(settings, threadPool, transportService, - mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), authorizationService); + applicationPrivileges = new ArrayList<>(); + NativePrivilegeStore privilegeStore = mock(NativePrivilegeStore.class); + Mockito.doAnswer(inv -> { + assertThat(inv.getArguments(), arrayWithSize(3)); + ActionListener> listener + = (ActionListener>) inv.getArguments()[2]; + logger.info("Privileges for ({}) are {}", Arrays.toString(inv.getArguments()), applicationPrivileges); + listener.onResponse(applicationPrivileges); + return null; + }).when(privilegeStore).getPrivileges(any(Collection.class), any(Collection.class), any(ActionListener.class)); + + action = new TransportHasPrivilegesAction(settings, threadPool, transportService, mock(ActionFilters.class), + mock(IndexNameExpressionResolver.class), authorizationService, privilegeStore); } /** @@ -93,6 +121,7 @@ public void testNamedIndexPrivilegesMatchApplicableActions() throws Exception { .indices("academy") .privileges(DeleteAction.NAME, IndexAction.NAME) .build()); + request.applicationPrivileges(new RoleDescriptor.ApplicationResourcePrivileges[0]); final PlainActionFuture future = new PlainActionFuture(); action.doExecute(request, future); @@ -104,8 +133,8 @@ public void testNamedIndexPrivilegesMatchApplicableActions() throws Exception { assertThat(response.getClusterPrivileges().get(ClusterHealthAction.NAME), equalTo(true)); assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(1)); - final IndexPrivileges result = response.getIndexPrivileges().get(0); - assertThat(result.getIndex(), equalTo("academy")); + final ResourcePrivileges result = response.getIndexPrivileges().get(0); + assertThat(result.getResource(), equalTo("academy")); assertThat(result.getPrivileges().size(), equalTo(2)); assertThat(result.getPrivileges().get(DeleteAction.NAME), equalTo(true)); assertThat(result.getPrivileges().get(IndexAction.NAME), equalTo(true)); @@ -129,6 +158,7 @@ public void testMatchSubsetOfPrivileges() throws Exception { .indices("academy", "initiative", "school") .privileges("delete", "index", "manage") .build()); + request.applicationPrivileges(new RoleDescriptor.ApplicationResourcePrivileges[0]); final PlainActionFuture future = new PlainActionFuture(); action.doExecute(request, future); @@ -140,23 +170,23 @@ public void testMatchSubsetOfPrivileges() throws Exception { assertThat(response.getClusterPrivileges().get("manage"), equalTo(false)); assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(3)); - final IndexPrivileges academy = response.getIndexPrivileges().get(0); - final IndexPrivileges initiative = response.getIndexPrivileges().get(1); - final IndexPrivileges school = response.getIndexPrivileges().get(2); + final ResourcePrivileges academy = response.getIndexPrivileges().get(0); + final ResourcePrivileges initiative = response.getIndexPrivileges().get(1); + final ResourcePrivileges school = response.getIndexPrivileges().get(2); - assertThat(academy.getIndex(), equalTo("academy")); + assertThat(academy.getResource(), equalTo("academy")); assertThat(academy.getPrivileges().size(), equalTo(3)); assertThat(academy.getPrivileges().get("index"), equalTo(true)); // explicit assertThat(academy.getPrivileges().get("delete"), equalTo(false)); assertThat(academy.getPrivileges().get("manage"), equalTo(false)); - assertThat(initiative.getIndex(), equalTo("initiative")); + assertThat(initiative.getResource(), equalTo("initiative")); assertThat(initiative.getPrivileges().size(), equalTo(3)); assertThat(initiative.getPrivileges().get("index"), equalTo(true)); // implied by write assertThat(initiative.getPrivileges().get("delete"), equalTo(true)); // implied by write assertThat(initiative.getPrivileges().get("manage"), equalTo(false)); - assertThat(school.getIndex(), equalTo("school")); + assertThat(school.getResource(), equalTo("school")); assertThat(school.getPrivileges().size(), equalTo(3)); assertThat(school.getPrivileges().get("index"), equalTo(false)); assertThat(school.getPrivileges().get("delete"), equalTo(false)); @@ -178,8 +208,8 @@ public void testMatchNothing() throws Exception { .build(), Strings.EMPTY_ARRAY); assertThat(response.isCompleteMatch(), is(false)); assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(1)); - final IndexPrivileges result = response.getIndexPrivileges().get(0); - assertThat(result.getIndex(), equalTo("academy")); + final ResourcePrivileges result = response.getIndexPrivileges().get(0); + assertThat(result.getResource(), equalTo("academy")); assertThat(result.getPrivileges().size(), equalTo(2)); assertThat(result.getPrivileges().get("read"), equalTo(false)); assertThat(result.getPrivileges().get("write"), equalTo(false)); @@ -192,10 +222,20 @@ public void testMatchNothing() throws Exception { * does the user have ___ privilege on a wildcard that covers (is a superset of) this pattern? */ public void testWildcardHandling() throws Exception { + final ApplicationPrivilege kibanaRead = defineApplicationPrivilege("kibana", "read", + "data:read/*", "action:login", "action:view/dashboard"); + final ApplicationPrivilege kibanaWrite = defineApplicationPrivilege("kibana", "write", + "data:write/*", "action:login", "action:view/dashboard"); + final ApplicationPrivilege kibanaAdmin = defineApplicationPrivilege("kibana", "admin", + "action:login", "action:manage/*"); + final ApplicationPrivilege kibanaViewSpace = defineApplicationPrivilege("kibana", "view-space", + "action:login", "space:view/*"); role = Role.builder("test3") .add(IndexPrivilege.ALL, "logstash-*", "foo?") .add(IndexPrivilege.READ, "abc*") .add(IndexPrivilege.WRITE, "*xyz") + .addApplicationPrivilege(kibanaRead, Collections.singleton("*")) + .addApplicationPrivilege(kibanaViewSpace, newHashSet("space/engineering/*", "space/builds")) .build(); final HasPrivilegesRequest request = new HasPrivilegesRequest(); @@ -231,6 +271,20 @@ public void testWildcardHandling() throws Exception { .privileges("read", "write", "manage") // read = No, write = Yes (WRITE, "*xyz"), manage = No .build() ); + + request.applicationPrivileges( + RoleDescriptor.ApplicationResourcePrivileges.builder() + .resources("*") + .application("kibana") + .privileges(Sets.union(kibanaRead.name(), kibanaWrite.name())) // read = Yes, write = No + .build(), + RoleDescriptor.ApplicationResourcePrivileges.builder() + .resources("space/engineering/project-*", "space/*") // project-* = Yes, space/* = Not + .application("kibana") + .privileges("space:view/dashboard") + .build() + ); + final PlainActionFuture future = new PlainActionFuture(); action.doExecute(request, future); @@ -239,15 +293,28 @@ public void testWildcardHandling() throws Exception { assertThat(response.isCompleteMatch(), is(false)); assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(8)); assertThat(response.getIndexPrivileges(), containsInAnyOrder( - new IndexPrivileges("logstash-2016-*", Collections.singletonMap("write", true)), - new IndexPrivileges("logstash-*", Collections.singletonMap("read", true)), - new IndexPrivileges("log*", Collections.singletonMap("manage", false)), - new IndexPrivileges("foo?", Collections.singletonMap("read", true)), - new IndexPrivileges("foo*", Collections.singletonMap("read", false)), - new IndexPrivileges("abcd*", mapBuilder().put("read", true).put("write", false).map()), - new IndexPrivileges("abc*xyz", mapBuilder().put("read", true).put("write", true).put("manage", false).map()), - new IndexPrivileges("a*xyz", mapBuilder().put("read", false).put("write", true).put("manage", false).map()) + new ResourcePrivileges("logstash-2016-*", Collections.singletonMap("write", true)), + new ResourcePrivileges("logstash-*", Collections.singletonMap("read", true)), + new ResourcePrivileges("log*", Collections.singletonMap("manage", false)), + new ResourcePrivileges("foo?", Collections.singletonMap("read", true)), + new ResourcePrivileges("foo*", Collections.singletonMap("read", false)), + new ResourcePrivileges("abcd*", mapBuilder().put("read", true).put("write", false).map()), + new ResourcePrivileges("abc*xyz", mapBuilder().put("read", true).put("write", true).put("manage", false).map()), + new ResourcePrivileges("a*xyz", mapBuilder().put("read", false).put("write", true).put("manage", false).map()) )); + assertThat(response.getApplicationPrivileges().entrySet(), Matchers.iterableWithSize(1)); + final List kibanaPrivileges = response.getApplicationPrivileges().get("kibana"); + assertThat(kibanaPrivileges, Matchers.iterableWithSize(3)); + assertThat(Strings.collectionToCommaDelimitedString(kibanaPrivileges), kibanaPrivileges, containsInAnyOrder( + new ResourcePrivileges("*", mapBuilder().put("read", true).put("write", false).map()), + new ResourcePrivileges("space/engineering/project-*", Collections.singletonMap("space:view/dashboard", true)), + new ResourcePrivileges("space/*", Collections.singletonMap("space:view/dashboard", false)) + )); + } + + private ApplicationPrivilege defineApplicationPrivilege(String app, String name, String ... actions) { + this.applicationPrivileges.add(new ApplicationPrivilegeDescriptor(app, name, newHashSet(actions), emptyMap())); + return new ApplicationPrivilege(app, name, actions); } public void testCheckingIndexPermissionsDefinedOnDifferentPatterns() throws Exception { @@ -263,27 +330,152 @@ public void testCheckingIndexPermissionsDefinedOnDifferentPatterns() throws Exce assertThat(response.isCompleteMatch(), is(false)); assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(2)); assertThat(response.getIndexPrivileges(), containsInAnyOrder( - new IndexPrivileges("apache-2016-12", + new ResourcePrivileges("apache-2016-12", MapBuilder.newMapBuilder(new LinkedHashMap()) .put("index", true).put("delete", true).map()), - new IndexPrivileges("apache-2017-01", + new ResourcePrivileges("apache-2017-01", MapBuilder.newMapBuilder(new LinkedHashMap()) .put("index", true).put("delete", false).map() ) )); } + public void testCheckingApplicationPrivilegesOnDifferentApplicationsAndResources() throws Exception { + final ApplicationPrivilege app1Read = defineApplicationPrivilege("app1", "read", "data:read/*"); + final ApplicationPrivilege app1Write = defineApplicationPrivilege("app1", "write", "data:write/*"); + final ApplicationPrivilege app1All = defineApplicationPrivilege("app1", "all", "*"); + final ApplicationPrivilege app2Read = defineApplicationPrivilege("app2", "read", "data:read/*"); + final ApplicationPrivilege app2Write = defineApplicationPrivilege("app2", "write", "data:write/*"); + final ApplicationPrivilege app2All = defineApplicationPrivilege("app2", "all", "*"); + + role = Role.builder("test-role") + .addApplicationPrivilege(app1Read, Collections.singleton("foo/*")) + .addApplicationPrivilege(app1All, Collections.singleton("foo/bar/baz")) + .addApplicationPrivilege(app2Read, Collections.singleton("foo/bar/*")) + .addApplicationPrivilege(app2Write, Collections.singleton("*/bar/*")) + .build(); + + final HasPrivilegesResponse response = hasPrivileges(new RoleDescriptor.IndicesPrivileges[0], + new RoleDescriptor.ApplicationResourcePrivileges[]{ + RoleDescriptor.ApplicationResourcePrivileges.builder() + .application("app1") + .resources("foo/1", "foo/bar/2", "foo/bar/baz", "baz/bar/foo") + .privileges("read", "write", "all") + .build(), + RoleDescriptor.ApplicationResourcePrivileges.builder() + .application("app2") + .resources("foo/1", "foo/bar/2", "foo/bar/baz", "baz/bar/foo") + .privileges("read", "write", "all") + .build() + }, Strings.EMPTY_ARRAY); + + assertThat(response.isCompleteMatch(), is(false)); + assertThat(response.getIndexPrivileges(), Matchers.emptyIterable()); + assertThat(response.getApplicationPrivileges().entrySet(), Matchers.iterableWithSize(2)); + final List app1 = response.getApplicationPrivileges().get("app1"); + assertThat(app1, Matchers.iterableWithSize(4)); + assertThat(Strings.collectionToCommaDelimitedString(app1), app1, containsInAnyOrder( + new ResourcePrivileges("foo/1", MapBuilder.newMapBuilder(new LinkedHashMap()) + .put("read", true).put("write", false).put("all", false).map()), + new ResourcePrivileges("foo/bar/2", MapBuilder.newMapBuilder(new LinkedHashMap()) + .put("read", true).put("write", false).put("all", false).map()), + new ResourcePrivileges("foo/bar/baz", MapBuilder.newMapBuilder(new LinkedHashMap()) + .put("read", true).put("write", true).put("all", true).map()), + new ResourcePrivileges("baz/bar/foo", MapBuilder.newMapBuilder(new LinkedHashMap()) + .put("read", false).put("write", false).put("all", false).map()) + )); + final List app2 = response.getApplicationPrivileges().get("app2"); + assertThat(app2, Matchers.iterableWithSize(4)); + assertThat(Strings.collectionToCommaDelimitedString(app2), app2, containsInAnyOrder( + new ResourcePrivileges("foo/1", MapBuilder.newMapBuilder(new LinkedHashMap()) + .put("read", false).put("write", false).put("all", false).map()), + new ResourcePrivileges("foo/bar/2", MapBuilder.newMapBuilder(new LinkedHashMap()) + .put("read", true).put("write", true).put("all", false).map()), + new ResourcePrivileges("foo/bar/baz", MapBuilder.newMapBuilder(new LinkedHashMap()) + .put("read", true).put("write", true).put("all", false).map()), + new ResourcePrivileges("baz/bar/foo", MapBuilder.newMapBuilder(new LinkedHashMap()) + .put("read", false).put("write", true).put("all", false).map()) + )); + } + + public void testCheckingApplicationPrivilegesWithComplexNames() throws Exception { + final String appName = randomAlphaOfLength(1).toLowerCase(Locale.ROOT) + randomAlphaOfLengthBetween(3, 10); + final String action1 = randomAlphaOfLength(1).toLowerCase(Locale.ROOT) + randomAlphaOfLengthBetween(2, 5); + final String action2 = randomAlphaOfLength(1).toLowerCase(Locale.ROOT) + randomAlphaOfLengthBetween(6, 9); + + final ApplicationPrivilege priv1 = defineApplicationPrivilege(appName, action1, "DATA:read/*", "ACTION:" + action1); + final ApplicationPrivilege priv2 = defineApplicationPrivilege(appName, action2, "DATA:read/*", "ACTION:" + action2); + + role = Role.builder("test-write") + .addApplicationPrivilege(priv1, Collections.singleton("user/*/name")) + .build(); + + final HasPrivilegesResponse response = hasPrivileges( + new RoleDescriptor.IndicesPrivileges[0], + new RoleDescriptor.ApplicationResourcePrivileges[]{ + RoleDescriptor.ApplicationResourcePrivileges.builder() + .application(appName) + .resources("user/hawkeye/name") + .privileges("DATA:read/user/*", "ACTION:" + action1, "ACTION:" + action2, action1, action2) + .build() + }, + "monitor"); + assertThat(response.isCompleteMatch(), is(false)); + assertThat(response.getApplicationPrivileges().keySet(), containsInAnyOrder(appName)); + assertThat(response.getApplicationPrivileges().get(appName), iterableWithSize(1)); + assertThat(response.getApplicationPrivileges().get(appName), containsInAnyOrder( + new ResourcePrivileges("user/hawkeye/name", MapBuilder.newMapBuilder(new LinkedHashMap()) + .put("DATA:read/user/*", true) + .put("ACTION:" + action1, true) + .put("ACTION:" + action2, false) + .put(action1, true) + .put(action2, false) + .map()) + )); + } + public void testIsCompleteMatch() throws Exception { + final ApplicationPrivilege kibanaRead = defineApplicationPrivilege("kibana", "read", "data:read/*"); + final ApplicationPrivilege kibanaWrite = defineApplicationPrivilege("kibana", "write", "data:write/*"); role = Role.builder("test-write") .cluster(ClusterPrivilege.MONITOR) .add(IndexPrivilege.READ, "read-*") .add(IndexPrivilege.ALL, "all-*") + .addApplicationPrivilege(kibanaRead, Collections.singleton("*")) .build(); assertThat(hasPrivileges(indexPrivileges("read", "read-123", "read-456", "all-999"), "monitor").isCompleteMatch(), is(true)); assertThat(hasPrivileges(indexPrivileges("read", "read-123", "read-456", "all-999"), "manage").isCompleteMatch(), is(false)); assertThat(hasPrivileges(indexPrivileges("write", "read-123", "read-456", "all-999"), "monitor").isCompleteMatch(), is(false)); assertThat(hasPrivileges(indexPrivileges("write", "read-123", "read-456", "all-999"), "manage").isCompleteMatch(), is(false)); + assertThat(hasPrivileges( + new RoleDescriptor.IndicesPrivileges[]{ + RoleDescriptor.IndicesPrivileges.builder() + .indices("read-a") + .privileges("read") + .build(), + RoleDescriptor.IndicesPrivileges.builder() + .indices("all-b") + .privileges("read", "write") + .build() + }, + new RoleDescriptor.ApplicationResourcePrivileges[]{ + RoleDescriptor.ApplicationResourcePrivileges.builder() + .application("kibana") + .resources("*") + .privileges("read") + .build() + }, + "monitor").isCompleteMatch(), is(true)); + assertThat(hasPrivileges( + new RoleDescriptor.IndicesPrivileges[]{indexPrivileges("read", "read-123", "read-456", "all-999")}, + new RoleDescriptor.ApplicationResourcePrivileges[]{ + RoleDescriptor.ApplicationResourcePrivileges.builder() + .application("kibana").resources("*").privileges("read").build(), + RoleDescriptor.ApplicationResourcePrivileges.builder() + .application("kibana").resources("*").privileges("write").build() + }, + "monitor").isCompleteMatch(), is(false)); } private RoleDescriptor.IndicesPrivileges indexPrivileges(String priv, String... indices) { @@ -295,10 +487,21 @@ private RoleDescriptor.IndicesPrivileges indexPrivileges(String priv, String... private HasPrivilegesResponse hasPrivileges(RoleDescriptor.IndicesPrivileges indicesPrivileges, String... clusterPrivileges) throws Exception { + return hasPrivileges( + new RoleDescriptor.IndicesPrivileges[]{indicesPrivileges}, + new RoleDescriptor.ApplicationResourcePrivileges[0], + clusterPrivileges + ); + } + + private HasPrivilegesResponse hasPrivileges(RoleDescriptor.IndicesPrivileges[] indicesPrivileges, + RoleDescriptor.ApplicationResourcePrivileges[] appPrivileges, + String... clusterPrivileges) throws Exception { final HasPrivilegesRequest request = new HasPrivilegesRequest(); request.username(user.principal()); request.clusterPrivileges(clusterPrivileges); request.indexPrivileges(indicesPrivileges); + request.applicationPrivileges(appPrivileges); final PlainActionFuture future = new PlainActionFuture(); action.doExecute(request, future); final HasPrivilegesResponse response = future.get(); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/AuditTrailTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/AuditTrailTests.java index e64d9bb7e4447..b00bff01aa7f7 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/AuditTrailTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/AuditTrailTests.java @@ -76,7 +76,7 @@ public String configUsers() { public String configUsersRoles() { return super.configUsersRoles() + ROLE_CAN_RUN_AS + ":" + AUTHENTICATE_USER + "\n" - + "kibana_user:" + EXECUTE_USER; + + "monitoring_user:" + EXECUTE_USER; } @Override diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ESNativeRealmMigrateToolTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ESNativeRealmMigrateToolTests.java index 04558f148dae9..7526aa5c9bcbb 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ESNativeRealmMigrateToolTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ESNativeRealmMigrateToolTests.java @@ -48,7 +48,7 @@ protected MigrateUserOrRoles newMigrateUserOrRoles() { @Override protected Environment createEnv(Terminal terminal, Map settings) throws UserException { Settings.Builder builder = Settings.builder(); - settings.forEach((k,v) -> builder.put(k, v)); + settings.forEach((k, v) -> builder.put(k, v)); return TestEnvironment.newEnvironment(builder.build()); } @@ -76,9 +76,11 @@ public void testRoleJson() throws Exception { String[] runAs = Strings.EMPTY_ARRAY; RoleDescriptor rd = new RoleDescriptor("rolename", cluster, ips, runAs); assertThat(ESNativeRealmMigrateTool.MigrateUserOrRoles.createRoleJson(rd), - equalTo("{\"cluster\":[],\"indices\":[{\"names\":[\"i1\",\"i2\",\"i3\"]," + - "\"privileges\":[\"all\"],\"field_security\":{\"grant\":[\"body\"]}}]," + - "\"run_as\":[],\"metadata\":{},\"type\":\"role\"}")); + equalTo("{\"cluster\":[]," + + "\"indices\":[{\"names\":[\"i1\",\"i2\",\"i3\"]," + + "\"privileges\":[\"all\"],\"field_security\":{\"grant\":[\"body\"]}}]," + + "\"applications\":[]," + + "\"run_as\":[],\"metadata\":{},\"type\":\"role\"}")); } public void testTerminalLogger() throws Exception { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealmIntegTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealmIntegTests.java index a880c22ccda8b..78e56a3e1c45b 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealmIntegTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealmIntegTests.java @@ -22,6 +22,7 @@ import org.elasticsearch.test.NativeRealmIntegTestCase; import org.elasticsearch.test.SecuritySettingsSource; import org.elasticsearch.test.SecuritySettingsSourceField; +import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.xpack.core.XPackFeatureSet; import org.elasticsearch.xpack.core.action.XPackUsageRequestBuilder; import org.elasticsearch.xpack.core.action.XPackUsageResponse; @@ -66,6 +67,7 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; +import static org.mockito.Mockito.mock; /** * Tests for the NativeUsersStore and NativeRolesStore @@ -357,10 +359,11 @@ public void testCreateAndUpdateRole() { assertThat(e.status(), is(RestStatus.FORBIDDEN)); } } else { + final TransportRequest request = mock(TransportRequest.class); GetRolesResponse getRolesResponse = c.prepareGetRoles().names("test_role").get(); assertTrue("test_role does not exist!", getRolesResponse.hasRoles()); assertTrue("any cluster permission should be authorized", - Role.builder(getRolesResponse.roles()[0], null).build().cluster().check("cluster:admin/foo")); + Role.builder(getRolesResponse.roles()[0], null).build().cluster().check("cluster:admin/foo", request)); c.preparePutRole("test_role") .cluster("none") @@ -371,7 +374,7 @@ public void testCreateAndUpdateRole() { assertTrue("test_role does not exist!", getRolesResponse.hasRoles()); assertFalse("no cluster permission should be authorized", - Role.builder(getRolesResponse.roles()[0], null).build().cluster().check("cluster:admin/bar")); + Role.builder(getRolesResponse.roles()[0], null).build().cluster().check("cluster:admin/bar", request)); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java index a125bf0bee1d3..3215e90417928 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java @@ -90,6 +90,8 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportActionProxy; import org.elasticsearch.transport.TransportRequest; +import org.elasticsearch.xpack.core.security.action.privilege.DeletePrivilegesAction; +import org.elasticsearch.xpack.core.security.action.privilege.DeletePrivilegesRequest; import org.elasticsearch.xpack.core.security.action.user.AuthenticateAction; import org.elasticsearch.xpack.core.security.action.user.AuthenticateRequest; import org.elasticsearch.xpack.core.security.action.user.AuthenticateRequestBuilder; @@ -113,6 +115,9 @@ import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsCache; import org.elasticsearch.xpack.core.security.authz.permission.Role; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege; import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; import org.elasticsearch.xpack.core.security.user.AnonymousUser; import org.elasticsearch.xpack.core.security.user.ElasticUser; @@ -122,6 +127,7 @@ import org.elasticsearch.xpack.security.audit.AuditTrailService; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; +import org.elasticsearch.xpack.security.authz.store.NativePrivilegeStore; import org.elasticsearch.xpack.sql.action.SqlQueryAction; import org.elasticsearch.xpack.sql.action.SqlQueryRequest; import org.junit.Before; @@ -129,12 +135,15 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.function.Predicate; import static java.util.Arrays.asList; import static org.elasticsearch.test.SecurityTestsUtils.assertAuthenticationException; @@ -173,8 +182,8 @@ public void setup() { rolesStore = mock(CompositeRolesStore.class); clusterService = mock(ClusterService.class); final Settings settings = Settings.builder() - .put("search.remote.other_cluster.seeds", "localhost:9999") - .build(); + .put("search.remote.other_cluster.seeds", "localhost:9999") + .build(); final ClusterSettings clusterSettings = new ClusterSettings(settings, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS); when(clusterService.getClusterSettings()).thenReturn(clusterSettings); auditTrail = mock(AuditTrailService.class); @@ -182,9 +191,20 @@ public void setup() { threadPool = mock(ThreadPool.class); when(threadPool.getThreadContext()).thenReturn(threadContext); final FieldPermissionsCache fieldPermissionsCache = new FieldPermissionsCache(settings); + + final NativePrivilegeStore privilegesStore = mock(NativePrivilegeStore.class); + doAnswer(i -> { + assertThat(i.getArguments().length, equalTo(3)); + final Object arg2 = i.getArguments()[2]; + assertThat(arg2, instanceOf(ActionListener.class)); + ActionListener> listener = (ActionListener>) arg2; + listener.onResponse(Collections.emptyList()); + return null; + } + ).when(privilegesStore).getPrivileges(any(Collection.class), any(Collection.class), any(ActionListener.class)); + doAnswer((i) -> { - ActionListener callback = - (ActionListener) i.getArguments()[2]; + ActionListener callback = (ActionListener) i.getArguments()[2]; Set names = (Set) i.getArguments()[0]; assertNotNull(names); Set roleDescriptors = new HashSet<>(); @@ -198,22 +218,23 @@ public void setup() { if (roleDescriptors.isEmpty()) { callback.onResponse(Role.EMPTY); } else { - callback.onResponse( - CompositeRolesStore.buildRoleFromDescriptors(roleDescriptors, fieldPermissionsCache)); + CompositeRolesStore.buildRoleFromDescriptors(roleDescriptors, fieldPermissionsCache, privilegesStore, + ActionListener.wrap(r -> callback.onResponse(r), callback::onFailure) + ); } return Void.TYPE; }).when(rolesStore).roles(any(Set.class), any(FieldPermissionsCache.class), any(ActionListener.class)); authorizationService = new AuthorizationService(settings, rolesStore, clusterService, - auditTrail, new DefaultAuthenticationFailureHandler(), threadPool, new AnonymousUser(settings)); + auditTrail, new DefaultAuthenticationFailureHandler(), threadPool, new AnonymousUser(settings)); } private void authorize(Authentication authentication, String action, TransportRequest request) { PlainActionFuture future = new PlainActionFuture(); AuthorizationUtils.AsyncAuthorizer authorizer = new AuthorizationUtils.AsyncAuthorizer(authentication, future, - (userRoles, runAsRoles) -> { - authorizationService.authorize(authentication, action, request, userRoles, runAsRoles); - future.onResponse(null); - }); + (userRoles, runAsRoles) -> { + authorizationService.authorize(authentication, action, request, userRoles, runAsRoles); + future.onResponse(null); + }); authorizer.authorize(authorizationService); future.actionGet(); } @@ -225,11 +246,11 @@ public void testActionsSystemUserIsAuthorized() { Authentication authentication = createAuthentication(SystemUser.INSTANCE); authorize(authentication, "indices:monitor/whatever", request); verify(auditTrail).accessGranted(authentication, "indices:monitor/whatever", request, - new String[] { SystemUser.ROLE_NAME }); + new String[]{SystemUser.ROLE_NAME}); authentication = createAuthentication(SystemUser.INSTANCE); authorize(authentication, "internal:whatever", request); - verify(auditTrail).accessGranted(authentication, "internal:whatever", request, new String[] { SystemUser.ROLE_NAME }); + verify(auditTrail).accessGranted(authentication, "internal:whatever", request, new String[]{SystemUser.ROLE_NAME}); verifyNoMoreInteractions(auditTrail); } @@ -237,9 +258,9 @@ public void testIndicesActionsAreNotAuthorized() { final TransportRequest request = mock(TransportRequest.class); final Authentication authentication = createAuthentication(SystemUser.INSTANCE); assertThrowsAuthorizationException( - () -> authorize(authentication, "indices:", request), - "indices:", SystemUser.INSTANCE.principal()); - verify(auditTrail).accessDenied(authentication, "indices:", request, new String[] { SystemUser.ROLE_NAME }); + () -> authorize(authentication, "indices:", request), + "indices:", SystemUser.INSTANCE.principal()); + verify(auditTrail).accessDenied(authentication, "indices:", request, new String[]{SystemUser.ROLE_NAME}); verifyNoMoreInteractions(auditTrail); } @@ -247,10 +268,10 @@ public void testClusterAdminActionsAreNotAuthorized() { final TransportRequest request = mock(TransportRequest.class); final Authentication authentication = createAuthentication(SystemUser.INSTANCE); assertThrowsAuthorizationException( - () -> authorize(authentication, "cluster:admin/whatever", request), - "cluster:admin/whatever", SystemUser.INSTANCE.principal()); + () -> authorize(authentication, "cluster:admin/whatever", request), + "cluster:admin/whatever", SystemUser.INSTANCE.principal()); verify(auditTrail).accessDenied(authentication, "cluster:admin/whatever", request, - new String[] { SystemUser.ROLE_NAME }); + new String[]{SystemUser.ROLE_NAME}); verifyNoMoreInteractions(auditTrail); } @@ -258,10 +279,50 @@ public void testClusterAdminSnapshotStatusActionIsNotAuthorized() { final TransportRequest request = mock(TransportRequest.class); final Authentication authentication = createAuthentication(SystemUser.INSTANCE); assertThrowsAuthorizationException( - () -> authorize(authentication, "cluster:admin/snapshot/status", request), - "cluster:admin/snapshot/status", SystemUser.INSTANCE.principal()); + () -> authorize(authentication, "cluster:admin/snapshot/status", request), + "cluster:admin/snapshot/status", SystemUser.INSTANCE.principal()); verify(auditTrail).accessDenied(authentication, "cluster:admin/snapshot/status", request, - new String[] { SystemUser.ROLE_NAME }); + new String[]{SystemUser.ROLE_NAME}); + verifyNoMoreInteractions(auditTrail); + } + + public void testAuthorizeUsingConditionalPrivileges() { + final DeletePrivilegesRequest request = new DeletePrivilegesRequest(); + final Authentication authentication = createAuthentication(new User("user1", "role1")); + + final ConditionalClusterPrivilege conditionalClusterPrivilege = Mockito.mock(ConditionalClusterPrivilege.class); + final Predicate requestPredicate = r -> r == request; + Mockito.when(conditionalClusterPrivilege.getRequestPredicate()).thenReturn(requestPredicate); + Mockito.when(conditionalClusterPrivilege.getPrivilege()).thenReturn(ClusterPrivilege.MANAGE_SECURITY); + final ConditionalClusterPrivilege[] conditionalClusterPrivileges = new ConditionalClusterPrivilege[] { + conditionalClusterPrivilege + }; + RoleDescriptor role = new RoleDescriptor("role1", null, null, null, conditionalClusterPrivileges, null, null ,null); + roleMap.put("role1", role); + + authorize(authentication, DeletePrivilegesAction.NAME, request); + verify(auditTrail).accessGranted(authentication, DeletePrivilegesAction.NAME, request, new String[]{role.getName()}); + verifyNoMoreInteractions(auditTrail); + } + + public void testAuthorizationDeniedWhenConditionalPrivilegesDoNotMatch() { + final DeletePrivilegesRequest request = new DeletePrivilegesRequest(); + final Authentication authentication = createAuthentication(new User("user1", "role1")); + + final ConditionalClusterPrivilege conditionalClusterPrivilege = Mockito.mock(ConditionalClusterPrivilege.class); + final Predicate requestPredicate = r -> false; + Mockito.when(conditionalClusterPrivilege.getRequestPredicate()).thenReturn(requestPredicate); + Mockito.when(conditionalClusterPrivilege.getPrivilege()).thenReturn(ClusterPrivilege.MANAGE_SECURITY); + final ConditionalClusterPrivilege[] conditionalClusterPrivileges = new ConditionalClusterPrivilege[] { + conditionalClusterPrivilege + }; + RoleDescriptor role = new RoleDescriptor("role1", null, null, null, conditionalClusterPrivileges, null, null ,null); + roleMap.put("role1", role); + + assertThrowsAuthorizationException( + () -> authorize(authentication, DeletePrivilegesAction.NAME, request), + DeletePrivilegesAction.NAME, "user1"); + verify(auditTrail).accessDenied(authentication, DeletePrivilegesAction.NAME, request, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); } @@ -270,8 +331,8 @@ public void testNoRolesCausesDenial() { final Authentication authentication = createAuthentication(new User("test user")); mockEmptyMetaData(); assertThrowsAuthorizationException( - () -> authorize(authentication, "indices:a", request), - "indices:a", "test user"); + () -> authorize(authentication, "indices:a", request), + "indices:a", "test user"); verify(auditTrail).accessDenied(authentication, "indices:a", request, Role.EMPTY.names()); verifyNoMoreInteractions(auditTrail); } @@ -297,8 +358,8 @@ public void testUserWithNoRolesCannotPerformLocalSearch() { final Authentication authentication = createAuthentication(new User("test user")); mockEmptyMetaData(); assertThrowsAuthorizationException( - () -> authorize(authentication, SearchAction.NAME, request), - SearchAction.NAME, "test user"); + () -> authorize(authentication, SearchAction.NAME, request), + SearchAction.NAME, "test user"); verify(auditTrail).accessDenied(authentication, SearchAction.NAME, request, Role.EMPTY.names()); verifyNoMoreInteractions(auditTrail); } @@ -313,8 +374,8 @@ public void testUserWithNoRolesCanPerformMultiClusterSearch() { final Authentication authentication = createAuthentication(new User("test user")); mockEmptyMetaData(); assertThrowsAuthorizationException( - () -> authorize(authentication, SearchAction.NAME, request), - SearchAction.NAME, "test user"); + () -> authorize(authentication, SearchAction.NAME, request), + SearchAction.NAME, "test user"); verify(auditTrail).accessDenied(authentication, SearchAction.NAME, request, Role.EMPTY.names()); verifyNoMoreInteractions(auditTrail); } @@ -324,12 +385,11 @@ public void testUserWithNoRolesCannotSql() { Authentication authentication = createAuthentication(new User("test user")); mockEmptyMetaData(); assertThrowsAuthorizationException( - () -> authorize(authentication, SqlQueryAction.NAME, request), - SqlQueryAction.NAME, "test user"); + () -> authorize(authentication, SqlQueryAction.NAME, request), + SqlQueryAction.NAME, "test user"); verify(auditTrail).accessDenied(authentication, SqlQueryAction.NAME, request, Role.EMPTY.names()); verifyNoMoreInteractions(auditTrail); } - /** * Verifies that the behaviour tested in {@link #testUserWithNoRolesCanPerformRemoteSearch} * does not work for requests that are not remote-index-capable. @@ -340,24 +400,24 @@ public void testRemoteIndicesOnlyWorkWithApplicableRequestTypes() { final Authentication authentication = createAuthentication(new User("test user")); mockEmptyMetaData(); assertThrowsAuthorizationException( - () -> authorize(authentication, DeleteIndexAction.NAME, request), - DeleteIndexAction.NAME, "test user"); + () -> authorize(authentication, DeleteIndexAction.NAME, request), + DeleteIndexAction.NAME, "test user"); verify(auditTrail).accessDenied(authentication, DeleteIndexAction.NAME, request, Role.EMPTY.names()); verifyNoMoreInteractions(auditTrail); } public void testUnknownRoleCausesDenial() { Tuple tuple = randomFrom(asList( - new Tuple<>(SearchAction.NAME, new SearchRequest()), - new Tuple<>(IndicesExistsAction.NAME, new IndicesExistsRequest()), - new Tuple<>(SqlQueryAction.NAME, new SqlQueryRequest()))); + new Tuple<>(SearchAction.NAME, new SearchRequest()), + new Tuple<>(IndicesExistsAction.NAME, new IndicesExistsRequest()), + new Tuple<>(SqlQueryAction.NAME, new SqlQueryRequest()))); String action = tuple.v1(); TransportRequest request = tuple.v2(); final Authentication authentication = createAuthentication(new User("test user", "non-existent-role")); mockEmptyMetaData(); assertThrowsAuthorizationException( - () -> authorize(authentication, action, request), - action, "test user"); + () -> authorize(authentication, action, request), + action, "test user"); verify(auditTrail).accessDenied(authentication, action, request, Role.EMPTY.names()); verifyNoMoreInteractions(auditTrail); } @@ -366,22 +426,22 @@ public void testThatNonIndicesAndNonClusterActionIsDenied() { final TransportRequest request = mock(TransportRequest.class); final Authentication authentication = createAuthentication(new User("test user", "a_all")); final RoleDescriptor role = new RoleDescriptor("a_role", null, - new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a").privileges("all").build() }, null); + new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null); roleMap.put("a_all", role); assertThrowsAuthorizationException( - () -> authorize(authentication, "whatever", request), - "whatever", "test user"); - verify(auditTrail).accessDenied(authentication, "whatever", request, new String[] { role.getName() }); + () -> authorize(authentication, "whatever", request), + "whatever", "test user"); + verify(auditTrail).accessDenied(authentication, "whatever", request, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); } public void testThatRoleWithNoIndicesIsDenied() { @SuppressWarnings("unchecked") Tuple tuple = randomFrom( - new Tuple<>(SearchAction.NAME, new SearchRequest()), - new Tuple<>(IndicesExistsAction.NAME, new IndicesExistsRequest()), - new Tuple<>(SqlQueryAction.NAME, new SqlQueryRequest())); + new Tuple<>(SearchAction.NAME, new SearchRequest()), + new Tuple<>(IndicesExistsAction.NAME, new IndicesExistsRequest()), + new Tuple<>(SqlQueryAction.NAME, new SqlQueryRequest())); String action = tuple.v1(); TransportRequest request = tuple.v2(); final Authentication authentication = createAuthentication(new User("test user", "no_indices")); @@ -390,9 +450,9 @@ public void testThatRoleWithNoIndicesIsDenied() { mockEmptyMetaData(); assertThrowsAuthorizationException( - () -> authorize(authentication, action, request), - action, "test user"); - verify(auditTrail).accessDenied(authentication, action, request, new String[] { role.getName() }); + () -> authorize(authentication, action, request), + action, "test user"); + verify(auditTrail).accessDenied(authentication, action, request, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); } @@ -401,12 +461,12 @@ public void testElasticUserAuthorizedForNonChangePasswordRequestsWhenNotInSetupM final Tuple request = randomCompositeRequest(); authorize(authentication, request.v1(), request.v2()); - verify(auditTrail).accessGranted(authentication, request.v1(), request.v2(), new String[] { ElasticUser.ROLE_NAME }); + verify(auditTrail).accessGranted(authentication, request.v1(), request.v2(), new String[]{ElasticUser.ROLE_NAME}); } public void testSearchAgainstEmptyCluster() { RoleDescriptor role = new RoleDescriptor("a_role", null, - new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a").privileges("all").build() }, null); + new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null); final Authentication authentication = createAuthentication(new User("test user", "a_all")); roleMap.put("a_all", role); mockEmptyMetaData(); @@ -414,25 +474,25 @@ public void testSearchAgainstEmptyCluster() { { //ignore_unavailable set to false, user is not authorized for this index nor does it exist SearchRequest searchRequest = new SearchRequest("does_not_exist") - .indicesOptions(IndicesOptions.fromOptions(false, true, - true, false)); + .indicesOptions(IndicesOptions.fromOptions(false, true, + true, false)); assertThrowsAuthorizationException( - () -> authorize(authentication, SearchAction.NAME, searchRequest), - SearchAction.NAME, "test user"); - verify(auditTrail).accessDenied(authentication, SearchAction.NAME, searchRequest, new String[] { role.getName() }); + () -> authorize(authentication, SearchAction.NAME, searchRequest), + SearchAction.NAME, "test user"); + verify(auditTrail).accessDenied(authentication, SearchAction.NAME, searchRequest, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); } { //ignore_unavailable and allow_no_indices both set to true, user is not authorized for this index nor does it exist SearchRequest searchRequest = new SearchRequest("does_not_exist") - .indicesOptions(IndicesOptions.fromOptions(true, true, true, false)); + .indicesOptions(IndicesOptions.fromOptions(true, true, true, false)); authorize(authentication, SearchAction.NAME, searchRequest); - verify(auditTrail).accessGranted(authentication, SearchAction.NAME, searchRequest, new String[] { role.getName() }); + verify(auditTrail).accessGranted(authentication, SearchAction.NAME, searchRequest, new String[]{role.getName()}); final IndicesAccessControl indicesAccessControl = threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY); final IndicesAccessControl.IndexAccessControl indexAccessControl = - indicesAccessControl.getIndexPermissions(IndicesAndAliasesResolverField.NO_INDEX_PLACEHOLDER); + indicesAccessControl.getIndexPermissions(IndicesAndAliasesResolverField.NO_INDEX_PLACEHOLDER); assertFalse(indexAccessControl.getFieldPermissions().hasFieldLevelSecurity()); assertNull(indexAccessControl.getQueries()); } @@ -440,40 +500,40 @@ public void testSearchAgainstEmptyCluster() { public void testScrollRelatedRequestsAllowed() { RoleDescriptor role = new RoleDescriptor("a_role", null, - new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a").privileges("all").build() }, null); + new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null); final Authentication authentication = createAuthentication(new User("test user", "a_all")); roleMap.put("a_all", role); mockEmptyMetaData(); final ClearScrollRequest clearScrollRequest = new ClearScrollRequest(); authorize(authentication, ClearScrollAction.NAME, clearScrollRequest); - verify(auditTrail).accessGranted(authentication, ClearScrollAction.NAME, clearScrollRequest, new String[] { role.getName() }); + verify(auditTrail).accessGranted(authentication, ClearScrollAction.NAME, clearScrollRequest, new String[]{role.getName()}); final SearchScrollRequest searchScrollRequest = new SearchScrollRequest(); authorize(authentication, SearchScrollAction.NAME, searchScrollRequest); - verify(auditTrail).accessGranted(authentication, SearchScrollAction.NAME, searchScrollRequest, new String[] { role.getName() }); + verify(auditTrail).accessGranted(authentication, SearchScrollAction.NAME, searchScrollRequest, new String[]{role.getName()}); // We have to use a mock request for other Scroll actions as the actual requests are package private to SearchTransportService final TransportRequest request = mock(TransportRequest.class); authorize(authentication, SearchTransportService.CLEAR_SCROLL_CONTEXTS_ACTION_NAME, request); verify(auditTrail).accessGranted(authentication, SearchTransportService.CLEAR_SCROLL_CONTEXTS_ACTION_NAME, request, - new String[] { role.getName() }); + new String[]{role.getName()}); authorize(authentication, SearchTransportService.FETCH_ID_SCROLL_ACTION_NAME, request); verify(auditTrail).accessGranted(authentication, SearchTransportService.FETCH_ID_SCROLL_ACTION_NAME, request, - new String[] { role.getName() }); + new String[]{role.getName()}); authorize(authentication, SearchTransportService.QUERY_FETCH_SCROLL_ACTION_NAME, request); verify(auditTrail).accessGranted(authentication, SearchTransportService.QUERY_FETCH_SCROLL_ACTION_NAME, request, - new String[] { role.getName() }); + new String[]{role.getName()}); authorize(authentication, SearchTransportService.QUERY_SCROLL_ACTION_NAME, request); verify(auditTrail).accessGranted(authentication, SearchTransportService.QUERY_SCROLL_ACTION_NAME, request, - new String[] { role.getName() }); + new String[]{role.getName()}); authorize(authentication, SearchTransportService.FREE_CONTEXT_SCROLL_ACTION_NAME, request); verify(auditTrail).accessGranted(authentication, SearchTransportService.FREE_CONTEXT_SCROLL_ACTION_NAME, request, - new String[] { role.getName() }); + new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); } @@ -481,14 +541,14 @@ public void testAuthorizeIndicesFailures() { TransportRequest request = new GetIndexRequest().indices("b"); ClusterState state = mockEmptyMetaData(); RoleDescriptor role = new RoleDescriptor("a_role", null, - new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a").privileges("all").build() }, null); + new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null); final Authentication authentication = createAuthentication(new User("test user", "a_all")); roleMap.put("a_all", role); assertThrowsAuthorizationException( - () -> authorize(authentication, "indices:a", request), - "indices:a", "test user"); - verify(auditTrail).accessDenied(authentication, "indices:a", request, new String[] { role.getName() }); + () -> authorize(authentication, "indices:a", request), + "indices:a", "test user"); + verify(auditTrail).accessDenied(authentication, "indices:a", request, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); verify(clusterService, times(1)).state(); verify(state, times(1)).metaData(); @@ -499,14 +559,14 @@ public void testCreateIndexWithAliasWithoutPermissions() { request.alias(new Alias("a2")); ClusterState state = mockEmptyMetaData(); RoleDescriptor role = new RoleDescriptor("a_role", null, - new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a").privileges("all").build() }, null); + new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null); final Authentication authentication = createAuthentication(new User("test user", "a_all")); roleMap.put("a_all", role); assertThrowsAuthorizationException( - () -> authorize(authentication, CreateIndexAction.NAME, request), - IndicesAliasesAction.NAME, "test user"); - verify(auditTrail).accessDenied(authentication, IndicesAliasesAction.NAME, request, new String[] { role.getName() }); + () -> authorize(authentication, CreateIndexAction.NAME, request), + IndicesAliasesAction.NAME, "test user"); + verify(auditTrail).accessDenied(authentication, IndicesAliasesAction.NAME, request, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); verify(clusterService).state(); verify(state, times(1)).metaData(); @@ -517,13 +577,13 @@ public void testCreateIndexWithAlias() { request.alias(new Alias("a2")); ClusterState state = mockEmptyMetaData(); RoleDescriptor role = new RoleDescriptor("a_all", null, - new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a", "a2").privileges("all").build() }, null); + new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a", "a2").privileges("all").build()}, null); final Authentication authentication = createAuthentication(new User("test user", "a_all")); roleMap.put("a_all", role); authorize(authentication, CreateIndexAction.NAME, request); - verify(auditTrail).accessGranted(authentication, CreateIndexAction.NAME, request, new String[] { role.getName() }); + verify(auditTrail).accessGranted(authentication, CreateIndexAction.NAME, request, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); verify(clusterService).state(); verify(state, times(1)).metaData(); @@ -535,17 +595,17 @@ public void testDenialForAnonymousUser() { Settings settings = Settings.builder().put(AnonymousUser.ROLES_SETTING.getKey(), "a_all").build(); final AnonymousUser anonymousUser = new AnonymousUser(settings); authorizationService = new AuthorizationService(settings, rolesStore, clusterService, auditTrail, - new DefaultAuthenticationFailureHandler(), threadPool, anonymousUser); + new DefaultAuthenticationFailureHandler(), threadPool, anonymousUser); RoleDescriptor role = new RoleDescriptor("a_all", null, - new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a").privileges("all").build() }, null); + new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a").privileges("all").build() }, null); roleMap.put("a_all", role); final Authentication authentication = createAuthentication(anonymousUser); assertThrowsAuthorizationException( - () -> authorize(authentication, "indices:a", request), - "indices:a", anonymousUser.principal()); - verify(auditTrail).accessDenied(authentication, "indices:a", request, new String[] { role.getName() }); + () -> authorize(authentication, "indices:a", request), + "indices:a", anonymousUser.principal()); + verify(auditTrail).accessDenied(authentication, "indices:a", request, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); verify(clusterService, times(1)).state(); verify(state, times(1)).metaData(); @@ -555,21 +615,21 @@ public void testDenialForAnonymousUserAuthorizationExceptionDisabled() { TransportRequest request = new GetIndexRequest().indices("b"); ClusterState state = mockEmptyMetaData(); Settings settings = Settings.builder() - .put(AnonymousUser.ROLES_SETTING.getKey(), "a_all") - .put(AuthorizationService.ANONYMOUS_AUTHORIZATION_EXCEPTION_SETTING.getKey(), false) - .build(); + .put(AnonymousUser.ROLES_SETTING.getKey(), "a_all") + .put(AuthorizationService.ANONYMOUS_AUTHORIZATION_EXCEPTION_SETTING.getKey(), false) + .build(); final Authentication authentication = createAuthentication(new AnonymousUser(settings)); authorizationService = new AuthorizationService(settings, rolesStore, clusterService, auditTrail, - new DefaultAuthenticationFailureHandler(), threadPool, new AnonymousUser(settings)); + new DefaultAuthenticationFailureHandler(), threadPool, new AnonymousUser(settings)); RoleDescriptor role = new RoleDescriptor("a_all", null, - new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a").privileges("all").build() }, null); + new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null); roleMap.put("a_all", role); final ElasticsearchSecurityException securityException = expectThrows(ElasticsearchSecurityException.class, - () -> authorize(authentication, "indices:a", request)); + () -> authorize(authentication, "indices:a", request)); assertAuthenticationException(securityException, containsString("action [indices:a] requires authentication")); - verify(auditTrail).accessDenied(authentication, "indices:a", request, new String[] { role.getName() }); + verify(auditTrail).accessDenied(authentication, "indices:a", request, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); verify(clusterService, times(1)).state(); verify(state, times(1)).metaData(); @@ -580,16 +640,16 @@ public void testAuditTrailIsRecordedWhenIndexWildcardThrowsError() { TransportRequest request = new GetIndexRequest().indices("not-an-index-*").indicesOptions(options); ClusterState state = mockEmptyMetaData(); RoleDescriptor role = new RoleDescriptor("a_all", null, - new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a").privileges("all").build() }, null); + new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null); final Authentication authentication = createAuthentication(new User("test user", "a_all")); roleMap.put("a_all", role); final IndexNotFoundException nfe = expectThrows( - IndexNotFoundException.class, - () -> authorize(authentication, GetIndexAction.NAME, request)); + IndexNotFoundException.class, + () -> authorize(authentication, GetIndexAction.NAME, request)); assertThat(nfe.getIndex(), is(notNullValue())); assertThat(nfe.getIndex().getName(), is("not-an-index-*")); - verify(auditTrail).accessDenied(authentication, GetIndexAction.NAME, request, new String[] { role.getName() }); + verify(auditTrail).accessDenied(authentication, GetIndexAction.NAME, request, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); verify(clusterService).state(); verify(state, times(1)).metaData(); @@ -601,8 +661,8 @@ public void testRunAsRequestWithNoRolesUser() { final User user = new User("run as me", null, new User("test user", "admin")); assertNotEquals(authentication.getUser().authenticatedUser(), authentication); assertThrowsAuthorizationExceptionRunAs( - () -> authorize(authentication, "indices:a", request), - "indices:a", "test user", "run as me"); // run as [run as me] + () -> authorize(authentication, "indices:a", request), + "indices:a", "test user", "run as me"); // run as [run as me] verify(auditTrail).runAsDenied(authentication, "indices:a", request, Role.EMPTY.names()); verifyNoMoreInteractions(auditTrail); } @@ -610,67 +670,67 @@ public void testRunAsRequestWithNoRolesUser() { public void testRunAsRequestWithoutLookedUpBy() { AuthenticateRequest request = new AuthenticateRequest("run as me"); roleMap.put("can run as", ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR); - User user = new User("run as me", Strings.EMPTY_ARRAY, new User("test user", new String[] { "can run as" })); + User user = new User("run as me", Strings.EMPTY_ARRAY, new User("test user", new String[]{"can run as"})); Authentication authentication = new Authentication(user, new RealmRef("foo", "bar", "baz"), null); assertNotEquals(user.authenticatedUser(), user); assertThrowsAuthorizationExceptionRunAs( - () -> authorize(authentication, AuthenticateAction.NAME, request), - AuthenticateAction.NAME, "test user", "run as me"); // run as [run as me] + () -> authorize(authentication, AuthenticateAction.NAME, request), + AuthenticateAction.NAME, "test user", "run as me"); // run as [run as me] verify(auditTrail).runAsDenied(authentication, AuthenticateAction.NAME, request, - new String[] { ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName() }); + new String[]{ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName()}); verifyNoMoreInteractions(auditTrail); } public void testRunAsRequestRunningAsUnAllowedUser() { TransportRequest request = mock(TransportRequest.class); - User user = new User("run as me", new String[] { "doesn't exist" }, new User("test user", "can run as")); + User user = new User("run as me", new String[]{"doesn't exist"}, new User("test user", "can run as")); assertNotEquals(user.authenticatedUser(), user); final Authentication authentication = createAuthentication(user); final RoleDescriptor role = new RoleDescriptor("can run as", null, - new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a").privileges("all").build() }, - new String[] { "not the right user" }); + new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, + new String[]{"not the right user"}); roleMap.put("can run as", role); assertThrowsAuthorizationExceptionRunAs( - () -> authorize(authentication, "indices:a", request), - "indices:a", "test user", "run as me"); - verify(auditTrail).runAsDenied(authentication, "indices:a", request, new String[] { role.getName() }); + () -> authorize(authentication, "indices:a", request), + "indices:a", "test user", "run as me"); + verify(auditTrail).runAsDenied(authentication, "indices:a", request, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); } public void testRunAsRequestWithRunAsUserWithoutPermission() { TransportRequest request = new GetIndexRequest().indices("a"); User authenticatedUser = new User("test user", "can run as"); - User user = new User("run as me", new String[] { "b" }, authenticatedUser); + User user = new User("run as me", new String[]{"b"}, authenticatedUser); assertNotEquals(user.authenticatedUser(), user); final Authentication authentication = createAuthentication(user); final RoleDescriptor runAsRole = new RoleDescriptor("can run as", null, - new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a").privileges("all").build() }, - new String[] { "run as me" }); + new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, + new String[]{"run as me"}); roleMap.put("can run as", runAsRole); RoleDescriptor bRole = new RoleDescriptor("b", null, - new IndicesPrivileges[] { IndicesPrivileges.builder().indices("b").privileges("all").build() }, null); + new IndicesPrivileges[]{IndicesPrivileges.builder().indices("b").privileges("all").build()}, null); boolean indexExists = randomBoolean(); if (indexExists) { ClusterState state = mock(ClusterState.class); when(clusterService.state()).thenReturn(state); when(state.metaData()).thenReturn(MetaData.builder() - .put(new IndexMetaData.Builder("a") - .settings(Settings.builder().put("index.version.created", Version.CURRENT).build()) - .numberOfShards(1).numberOfReplicas(0).build(), true) - .build()); + .put(new IndexMetaData.Builder("a") + .settings(Settings.builder().put("index.version.created", Version.CURRENT).build()) + .numberOfShards(1).numberOfReplicas(0).build(), true) + .build()); roleMap.put("b", bRole); } else { mockEmptyMetaData(); } assertThrowsAuthorizationExceptionRunAs( - () -> authorize(authentication, "indices:a", request), - "indices:a", "test user", "run as me"); - verify(auditTrail).runAsGranted(authentication, "indices:a", request, new String[] { runAsRole.getName() }); + () -> authorize(authentication, "indices:a", request), + "indices:a", "test user", "run as me"); + verify(auditTrail).runAsGranted(authentication, "indices:a", request, new String[]{runAsRole.getName()}); if (indexExists) { - verify(auditTrail).accessDenied(authentication, "indices:a", request, new String[] { bRole.getName() }); + verify(auditTrail).accessDenied(authentication, "indices:a", request, new String[]{bRole.getName()}); } else { verify(auditTrail).accessDenied(authentication, "indices:a", request, Role.EMPTY.names()); } @@ -679,43 +739,43 @@ public void testRunAsRequestWithRunAsUserWithoutPermission() { public void testRunAsRequestWithValidPermissions() { TransportRequest request = new GetIndexRequest().indices("b"); - User authenticatedUser = new User("test user", new String[] { "can run as" }); - User user = new User("run as me", new String[] { "b" }, authenticatedUser); + User authenticatedUser = new User("test user", new String[]{"can run as"}); + User user = new User("run as me", new String[]{"b"}, authenticatedUser); assertNotEquals(user.authenticatedUser(), user); final Authentication authentication = createAuthentication(user); final RoleDescriptor runAsRole = new RoleDescriptor("can run as", null, - new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a").privileges("all").build() }, - new String[] { "run as me" }); + new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, + new String[]{"run as me"}); roleMap.put("can run as", runAsRole); ClusterState state = mock(ClusterState.class); when(clusterService.state()).thenReturn(state); when(state.metaData()).thenReturn(MetaData.builder() - .put(new IndexMetaData.Builder("b") - .settings(Settings.builder().put("index.version.created", Version.CURRENT).build()) - .numberOfShards(1).numberOfReplicas(0).build(), true) - .build()); + .put(new IndexMetaData.Builder("b") + .settings(Settings.builder().put("index.version.created", Version.CURRENT).build()) + .numberOfShards(1).numberOfReplicas(0).build(), true) + .build()); RoleDescriptor bRole = new RoleDescriptor("b", null, - new IndicesPrivileges[] { IndicesPrivileges.builder().indices("b").privileges("all").build() }, null); + new IndicesPrivileges[]{IndicesPrivileges.builder().indices("b").privileges("all").build()}, null); roleMap.put("b", bRole); authorize(authentication, "indices:a", request); - verify(auditTrail).runAsGranted(authentication, "indices:a", request, new String[] { runAsRole.getName() }); - verify(auditTrail).accessGranted(authentication, "indices:a", request, new String[] { bRole.getName() }); + verify(auditTrail).runAsGranted(authentication, "indices:a", request, new String[]{runAsRole.getName()}); + verify(auditTrail).accessGranted(authentication, "indices:a", request, new String[]{bRole.getName()}); verifyNoMoreInteractions(auditTrail); } public void testNonXPackUserCannotExecuteOperationAgainstSecurityIndex() { - RoleDescriptor role = new RoleDescriptor("all access", new String[] { "all" }, - new IndicesPrivileges[] { IndicesPrivileges.builder().indices("*").privileges("all").build() }, null); + RoleDescriptor role = new RoleDescriptor("all access", new String[]{"all"}, + new IndicesPrivileges[]{IndicesPrivileges.builder().indices("*").privileges("all").build()}, null); final Authentication authentication = createAuthentication(new User("all_access_user", "all_access")); roleMap.put("all_access", role); ClusterState state = mock(ClusterState.class); when(clusterService.state()).thenReturn(state); when(state.metaData()).thenReturn(MetaData.builder() - .put(new IndexMetaData.Builder(SECURITY_INDEX_NAME) - .settings(Settings.builder().put("index.version.created", Version.CURRENT).build()) - .numberOfShards(1).numberOfReplicas(0).build(), true) - .build()); + .put(new IndexMetaData.Builder(SECURITY_INDEX_NAME) + .settings(Settings.builder().put("index.version.created", Version.CURRENT).build()) + .numberOfShards(1).numberOfReplicas(0).build(), true) + .build()); List> requests = new ArrayList<>(); requests.add(new Tuple<>(BulkAction.NAME + "[s]", @@ -726,34 +786,34 @@ public void testNonXPackUserCannotExecuteOperationAgainstSecurityIndex() { new IndexRequest(SECURITY_INDEX_NAME, "type", "id"))); requests.add(new Tuple<>(SearchAction.NAME, new SearchRequest(SECURITY_INDEX_NAME))); requests.add(new Tuple<>(TermVectorsAction.NAME, - new TermVectorsRequest(SECURITY_INDEX_NAME, "type", "id"))); + new TermVectorsRequest(SECURITY_INDEX_NAME, "type", "id"))); requests.add(new Tuple<>(GetAction.NAME, new GetRequest(SECURITY_INDEX_NAME, "type", "id"))); requests.add(new Tuple<>(TermVectorsAction.NAME, - new TermVectorsRequest(SECURITY_INDEX_NAME, "type", "id"))); + new TermVectorsRequest(SECURITY_INDEX_NAME, "type", "id"))); requests.add(new Tuple<>(IndicesAliasesAction.NAME, new IndicesAliasesRequest() - .addAliasAction(AliasActions.add().alias("security_alias").index(SECURITY_INDEX_NAME)))); + .addAliasAction(AliasActions.add().alias("security_alias").index(SECURITY_INDEX_NAME)))); requests.add( - new Tuple<>(UpdateSettingsAction.NAME, new UpdateSettingsRequest().indices(SECURITY_INDEX_NAME))); + new Tuple<>(UpdateSettingsAction.NAME, new UpdateSettingsRequest().indices(SECURITY_INDEX_NAME))); for (Tuple requestTuple : requests) { String action = requestTuple.v1(); TransportRequest request = requestTuple.v2(); assertThrowsAuthorizationException( - () -> authorize(authentication, action, request), - action, "all_access_user"); - verify(auditTrail).accessDenied(authentication, action, request, new String[] { role.getName() }); + () -> authorize(authentication, action, request), + action, "all_access_user"); + verify(auditTrail).accessDenied(authentication, action, request, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); } // we should allow waiting for the health of the index or any index if the user has this permission ClusterHealthRequest request = new ClusterHealthRequest(SECURITY_INDEX_NAME); authorize(authentication, ClusterHealthAction.NAME, request); - verify(auditTrail).accessGranted(authentication, ClusterHealthAction.NAME, request, new String[] { role.getName() }); + verify(auditTrail).accessGranted(authentication, ClusterHealthAction.NAME, request, new String[]{role.getName()}); // multiple indices request = new ClusterHealthRequest(SECURITY_INDEX_NAME, "foo", "bar"); authorize(authentication, ClusterHealthAction.NAME, request); - verify(auditTrail).accessGranted(authentication, ClusterHealthAction.NAME, request, new String[] { role.getName() }); + verify(auditTrail).accessGranted(authentication, ClusterHealthAction.NAME, request, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); final SearchRequest searchRequest = new SearchRequest("_all"); @@ -763,17 +823,17 @@ public void testNonXPackUserCannotExecuteOperationAgainstSecurityIndex() { } public void testGrantedNonXPackUserCanExecuteMonitoringOperationsAgainstSecurityIndex() { - RoleDescriptor role = new RoleDescriptor("all access", new String[] { "all" }, - new IndicesPrivileges[] { IndicesPrivileges.builder().indices("*").privileges("all").build() }, null); + RoleDescriptor role = new RoleDescriptor("all access", new String[]{"all"}, + new IndicesPrivileges[]{IndicesPrivileges.builder().indices("*").privileges("all").build()}, null); final Authentication authentication = createAuthentication(new User("all_access_user", "all_access")); roleMap.put("all_access", role); ClusterState state = mock(ClusterState.class); when(clusterService.state()).thenReturn(state); when(state.metaData()).thenReturn(MetaData.builder() - .put(new IndexMetaData.Builder(SECURITY_INDEX_NAME) - .settings(Settings.builder().put("index.version.created", Version.CURRENT).build()) - .numberOfShards(1).numberOfReplicas(0).build(), true) - .build()); + .put(new IndexMetaData.Builder(SECURITY_INDEX_NAME) + .settings(Settings.builder().put("index.version.created", Version.CURRENT).build()) + .numberOfShards(1).numberOfReplicas(0).build(), true) + .build()); List> requests = new ArrayList<>(); requests.add(new Tuple<>(IndicesStatsAction.NAME, new IndicesStatsRequest().indices(SECURITY_INDEX_NAME))); @@ -781,15 +841,15 @@ public void testGrantedNonXPackUserCanExecuteMonitoringOperationsAgainstSecurity requests.add(new Tuple<>(IndicesSegmentsAction.NAME, new IndicesSegmentsRequest().indices(SECURITY_INDEX_NAME))); requests.add(new Tuple<>(GetSettingsAction.NAME, new GetSettingsRequest().indices(SECURITY_INDEX_NAME))); requests.add(new Tuple<>(IndicesShardStoresAction.NAME, - new IndicesShardStoresRequest().indices(SECURITY_INDEX_NAME))); + new IndicesShardStoresRequest().indices(SECURITY_INDEX_NAME))); requests.add(new Tuple<>(UpgradeStatusAction.NAME, - new UpgradeStatusRequest().indices(SECURITY_INDEX_NAME))); + new UpgradeStatusRequest().indices(SECURITY_INDEX_NAME))); for (final Tuple requestTuple : requests) { final String action = requestTuple.v1(); final TransportRequest request = requestTuple.v2(); authorize(authentication, action, request); - verify(auditTrail).accessGranted(authentication, action, request, new String[] { role.getName() }); + verify(auditTrail).accessGranted(authentication, action, request, new String[]{role.getName()}); } } @@ -799,33 +859,33 @@ public void testSuperusersCanExecuteOperationAgainstSecurityIndex() { ClusterState state = mock(ClusterState.class); when(clusterService.state()).thenReturn(state); when(state.metaData()).thenReturn(MetaData.builder() - .put(new IndexMetaData.Builder(SECURITY_INDEX_NAME) - .settings(Settings.builder().put("index.version.created", Version.CURRENT).build()) - .numberOfShards(1).numberOfReplicas(0).build(), true) - .build()); + .put(new IndexMetaData.Builder(SECURITY_INDEX_NAME) + .settings(Settings.builder().put("index.version.created", Version.CURRENT).build()) + .numberOfShards(1).numberOfReplicas(0).build(), true) + .build()); List> requests = new ArrayList<>(); requests.add(new Tuple<>(DeleteAction.NAME, new DeleteRequest(SECURITY_INDEX_NAME, "type", "id"))); requests.add(new Tuple<>(BulkAction.NAME + "[s]", - createBulkShardRequest(SECURITY_INDEX_NAME, DeleteRequest::new))); + createBulkShardRequest(SECURITY_INDEX_NAME, DeleteRequest::new))); requests.add(new Tuple<>(UpdateAction.NAME, new UpdateRequest(SECURITY_INDEX_NAME, "type", "id"))); requests.add(new Tuple<>(IndexAction.NAME, new IndexRequest(SECURITY_INDEX_NAME, "type", "id"))); requests.add(new Tuple<>(BulkAction.NAME + "[s]", - createBulkShardRequest(SECURITY_INDEX_NAME, IndexRequest::new))); + createBulkShardRequest(SECURITY_INDEX_NAME, IndexRequest::new))); requests.add(new Tuple<>(SearchAction.NAME, new SearchRequest(SECURITY_INDEX_NAME))); requests.add(new Tuple<>(TermVectorsAction.NAME, - new TermVectorsRequest(SECURITY_INDEX_NAME, "type", "id"))); + new TermVectorsRequest(SECURITY_INDEX_NAME, "type", "id"))); requests.add(new Tuple<>(GetAction.NAME, new GetRequest(SECURITY_INDEX_NAME, "type", "id"))); requests.add(new Tuple<>(TermVectorsAction.NAME, - new TermVectorsRequest(SECURITY_INDEX_NAME, "type", "id"))); + new TermVectorsRequest(SECURITY_INDEX_NAME, "type", "id"))); requests.add(new Tuple<>(IndicesAliasesAction.NAME, new IndicesAliasesRequest() - .addAliasAction(AliasActions.add().alias("security_alias").index(SECURITY_INDEX_NAME)))); + .addAliasAction(AliasActions.add().alias("security_alias").index(SECURITY_INDEX_NAME)))); requests.add(new Tuple<>(ClusterHealthAction.NAME, new ClusterHealthRequest(SECURITY_INDEX_NAME))); requests.add(new Tuple<>(ClusterHealthAction.NAME, - new ClusterHealthRequest(SECURITY_INDEX_NAME, "foo", "bar"))); + new ClusterHealthRequest(SECURITY_INDEX_NAME, "foo", "bar"))); for (final Tuple requestTuple : requests) { final String action = requestTuple.v1(); @@ -843,10 +903,10 @@ public void testSuperusersCanExecuteOperationAgainstSecurityIndexWithWildcard() ClusterState state = mock(ClusterState.class); when(clusterService.state()).thenReturn(state); when(state.metaData()).thenReturn(MetaData.builder() - .put(new IndexMetaData.Builder(SECURITY_INDEX_NAME) - .settings(Settings.builder().put("index.version.created", Version.CURRENT).build()) - .numberOfShards(1).numberOfReplicas(0).build(), true) - .build()); + .put(new IndexMetaData.Builder(SECURITY_INDEX_NAME) + .settings(Settings.builder().put("index.version.created", Version.CURRENT).build()) + .numberOfShards(1).numberOfReplicas(0).build(), true) + .build()); String action = SearchAction.NAME; SearchRequest request = new SearchRequest("_all"); @@ -860,9 +920,9 @@ public void testAnonymousRolesAreAppliedToOtherUsers() { Settings settings = Settings.builder().put(AnonymousUser.ROLES_SETTING.getKey(), "anonymous_user_role").build(); final AnonymousUser anonymousUser = new AnonymousUser(settings); authorizationService = new AuthorizationService(settings, rolesStore, clusterService, auditTrail, - new DefaultAuthenticationFailureHandler(), threadPool, anonymousUser); - roleMap.put("anonymous_user_role", new RoleDescriptor("anonymous_user_role", new String[] { "all" }, - new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a").privileges("all").build() }, null)); + new DefaultAuthenticationFailureHandler(), threadPool, anonymousUser); + roleMap.put("anonymous_user_role", new RoleDescriptor("anonymous_user_role", new String[]{"all"}, + new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null)); mockEmptyMetaData(); // sanity check the anonymous user @@ -886,9 +946,9 @@ public void testAnonymousUserEnabledRoleAdded() { Settings settings = Settings.builder().put(AnonymousUser.ROLES_SETTING.getKey(), "anonymous_user_role").build(); final AnonymousUser anonymousUser = new AnonymousUser(settings); authorizationService = new AuthorizationService(settings, rolesStore, clusterService, auditTrail, - new DefaultAuthenticationFailureHandler(), threadPool, anonymousUser); - roleMap.put("anonymous_user_role", new RoleDescriptor("anonymous_user_role", new String[] { "all" }, - new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a").privileges("all").build() }, null)); + new DefaultAuthenticationFailureHandler(), threadPool, anonymousUser); + roleMap.put("anonymous_user_role", new RoleDescriptor("anonymous_user_role", new String[]{"all"}, + new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null)); mockEmptyMetaData(); PlainActionFuture rolesFuture = new PlainActionFuture<>(); authorizationService.roles(new User("no role user"), rolesFuture); @@ -905,8 +965,8 @@ public void testCompositeActionsAreImmediatelyRejected() { final RoleDescriptor role = new RoleDescriptor("no_indices", null, null, null); roleMap.put("no_indices", role); assertThrowsAuthorizationException( - () -> authorize(authentication, action, request), action, "test user"); - verify(auditTrail).accessDenied(authentication, action, request, new String[] { role.getName() }); + () -> authorize(authentication, action, request), action, "test user"); + verify(auditTrail).accessDenied(authentication, action, request, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); } @@ -917,11 +977,11 @@ public void testCompositeActionsIndicesAreNotChecked() { final TransportRequest request = compositeRequest.v2(); final Authentication authentication = createAuthentication(new User("test user", "role")); final RoleDescriptor role = new RoleDescriptor("role", null, - new IndicesPrivileges[] { IndicesPrivileges.builder().indices(randomBoolean() ? "a" : "index").privileges("all").build() }, - null); + new IndicesPrivileges[]{IndicesPrivileges.builder().indices(randomBoolean() ? "a" : "index").privileges("all").build()}, + null); roleMap.put("role", role); authorize(authentication, action, request); - verify(auditTrail).accessGranted(authentication, action, request, new String[] { role.getName() }); + verify(auditTrail).accessGranted(authentication, action, request, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); } @@ -930,10 +990,10 @@ public void testCompositeActionsMustImplementCompositeIndicesRequest() { TransportRequest request = mock(TransportRequest.class); User user = new User("test user", "role"); roleMap.put("role", new RoleDescriptor("role", null, - new IndicesPrivileges[] { IndicesPrivileges.builder().indices(randomBoolean() ? "a" : "index").privileges("all").build() }, - null)); + new IndicesPrivileges[]{IndicesPrivileges.builder().indices(randomBoolean() ? "a" : "index").privileges("all").build()}, + null)); IllegalStateException illegalStateException = expectThrows(IllegalStateException.class, - () -> authorize(createAuthentication(user), action, request)); + () -> authorize(createAuthentication(user), action, request)); assertThat(illegalStateException.getMessage(), containsString("Composite actions must implement CompositeIndicesRequest")); } @@ -970,62 +1030,62 @@ public void testCompositeActionsIndicesAreCheckedAtTheShardLevel() { User userAllowed = new User("userAllowed", "roleAllowed"); roleMap.put("roleAllowed", new RoleDescriptor("roleAllowed", null, - new IndicesPrivileges[] { IndicesPrivileges.builder().indices("index").privileges("all").build() }, null)); + new IndicesPrivileges[]{IndicesPrivileges.builder().indices("index").privileges("all").build()}, null)); User userDenied = new User("userDenied", "roleDenied"); roleMap.put("roleDenied", new RoleDescriptor("roleDenied", null, - new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a").privileges("all").build() }, null)); + new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null)); mockEmptyMetaData(); authorize(createAuthentication(userAllowed), action, request); assertThrowsAuthorizationException( - () -> authorize(createAuthentication(userDenied), action, request), action, "userDenied"); + () -> authorize(createAuthentication(userDenied), action, request), action, "userDenied"); } public void testAuthorizationOfIndividualBulkItems() { final String action = BulkAction.NAME + "[s]"; final BulkItemRequest[] items = { - new BulkItemRequest(1, new DeleteRequest("concrete-index", "doc", "c1")), - new BulkItemRequest(2, new IndexRequest("concrete-index", "doc", "c2")), - new BulkItemRequest(3, new DeleteRequest("alias-1", "doc", "a1a")), - new BulkItemRequest(4, new IndexRequest("alias-1", "doc", "a1b")), - new BulkItemRequest(5, new DeleteRequest("alias-2", "doc", "a2a")), - new BulkItemRequest(6, new IndexRequest("alias-2", "doc", "a2b")) + new BulkItemRequest(1, new DeleteRequest("concrete-index", "doc", "c1")), + new BulkItemRequest(2, new IndexRequest("concrete-index", "doc", "c2")), + new BulkItemRequest(3, new DeleteRequest("alias-1", "doc", "a1a")), + new BulkItemRequest(4, new IndexRequest("alias-1", "doc", "a1b")), + new BulkItemRequest(5, new DeleteRequest("alias-2", "doc", "a2a")), + new BulkItemRequest(6, new IndexRequest("alias-2", "doc", "a2b")) }; final ShardId shardId = new ShardId("concrete-index", UUID.randomUUID().toString(), 1); final TransportRequest request = new BulkShardRequest(shardId, WriteRequest.RefreshPolicy.IMMEDIATE, items); final Authentication authentication = createAuthentication(new User("user", "my-role")); - RoleDescriptor role = new RoleDescriptor("my-role", null, new IndicesPrivileges[] { - IndicesPrivileges.builder().indices("concrete-index").privileges("all").build(), - IndicesPrivileges.builder().indices("alias-1").privileges("index").build(), - IndicesPrivileges.builder().indices("alias-2").privileges("delete").build() + RoleDescriptor role = new RoleDescriptor("my-role", null, new IndicesPrivileges[]{ + IndicesPrivileges.builder().indices("concrete-index").privileges("all").build(), + IndicesPrivileges.builder().indices("alias-1").privileges("index").build(), + IndicesPrivileges.builder().indices("alias-2").privileges("delete").build() }, null); roleMap.put("my-role", role); mockEmptyMetaData(); authorize(authentication, action, request); - verify(auditTrail).accessDenied(authentication, DeleteAction.NAME, request, new String[] { role.getName() }); // alias-1 delete - verify(auditTrail).accessDenied(authentication, IndexAction.NAME, request, new String[] { role.getName() }); // alias-2 index - verify(auditTrail).accessGranted(authentication, action, request, new String[] { role.getName() }); // bulk request is allowed + verify(auditTrail).accessDenied(authentication, DeleteAction.NAME, request, new String[]{role.getName()}); // alias-1 delete + verify(auditTrail).accessDenied(authentication, IndexAction.NAME, request, new String[]{role.getName()}); // alias-2 index + verify(auditTrail).accessGranted(authentication, action, request, new String[]{role.getName()}); // bulk request is allowed verifyNoMoreInteractions(auditTrail); } public void testAuthorizationOfIndividualBulkItemsWithDateMath() { final String action = BulkAction.NAME + "[s]"; final BulkItemRequest[] items = { - new BulkItemRequest(1, new IndexRequest("", "doc", "dy1")), - new BulkItemRequest(2, - new DeleteRequest("", "doc", "dy2")), // resolves to same as above - new BulkItemRequest(3, new IndexRequest("", "doc", "dm1")), - new BulkItemRequest(4, - new DeleteRequest("", "doc", "dm2")), // resolves to same as above + new BulkItemRequest(1, new IndexRequest("", "doc", "dy1")), + new BulkItemRequest(2, + new DeleteRequest("", "doc", "dy2")), // resolves to same as above + new BulkItemRequest(3, new IndexRequest("", "doc", "dm1")), + new BulkItemRequest(4, + new DeleteRequest("", "doc", "dm2")), // resolves to same as above }; final ShardId shardId = new ShardId("concrete-index", UUID.randomUUID().toString(), 1); final TransportRequest request = new BulkShardRequest(shardId, WriteRequest.RefreshPolicy.IMMEDIATE, items); final Authentication authentication = createAuthentication(new User("user", "my-role")); final RoleDescriptor role = new RoleDescriptor("my-role", null, - new IndicesPrivileges[] { IndicesPrivileges.builder().indices("datemath-*").privileges("index").build() }, null); + new IndicesPrivileges[]{IndicesPrivileges.builder().indices("datemath-*").privileges("index").build()}, null); roleMap.put("my-role", role); mockEmptyMetaData(); @@ -1033,14 +1093,14 @@ public void testAuthorizationOfIndividualBulkItemsWithDateMath() { // both deletes should fail verify(auditTrail, Mockito.times(2)).accessDenied(authentication, DeleteAction.NAME, request, - new String[] { role.getName() }); + new String[]{role.getName()}); // bulk request is allowed - verify(auditTrail).accessGranted(authentication, action, request, new String[] { role.getName() }); + verify(auditTrail).accessGranted(authentication, action, request, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); } private BulkShardRequest createBulkShardRequest(String indexName, TriFunction> req) { - final BulkItemRequest[] items = { new BulkItemRequest(1, req.apply(indexName, "type", "id")) }; + final BulkItemRequest[] items = {new BulkItemRequest(1, req.apply(indexName, "type", "id"))}; return new BulkShardRequest(new ShardId(indexName, UUID.randomUUID().toString(), 1), WriteRequest.RefreshPolicy.IMMEDIATE, items); } @@ -1049,37 +1109,37 @@ public void testSameUserPermission() { final User user = new User("joe"); final boolean changePasswordRequest = randomBoolean(); final TransportRequest request = changePasswordRequest ? - new ChangePasswordRequestBuilder(mock(Client.class)).username(user.principal()).request() : - new AuthenticateRequestBuilder(mock(Client.class)).username(user.principal()).request(); + new ChangePasswordRequestBuilder(mock(Client.class)).username(user.principal()).request() : + new AuthenticateRequestBuilder(mock(Client.class)).username(user.principal()).request(); final String action = changePasswordRequest ? ChangePasswordAction.NAME : AuthenticateAction.NAME; final Authentication authentication = mock(Authentication.class); final RealmRef authenticatedBy = mock(RealmRef.class); when(authentication.getUser()).thenReturn(user); when(authentication.getAuthenticatedBy()).thenReturn(authenticatedBy); when(authenticatedBy.getType()) - .thenReturn(changePasswordRequest ? randomFrom(ReservedRealm.TYPE, NativeRealmSettings.TYPE) : - randomAlphaOfLengthBetween(4, 12)); + .thenReturn(changePasswordRequest ? randomFrom(ReservedRealm.TYPE, NativeRealmSettings.TYPE) : + randomAlphaOfLengthBetween(4, 12)); assertThat(request, instanceOf(UserRequest.class)); assertTrue(AuthorizationService.checkSameUserPermissions(action, request, authentication)); } public void testSameUserPermissionDoesNotAllowNonMatchingUsername() { - final User authUser = new User("admin", new String[] { "bar" }); + final User authUser = new User("admin", new String[]{"bar"}); final User user = new User("joe", null, authUser); final boolean changePasswordRequest = randomBoolean(); final String username = randomFrom("", "joe" + randomAlphaOfLengthBetween(1, 5), randomAlphaOfLengthBetween(3, 10)); final TransportRequest request = changePasswordRequest ? - new ChangePasswordRequestBuilder(mock(Client.class)).username(username).request() : - new AuthenticateRequestBuilder(mock(Client.class)).username(username).request(); + new ChangePasswordRequestBuilder(mock(Client.class)).username(username).request() : + new AuthenticateRequestBuilder(mock(Client.class)).username(username).request(); final String action = changePasswordRequest ? ChangePasswordAction.NAME : AuthenticateAction.NAME; final Authentication authentication = mock(Authentication.class); final RealmRef authenticatedBy = mock(RealmRef.class); when(authentication.getUser()).thenReturn(user); when(authentication.getAuthenticatedBy()).thenReturn(authenticatedBy); when(authenticatedBy.getType()) - .thenReturn(changePasswordRequest ? randomFrom(ReservedRealm.TYPE, NativeRealmSettings.TYPE) : - randomAlphaOfLengthBetween(4, 12)); + .thenReturn(changePasswordRequest ? randomFrom(ReservedRealm.TYPE, NativeRealmSettings.TYPE) : + randomAlphaOfLengthBetween(4, 12)); assertThat(request, instanceOf(UserRequest.class)); assertFalse(AuthorizationService.checkSameUserPermissions(action, request, authentication)); @@ -1088,8 +1148,8 @@ public void testSameUserPermissionDoesNotAllowNonMatchingUsername() { final RealmRef lookedUpBy = mock(RealmRef.class); when(authentication.getLookedUpBy()).thenReturn(lookedUpBy); when(lookedUpBy.getType()) - .thenReturn(changePasswordRequest ? randomFrom(ReservedRealm.TYPE, NativeRealmSettings.TYPE) : - randomAlphaOfLengthBetween(4, 12)); + .thenReturn(changePasswordRequest ? randomFrom(ReservedRealm.TYPE, NativeRealmSettings.TYPE) : + randomAlphaOfLengthBetween(4, 12)); // this should still fail since the username is still different assertFalse(AuthorizationService.checkSameUserPermissions(action, request, authentication)); @@ -1105,7 +1165,7 @@ public void testSameUserPermissionDoesNotAllowOtherActions() { final User user = mock(User.class); final TransportRequest request = mock(TransportRequest.class); final String action = randomFrom(PutUserAction.NAME, DeleteUserAction.NAME, ClusterHealthAction.NAME, ClusterStateAction.NAME, - ClusterStatsAction.NAME, GetLicenseAction.NAME); + ClusterStatsAction.NAME, GetLicenseAction.NAME); final Authentication authentication = mock(Authentication.class); final RealmRef authenticatedBy = mock(RealmRef.class); final boolean runAs = randomBoolean(); @@ -1114,20 +1174,20 @@ public void testSameUserPermissionDoesNotAllowOtherActions() { when(user.isRunAs()).thenReturn(runAs); when(authentication.getAuthenticatedBy()).thenReturn(authenticatedBy); when(authenticatedBy.getType()) - .thenReturn(randomAlphaOfLengthBetween(4, 12)); + .thenReturn(randomAlphaOfLengthBetween(4, 12)); assertFalse(AuthorizationService.checkSameUserPermissions(action, request, authentication)); verifyZeroInteractions(user, request, authentication); } public void testSameUserPermissionRunAsChecksAuthenticatedBy() { - final User authUser = new User("admin", new String[] { "bar" }); + final User authUser = new User("admin", new String[]{"bar"}); final String username = "joe"; final User user = new User(username, null, authUser); final boolean changePasswordRequest = randomBoolean(); final TransportRequest request = changePasswordRequest ? - new ChangePasswordRequestBuilder(mock(Client.class)).username(username).request() : - new AuthenticateRequestBuilder(mock(Client.class)).username(username).request(); + new ChangePasswordRequestBuilder(mock(Client.class)).username(username).request() : + new AuthenticateRequestBuilder(mock(Client.class)).username(username).request(); final String action = changePasswordRequest ? ChangePasswordAction.NAME : AuthenticateAction.NAME; final Authentication authentication = mock(Authentication.class); final RealmRef authenticatedBy = mock(RealmRef.class); @@ -1136,8 +1196,8 @@ public void testSameUserPermissionRunAsChecksAuthenticatedBy() { when(authentication.getAuthenticatedBy()).thenReturn(authenticatedBy); when(authentication.getLookedUpBy()).thenReturn(lookedUpBy); when(lookedUpBy.getType()) - .thenReturn(changePasswordRequest ? randomFrom(ReservedRealm.TYPE, NativeRealmSettings.TYPE) : - randomAlphaOfLengthBetween(4, 12)); + .thenReturn(changePasswordRequest ? randomFrom(ReservedRealm.TYPE, NativeRealmSettings.TYPE) : + randomAlphaOfLengthBetween(4, 12)); assertTrue(AuthorizationService.checkSameUserPermissions(action, request, authentication)); when(authentication.getUser()).thenReturn(authUser); @@ -1153,8 +1213,8 @@ public void testSameUserPermissionDoesNotAllowChangePasswordForOtherRealms() { when(authentication.getUser()).thenReturn(user); when(authentication.getAuthenticatedBy()).thenReturn(authenticatedBy); when(authenticatedBy.getType()).thenReturn(randomFrom(LdapRealmSettings.LDAP_TYPE, FileRealmSettings.TYPE, - LdapRealmSettings.AD_TYPE, PkiRealmSettings.TYPE, - randomAlphaOfLengthBetween(4, 12))); + LdapRealmSettings.AD_TYPE, PkiRealmSettings.TYPE, + randomAlphaOfLengthBetween(4, 12))); assertThat(request, instanceOf(UserRequest.class)); assertFalse(AuthorizationService.checkSameUserPermissions(action, request, authentication)); @@ -1165,7 +1225,7 @@ public void testSameUserPermissionDoesNotAllowChangePasswordForOtherRealms() { } public void testSameUserPermissionDoesNotAllowChangePasswordForLookedUpByOtherRealms() { - final User authUser = new User("admin", new String[] { "bar" }); + final User authUser = new User("admin", new String[]{"bar"}); final User user = new User("joe", null, authUser); final ChangePasswordRequest request = new ChangePasswordRequestBuilder(mock(Client.class)).username(user.principal()).request(); final String action = ChangePasswordAction.NAME; @@ -1176,8 +1236,8 @@ public void testSameUserPermissionDoesNotAllowChangePasswordForLookedUpByOtherRe when(authentication.getAuthenticatedBy()).thenReturn(authenticatedBy); when(authentication.getLookedUpBy()).thenReturn(lookedUpBy); when(lookedUpBy.getType()).thenReturn(randomFrom(LdapRealmSettings.LDAP_TYPE, FileRealmSettings.TYPE, - LdapRealmSettings.AD_TYPE, PkiRealmSettings.TYPE, - randomAlphaOfLengthBetween(4, 12))); + LdapRealmSettings.AD_TYPE, PkiRealmSettings.TYPE, + randomAlphaOfLengthBetween(4, 12))); assertThat(request, instanceOf(UserRequest.class)); assertFalse(AuthorizationService.checkSameUserPermissions(action, request, authentication)); @@ -1223,7 +1283,7 @@ public void testDoesNotUseRolesStoreForXPackUser() { public void testGetRolesForSystemUserThrowsException() { IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> authorizationService.roles(SystemUser.INSTANCE, - null)); + null)); assertEquals("the user [_system] is the system user and we should never try to get its roles", iae.getMessage()); } @@ -1245,9 +1305,9 @@ public void testProxyRequestFailsOnNonProxyAction() { TransportRequest transportRequest = TransportActionProxy.wrapRequest(node, request); User user = new User("test user", "role"); IllegalStateException illegalStateException = expectThrows(IllegalStateException.class, - () -> authorize(createAuthentication(user), "indices:some/action", transportRequest)); + () -> authorize(createAuthentication(user), "indices:some/action", transportRequest)); assertThat(illegalStateException.getMessage(), - startsWith("originalRequest is a proxy request for: [org.elasticsearch.transport.TransportRequest$")); + startsWith("originalRequest is a proxy request for: [org.elasticsearch.transport.TransportRequest$")); assertThat(illegalStateException.getMessage(), endsWith("] but action: [indices:some/action] isn't")); } @@ -1255,11 +1315,11 @@ public void testProxyRequestFailsOnNonProxyRequest() { TransportRequest request = TransportRequest.Empty.INSTANCE; User user = new User("test user", "role"); IllegalStateException illegalStateException = expectThrows(IllegalStateException.class, - () -> authorize(createAuthentication(user), TransportActionProxy.getProxyAction("indices:some/action"), request)); + () -> authorize(createAuthentication(user), TransportActionProxy.getProxyAction("indices:some/action"), request)); assertThat(illegalStateException.getMessage(), - startsWith("originalRequest is not a proxy request: [org.elasticsearch.transport.TransportRequest$")); + startsWith("originalRequest is not a proxy request: [org.elasticsearch.transport.TransportRequest$")); assertThat(illegalStateException.getMessage(), - endsWith("] but action: [internal:transport/proxy/indices:some/action] is a proxy action")); + endsWith("] but action: [internal:transport/proxy/indices:some/action] is a proxy action")); } public void testProxyRequestAuthenticationDenied() { @@ -1271,14 +1331,14 @@ public void testProxyRequestAuthenticationDenied() { final RoleDescriptor role = new RoleDescriptor("no_indices", null, null, null); roleMap.put("no_indices", role); assertThrowsAuthorizationException( - () -> authorize(authentication, action, transportRequest), action, "test user"); - verify(auditTrail).accessDenied(authentication, action, proxiedRequest, new String[] { role.getName() }); + () -> authorize(authentication, action, transportRequest), action, "test user"); + verify(auditTrail).accessDenied(authentication, action, proxiedRequest, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); } public void testProxyRequestAuthenticationGrantedWithAllPrivileges() { RoleDescriptor role = new RoleDescriptor("a_role", null, - new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a").privileges("all").build() }, null); + new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null); final Authentication authentication = createAuthentication(new User("test user", "a_all")); roleMap.put("a_all", role); mockEmptyMetaData(); @@ -1288,12 +1348,12 @@ public void testProxyRequestAuthenticationGrantedWithAllPrivileges() { final TransportRequest transportRequest = TransportActionProxy.wrapRequest(node, clearScrollRequest); final String action = TransportActionProxy.getProxyAction(SearchTransportService.CLEAR_SCROLL_CONTEXTS_ACTION_NAME); authorize(authentication, action, transportRequest); - verify(auditTrail).accessGranted(authentication, action, clearScrollRequest, new String[] { role.getName() }); + verify(auditTrail).accessGranted(authentication, action, clearScrollRequest, new String[]{role.getName()}); } public void testProxyRequestAuthenticationGranted() { RoleDescriptor role = new RoleDescriptor("a_role", null, - new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a").privileges("read_cross_cluster").build() }, null); + new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("read_cross_cluster").build()}, null); final Authentication authentication = createAuthentication(new User("test user", "a_all")); roleMap.put("a_all", role); mockEmptyMetaData(); @@ -1303,13 +1363,13 @@ public void testProxyRequestAuthenticationGranted() { final TransportRequest transportRequest = TransportActionProxy.wrapRequest(node, clearScrollRequest); final String action = TransportActionProxy.getProxyAction(SearchTransportService.CLEAR_SCROLL_CONTEXTS_ACTION_NAME); authorize(authentication, action, transportRequest); - verify(auditTrail).accessGranted(authentication, action, clearScrollRequest, new String[] { role.getName() }); + verify(auditTrail).accessGranted(authentication, action, clearScrollRequest, new String[]{role.getName()}); } public void testProxyRequestAuthenticationDeniedWithReadPrivileges() { final Authentication authentication = createAuthentication(new User("test user", "a_all")); final RoleDescriptor role = new RoleDescriptor("a_role", null, - new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a").privileges("read").build() }, null); + new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("read").build()}, null); roleMap.put("a_all", role); mockEmptyMetaData(); DiscoveryNode node = new DiscoveryNode("foo", buildNewFakeTransportAddress(), Version.CURRENT); @@ -1317,7 +1377,7 @@ public void testProxyRequestAuthenticationDeniedWithReadPrivileges() { TransportRequest transportRequest = TransportActionProxy.wrapRequest(node, clearScrollRequest); String action = TransportActionProxy.getProxyAction(SearchTransportService.CLEAR_SCROLL_CONTEXTS_ACTION_NAME); assertThrowsAuthorizationException( - () -> authorize(authentication, action, transportRequest), action, "test user"); - verify(auditTrail).accessDenied(authentication, action, clearScrollRequest, new String[] { role.getName() }); + () -> authorize(authentication, action, transportRequest), action, "test user"); + verify(auditTrail).accessDenied(authentication, action, clearScrollRequest, new String[]{role.getName()}); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java index 1d0e5c179a9cd..c48ac4568989b 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java @@ -7,6 +7,7 @@ import org.elasticsearch.Version; import org.elasticsearch.action.search.SearchAction; +import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.cluster.metadata.AliasMetaData; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.MetaData; @@ -24,6 +25,7 @@ import org.elasticsearch.xpack.security.support.SecurityIndexManager; import java.util.List; +import java.util.Set; import static org.hamcrest.Matchers.containsInAnyOrder; @@ -57,8 +59,10 @@ public void testAuthorizedIndicesUserWithSomeRoles() { .putAlias(new AliasMetaData.Builder("ba").build()) .build(), true) .build(); - Role roles = CompositeRolesStore.buildRoleFromDescriptors(Sets.newHashSet(aStarRole, bRole), - new FieldPermissionsCache(Settings.EMPTY)); + final PlainActionFuture future = new PlainActionFuture<>(); + final Set descriptors = Sets.newHashSet(aStarRole, bRole); + CompositeRolesStore.buildRoleFromDescriptors(descriptors, new FieldPermissionsCache(Settings.EMPTY), null, future); + Role roles = future.actionGet(); AuthorizedIndices authorizedIndices = new AuthorizedIndices(user, roles, SearchAction.NAME, metaData); List list = authorizedIndices.get(); assertThat(list, containsInAnyOrder("a1", "a2", "aaaaaa", "b", "ab")); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java index d7c974bdc6e2a..9c3efcd67369f 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java @@ -173,8 +173,9 @@ public void setup() { if (roleDescriptors.isEmpty()) { callback.onResponse(Role.EMPTY); } else { - callback.onResponse( - CompositeRolesStore.buildRoleFromDescriptors(roleDescriptors, fieldPermissionsCache)); + CompositeRolesStore.buildRoleFromDescriptors(roleDescriptors, fieldPermissionsCache, null, + ActionListener.wrap(r -> callback.onResponse(r), callback::onFailure) + ); } return Void.TYPE; }).when(rolesStore).roles(any(Set.class), any(FieldPermissionsCache.class), any(ActionListener.class)); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RoleDescriptorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RoleDescriptorTests.java index 9d34382d566fb..07686838ad0e2 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RoleDescriptorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RoleDescriptorTests.java @@ -10,18 +10,34 @@ import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.ByteBufferStreamInput; import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.NamedWriteableAwareStreamInput; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.XPackClientPlugin; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivileges; +import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege; import org.elasticsearch.xpack.core.security.support.MetadataUtils; +import org.hamcrest.Matchers; +import java.util.Arrays; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.Map; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.hamcrest.Matchers.arrayContaining; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.emptyArray; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.core.Is.is; public class RoleDescriptorTests extends ESTestCase { @@ -45,9 +61,26 @@ public void testToString() throws Exception { .query("{\"query\": {\"match_all\": {}}}") .build() }; - RoleDescriptor descriptor = new RoleDescriptor("test", new String[] { "all", "none" }, groups, new String[] { "sudo" }); - assertThat(descriptor.toString(), is("Role[name=test, cluster=[all,none], indicesPrivileges=[IndicesPrivileges[indices=[i1,i2], " + - "privileges=[read], field_security=[grant=[body,title], except=null], query={\"query\": {\"match_all\": {}}}],]" + + final RoleDescriptor.ApplicationResourcePrivileges[] applicationPrivileges = { + RoleDescriptor.ApplicationResourcePrivileges.builder() + .application("my_app") + .privileges("read", "write") + .resources("*") + .build() + }; + + final ConditionalClusterPrivilege[] conditionalClusterPrivileges = new ConditionalClusterPrivilege[]{ + new ConditionalClusterPrivileges.ManageApplicationPrivileges(new LinkedHashSet<>(Arrays.asList("app01", "app02"))) + }; + + RoleDescriptor descriptor = new RoleDescriptor("test", new String[] { "all", "none" }, groups, applicationPrivileges, + conditionalClusterPrivileges, new String[] { "sudo" }, Collections.emptyMap(), Collections.emptyMap()); + + assertThat(descriptor.toString(), is("Role[name=test, cluster=[all,none]" + + ", global=[{APPLICATION:manage:applications=app01,app02}]" + + ", indicesPrivileges=[IndicesPrivileges[indices=[i1,i2], privileges=[read]" + + ", field_security=[grant=[body,title], except=null], query={\"query\": {\"match_all\": {}}}],]" + + ", applicationPrivileges=[ApplicationResourcePrivileges[application=my_app, privileges=[read,write], resources=[*]],]" + ", runAs=[sudo], metadata=[{}]]")); } @@ -60,11 +93,23 @@ public void testToXContent() throws Exception { .query("{\"query\": {\"match_all\": {}}}") .build() }; + final RoleDescriptor.ApplicationResourcePrivileges[] applicationPrivileges = { + RoleDescriptor.ApplicationResourcePrivileges.builder() + .application("my_app") + .privileges("read", "write") + .resources("*") + .build() + }; + final ConditionalClusterPrivilege[] conditionalClusterPrivileges = { + new ConditionalClusterPrivileges.ManageApplicationPrivileges(new LinkedHashSet<>(Arrays.asList("app01", "app02"))) + }; + Map metadata = randomBoolean() ? MetadataUtils.DEFAULT_RESERVED_METADATA : null; - RoleDescriptor descriptor = new RoleDescriptor("test", new String[] { "all", "none" }, groups, new String[] { "sudo" }, metadata); + RoleDescriptor descriptor = new RoleDescriptor("test", new String[] { "all", "none" }, groups, applicationPrivileges, + conditionalClusterPrivileges, new String[]{ "sudo" }, metadata, Collections.emptyMap()); XContentBuilder builder = descriptor.toXContent(jsonBuilder(), ToXContent.EMPTY_PARAMS); RoleDescriptor parsed = RoleDescriptor.parse("test", BytesReference.bytes(builder), false, XContentType.JSON); - assertEquals(parsed, descriptor); + assertThat(parsed, equalTo(descriptor)); } public void testParse() throws Exception { @@ -113,6 +158,53 @@ public void testParse() throws Exception { assertNotNull(rd.getMetadata()); assertThat(rd.getMetadata().size(), is(1)); assertThat(rd.getMetadata().get("foo"), is("bar")); + + q = "{\"cluster\":[\"a\", \"b\"], \"run_as\": [\"m\", \"n\"]," + + " \"index\": [{\"names\": [\"idx1\",\"idx2\"], \"privileges\": [\"p1\", \"p2\"]}]," + + " \"applications\": [" + + " {\"resources\": [\"object-123\",\"object-456\"], \"privileges\":[\"read\", \"delete\"], \"application\":\"app1\"}," + + " {\"resources\": [\"*\"], \"privileges\":[\"admin\"], \"application\":\"app2\" }" + + " ]," + + " \"global\": { \"application\": { \"manage\": { \"applications\" : [ \"kibana\", \"logstash\" ] } } }" + + "}"; + rd = RoleDescriptor.parse("test", new BytesArray(q), false, XContentType.JSON); + assertThat(rd.getName(), equalTo("test")); + assertThat(rd.getClusterPrivileges(), arrayContaining("a", "b")); + assertThat(rd.getIndicesPrivileges().length, equalTo(1)); + assertThat(rd.getIndicesPrivileges()[0].getIndices(), arrayContaining("idx1", "idx2")); + assertThat(rd.getRunAs(), arrayContaining("m", "n")); + assertThat(rd.getIndicesPrivileges()[0].getQuery(), nullValue()); + assertThat(rd.getApplicationPrivileges().length, equalTo(2)); + assertThat(rd.getApplicationPrivileges()[0].getResources(), arrayContaining("object-123", "object-456")); + assertThat(rd.getApplicationPrivileges()[0].getPrivileges(), arrayContaining("read", "delete")); + assertThat(rd.getApplicationPrivileges()[0].getApplication(), equalTo("app1")); + assertThat(rd.getApplicationPrivileges()[1].getResources(), arrayContaining("*")); + assertThat(rd.getApplicationPrivileges()[1].getPrivileges(), arrayContaining("admin")); + assertThat(rd.getApplicationPrivileges()[1].getApplication(), equalTo("app2")); + assertThat(rd.getConditionalClusterPrivileges(), Matchers.arrayWithSize(1)); + + final ConditionalClusterPrivilege conditionalPrivilege = rd.getConditionalClusterPrivileges()[0]; + assertThat(conditionalPrivilege.getCategory(), equalTo(ConditionalClusterPrivilege.Category.APPLICATION)); + assertThat(conditionalPrivilege, instanceOf(ConditionalClusterPrivileges.ManageApplicationPrivileges.class)); + assertThat(((ConditionalClusterPrivileges.ManageApplicationPrivileges) conditionalPrivilege).getApplicationNames(), + containsInAnyOrder("kibana", "logstash")); + + q = "{\"applications\": [{\"application\": \"myapp\", \"resources\": [\"*\"], \"privileges\": [\"login\" ]}] }"; + rd = RoleDescriptor.parse("test", new BytesArray(q), false, XContentType.JSON); + assertThat(rd.getName(), equalTo("test")); + assertThat(rd.getClusterPrivileges(), emptyArray()); + assertThat(rd.getIndicesPrivileges(), emptyArray()); + assertThat(rd.getApplicationPrivileges().length, equalTo(1)); + assertThat(rd.getApplicationPrivileges()[0].getResources(), arrayContaining("*")); + assertThat(rd.getApplicationPrivileges()[0].getPrivileges(), arrayContaining("login")); + assertThat(rd.getApplicationPrivileges()[0].getApplication(), equalTo("myapp")); + assertThat(rd.getConditionalClusterPrivileges(), Matchers.arrayWithSize(0)); + + final String badJson + = "{\"applications\":[{\"not_supported\": true, \"resources\": [\"*\"], \"privileges\": [\"my-app:login\" ]}] }"; + final IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, + () -> RoleDescriptor.parse("test", new BytesArray(badJson), false, XContentType.JSON)); + assertThat(ex.getMessage(), containsString("not_supported")); } public void testSerialization() throws Exception { @@ -125,11 +217,24 @@ public void testSerialization() throws Exception { .query("{\"query\": {\"match_all\": {}}}") .build() }; + final RoleDescriptor.ApplicationResourcePrivileges[] applicationPrivileges = { + RoleDescriptor.ApplicationResourcePrivileges.builder() + .application("my_app") + .privileges("read", "write") + .resources("*") + .build() + }; + final ConditionalClusterPrivilege[] conditionalClusterPrivileges = { + new ConditionalClusterPrivileges.ManageApplicationPrivileges(new LinkedHashSet<>(Arrays.asList("app01", "app02"))) + }; + Map metadata = randomBoolean() ? MetadataUtils.DEFAULT_RESERVED_METADATA : null; - final RoleDescriptor descriptor = - new RoleDescriptor("test", new String[] { "all", "none" }, groups, new String[] { "sudo" }, metadata); + final RoleDescriptor descriptor = new RoleDescriptor("test", new String[]{"all", "none"}, groups, applicationPrivileges, + conditionalClusterPrivileges, new String[] { "sudo" }, metadata, null); RoleDescriptor.writeTo(descriptor, output); - StreamInput streamInput = ByteBufferStreamInput.wrap(BytesReference.toBytes(output.bytes())); + final NamedWriteableRegistry registry = new NamedWriteableRegistry(new XPackClientPlugin(Settings.EMPTY).getNamedWriteables()); + StreamInput streamInput = new NamedWriteableAwareStreamInput(ByteBufferStreamInput.wrap(BytesReference.toBytes(output.bytes())), + registry); final RoleDescriptor serialized = RoleDescriptor.readFrom(streamInput); assertEquals(descriptor, serialized); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java index b33e93d0806f8..0c2ab1ecc7650 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java @@ -7,6 +7,10 @@ import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsAction; +import org.elasticsearch.action.admin.cluster.state.ClusterStateAction; +import org.elasticsearch.action.get.GetAction; +import org.elasticsearch.action.index.IndexAction; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.cluster.metadata.IndexMetaData; @@ -22,18 +26,26 @@ import org.elasticsearch.license.TestUtils.UpdatableLicenseState; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.xpack.core.XPackSettings; +import org.elasticsearch.xpack.core.security.action.saml.SamlAuthenticateAction; +import org.elasticsearch.xpack.core.security.action.user.PutUserAction; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.IndicesPrivileges; import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsCache; import org.elasticsearch.xpack.core.security.authz.permission.Role; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; +import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; import org.elasticsearch.xpack.security.support.SecurityIndexManager; import java.io.IOException; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Map; @@ -42,6 +54,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiConsumer; import java.util.function.Function; +import java.util.function.Predicate; import static org.elasticsearch.mock.orig.Mockito.times; import static org.elasticsearch.mock.orig.Mockito.verifyNoMoreInteractions; @@ -103,7 +116,8 @@ public void testRolesWhenDlsFlsUnlicensed() throws IOException { when(fileRolesStore.roleDescriptors(Collections.singleton("fls_dls"))).thenReturn(Collections.singleton(flsDlsRole)); when(fileRolesStore.roleDescriptors(Collections.singleton("no_fls_dls"))).thenReturn(Collections.singleton(noFlsDlsRole)); CompositeRolesStore compositeRolesStore = new CompositeRolesStore(Settings.EMPTY, fileRolesStore, mock(NativeRolesStore.class), - mock(ReservedRolesStore.class), Collections.emptyList(), new ThreadContext(Settings.EMPTY), licenseState); + mock(ReservedRolesStore.class), mock(NativePrivilegeStore.class), Collections.emptyList(), + new ThreadContext(Settings.EMPTY), licenseState); FieldPermissionsCache fieldPermissionsCache = new FieldPermissionsCache(Settings.EMPTY); PlainActionFuture roleFuture = new PlainActionFuture<>(); @@ -163,7 +177,8 @@ public void testRolesWhenDlsFlsLicensed() throws IOException { when(fileRolesStore.roleDescriptors(Collections.singleton("fls_dls"))).thenReturn(Collections.singleton(flsDlsRole)); when(fileRolesStore.roleDescriptors(Collections.singleton("no_fls_dls"))).thenReturn(Collections.singleton(noFlsDlsRole)); CompositeRolesStore compositeRolesStore = new CompositeRolesStore(Settings.EMPTY, fileRolesStore, mock(NativeRolesStore.class), - mock(ReservedRolesStore.class), Collections.emptyList(), new ThreadContext(Settings.EMPTY), licenseState); + mock(ReservedRolesStore.class), mock(NativePrivilegeStore.class), Collections.emptyList(), + new ThreadContext(Settings.EMPTY), licenseState); FieldPermissionsCache fieldPermissionsCache = new FieldPermissionsCache(Settings.EMPTY); PlainActionFuture roleFuture = new PlainActionFuture<>(); @@ -196,7 +211,7 @@ public void testNegativeLookupsAreCached() { final CompositeRolesStore compositeRolesStore = new CompositeRolesStore(SECURITY_ENABLED_SETTINGS, fileRolesStore, nativeRolesStore, reservedRolesStore, - Collections.emptyList(), new ThreadContext(SECURITY_ENABLED_SETTINGS), + mock(NativePrivilegeStore.class), Collections.emptyList(), new ThreadContext(SECURITY_ENABLED_SETTINGS), new XPackLicenseState(SECURITY_ENABLED_SETTINGS)); verify(fileRolesStore).addListener(any(Runnable.class)); // adds a listener in ctor @@ -274,8 +289,8 @@ public void testCustomRolesProviders() { final CompositeRolesStore compositeRolesStore = new CompositeRolesStore(SECURITY_ENABLED_SETTINGS, fileRolesStore, nativeRolesStore, reservedRolesStore, - Arrays.asList(inMemoryProvider1, inMemoryProvider2), new ThreadContext(SECURITY_ENABLED_SETTINGS), - new XPackLicenseState(SECURITY_ENABLED_SETTINGS)); + mock(NativePrivilegeStore.class), Arrays.asList(inMemoryProvider1, inMemoryProvider2), + new ThreadContext(SECURITY_ENABLED_SETTINGS), new XPackLicenseState(SECURITY_ENABLED_SETTINGS)); final Set roleNames = Sets.newHashSet("roleA", "roleB", "unknown"); PlainActionFuture future = new PlainActionFuture<>(); @@ -328,7 +343,9 @@ public void testMergingRolesWithFls() { .build() }, null); FieldPermissionsCache cache = new FieldPermissionsCache(Settings.EMPTY); - Role role = CompositeRolesStore.buildRoleFromDescriptors(Sets.newHashSet(flsRole, addsL1Fields), cache); + PlainActionFuture future = new PlainActionFuture<>(); + CompositeRolesStore.buildRoleFromDescriptors(Sets.newHashSet(flsRole, addsL1Fields), cache, null, future); + Role role = future.actionGet(); MetaData metaData = MetaData.builder() .put(new IndexMetaData.Builder("test") @@ -343,6 +360,111 @@ public void testMergingRolesWithFls() { assertTrue(acls.get("test").getFieldPermissions().grantsAccessTo("L3.foo")); } + public void testMergingBasicRoles() { + final TransportRequest request1 = mock(TransportRequest.class); + final TransportRequest request2 = mock(TransportRequest.class); + final TransportRequest request3 = mock(TransportRequest.class); + + ConditionalClusterPrivilege ccp1 = mock(ConditionalClusterPrivilege.class); + when(ccp1.getPrivilege()).thenReturn(ClusterPrivilege.MANAGE_SECURITY); + when(ccp1.getRequestPredicate()).thenReturn(req -> req == request1); + RoleDescriptor role1 = new RoleDescriptor("r1", new String[]{"monitor"}, new IndicesPrivileges[]{ + IndicesPrivileges.builder() + .indices("abc-*", "xyz-*") + .privileges("read") + .build(), + IndicesPrivileges.builder() + .indices("ind-1-*") + .privileges("all") + .build(), + }, new RoleDescriptor.ApplicationResourcePrivileges[]{ + RoleDescriptor.ApplicationResourcePrivileges.builder() + .application("app1") + .resources("user/*") + .privileges("read", "write") + .build(), + RoleDescriptor.ApplicationResourcePrivileges.builder() + .application("app1") + .resources("settings/*") + .privileges("read") + .build() + }, new ConditionalClusterPrivilege[] { ccp1 }, + new String[]{"app-user-1"}, null, null); + + ConditionalClusterPrivilege ccp2 = mock(ConditionalClusterPrivilege.class); + when(ccp2.getPrivilege()).thenReturn(ClusterPrivilege.MANAGE_SECURITY); + when(ccp2.getRequestPredicate()).thenReturn(req -> req == request2); + RoleDescriptor role2 = new RoleDescriptor("r2", new String[]{"manage_saml"}, new IndicesPrivileges[]{ + IndicesPrivileges.builder() + .indices("abc-*", "ind-2-*") + .privileges("all") + .build() + }, new RoleDescriptor.ApplicationResourcePrivileges[]{ + RoleDescriptor.ApplicationResourcePrivileges.builder() + .application("app2a") + .resources("*") + .privileges("all") + .build(), + RoleDescriptor.ApplicationResourcePrivileges.builder() + .application("app2b") + .resources("*") + .privileges("read") + .build() + }, new ConditionalClusterPrivilege[] { ccp2 }, + new String[]{"app-user-2"}, null, null); + + FieldPermissionsCache cache = new FieldPermissionsCache(Settings.EMPTY); + PlainActionFuture future = new PlainActionFuture<>(); + final NativePrivilegeStore privilegeStore = mock(NativePrivilegeStore.class); + doAnswer(inv -> { + assertTrue(inv.getArguments().length == 3); + ActionListener> listener + = (ActionListener>) inv.getArguments()[2]; + Set set = new HashSet<>(); + Arrays.asList("app1", "app2a", "app2b").forEach( + app -> Arrays.asList("read", "write", "all").forEach( + perm -> set.add( + new ApplicationPrivilegeDescriptor(app, perm, Collections.emptySet(), Collections.emptyMap()) + ))); + listener.onResponse(set); + return null; + }).when(privilegeStore).getPrivileges(any(Collection.class), any(Collection.class), any(ActionListener.class)); + CompositeRolesStore.buildRoleFromDescriptors(Sets.newHashSet(role1, role2), cache, privilegeStore, future); + Role role = future.actionGet(); + + assertThat(role.cluster().check(ClusterStateAction.NAME, randomFrom(request1, request2, request3)), equalTo(true)); + assertThat(role.cluster().check(SamlAuthenticateAction.NAME, randomFrom(request1, request2, request3)), equalTo(true)); + assertThat(role.cluster().check(ClusterUpdateSettingsAction.NAME, randomFrom(request1, request2, request3)), equalTo(false)); + + assertThat(role.cluster().check(PutUserAction.NAME, randomFrom(request1, request2)), equalTo(true)); + assertThat(role.cluster().check(PutUserAction.NAME, request3), equalTo(false)); + + final Predicate allowedRead = role.indices().allowedIndicesMatcher(GetAction.NAME); + assertThat(allowedRead.test("abc-123"), equalTo(true)); + assertThat(allowedRead.test("xyz-000"), equalTo(true)); + assertThat(allowedRead.test("ind-1-a"), equalTo(true)); + assertThat(allowedRead.test("ind-2-a"), equalTo(true)); + assertThat(allowedRead.test("foo"), equalTo(false)); + assertThat(allowedRead.test("abc"), equalTo(false)); + assertThat(allowedRead.test("xyz"), equalTo(false)); + assertThat(allowedRead.test("ind-3-a"), equalTo(false)); + + final Predicate allowedWrite = role.indices().allowedIndicesMatcher(IndexAction.NAME); + assertThat(allowedWrite.test("abc-123"), equalTo(true)); + assertThat(allowedWrite.test("xyz-000"), equalTo(false)); + assertThat(allowedWrite.test("ind-1-a"), equalTo(true)); + assertThat(allowedWrite.test("ind-2-a"), equalTo(true)); + assertThat(allowedWrite.test("foo"), equalTo(false)); + assertThat(allowedWrite.test("abc"), equalTo(false)); + assertThat(allowedWrite.test("xyz"), equalTo(false)); + assertThat(allowedWrite.test("ind-3-a"), equalTo(false)); + + role.application().grants(new ApplicationPrivilege("app1", "app1-read", "write"), "user/joe"); + role.application().grants(new ApplicationPrivilege("app1", "app1-read", "read"), "settings/hostname"); + role.application().grants(new ApplicationPrivilege("app2a", "app2a-all", "all"), "user/joe"); + role.application().grants(new ApplicationPrivilege("app2b", "app2b-read", "read"), "settings/hostname"); + } + public void testCustomRolesProviderFailures() throws Exception { final FileRolesStore fileRolesStore = mock(FileRolesStore.class); when(fileRolesStore.roleDescriptors(anySetOf(String.class))).thenReturn(Collections.emptySet()); @@ -370,8 +492,8 @@ public void testCustomRolesProviderFailures() throws Exception { final CompositeRolesStore compositeRolesStore = new CompositeRolesStore(SECURITY_ENABLED_SETTINGS, fileRolesStore, nativeRolesStore, reservedRolesStore, - Arrays.asList(inMemoryProvider1, failingProvider), new ThreadContext(SECURITY_ENABLED_SETTINGS), - new XPackLicenseState(SECURITY_ENABLED_SETTINGS)); + mock(NativePrivilegeStore.class), Arrays.asList(inMemoryProvider1, failingProvider), + new ThreadContext(SECURITY_ENABLED_SETTINGS), new XPackLicenseState(SECURITY_ENABLED_SETTINGS)); final Set roleNames = Sets.newHashSet("roleA", "roleB", "unknown"); PlainActionFuture future = new PlainActionFuture<>(); @@ -411,7 +533,7 @@ public void testCustomRolesProvidersLicensing() { // these licenses don't allow custom role providers xPackLicenseState.update(randomFrom(OperationMode.BASIC, OperationMode.GOLD, OperationMode.STANDARD), true, null); CompositeRolesStore compositeRolesStore = new CompositeRolesStore( - Settings.EMPTY, fileRolesStore, nativeRolesStore, reservedRolesStore, + Settings.EMPTY, fileRolesStore, nativeRolesStore, reservedRolesStore, mock(NativePrivilegeStore.class), Arrays.asList(inMemoryProvider), new ThreadContext(Settings.EMPTY), xPackLicenseState); Set roleNames = Sets.newHashSet("roleA"); @@ -424,7 +546,7 @@ public void testCustomRolesProvidersLicensing() { assertEquals(0, role.indices().groups().length); compositeRolesStore = new CompositeRolesStore( - Settings.EMPTY, fileRolesStore, nativeRolesStore, reservedRolesStore, + Settings.EMPTY, fileRolesStore, nativeRolesStore, reservedRolesStore, mock(NativePrivilegeStore.class), Arrays.asList(inMemoryProvider), new ThreadContext(Settings.EMPTY), xPackLicenseState); // these licenses allow custom role providers xPackLicenseState.update(randomFrom(OperationMode.PLATINUM, OperationMode.TRIAL), true, null); @@ -439,7 +561,7 @@ public void testCustomRolesProvidersLicensing() { // license expired, don't allow custom role providers compositeRolesStore = new CompositeRolesStore( - Settings.EMPTY, fileRolesStore, nativeRolesStore, reservedRolesStore, + Settings.EMPTY, fileRolesStore, nativeRolesStore, reservedRolesStore, mock(NativePrivilegeStore.class), Arrays.asList(inMemoryProvider), new ThreadContext(Settings.EMPTY), xPackLicenseState); xPackLicenseState.update(randomFrom(OperationMode.PLATINUM, OperationMode.TRIAL), false, null); roleNames = Sets.newHashSet("roleA"); @@ -459,7 +581,8 @@ public void testCacheClearOnIndexHealthChange() { CompositeRolesStore compositeRolesStore = new CompositeRolesStore( Settings.EMPTY, mock(FileRolesStore.class), mock(NativeRolesStore.class), mock(ReservedRolesStore.class), - Collections.emptyList(), new ThreadContext(Settings.EMPTY), new XPackLicenseState(SECURITY_ENABLED_SETTINGS)) { + mock(NativePrivilegeStore.class), Collections.emptyList(), new ThreadContext(Settings.EMPTY), + new XPackLicenseState(SECURITY_ENABLED_SETTINGS)) { @Override public void invalidateAll() { numInvalidation.incrementAndGet(); @@ -502,9 +625,10 @@ public void invalidateAll() { public void testCacheClearOnIndexOutOfDateChange() { final AtomicInteger numInvalidation = new AtomicInteger(0); - CompositeRolesStore compositeRolesStore = new CompositeRolesStore(SECURITY_ENABLED_SETTINGS, mock(FileRolesStore.class), - mock(NativeRolesStore.class), mock(ReservedRolesStore.class), - Collections.emptyList(), new ThreadContext(SECURITY_ENABLED_SETTINGS), new XPackLicenseState(SECURITY_ENABLED_SETTINGS)) { + CompositeRolesStore compositeRolesStore = new CompositeRolesStore(SECURITY_ENABLED_SETTINGS, + mock(FileRolesStore.class), mock(NativeRolesStore.class), mock(ReservedRolesStore.class), + mock(NativePrivilegeStore.class), Collections.emptyList(), new ThreadContext(SECURITY_ENABLED_SETTINGS), + new XPackLicenseState(SECURITY_ENABLED_SETTINGS)) { @Override public void invalidateAll() { numInvalidation.incrementAndGet(); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/FileRolesStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/FileRolesStoreTests.java index 14be1e260db36..1e2428e77791b 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/FileRolesStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/FileRolesStoreTests.java @@ -16,6 +16,7 @@ import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.core.security.audit.logfile.CapturingLogger; @@ -123,7 +124,7 @@ public void testParseFile() throws Exception { assertThat(role, notNullValue()); assertThat(role.names(), equalTo(new String[] { "role3" })); assertThat(role.cluster(), notNullValue()); - assertThat(role.cluster(), is(ClusterPermission.NONE)); + assertThat(role.cluster(), is(ClusterPermission.SimpleClusterPermission.NONE)); assertThat(role.indices(), notNullValue()); assertThat(role.indices().groups(), notNullValue()); assertThat(role.indices().groups().length, is(1)); @@ -147,7 +148,7 @@ public void testParseFile() throws Exception { assertThat(role, notNullValue()); assertThat(role.names(), equalTo(new String[] { "role_run_as" })); assertThat(role.cluster(), notNullValue()); - assertThat(role.cluster(), is(ClusterPermission.NONE)); + assertThat(role.cluster(), is(ClusterPermission.SimpleClusterPermission.NONE)); assertThat(role.indices(), is(IndicesPermission.NONE)); assertThat(role.runAs(), notNullValue()); assertThat(role.runAs().check("user1"), is(true)); @@ -160,7 +161,7 @@ public void testParseFile() throws Exception { assertThat(role, notNullValue()); assertThat(role.names(), equalTo(new String[] { "role_run_as1" })); assertThat(role.cluster(), notNullValue()); - assertThat(role.cluster(), is(ClusterPermission.NONE)); + assertThat(role.cluster(), is(ClusterPermission.SimpleClusterPermission.NONE)); assertThat(role.indices(), is(IndicesPermission.NONE)); assertThat(role.runAs(), notNullValue()); assertThat(role.runAs().check("user1"), is(true)); @@ -173,7 +174,7 @@ public void testParseFile() throws Exception { assertThat(role, notNullValue()); assertThat(role.names(), equalTo(new String[] { "role_fields" })); assertThat(role.cluster(), notNullValue()); - assertThat(role.cluster(), is(ClusterPermission.NONE)); + assertThat(role.cluster(), is(ClusterPermission.SimpleClusterPermission.NONE)); assertThat(role.runAs(), is(RunAsPermission.NONE)); assertThat(role.indices(), notNullValue()); assertThat(role.indices().groups(), notNullValue()); @@ -195,7 +196,7 @@ public void testParseFile() throws Exception { assertThat(role, notNullValue()); assertThat(role.names(), equalTo(new String[] { "role_query" })); assertThat(role.cluster(), notNullValue()); - assertThat(role.cluster(), is(ClusterPermission.NONE)); + assertThat(role.cluster(), is(ClusterPermission.SimpleClusterPermission.NONE)); assertThat(role.runAs(), is(RunAsPermission.NONE)); assertThat(role.indices(), notNullValue()); assertThat(role.indices().groups(), notNullValue()); @@ -216,7 +217,7 @@ public void testParseFile() throws Exception { assertThat(role, notNullValue()); assertThat(role.names(), equalTo(new String[] { "role_query_fields" })); assertThat(role.cluster(), notNullValue()); - assertThat(role.cluster(), is(ClusterPermission.NONE)); + assertThat(role.cluster(), is(ClusterPermission.SimpleClusterPermission.NONE)); assertThat(role.runAs(), is(RunAsPermission.NONE)); assertThat(role.indices(), notNullValue()); assertThat(role.indices().groups(), notNullValue()); @@ -341,14 +342,15 @@ public void testAutoReload() throws Exception { fail("Waited too long for the updated file to be picked up"); } + final TransportRequest request = mock(TransportRequest.class); descriptors = store.roleDescriptors(Collections.singleton("role5")); assertThat(descriptors, notNullValue()); assertEquals(1, descriptors.size()); Role role = Role.builder(descriptors.iterator().next(), null).build(); assertThat(role, notNullValue()); assertThat(role.names(), equalTo(new String[] { "role5" })); - assertThat(role.cluster().check("cluster:monitor/foo/bar"), is(true)); - assertThat(role.cluster().check("cluster:admin/foo/bar"), is(false)); + assertThat(role.cluster().check("cluster:monitor/foo/bar", request), is(true)); + assertThat(role.cluster().check("cluster:admin/foo/bar", request), is(false)); } finally { if (watcherService != null) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/NativePrivilegeStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/NativePrivilegeStoreTests.java new file mode 100644 index 0000000000000..9306eb0b672af --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/NativePrivilegeStoreTests.java @@ -0,0 +1,303 @@ +/* + * 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.authz.store; + +import org.elasticsearch.action.Action; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestBuilder; +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.action.delete.DeleteRequest; +import org.elasticsearch.action.delete.DeleteResponse; +import org.elasticsearch.action.get.GetRequest; +import org.elasticsearch.action.get.GetResponse; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.index.IndexResponse; +import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.action.search.SearchResponseSections; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.action.support.WriteRequest; +import org.elasticsearch.client.Client; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.UUIDs; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.index.get.GetResult; +import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.search.SearchHit; +import org.elasticsearch.search.SearchHits; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.client.NoOpClient; +import org.elasticsearch.test.junit.annotations.TestLogging; +import org.elasticsearch.xpack.core.security.action.role.ClearRolesCacheRequest; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; +import org.elasticsearch.xpack.security.support.SecurityIndexManager; +import org.hamcrest.Matchers; +import org.junit.After; +import org.junit.Before; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +import static java.util.Collections.emptyMap; +import static org.elasticsearch.common.util.set.Sets.newHashSet; +import static org.hamcrest.Matchers.arrayContaining; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.everyItem; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.iterableWithSize; +import static org.hamcrest.Matchers.not; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@TestLogging("org.elasticsearch.xpack.security.authz.store.NativePrivilegeStore:TRACE") +public class NativePrivilegeStoreTests extends ESTestCase { + + private NativePrivilegeStore store; + private List requests; + private AtomicReference listener; + private Client client; + + @Before + public void setup() { + requests = new ArrayList<>(); + listener = new AtomicReference<>(); + client = new NoOpClient(getTestName()) { + @Override + protected > + void doExecute(Action action, Request request, ActionListener listener) { + NativePrivilegeStoreTests.this.requests.add(request); + NativePrivilegeStoreTests.this.listener.set(listener); + } + }; + final SecurityIndexManager securityIndex = mock(SecurityIndexManager.class); + when(securityIndex.isAvailable()).thenReturn(true); + Mockito.doAnswer(invocationOnMock -> { + assertThat(invocationOnMock.getArguments().length, equalTo(2)); + assertThat(invocationOnMock.getArguments()[1], instanceOf(Runnable.class)); + ((Runnable) invocationOnMock.getArguments()[1]).run(); + return null; + }).when(securityIndex).prepareIndexIfNeededThenExecute(any(Consumer.class), any(Runnable.class)); + store = new NativePrivilegeStore(Settings.EMPTY, client, securityIndex); + } + + @After + public void cleanup() { + client.close(); + } + + public void testGetSinglePrivilegeByName() throws Exception { + final ApplicationPrivilegeDescriptor sourcePrivilege = new ApplicationPrivilegeDescriptor("myapp", "admin", + newHashSet("action:admin/*", "action:login", "data:read/*"), emptyMap() + ); + + final PlainActionFuture future = new PlainActionFuture<>(); + store.getPrivilege("myapp", "admin", future); + assertThat(requests, iterableWithSize(1)); + assertThat(requests.get(0), instanceOf(GetRequest.class)); + GetRequest request = (GetRequest) requests.get(0); + assertThat(request.index(), equalTo(SecurityIndexManager.SECURITY_INDEX_NAME)); + assertThat(request.type(), equalTo("doc")); + assertThat(request.id(), equalTo("application-privilege_myapp:admin")); + + final String docSource = Strings.toString(sourcePrivilege); + listener.get().onResponse(new GetResponse( + new GetResult(request.index(), request.type(), request.id(), 1L, true, new BytesArray(docSource), emptyMap()) + )); + final ApplicationPrivilegeDescriptor getPrivilege = future.get(1, TimeUnit.SECONDS); + assertThat(getPrivilege, equalTo(sourcePrivilege)); + } + + public void testGetMissingPrivilege() throws Exception { + final PlainActionFuture future = new PlainActionFuture<>(); + store.getPrivilege("myapp", "admin", future); + assertThat(requests, iterableWithSize(1)); + assertThat(requests.get(0), instanceOf(GetRequest.class)); + GetRequest request = (GetRequest) requests.get(0); + assertThat(request.index(), equalTo(SecurityIndexManager.SECURITY_INDEX_NAME)); + assertThat(request.type(), equalTo("doc")); + assertThat(request.id(), equalTo("application-privilege_myapp:admin")); + + listener.get().onResponse(new GetResponse( + new GetResult(request.index(), request.type(), request.id(), -1, false, null, emptyMap()) + )); + final ApplicationPrivilegeDescriptor getPrivilege = future.get(1, TimeUnit.SECONDS); + assertThat(getPrivilege, Matchers.nullValue()); + } + + public void testGetPrivilegesByApplicationName() throws Exception { + final List sourcePrivileges = Arrays.asList( + new ApplicationPrivilegeDescriptor("myapp", "admin", newHashSet("action:admin/*", "action:login", "data:read/*"), emptyMap()), + new ApplicationPrivilegeDescriptor("myapp", "user", newHashSet("action:login", "data:read/*"), emptyMap()), + new ApplicationPrivilegeDescriptor("myapp", "author", newHashSet("action:login", "data:read/*", "data:write/*"), emptyMap()) + ); + + final PlainActionFuture> future = new PlainActionFuture<>(); + store.getPrivileges(Arrays.asList("myapp", "yourapp"), null, future); + assertThat(requests, iterableWithSize(1)); + assertThat(requests.get(0), instanceOf(SearchRequest.class)); + SearchRequest request = (SearchRequest) requests.get(0); + assertThat(request.indices(), arrayContaining(SecurityIndexManager.SECURITY_INDEX_NAME)); + + final String query = Strings.toString(request.source().query()); + assertThat(query, containsString("{\"terms\":{\"application\":[\"myapp\",\"yourapp\"]")); + assertThat(query, containsString("{\"term\":{\"type\":{\"value\":\"application-privilege\"")); + + final SearchHit[] hits = buildHits(sourcePrivileges); + listener.get().onResponse(new SearchResponse(new SearchResponseSections( + new SearchHits(hits, hits.length, 0f), null, null, false, false, null, 1), "_scrollId1", 1, 1, 0, 1, null, null)); + + assertResult(sourcePrivileges, future); + } + + public void testGetAllPrivileges() throws Exception { + final List sourcePrivileges = Arrays.asList( + new ApplicationPrivilegeDescriptor("app1", "admin", newHashSet("action:admin/*", "action:login", "data:read/*"), emptyMap()), + new ApplicationPrivilegeDescriptor("app2", "user", newHashSet("action:login", "data:read/*"), emptyMap()), + new ApplicationPrivilegeDescriptor("app3", "all", newHashSet("*"), emptyMap()) + ); + + final PlainActionFuture> future = new PlainActionFuture<>(); + store.getPrivileges(null, null, future); + assertThat(requests, iterableWithSize(1)); + assertThat(requests.get(0), instanceOf(SearchRequest.class)); + SearchRequest request = (SearchRequest) requests.get(0); + assertThat(request.indices(), arrayContaining(SecurityIndexManager.SECURITY_INDEX_NAME)); + + final String query = Strings.toString(request.source().query()); + assertThat(query, containsString("{\"term\":{\"type\":{\"value\":\"application-privilege\"")); + assertThat(query, not(containsString("{\"terms\""))); + + final SearchHit[] hits = buildHits(sourcePrivileges); + listener.get().onResponse(new SearchResponse(new SearchResponseSections( + new SearchHits(hits, hits.length, 0f), null, null, false, false, null, 1), "_scrollId1", 1, 1, 0, 1, null, null)); + + assertResult(sourcePrivileges, future); + } + + public void testPutPrivileges() throws Exception { + final List putPrivileges = Arrays.asList( + new ApplicationPrivilegeDescriptor("app1", "admin", newHashSet("action:admin/*", "action:login", "data:read/*"), emptyMap()), + new ApplicationPrivilegeDescriptor("app1", "user", newHashSet("action:login", "data:read/*"), emptyMap()), + new ApplicationPrivilegeDescriptor("app2", "all", newHashSet("*"), emptyMap()) + ); + + final PlainActionFuture>> future = new PlainActionFuture<>(); + store.putPrivileges(putPrivileges, WriteRequest.RefreshPolicy.IMMEDIATE, future); + assertThat(requests, iterableWithSize(putPrivileges.size())); + assertThat(requests, everyItem(instanceOf(IndexRequest.class))); + + final List indexRequests = new ArrayList<>(requests.size()); + requests.stream().map(IndexRequest.class::cast).forEach(indexRequests::add); + requests.clear(); + + final ActionListener indexListener = listener.get(); + final String uuid = UUIDs.randomBase64UUID(random()); + for (int i = 0; i < putPrivileges.size(); i++) { + ApplicationPrivilegeDescriptor privilege = putPrivileges.get(i); + IndexRequest request = indexRequests.get(i); + assertThat(request.indices(), arrayContaining(SecurityIndexManager.SECURITY_INDEX_NAME)); + assertThat(request.type(), equalTo("doc")); + assertThat(request.id(), equalTo( + "application-privilege_" + privilege.getApplication() + ":" + privilege.getName() + )); + final XContentBuilder builder = privilege.toXContent(XContentBuilder.builder(XContentType.JSON.xContent()), true); + assertThat(request.source(), equalTo(BytesReference.bytes(builder))); + final boolean created = privilege.getName().equals("user") == false; + indexListener.onResponse(new IndexResponse( + new ShardId(SecurityIndexManager.SECURITY_INDEX_NAME, uuid, i), + request.type(), request.id(), 1, 1, 1, created + )); + } + + awaitBusy(() -> requests.size() > 0, 1, TimeUnit.SECONDS); + + assertThat(requests, iterableWithSize(1)); + assertThat(requests.get(0), instanceOf(ClearRolesCacheRequest.class)); + listener.get().onResponse(null); + + final Map> map = future.actionGet(); + assertThat(map.entrySet(), iterableWithSize(2)); + assertThat(map.get("app1"), iterableWithSize(1)); + assertThat(map.get("app2"), iterableWithSize(1)); + assertThat(map.get("app1"), contains("admin")); + assertThat(map.get("app2"), contains("all")); + } + + public void testDeletePrivileges() throws Exception { + final List privilegeNames = Arrays.asList("p1", "p2", "p3"); + + final PlainActionFuture>> future = new PlainActionFuture<>(); + store.deletePrivileges("app1", privilegeNames, WriteRequest.RefreshPolicy.IMMEDIATE, future); + assertThat(requests, iterableWithSize(privilegeNames.size())); + assertThat(requests, everyItem(instanceOf(DeleteRequest.class))); + + final List deletes = new ArrayList<>(requests.size()); + requests.stream().map(DeleteRequest.class::cast).forEach(deletes::add); + requests.clear(); + + final ActionListener deleteListener = listener.get(); + final String uuid = UUIDs.randomBase64UUID(random()); + for (int i = 0; i < privilegeNames.size(); i++) { + String name = privilegeNames.get(i); + DeleteRequest request = deletes.get(i); + assertThat(request.indices(), arrayContaining(SecurityIndexManager.SECURITY_INDEX_NAME)); + assertThat(request.type(), equalTo("doc")); + assertThat(request.id(), equalTo("application-privilege_app1:" + name)); + final boolean found = name.equals("p2") == false; + deleteListener.onResponse(new DeleteResponse( + new ShardId(SecurityIndexManager.SECURITY_INDEX_NAME, uuid, i), + request.type(), request.id(), 1, 1, 1, found + )); + } + + awaitBusy(() -> requests.size() > 0, 1, TimeUnit.SECONDS); + + assertThat(requests, iterableWithSize(1)); + assertThat(requests.get(0), instanceOf(ClearRolesCacheRequest.class)); + listener.get().onResponse(null); + + final Map> map = future.actionGet(); + assertThat(map.entrySet(), iterableWithSize(1)); + assertThat(map.get("app1"), iterableWithSize(2)); + assertThat(map.get("app1"), containsInAnyOrder("p1", "p3")); + } + + private SearchHit[] buildHits(List sourcePrivileges) { + final SearchHit[] hits = new SearchHit[sourcePrivileges.size()]; + for (int i = 0; i < hits.length; i++) { + final ApplicationPrivilegeDescriptor p = sourcePrivileges.get(i); + hits[i] = new SearchHit(i, "application-privilege_" + p.getApplication() + ":" + p.getName(), null, null); + hits[i].sourceRef(new BytesArray(Strings.toString(p))); + } + return hits; + } + + private void assertResult(List sourcePrivileges, + PlainActionFuture> future) throws Exception { + final Collection getPrivileges = future.get(1, TimeUnit.SECONDS); + assertThat(getPrivileges, iterableWithSize(sourcePrivileges.size())); + assertThat(new HashSet<>(getPrivileges), equalTo(new HashSet<>(sourcePrivileges))); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/user/HasPrivilegesRestResponseTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/user/HasPrivilegesRestResponseTests.java index 601cabf4f846a..645abbc8f1a6b 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/user/HasPrivilegesRestResponseTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/user/HasPrivilegesRestResponseTests.java @@ -5,10 +5,6 @@ */ package org.elasticsearch.xpack.security.rest.action.user; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashMap; - import org.elasticsearch.common.collect.MapBuilder; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentType; @@ -19,6 +15,10 @@ import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesResponse; import org.elasticsearch.xpack.security.rest.action.user.RestHasPrivilegesAction.HasPrivilegesRestResponseBuilder; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; + import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; import static org.mockito.Mockito.mock; @@ -30,13 +30,13 @@ public void testBuildValidJsonResponse() throws Exception { final HasPrivilegesResponse actionResponse = new HasPrivilegesResponse(false, Collections.singletonMap("manage", true), Arrays.asList( - new HasPrivilegesResponse.IndexPrivileges("staff", + new HasPrivilegesResponse.ResourcePrivileges("staff", MapBuilder.newMapBuilder(new LinkedHashMap<>()) .put("read", true).put("index", true).put("delete", false).put("manage", false).map()), - new HasPrivilegesResponse.IndexPrivileges("customers", + new HasPrivilegesResponse.ResourcePrivileges("customers", MapBuilder.newMapBuilder(new LinkedHashMap<>()) .put("read", true).put("index", true).put("delete", true).put("manage", false).map()) - )); + ), Collections.emptyMap()); final XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent()); final RestResponse rest = response.buildResponse(actionResponse, builder); @@ -50,6 +50,8 @@ public void testBuildValidJsonResponse() throws Exception { "\"index\":{" + "\"staff\":{\"read\":true,\"index\":true,\"delete\":false,\"manage\":false}," + "\"customers\":{\"read\":true,\"index\":true,\"delete\":true,\"manage\":false}" + - "}}")); + "}," + + "\"application\":{}" + + "}")); } } \ No newline at end of file diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.delete_privileges.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.delete_privileges.json new file mode 100644 index 0000000000000..6086e46eade65 --- /dev/null +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.delete_privileges.json @@ -0,0 +1,30 @@ +{ + "xpack.security.delete_privileges": { + "documentation": "TODO", + "methods": [ "DELETE" ], + "url": { + "path": "/_xpack/security/privilege/{application}/{name}", + "paths": [ "/_xpack/security/privilege/{application}/{name}" ], + "parts": { + "application": { + "type" : "string", + "description" : "Application name", + "required" : true + }, + "name": { + "type" : "string", + "description" : "Privilege name", + "required" : true + } + }, + "params": { + "refresh": { + "type" : "enum", + "options": ["true", "false", "wait_for"], + "description" : "If `true` (the default) then refresh the affected shards to make this operation visible to search, if `wait_for` then wait for a refresh to make this operation visible to search, if `false` then do nothing with refreshes." + } + } + }, + "body": null + } +} diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.get_privileges.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.get_privileges.json new file mode 100644 index 0000000000000..4286ffa954b99 --- /dev/null +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.get_privileges.json @@ -0,0 +1,24 @@ +{ + "xpack.security.get_privileges": { + "documentation": "TODO", + "methods": [ "GET" ], + "url": { + "path": "/_xpack/security/privilege/{application}/{name}", + "paths": [ "/_xpack/security/privilege/{application}/{name}" ], + "parts": { + "application": { + "type" : "string", + "description" : "Application name", + "required" : false + }, + "name": { + "type" : "string", + "description" : "Privilege name", + "required" : false + } + }, + "params": {} + }, + "body": null + } +} diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.has_privileges.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.has_privileges.json new file mode 100644 index 0000000000000..64b15ae9c0222 --- /dev/null +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.has_privileges.json @@ -0,0 +1,22 @@ +{ + "xpack.security.has_privileges": { + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-privileges.html", + "methods": [ "GET", "POST" ], + "url": { + "path": "/_xpack/security/user/_has_privileges", + "paths": [ "/_xpack/security/user/_has_privileges", "/_xpack/security/user/{user}/_has_privileges" ], + "parts": { + "user": { + "type" : "string", + "description" : "Username", + "required" : false + } + }, + "params": {} + }, + "body": { + "description" : "The privileges to test", + "required" : true + } + } +} diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.put_privilege.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.put_privilege.json new file mode 100644 index 0000000000000..3d453682c6431 --- /dev/null +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.put_privilege.json @@ -0,0 +1,33 @@ +{ + "xpack.security.put_privilege": { + "documentation": "TODO", + "methods": [ "POST", "PUT" ], + "url": { + "path": "/_xpack/security/privilege/{application}/{name}", + "paths": [ "/_xpack/security/privilege/{application}/{name}" ], + "parts": { + "application": { + "type" : "string", + "description" : "Application name", + "required" : true + }, + "name": { + "type" : "string", + "description" : "Privilege name", + "required" : true + } + }, + "params": { + "refresh": { + "type" : "enum", + "options": ["true", "false", "wait_for"], + "description" : "If `true` (the default) then refresh the affected shards to make this operation visible to search, if `wait_for` then wait for a refresh to make this operation visible to search, if `false` then do nothing with refreshes." + } + } + }, + "body": { + "description" : "The privilege to add", + "required" : true + } + } +} diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.put_privileges.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.put_privileges.json new file mode 100644 index 0000000000000..07eb541715810 --- /dev/null +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.put_privileges.json @@ -0,0 +1,27 @@ +{ + "xpack.security.put_privileges": { + "documentation": "TODO", + "methods": [ "POST" ], + "url": { + "path": "/_xpack/security/privilege/", + "paths": [ + "/_xpack/security/privilege/" + ], + "params": { + "refresh": { + "type": "enum", + "options": [ + "true", + "false", + "wait_for" + ], + "description": "If `true` (the default) then refresh the affected shards to make this operation visible to search, if `wait_for` then wait for a refresh to make this operation visible to search, if `false` then do nothing with refreshes." + } + } + }, + "body": { + "description" : "The privilege(s) to add", + "required" : true + } + } +} diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/10_basic.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/10_basic.yml new file mode 100644 index 0000000000000..e8dddf2153576 --- /dev/null +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/10_basic.yml @@ -0,0 +1,324 @@ +--- +setup: + - skip: + features: headers + + - do: + cluster.health: + wait_for_status: yellow +--- +teardown: + - do: + xpack.security.delete_privileges: + application: app + name: "p1,p2,p3" + ignore: 404 + - do: + xpack.security.delete_privileges: + application: app2 + name: "p1" + ignore: 404 + - do: + xpack.security.delete_privileges: + application: app3 + name: "p1,p2,p3,p4" + ignore: 404 + - do: + xpack.security.delete_privileges: + application: app4 + name: "p1" + ignore: 404 +--- +"Test put and get privileges": + # Single privilege, with names in URL + - do: + xpack.security.put_privilege: + application: app + name: p1 + body: > + { + "application": "app", + "name": "p1", + "actions": [ "data:read/*" , "action:login" ], + "metadata": { + "key1" : "val1a", + "key2" : "val2a" + } + } + - match: { "app.p1" : { created: true } } + + # Multiple privileges, no names in URL + - do: + xpack.security.put_privileges: + body: > + { + "app": { + "p2": { + "application": "app", + "name": "p2", + "actions": [ "data:read/*" , "action:login" ], + "metadata": { + "key1" : "val1b", + "key2" : "val2b" + } + }, + "p3": { + "application": "app", + "name": "p3", + "actions": [ "data:write/*" , "action:login" ], + "metadata": { + "key1" : "val1c", + "key2" : "val2c" + } + } + }, + "app2" : { + "p1" : { + "application": "app2", + "name": "p1", + "actions": [ "*" ] + } + } + } + - match: { "app.p2" : { created: true } } + - match: { "app.p3" : { created: true } } + - match: { "app2.p1" : { created: true } } + + # Update existing privilege, with names in URL + - do: + xpack.security.put_privilege: + application: app + name: p1 + body: > + { + "application": "app", + "name": "p1", + "actions": [ "data:read/*" , "action:login" ], + "metadata": { + "key3" : "val3" + } + } + - match: { "app.p1" : { created: false } } + + # Get the privilege back + - do: + xpack.security.get_privileges: + application: app + name: p1 + + - match: { + "app.p1" : { + "application": "app", + "name": "p1", + "actions": [ "data:read/*" , "action:login" ], + "metadata": { + "key3" : "val3" + } + } + } + + # Get 2 privileges back + - do: + xpack.security.get_privileges: + application: app + name: p1,p2 + + - match: { + "app.p1" : { + "application": "app", + "name": "p1", + "actions": [ "data:read/*" , "action:login" ], + "metadata": { + "key3" : "val3" + } + } + } + - match: { + "app.p2" : { + "application": "app", + "name": "p2", + "actions": [ "data:read/*" , "action:login" ], + "metadata": { + "key1" : "val1b", + "key2" : "val2b" + } + } + } + + # Get all (3) privileges back for "app" + - do: + xpack.security.get_privileges: + application: "app" + name: "" + + - match: { + "app.p1" : { + "application": "app", + "name": "p1", + "actions": [ "data:read/*" , "action:login" ], + "metadata": { + "key3" : "val3" + } + } + } + - match: { + "app.p2" : { + "application": "app", + "name": "p2", + "actions": [ "data:read/*" , "action:login" ], + "metadata": { + "key1" : "val1b", + "key2" : "val2b" + } + } + } + - match: { + "app.p3" : { + "application": "app", + "name": "p3", + "actions": [ "data:write/*" , "action:login" ], + "metadata": { + "key1" : "val1c", + "key2" : "val2c" + } + } + } + + # Get all (4) privileges back for all apps + - do: + xpack.security.get_privileges: + application: "" + name: "" + + - match: { + "app.p1" : { + "application": "app", + "name": "p1", + "actions": [ "data:read/*" , "action:login" ], + "metadata": { + "key3" : "val3" + } + } + } + - match: { + "app.p2" : { + "application": "app", + "name": "p2", + "actions": [ "data:read/*" , "action:login" ], + "metadata": { + "key1" : "val1b", + "key2" : "val2b" + } + } + } + - match: { + "app.p3" : { + "application": "app", + "name": "p3", + "actions": [ "data:write/*" , "action:login" ], + "metadata": { + "key1" : "val1c", + "key2" : "val2c" + } + } + } + - match: { + "app2.p1" : { + "application": "app2", + "name": "p1", + "actions": [ "*" ], + "metadata": { } + } + } + +--- +"Test put and delete privileges": + # Store some privileges + - do: + xpack.security.put_privileges: + body: > + { + "app3": { + "p1": { + "application": "app3", + "name": "p1", + "actions": [ "data:read/*" ] + }, + "p2": { + "application": "app3", + "name": "p2", + "actions": [ "data:write/*" ] + }, + "p3": { + "application": "app3", + "name": "p3", + "actions": [ "data:write/*", "data:read/*" ] + }, + "p4": { + "application": "app3", + "name": "p4", + "actions": [ "*" ] + } + }, + "app4": { + "p1": { + "application": "app4", + "name": "p1", + "actions": [ "*" ] + } + } + } + - match: { "app3.p1" : { created: true } } + - match: { "app3.p2" : { created: true } } + - match: { "app3.p3" : { created: true } } + - match: { "app3.p4" : { created: true } } + - match: { "app4.p1" : { created: true } } + + # Delete 1 privilege + - do: + xpack.security.delete_privileges: + application: app3 + name: p1 + + - match: { "app3.p1" : { "found" : true } } + + # Delete 2 more privileges (p2, p3) + # and try to delete two that don't exist (p1, p0) + - do: + xpack.security.delete_privileges: + application: app3 + name: p1,p2,p3,p0 + + - match: { "app3.p1" : { "found" : false} } + - match: { "app3.p2" : { "found" : true } } + - match: { "app3.p3" : { "found" : true } } + - match: { "app3.p0" : { "found" : false} } + + # Check the deleted privileges are gone + - do: + catch: missing + xpack.security.get_privileges: + application: app3 + name: p1,p2,p3 + + # Check the non-deleted privileges are there + - do: + xpack.security.get_privileges: + application: "" + name: "" + - match: { + "app3.p4" : { + "application": "app3", + "name": "p4", + "actions": [ "*" ], + "metadata": { } + } + } + - match: { + "app4.p1" : { + "application": "app4", + "name": "p1", + "actions": [ "*" ], + "metadata": { } + } + } diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/20_has_application_privs.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/20_has_application_privs.yml new file mode 100644 index 0000000000000..1860564863fb2 --- /dev/null +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/20_has_application_privs.yml @@ -0,0 +1,190 @@ +--- +setup: + - skip: + features: headers + + - do: + cluster.health: + wait_for_status: yellow + + # Create some privileges + - do: + xpack.security.put_privileges: + body: > + { + "myapp": { + "user": { + "application": "myapp", + "name": "user", + "actions": [ "action:login", "version:1.0.*" ] + }, + "read": { + "application": "myapp", + "name": "read", + "actions": [ "data:read/*" ] + }, + "write": { + "application": "myapp", + "name": "write", + "actions": [ "data:write/*" ] + } + } + } + + # Store 2 test roles + - do: + xpack.security.put_role: + name: "myapp_engineering_read" + body: > + { + "cluster": [], + "indices": [ + { + "names": "engineering-*", + "privileges": ["read"] + } + ], + "applications": [ + { + "application": "myapp", + "privileges": ["user"], + "resources": ["*"] + }, + { + "application": "myapp", + "privileges": ["read"], + "resources": ["engineering/*"] + } + ] + } + + - do: + xpack.security.put_role: + name: "myapp_engineering_write" + body: > + { + "cluster": [], + "indices": [ + { + "names": "engineering-*", + "privileges": ["read"] + } + ], + "applications": [ + { + "application": "myapp", + "privileges": ["user"], + "resources": ["*"] + }, + { + "application": "myapp", + "privileges": ["read", "write"], + "resources": ["engineering/*"] + } + ] + } + + # And a user for each role + - do: + xpack.security.put_user: + username: "eng_read" + body: > + { + "password": "p@ssw0rd", + "roles" : [ "myapp_engineering_read" ] + } + - do: + xpack.security.put_user: + username: "eng_write" + body: > + { + "password": "p@ssw0rd", + "roles" : [ "myapp_engineering_write" ] + } + +--- +teardown: + - do: + xpack.security.delete_privileges: + application: myapp + name: "user,read,write" + ignore: 404 + + - do: + xpack.security.delete_user: + username: "eng_read" + ignore: 404 + + - do: + xpack.security.delete_user: + username: "eng_write" + ignore: 404 + + - do: + xpack.security.delete_role: + name: "myapp_engineering_read" + ignore: 404 + + - do: + xpack.security.delete_role: + name: "myapp_engineering_write" + ignore: 404 +--- +"Test has_privileges with application-privileges": + - do: + headers: { Authorization: "Basic ZW5nX3JlYWQ6cEBzc3cwcmQ=" } # eng_read + xpack.security.has_privileges: + user: null + body: > + { + "index": [ + { + "names" :[ "engineering-logs", "product-logs" ], + "privileges" : [ "read", "index", "write" ] + } + ], + "application": [ + { + "application" : "myapp", + "resources" : [ "*" ], + "privileges" : [ "action:login", "version:1.0.3" ] + }, + { + "application" : "myapp", + "resources" : [ "engineering/logs/*", "product/logs/*" ], + "privileges" : [ "data:read/log/raw", "data:write/log/raw" ] + } + ] + } + + - match: { "username" : "eng_read" } + - match: { "has_all_requested" : false } + - match: { "index" : { + "engineering-logs" : { + "read": true, + "index": false, + "write": false + }, + "product-logs" : { + "read": false, + "index": false, + "write": false + } + } } + - match: { "application" : { + "myapp" : { + "*" : { + "action:login" : true, + "version:1.0.3" : true + }, + "engineering/logs/*" : { + "data:read/log/raw" : true, + "data:write/log/raw" : false + }, + "product/logs/*" : { + "data:read/log/raw" : false, + "data:write/log/raw" : false + } + } + } } + diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/30_superuser.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/30_superuser.yml new file mode 100644 index 0000000000000..cbf08e94d597a --- /dev/null +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/30_superuser.yml @@ -0,0 +1,131 @@ +--- +setup: + - skip: + features: headers + + - do: + cluster.health: + wait_for_status: yellow + + # Create some privileges + - do: + xpack.security.put_privileges: + body: > + { + "app01": { + "user": { + "application": "app01", + "name": "user", + "actions": [ "action:login" ] + }, + "read": { + "application": "app01", + "name": "read", + "actions": [ "data:read/*" ] + }, + "write": { + "application": "app01", + "name": "write", + "actions": [ "data:write/*" ] + } + }, + "app02": { + "user": { + "application": "app02", + "name": "user", + "actions": [ "action:login" ] + }, + "read": { + "application": "app02", + "name": "read", + "actions": [ "data:read/*" ] + }, + "write": { + "application": "app02", + "name": "write", + "actions": [ "data:write/*" ] + } + } + } + + # And a superuser + - do: + xpack.security.put_user: + username: "my_admin" + body: > + { + "password": "admin01", + "roles" : [ "superuser" ] + } + - do: + xpack.security.put_user: + username: "eng_write" + body: > + { + "password": "p@ssw0rd", + "roles" : [ "myapp_engineering_write" ] + } + +--- +teardown: + - do: + xpack.security.delete_privileges: + application: app01 + name: "user,read,write" + ignore: 404 + - do: + xpack.security.delete_privileges: + application: app02 + name: "user,read,write" + ignore: 404 + + - do: + xpack.security.delete_user: + username: "my_admin" + ignore: 404 + +--- +"Test superuser has all application-privileges": + - do: + headers: { Authorization: "Basic bXlfYWRtaW46YWRtaW4wMQ==" } # my_admin + xpack.security.has_privileges: + user: null + body: > + { + "cluster": [ "manage" ], + "index": [ + { + "names" :[ "*" ], + "privileges" : [ "read", "index", "write" ] + } + ], + "application": [ + { + "application" : "app01", + "resources" : [ "*" ], + "privileges" : [ "action:login", "data:read/secrets" ] + }, + { + "application" : "app02", + "resources" : [ "thing/1" ], + "privileges" : [ "data:write/thing" ] + } + ] + } + + - match: { "username" : "my_admin" } + - match: { "has_all_requested" : true } + - match: { "application" : { + "app01" : { + "*" : { + "action:login" : true, + "data:read/secrets" : true + } + }, + "app02" : { + "thing/1" : { + "data:write/thing" : true + } + } + } } + diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/roles/40_global_privileges.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/roles/40_global_privileges.yml new file mode 100644 index 0000000000000..b89efdfe56c33 --- /dev/null +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/roles/40_global_privileges.yml @@ -0,0 +1,51 @@ +--- +setup: + - skip: + features: headers + + - do: + cluster.health: + wait_for_status: yellow + - do: + xpack.security.put_user: + username: "joe" + body: > + { + "password": "s3krit", + "roles" : [ "with_global" ] + } + +--- +teardown: + - do: + xpack.security.delete_user: + username: "joe" + ignore: 404 + - do: + xpack.security.delete_role: + name: "with_global" + ignore: 404 + + +--- +"Test put role with conditional security privileges": + - do: + xpack.security.put_role: + name: "with_global" + body: > + { + "global": { + "application": { + "manage": { + "applications": [ "app1-*" , "app2-*" ] + } + } + } + } + - match: { role: { created: true } } + + - do: + xpack.security.get_role: + name: "with_global" + - match: { with_global.global.application.manage.applications.0: "app1-*" } + - match: { with_global.global.application.manage.applications.1: "app2-*" } diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/security/authz/40_condtional_cluster_priv.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/security/authz/40_condtional_cluster_priv.yml new file mode 100644 index 0000000000000..b3a1e22069083 --- /dev/null +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/security/authz/40_condtional_cluster_priv.yml @@ -0,0 +1,132 @@ +--- +setup: + - skip: + features: headers + + - do: + cluster.health: + wait_for_status: yellow + + - do: + xpack.security.put_user: + username: "test_user" + body: > + { + "password" : "x-pack-test-password", + "roles" : [ "app_manage" ] + } + + - do: + xpack.security.put_role: + name: "app_manage" + body: > + { + "global": { + "application": { + "manage": { + "applications": [ "app" , "app-*" ] + } + } + } + } + + - do: + xpack.security.put_privilege: + application: app-allow + name: read + body: > + { + "actions": [ "data:read/*" ] + } + + - do: + xpack.security.put_privilege: + application: app_deny + name: read + body: > + { + "actions": [ "data:read/*" ] + } + +--- +teardown: + - do: + xpack.security.delete_user: + username: "test_user" + ignore: 404 + - do: + xpack.security.delete_role: + name: "app_manage" + ignore: 404 + + - do: + xpack.security.delete_privileges: + application: app + name: read + ignore: 404 + + - do: + xpack.security.delete_privileges: + application: app-allow + name: read + ignore: 404 + + - do: + xpack.security.delete_privileges: + application: app_deny + name: read + ignore: 404 + +--- +"Test put application privileges when allowed": + + - do: + headers: { Authorization: "Basic dGVzdF91c2VyOngtcGFjay10ZXN0LXBhc3N3b3Jk" } # test_user + xpack.security.put_privilege: + application: app + name: read + body: > + { + "actions": [ "data:read/*" ] + } + - match: { "app.read" : { created: true } } + +--- +"Test get application privileges when allowed": + - do: + headers: { Authorization: "Basic dGVzdF91c2VyOngtcGFjay10ZXN0LXBhc3N3b3Jk" } # test_user + xpack.security.get_privileges: + application: app-allow + name: read + + - match: { + "app-allow.read" : { + "application": "app-allow", + "name": "read", + "actions": [ "data:read/*" ], + "metadata": {} + } + } + +--- +"Test put application privileges when not allowed": + - do: + headers: { Authorization: "Basic dGVzdF91c2VyOngtcGFjay10ZXN0LXBhc3N3b3Jk" } # test_user + xpack.security.put_privilege: + application: app_deny + name: write + body: > + { + "actions": [ "data:write/*" ] + } + catch: forbidden + +--- +"Test get application privileges when not allowed": + - do: + headers: { Authorization: "Basic dGVzdF91c2VyOngtcGFjay10ZXN0LXBhc3N3b3Jk" } # test_user + xpack.security.get_privileges: + application: app_deny + name: read + catch: forbidden +