From 03e5e72d108fac6130be84bf2465b0aa0c79bfc7 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Thu, 7 Jun 2018 14:56:37 +1000 Subject: [PATCH] Introduce Application Privileges to Roles (#30164) This commit introduces "Application Privileges" (aka custom privileges) to the X-Pack security model. Application Privileges are managed within Elasticsearch, and can be tested with the _has_privileges API, but do not grant access to any actions or resources within Elasticsearch. Their purpose is to allow applications outside of Elasticsearch to represent and store their own privileges model within Elasticsearch roles. Specifically, this adds - GET/PUT/DELETE actions for defining application level privileges - application privileges in role definitions - application privileges in the has_privileges API --- .../common/io/stream/StreamInput.java | 19 +- .../common/io/stream/StreamOutput.java | 11 + .../common/io/stream/StreamTests.java | 50 +- .../org/elasticsearch/test/ESTestCase.java | 11 + .../en/rest-api/security/privileges.asciidoc | 3 +- .../docs/en/rest-api/security/roles.asciidoc | 1 + .../privilege/DeletePrivilegesAction.java | 27 + .../privilege/DeletePrivilegesRequest.java | 93 ++++ .../DeletePrivilegesRequestBuilder.java | 33 ++ .../privilege/DeletePrivilegesResponse.java | 57 ++ .../action/privilege/GetPrivilegesAction.java | 27 + .../privilege/GetPrivilegesRequest.java | 71 +++ .../GetPrivilegesRequestBuilder.java | 29 + .../privilege/GetPrivilegesResponse.java | 47 ++ .../action/privilege/PutPrivilegesAction.java | 27 + .../privilege/PutPrivilegesRequest.java | 101 ++++ .../PutPrivilegesRequestBuilder.java | 136 +++++ .../privilege/PutPrivilegesResponse.java | 60 +++ .../security/action/role/PutRoleRequest.java | 19 +- .../action/role/PutRoleRequestBuilder.java | 1 + .../action/user/HasPrivilegesRequest.java | 36 +- .../user/HasPrivilegesRequestBuilder.java | 1 + .../action/user/HasPrivilegesResponse.java | 74 ++- .../core/security/authz/RoleDescriptor.java | 268 +++++++++- .../permission/ApplicationPermission.java | 109 ++++ .../core/security/authz/permission/Role.java | 48 +- .../authz/privilege/ApplicationPrivilege.java | 334 ++++++++++++ .../authz/store/ReservedRolesStore.java | 5 +- .../core/security/client/SecurityClient.java | 41 +- .../resources/security-index-template.json | 20 + .../DeletePrivilegesRequestTests.java | 62 +++ .../DeletePrivilegesResponseTests.java | 30 ++ .../privilege/GetPrivilegesRequestTests.java | 61 +++ .../privilege/GetPrivilegesResponseTests.java | 40 ++ .../privilege/PutPrivilegesRequestTests.java | 92 ++++ .../privilege/PutPrivilegesResponseTests.java | 42 ++ .../user/HasPrivilegesRequestTests.java | 126 +++++ .../ApplicationPermissionTests.java | 107 ++++ .../privilege/ApplicationPrivilegeTests.java | 233 ++++++++ .../ml/action/TransportPutDatafeedAction.java | 5 +- .../xpack/security/Security.java | 27 +- .../TransportDeletePrivilegesAction.java | 54 ++ .../TransportGetPrivilegesAction.java | 60 +++ .../TransportPutPrivilegesAction.java | 50 ++ .../user/TransportHasPrivilegesAction.java | 101 +++- .../authz/store/CompositeRolesStore.java | 94 +++- .../authz/store/NativePrivilegeStore.java | 281 ++++++++++ .../authz/store/NativeRolesStore.java | 15 +- .../rest/action/SecurityBaseRestHandler.java | 2 +- .../privilege/RestDeletePrivilegesAction.java | 66 +++ .../privilege/RestGetPrivilegesAction.java | 94 ++++ .../privilege/RestPutPrivilegeAction.java | 49 ++ .../privilege/RestPutPrivilegesAction.java | 81 +++ .../action/user/RestHasPrivilegesAction.java | 22 +- .../PutPrivilegesRequestBuilderTests.java | 123 +++++ .../HasPrivilegesRequestBuilderTests.java | 2 +- .../TransportHasPrivilegesActionTests.java | 209 +++++++- .../ESNativeRealmMigrateToolTests.java | 10 +- .../authz/AuthorizationServiceTests.java | 500 +++++++++--------- .../authz/AuthorizedIndicesTests.java | 10 +- .../authz/IndicesAndAliasesResolverTests.java | 5 +- .../security/authz/RoleDescriptorTests.java | 63 ++- .../authz/store/CompositeRolesStoreTests.java | 36 +- .../store/NativePrivilegeStoreTests.java | 305 +++++++++++ .../user/HasPrivilegesRestResponseTests.java | 18 +- .../api/xpack.security.delete_privileges.json | 30 ++ .../api/xpack.security.get_privileges.json | 24 + .../api/xpack.security.has_privileges.json | 22 + .../api/xpack.security.put_privilege.json | 33 ++ .../api/xpack.security.put_privileges.json | 27 + .../test/privileges/10_basic.yml | 324 ++++++++++++ .../privileges/20_has_application_privs.yml | 190 +++++++ .../test/privileges/30_superuser.yml | 131 +++++ 73 files changed, 5181 insertions(+), 434 deletions(-) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/DeletePrivilegesAction.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/DeletePrivilegesRequest.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/DeletePrivilegesRequestBuilder.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/DeletePrivilegesResponse.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetPrivilegesAction.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetPrivilegesRequest.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetPrivilegesRequestBuilder.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetPrivilegesResponse.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesAction.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesRequest.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesRequestBuilder.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesResponse.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ApplicationPermission.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ApplicationPrivilege.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/DeletePrivilegesRequestTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/DeletePrivilegesResponseTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/GetPrivilegesRequestTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/GetPrivilegesResponseTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesRequestTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesResponseTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesRequestTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/ApplicationPermissionTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/ApplicationPrivilegeTests.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/privilege/TransportDeletePrivilegesAction.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/privilege/TransportGetPrivilegesAction.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/privilege/TransportPutPrivilegesAction.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativePrivilegeStore.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestDeletePrivilegesAction.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetPrivilegesAction.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestPutPrivilegeAction.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestPutPrivilegesAction.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesRequestBuilderTests.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/NativePrivilegeStoreTests.java create mode 100644 x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.delete_privileges.json create mode 100644 x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.get_privileges.json create mode 100644 x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.has_privileges.json create mode 100644 x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.put_privilege.json create mode 100644 x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.put_privileges.json create mode 100644 x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/10_basic.yml create mode 100644 x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/20_has_application_privs.yml create mode 100644 x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/30_superuser.yml 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 b11aa9d4a9693..d069277aba393 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; @@ -70,6 +71,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.function.Function; import java.util.function.IntFunction; import java.util.function.Supplier; @@ -932,8 +934,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 7d44b3230a15f..48d41863336da 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java @@ -141,6 +141,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; @@ -675,6 +676,16 @@ public static String[] generateRandomStringArray(int maxArraySize, int stringSiz return generateRandomStringArray(maxArraySize, stringSize, allowNull, true); } + public static T[] randomArray(int maxArraySize, IntFunction arrayConstructor, Supplier valueConstructor) { + final int size = randomInt(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/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..395eafa878718 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/DeletePrivilegesAction.java @@ -0,0 +1,27 @@ +/* + * 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(); + } +} 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..92199f11d06aa --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/DeletePrivilegesRequest.java @@ -0,0 +1,93 @@ +/* + * 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 static org.elasticsearch.action.ValidateActions.addValidationError; + +/** + * A request to delete an application privilege. + */ +public final class DeletePrivilegesRequest extends ActionRequest implements 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; + } + + 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..a6f9a576a4719 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/DeletePrivilegesRequestBuilder.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.ActionRequestBuilder; +import org.elasticsearch.action.support.WriteRequestBuilder; +import org.elasticsearch.client.ElasticsearchClient; + +import java.util.Collection; + +/** + * 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..8f503f73c1447 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetPrivilegesAction.java @@ -0,0 +1,27 @@ +/* + * 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(); + } +} 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..78a245ad5bab0 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetPrivilegesRequest.java @@ -0,0 +1,71 @@ +/* + * 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 static org.elasticsearch.action.ValidateActions.addValidationError; + +/** + * Request to retrieve one or more application privileges. + */ +public final class GetPrivilegesRequest extends ActionRequest { + + @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; + } + + 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..305c8d1ff7946 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetPrivilegesRequestBuilder.java @@ -0,0 +1,29 @@ +/* + * 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..0f989c747afbf --- /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.ApplicationPrivilege; + +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 ApplicationPrivilege[] privileges; + + public GetPrivilegesResponse(ApplicationPrivilege... privileges) { + this.privileges = privileges; + } + + public GetPrivilegesResponse(Collection privileges) { + this(privileges.toArray(new ApplicationPrivilege[privileges.size()])); + } + + public ApplicationPrivilege[] privileges() { + return privileges; + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + this.privileges = in.readArray(ApplicationPrivilege::readFrom, ApplicationPrivilege[]::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..457761efaeb2f --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesAction.java @@ -0,0 +1,27 @@ +/* + * 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(); + } +} 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..26810c2feb971 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesRequest.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 org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; +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 WriteRequest { + + private List privileges; + private RefreshPolicy refreshPolicy = RefreshPolicy.IMMEDIATE; + + public PutPrivilegesRequest() { + privileges = Collections.emptyList(); + } + + @Override + public ActionRequestValidationException validate() { + ActionRequestValidationException validationException = null; + for (ApplicationPrivilege privilege : privileges) { + try { + ApplicationPrivilege.validateApplicationName(privilege.getApplication()); + } catch (IllegalArgumentException e) { + validationException = addValidationError(e.getMessage(), validationException); + } + if (privilege.name().size() != 1) { + validationException = addValidationError("application privileges must have a single name (found " + + privilege.name() + ")", validationException); + } + if (MetadataUtils.containsReservedMetadata(privilege.getMetadata())) { + validationException = addValidationError("metadata keys may not start with [" + MetadataUtils.RESERVED_PREFIX + + "] (in privilege " + privilege.name() + ")", 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 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(ApplicationPrivilege::readFrom)); + 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..3e60c48d42ff5 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesRequestBuilder.java @@ -0,0 +1,136 @@ +/* + * 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.ApplicationPrivilege; + +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 ApplicationPrivilege privilege = parsePrivilege(parser, applicationName, expectedName); + this.request.setPrivileges(Collections.singleton(privilege)); + } else { + throw new ElasticsearchParseException("expected an object but found {} instead", token); + } + } + return this; + } + + ApplicationPrivilege parsePrivilege(XContentParser parser, String applicationName, String privilegeName) throws IOException { + final ApplicationPrivilege privilege = ApplicationPrivilege.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 String checkPrivilegeName(ApplicationPrivilege privilege, String applicationName, String providedName) { + if (privilege.name().size() != 1) { + throw new IllegalArgumentException("privilege name [" + privilege.name() + + "] in source must contain exactly 1 value"); + } + final String privilegeName = privilege.getPrivilegeName(); + 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 + "]"); + } + return privilegeName; + } +} 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..1d7e4a0ebd09c 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 @@ -19,6 +19,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -32,10 +33,11 @@ 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() { } @@ -75,6 +77,17 @@ public void addIndex(String[] indices, String[] privileges, String[] grantedFiel .build()); } + void addApplicationPrivileges(RoleDescriptor.ApplicationResourcePrivileges... privileges) { + this.applicationPrivileges.addAll(Arrays.asList(privileges)); + } + + public void addApplicationPrivilege(String[] privileges, String[] resources) { + this.applicationPrivileges.add(RoleDescriptor.ApplicationResourcePrivileges.builder() + .resources(resources) + .privileges(privileges) + .build()); + } + public void runAs(String... usernames) { this.runAs = usernames; } @@ -151,7 +164,9 @@ public RoleDescriptor roleDescriptor() { return new RoleDescriptor(name, clusterPrivileges, indicesPrivileges.toArray(new RoleDescriptor.IndicesPrivileges[indicesPrivileges.size()]), + applicationPrivileges.toArray(new RoleDescriptor.ApplicationResourcePrivileges[applicationPrivileges.size()]), 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 25d443eda3fe9..fbaa21cad16a7 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 @@ -41,6 +41,7 @@ public PutRoleRequestBuilder source(String name, BytesReference source, XContent request.name(name); request.cluster(descriptor.getClusterPrivileges()); 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..dc43db0115e0a 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_7_0_0_alpha1)) { + 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_7_0_0_alpha1)) { + 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 4504a95962c13..bf705da1a1f45 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..e338bc0b6c467 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,63 @@ public Map getClusterPrivileges() { return Collections.unmodifiableMap(cluster); } - public List getIndexPrivileges() { + public List getIndexPrivileges() { return Collections.unmodifiableList(index); } + 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_7_0_0_alpha1)) { + 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_7_0_0_alpha1)) { + 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 +116,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 +137,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 2e03cbb24a320..88bef7b66fa8a 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,8 +18,8 @@ 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.XContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentType; @@ -32,6 +32,7 @@ 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; @@ -47,6 +48,7 @@ public class RoleDescriptor implements ToXContentObject { private final String name; private final String[] clusterPrivileges; private final IndicesPrivileges[] indicesPrivileges; + private final ApplicationResourcePrivileges[] applicationPrivileges; private final String[] runAs; private final Map metadata; private final Map transientMetadata; @@ -58,6 +60,10 @@ public RoleDescriptor(String name, this(name, clusterPrivileges, indicesPrivileges, runAs, null); } + /** + * @deprecated Use {@link #RoleDescriptor(String, String[], IndicesPrivileges[], ApplicationResourcePrivileges[], String[], Map, Map)} + */ + @Deprecated public RoleDescriptor(String name, @Nullable String[] clusterPrivileges, @Nullable IndicesPrivileges[] indicesPrivileges, @@ -66,16 +72,30 @@ public RoleDescriptor(String name, this(name, clusterPrivileges, indicesPrivileges, runAs, metadata, null); } + /** + * @deprecated Use {@link #RoleDescriptor(String, String[], IndicesPrivileges[], ApplicationResourcePrivileges[], 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, runAs, metadata, transientMetadata); + } public RoleDescriptor(String name, @Nullable String[] clusterPrivileges, @Nullable IndicesPrivileges[] indicesPrivileges, + @Nullable ApplicationResourcePrivileges[] applicationPrivileges, @Nullable String[] runAs, @Nullable Map metadata, @Nullable Map transientMetadata) { this.name = name; this.clusterPrivileges = clusterPrivileges != null ? clusterPrivileges : Strings.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) : @@ -94,6 +114,10 @@ public IndicesPrivileges[] getIndicesPrivileges() { return this.indicesPrivileges; } + public ApplicationResourcePrivileges[] getApplicationPrivileges() { + return this.applicationPrivileges; + } + public String[] getRunAs() { return this.runAs; } @@ -119,6 +143,10 @@ public String toString() { 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); @@ -136,6 +164,7 @@ public boolean equals(Object o) { if (!name.equals(that.name)) return false; if (!Arrays.equals(clusterPrivileges, that.clusterPrivileges)) 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); } @@ -145,6 +174,7 @@ public int hashCode() { int result = name.hashCode(); result = 31 * result + Arrays.hashCode(clusterPrivileges); 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; @@ -158,8 +188,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 @@ -170,6 +200,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params, boolea builder.startObject(); builder.array(Fields.CLUSTER.getPreferredName(), clusterPrivileges); builder.array(Fields.INDICES.getPreferredName(), (Object[]) indicesPrivileges); + builder.array(Fields.APPLICATIONS.getPreferredName(), (Object[]) applicationPrivileges); if (runAs != null) { builder.array(Fields.RUN_AS.getPreferredName(), runAs); } @@ -199,7 +230,15 @@ public static RoleDescriptor readFrom(StreamInput in) throws IOException { } else { transientMetadata = Collections.emptyMap(); } - return new RoleDescriptor(name, clusterPrivileges, indicesPrivileges, runAs, metadata, transientMetadata); + + final ApplicationResourcePrivileges[] applicationPrivileges; + if (in.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) { + applicationPrivileges = in.readArray(ApplicationResourcePrivileges::createFrom, ApplicationResourcePrivileges[]::new); + } else { + applicationPrivileges = ApplicationResourcePrivileges.NONE; + } + + return new RoleDescriptor(name, clusterPrivileges, indicesPrivileges, applicationPrivileges, runAs, metadata, transientMetadata); } public static void writeTo(RoleDescriptor descriptor, StreamOutput out) throws IOException { @@ -214,6 +253,9 @@ 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_7_0_0_alpha1)) { + out.writeArray(ApplicationResourcePrivileges::write, descriptor.applicationPrivileges); + } } public static RoleDescriptor parse(String name, BytesReference source, boolean allow2xFormat, XContentType xContentType) @@ -222,7 +264,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); } } @@ -244,6 +286,7 @@ public static RoleDescriptor parse(String name, XContentParser parser, boolean a String currentFieldName = null; IndicesPrivileges[] indicesPrivileges = null; String[] clusterPrivileges = null; + ApplicationResourcePrivileges[] applicationPrivileges = null; String[] runAsUsers = null; Map metadata = null; while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { @@ -256,6 +299,9 @@ 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.METADATA.match(currentFieldName, parser.getDeprecationHandler())) { if (token != XContentParser.Token.START_OBJECT) { throw new ElasticsearchParseException( @@ -267,8 +313,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 @@ -276,7 +321,7 @@ 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, runAsUsers, metadata, null); } private static String[] readStringArray(String roleName, XContentParser parser, boolean allowNull) throws IOException { @@ -292,7 +337,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) { @@ -302,6 +347,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(); @@ -309,14 +355,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)) { @@ -327,7 +376,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); } } @@ -362,7 +411,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) { @@ -475,6 +524,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. @@ -696,14 +776,174 @@ 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 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 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..4447cccc51d82 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ApplicationPermission.java @@ -0,0 +1,109 @@ +/* + * 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.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/Role.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java index 8fed501ece2c9..42c1c74fe3ed9 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,9 +8,11 @@ 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.IndexPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.Privilege; @@ -29,12 +31,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 +54,10 @@ public IndicesPermission indices() { return indices; } + public ApplicationPermission application() { + return application; + } + public RunAsPermission runAs() { return runAs; } @@ -74,7 +82,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: @@ -95,6 +103,7 @@ public static class Builder { 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; @@ -102,7 +111,7 @@ private Builder(String[] names, FieldPermissionsCache fieldPermissionsCache) { } private Builder(RoleDescriptor rd, @Nullable FieldPermissionsCache fieldPermissionsCache) { - this.names = new String[] { rd.getName() }; + this.names = new String[]{rd.getName()}; this.fieldPermissionsCache = fieldPermissionsCache; if (rd.getClusterPrivileges().length == 0) { cluster = ClusterPermission.NONE; @@ -110,6 +119,12 @@ private Builder(RoleDescriptor rd, @Nullable FieldPermissionsCache fieldPermissi this.cluster(ClusterPrivilege.get(Sets.newHashSet(rd.getClusterPrivileges()))); } 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)); @@ -136,10 +151,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 +173,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..8a071de980a10 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ApplicationPrivilege.java @@ -0,0 +1,334 @@ +/* + * 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.io.stream.Writeable; +import org.elasticsearch.common.regex.Regex; +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.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.Locale; +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; + +import static java.util.Collections.emptyMap; + +/** + * 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 zero 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 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 static final Pattern VALID_APPLICATION = Pattern.compile("^[a-z][A-Za-z0-9_-]{2,}$"); + private static final Pattern VALID_APPLICATION_OR_WILDCARD = Pattern.compile("^[A-Za-z0-9_*-]+"); + private static final Pattern VALID_NAME = Pattern.compile("^[a-z][a-zA-Z0-9_.-]*$"); + + public static final Function NONE = app -> new ApplicationPrivilege(app, "none", new String[0]); + + private final String application; + private final String[] patterns; + private final Map metadata; + + public ApplicationPrivilege(String application, String privilegeName, Collection patterns, Map metadata) { + this(application, Collections.singleton(privilegeName), patterns.toArray(new String[patterns.size()]), metadata, true); + } + + public ApplicationPrivilege(String application, String privilegeName, String... patterns) { + this(application, Collections.singleton(privilegeName), patterns, emptyMap(), true); + } + + private ApplicationPrivilege(String application, Set name, String[] patterns, Map metadata, + boolean validateNames) { + super(name, patterns); + this.application = application; + this.patterns = patterns; + this.metadata = new HashMap<>(metadata == null ? emptyMap() : metadata); + validate(validateNames); + } + + public String getApplication() { + return application; + } + + /** + * If this privilege has a single name, returns that name. Otherwise throws {@link IllegalStateException}. + * + * @see #name() + */ + public String getPrivilegeName() { + if (name.size() == 1) { + return name.iterator().next(); + } else { + throw new IllegalStateException(this + " has a multivariate name: " + Strings.collectionToCommaDelimitedString(name)); + } + } + + public Map getMetadata() { + return Collections.unmodifiableMap(metadata); + } + + // Package level for testing + String[] getPatterns() { + return patterns; + } + + private void validate(boolean validateNames) { + // Treat wildcards differently so that the error message matches the context + if (Regex.isSimpleMatchPattern(application)) { + validateApplicationName(application, VALID_APPLICATION_OR_WILDCARD); + } else { + validateApplicationName(application, VALID_APPLICATION); + } + + for (String name : super.name()) { + if (validateNames && isValidPrivilegeName(name) == false) { + throw new IllegalArgumentException("Application privilege names must match the pattern " + VALID_NAME.pattern() + + " (found '" + name + "')"); + } + } + for (String pattern : patterns) { + if (pattern.indexOf('/') == -1 && pattern.indexOf('*') == -1 && pattern.indexOf(':') == -1) { + throw new IllegalArgumentException( + "The application privilege pattern [" + pattern + "] must contain one of [ '/' , '*' , ':' ]"); + } + } + } + + /** + * 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, VALID_APPLICATION); + } + + private static void validateApplicationName(String application, Pattern pattern) { + if (pattern.matcher(application).matches() == false) { + throw new IllegalArgumentException("Application names must match the pattern " + pattern.pattern() + + " (but was '" + application + "')"); + } + } + + private static boolean isValidPrivilegeName(String name) { + return VALID_NAME.matcher(name).matches(); + } + + /** + * 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(cp -> cp.application.equals(application)) + .filter(cp -> cp.name.size() == 1) + .collect(Collectors.toMap(ApplicationPrivilege::getPrivilegeName, 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) { + name = name.toLowerCase(Locale.ROOT); + if (isValidPrivilegeName(name)) { + ApplicationPrivilege privilege = lookup.get(name); + if (privilege != null && size == 1) { + return privilege; + } else if (privilege != null) { + patterns.addAll(Arrays.asList(privilege.patterns)); + } else { + throw new IllegalArgumentException("unknown application privilege [" + names + "]"); + } + } else { + actions.add(name); + } + } + + if (actions.isEmpty()) { + return new ApplicationPrivilege(application, names, patterns.toArray(new String[patterns.size()]), emptyMap(), true); + } else { + patterns.addAll(actions); + return new ApplicationPrivilege(application, names, patterns.toArray(new String[patterns.size()]), emptyMap(), false); + } + } + + @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); + result = 31 * result + Objects.hashCode(metadata); + 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) + && Objects.equals(this.metadata, ((ApplicationPrivilege) o).metadata); + } + + /** + * Converts this object to XContent suitable for storing in the security index - this includes the "type" parameter that is needed + * for index-persistence, but is not used in the Rest API. + */ + public XContentBuilder toIndexContent(XContentBuilder builder) throws IOException { + return writeXContent(builder, true); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return writeXContent(builder, false); + } + + private XContentBuilder writeXContent(XContentBuilder builder, boolean includeType) throws IOException { + builder.startObject() + .field(Fields.APPLICATION.getPreferredName(), application) + .field(Fields.NAME.getPreferredName(), getPrivilegeName()) + .array(Fields.ACTIONS.getPreferredName(), this.patterns) + .field(Fields.METADATA.getPreferredName(), this.metadata); + + if (includeType) { + builder.field(Fields.TYPE.getPreferredName(), DOC_TYPE_VALUE); + } + return builder.endObject(); + } + + /** + * Construct a new {@link ApplicationPrivilege} from XContent. + * + * @param allowType If true, accept a "type" field (for which the value must match {@link #DOC_TYPE_VALUE}); + */ + public static ApplicationPrivilege parse(XContentParser parser, String defaultApplication, String defaultPrivilegeName, + boolean allowType) throws IOException { + final Builder builder = PARSER.parse(parser, allowType); + if (builder.applicationName == null) { + builder.applicationName(defaultApplication); + } + if (builder.privilegeName == null) { + builder.privilegeName(defaultPrivilegeName); + } + return builder.build(); + } + + public static ApplicationPrivilege readFrom(StreamInput in) throws IOException { + final String application = in.readString(); + Set names = in.readSet(StreamInput::readString); + String[] patterns = in.readStringArray(); + Map metadata = (Map) in.readGenericValue(); + return new ApplicationPrivilege(application, names, patterns, metadata, false); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(application); + out.writeCollection(name, StreamOutput::writeString); + out.writeStringArray(patterns); + out.writeGenericValue(metadata); + } + + private static final class Builder { + private String applicationName; + private String privilegeName; + private List actions; + private Map metadata; + + private Builder applicationName(String applicationName) { + this.applicationName = applicationName; + return this; + } + + private Builder privilegeName(String privilegeName) { + this.privilegeName = privilegeName; + return this; + } + + private Builder actions(List actions) { + this.actions = 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 (DOC_TYPE_VALUE.equals(type) == false) { + throw new IllegalStateException("XContent has wrong " + Fields.TYPE.getPreferredName() + " field " + type); + } + return this; + } + + private ApplicationPrivilege build() { + return new ApplicationPrivilege(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/store/ReservedRolesStore.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java index 059c4dfbb6547..c916b02029b0e 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 @@ -27,8 +27,11 @@ public class ReservedRolesStore { new String[] { "all" }, new RoleDescriptor.IndicesPrivileges[] { RoleDescriptor.IndicesPrivileges.builder().indices("*").privileges("all").build()}, + new RoleDescriptor.ApplicationResourcePrivileges[]{ + RoleDescriptor.ApplicationResourcePrivileges.builder().application("*").privileges("*").resources("*").build() + }, new String[] { "*" }, - MetadataUtils.DEFAULT_RESERVED_METADATA); + 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(); 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 af1cfe0579e03..0f90de778a1a6 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; @@ -169,7 +175,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) @@ -276,6 +288,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); } @@ -299,7 +332,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..1e4e0041a0a7e 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,23 @@ } } }, + "applications": { + "type": "object", + "properties": { + "application": { + "type": "keyword" + }, + "privileges": { + "type": "keyword" + }, + "resources": { + "type": "keyword" + } + } + }, + "application" : { + "type" : "keyword" + }, "name" : { "type" : "keyword" }, @@ -103,6 +120,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/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..be557138526ed --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/GetPrivilegesRequestTests.java @@ -0,0 +1,61 @@ +/* + * 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.containsString; +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 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..2d901bc4105ef --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/GetPrivilegesResponseTests.java @@ -0,0 +1,40 @@ +/* + * 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 org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; +import org.hamcrest.Matchers; + +import java.io.IOException; +import java.util.Locale; +import java.util.function.IntFunction; +import java.util.function.Supplier; + +public class GetPrivilegesResponseTests extends ESTestCase { + + public void testSerialization() throws IOException { + ApplicationPrivilege[] privileges = randomArray(6, ApplicationPrivilege[]::new, () -> + new ApplicationPrivilege( + randomAlphaOfLengthBetween(3, 8).toLowerCase(Locale.ROOT), + randomAlphaOfLengthBetween(3, 8).toLowerCase(Locale.ROOT), + randomArray(3, String[]::new, () -> randomAlphaOfLength(3).toLowerCase(Locale.ROOT) + "/*") + ) + ); + 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..3ae197dfe16bd --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesRequestTests.java @@ -0,0 +1,92 @@ +/* + * 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.ApplicationPrivilege; +import org.hamcrest.Matchers; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.Locale; +import java.util.function.IntFunction; +import java.util.function.Supplier; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.iterableWithSize; +import static org.hamcrest.Matchers.notNullValue; + +public class PutPrivilegesRequestTests extends ESTestCase { + + public void testSerialization() throws IOException { + final PutPrivilegesRequest original = request(randomArray(8, ApplicationPrivilege[]::new, () -> new ApplicationPrivilege( + randomAlphaOfLengthBetween(3, 8).toLowerCase(Locale.ROOT), + randomAlphaOfLengthBetween(3, 8).toLowerCase(Locale.ROOT), + randomArray(3, String[]::new, () -> randomAlphaOfLength(3).toLowerCase(Locale.ROOT) + "/*") + ) + )); + 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 ApplicationPrivilege wildcardApp = new ApplicationPrivilege("*", "all", "*"); + assertValidationFailure(request(wildcardApp), "Application names must match"); + + // multiple names + final ApplicationPrivilege read = new ApplicationPrivilege("app", "read", "read/*"); + final ApplicationPrivilege write = new ApplicationPrivilege("app", "write", "write/*"); + final ApplicationPrivilege multiName = ApplicationPrivilege.get("app", + Sets.newHashSet("read", "write"), Sets.newHashSet(read, write)); + assertThat(multiName.name(), iterableWithSize(2)); + assertValidationFailure(request(multiName), "must have a single name"); + + // reserved metadata + final ApplicationPrivilege reservedMetadata = new ApplicationPrivilege("app", "all", Collections.emptyList(), + Collections.singletonMap("_foo", "var")); + assertValidationFailure(request(reservedMetadata), "metadata keys may not start"); + + // mixed + assertValidationFailure(request(wildcardApp, multiName, reservedMetadata), + "Application names must match", "must have a single name", "metadata keys may not start"); + } + + private ApplicationPrivilege reservedMetadata() { + return new ApplicationPrivilege("app", "all", Collections.emptyList(), + Collections.singletonMap("_foo", "var")); + } + + 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(ApplicationPrivilege... 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/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..f58c645de308c --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesRequestTests.java @@ -0,0 +1,126 @@ +/* + * 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_7_0_0_alpha1); + + 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 must match the pattern ^[a-z][A-Za-z0-9_-]{2,}$" + + " (but was '*')")); + } + + 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..16f512cf75f44 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/ApplicationPermissionTests.java @@ -0,0 +1,107 @@ +/* + * 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 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 ApplicationPrivilege app1All = new ApplicationPrivilege("app1", "all", "*"); + private ApplicationPrivilege app1Read = new ApplicationPrivilege("app1", "read", "read/*"); + private ApplicationPrivilege app1Write = new ApplicationPrivilege("app1", "write", "write/*"); + private ApplicationPrivilege app1Delete = new ApplicationPrivilege("app1", "delete", "write/delete"); + private ApplicationPrivilege app1Create = new ApplicationPrivilege("app1", "create", "write/create"); + private ApplicationPrivilege app2Read = new ApplicationPrivilege("app2", "read", "read/*"); + + private List all = Arrays.asList(app1All, app1Read, app1Write, app1Create, app1Delete, app2Read); + + 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 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()), all); + 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/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..f4affdcb994c7 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/ApplicationPrivilegeTests.java @@ -0,0 +1,233 @@ +/* + * 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.apache.lucene.util.automaton.CharacterRunAutomaton; +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.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; + +import static java.util.Arrays.asList; +import static org.elasticsearch.common.xcontent.DeprecationHandler.THROW_UNSUPPORTED_OPERATION; +import static org.hamcrest.Matchers.arrayContainingInAnyOrder; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.iterableWithSize; +import static org.hamcrest.Matchers.sameInstance; + +public class ApplicationPrivilegeTests extends ESTestCase { + + public void testValidationOfApplicationName() { + // too short + assertValidationFailure("Application names", () -> new ApplicationPrivilege("ap", "read", "data:read")); + // must start with lowercase + assertValidationFailure("Application names", () -> new ApplicationPrivilege("App", "read", "data:read")); + // must start with letter + assertValidationFailure("Application names", () -> new ApplicationPrivilege("1app", "read", "data:read")); + // cannot contain special characters + assertValidationFailure("Application names", + () -> new ApplicationPrivilege("app" + randomFrom(":;$#%()+=/'.,".toCharArray()), "read", "data:read")); + // these should all be OK + assertNotNull(new ApplicationPrivilege("app", "read", "data:read")); + assertNotNull(new ApplicationPrivilege("app1", "read", "data:read")); + assertNotNull(new ApplicationPrivilege("myApp", "read", "data:read")); + assertNotNull(new ApplicationPrivilege("my-App", "read", "data:read")); + assertNotNull(new ApplicationPrivilege("my_App", "read", "data:read")); + } + + public void testValidationOfPrivilegeName() { + // must start with lowercase + assertValidationFailure("privilege names", () -> new ApplicationPrivilege("app", "Read", "data:read")); + // must start with letter + assertValidationFailure("privilege names", () -> new ApplicationPrivilege("app", "1read", "data:read")); + // cannot contain special characters + assertValidationFailure("privilege names", + () -> new ApplicationPrivilege("app", "read" + randomFrom(":;$#%()+=/',".toCharArray()), "data:read")); + // these should all be OK + assertNotNull(new ApplicationPrivilege("app", "read", "data:read")); + assertNotNull(new ApplicationPrivilege("app", "read1", "data:read")); + assertNotNull(new ApplicationPrivilege("app", "readData", "data:read")); + assertNotNull(new ApplicationPrivilege("app", "read-data", "data:read")); + assertNotNull(new ApplicationPrivilege("app", "read.data", "data:read")); + assertNotNull(new ApplicationPrivilege("app", "read_data", "data:read")); + } + + public void testValidationOfActions() { + // must contain '/' ':' or '*' + final List invalid = Arrays.asList("data.read", "data_read", "data+read", "read"); + for (String action : invalid) { + assertValidationFailure("privilege pattern", () -> new ApplicationPrivilege("app", "read", action)); + assertValidationFailure("privilege pattern", () -> new ApplicationPrivilege("app", "read", "data:read", action)); + assertValidationFailure("privilege pattern", () -> new ApplicationPrivilege("app", "read", action, "data/read")); + } + + // these should all be OK + assertNotNull(new ApplicationPrivilege("app", "read", "data:read")); + assertNotNull(new ApplicationPrivilege("app", "read", "data/read")); + assertNotNull(new ApplicationPrivilege("app", "read", "data/*")); + assertNotNull(new ApplicationPrivilege("app", "read", "*/read")); + assertNotNull(new ApplicationPrivilege("app", "read", "*/read", "read:*", "data:read")); + } + + public void testGetPrivilegeByName() { + final ApplicationPrivilege myRead = new ApplicationPrivilege("my-app", "read", "data:read/*", "action:login"); + final ApplicationPrivilege myWrite = new ApplicationPrivilege("my-app", "write", "data:write/*", "action:login"); + final ApplicationPrivilege myAdmin = new ApplicationPrivilege("my-app", "admin", "data:read/*", "action:*"); + final ApplicationPrivilege yourRead = new ApplicationPrivilege("your-app", "read", "data:read/*", "action:login"); + final Set stored = Sets.newHashSet(myRead, myWrite, myAdmin, yourRead); + + assertThat(ApplicationPrivilege.get("my-app", Collections.singleton("read"), stored), sameInstance(myRead)); + assertThat(ApplicationPrivilege.get("my-app", Collections.singleton("write"), stored), sameInstance(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")); + assertThat(readWrite.getMetadata().entrySet(), empty()); + + 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)); + } + } + + public void testEqualsAndHashCode() { + final ApplicationPrivilege privilege = randomPrivilege(); + final EqualsHashCodeTestUtils.MutateFunction mutate = randomFrom( + orig -> new ApplicationPrivilege( + "x" + orig.getApplication(), orig.getPrivilegeName(), asList(orig.getPatterns()), orig.getMetadata()), + orig -> new ApplicationPrivilege( + orig.getApplication(), "x" + orig.getPrivilegeName(), asList(orig.getPatterns()), orig.getMetadata()), + orig -> new ApplicationPrivilege( + orig.getApplication(), orig.getPrivilegeName(), Collections.singleton("*"), orig.getMetadata()), + orig -> new ApplicationPrivilege( + orig.getApplication(), orig.getPrivilegeName(), asList(orig.getPatterns()), Collections.singletonMap("clone", "yes")) + ); + EqualsHashCodeTestUtils.checkEqualsAndHashCode(privilege, + original -> new ApplicationPrivilege( + original.getApplication(), original.getPrivilegeName(), asList(original.getPatterns()), original.getMetadata()), + mutate + ); + } + + public void testSerialization() throws IOException { + try (BytesStreamOutput out = new BytesStreamOutput()) { + final ApplicationPrivilege original = randomPrivilege(); + original.writeTo(out); + final ApplicationPrivilege clone = ApplicationPrivilege.readFrom(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 ApplicationPrivilege original = randomPrivilege(); + if (includeTypeField) { + original.toIndexContent(builder); + } 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 ApplicationPrivilege clone = ApplicationPrivilege.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 ApplicationPrivilege privilege = ApplicationPrivilege.parse(parser, "my_app", "read", false); + assertThat(privilege.getApplication(), equalTo("my_app")); + assertThat(privilege.getPrivilegeName(), equalTo("read")); + assertThat(privilege.getPatterns(), equalTo(new String[] { "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 ApplicationPrivilege privilege = ApplicationPrivilege.parse(parser, "my_app", "read", false); + assertThat(privilege.getApplication(), equalTo("your_app")); + assertThat(privilege.getPrivilegeName(), equalTo("write")); + assertThat(privilege.getPatterns(), equalTo(new String[] { "data:write" })); + assertThat(privilege.getMetadata().entrySet(), iterableWithSize(0)); + } + } + + private void assertValidationFailure(String messageContent, Supplier supplier) { + final IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, supplier::get); + assertThat(exception.getMessage(), containsString(messageContent)); + } + + 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 new ApplicationPrivilege(applicationName, privilegeName, asList(patterns), metadata); + } + +} 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 08a9dfb09c1d9..d92a72e3514b1 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 d2e359990966b..2326c2ad9418b 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 @@ -83,6 +83,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; @@ -118,6 +121,9 @@ import org.elasticsearch.xpack.core.security.authz.accesscontrol.SetSecurityUserProcessor; 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.security.authz.store.FileRolesStore; import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; import org.elasticsearch.xpack.core.security.index.IndexAuditTrailField; @@ -173,11 +179,16 @@ 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.NativeRolesStore; import org.elasticsearch.xpack.security.rest.SecurityRestFilter; 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; @@ -448,6 +459,9 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste 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(); @@ -456,7 +470,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 @@ -664,7 +678,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) ); } @@ -709,7 +726,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..4df385cdfcecc --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/privilege/TransportDeletePrivilegesAction.java @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security.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 indexNameExpressionResolver, NativePrivilegeStore privilegeStore, + TransportService transportService) { + super(settings, DeletePrivilegesAction.NAME, threadPool, transportService, actionFilters, indexNameExpressionResolver, + 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..926c1dc2aa991 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/privilege/TransportGetPrivilegesAction.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.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.Strings; +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 indexNameExpressionResolver, NativePrivilegeStore privilegeStore, + TransportService transportService) { + super(settings, GetPrivilegesAction.NAME, threadPool, transportService, actionFilters, indexNameExpressionResolver, + 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..51a5e7099d038 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/privilege/TransportPutPrivilegesAction.java @@ -0,0 +1,50 @@ +/* + * 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 indexNameExpressionResolver, NativePrivilegeStore privilegeStore, + TransportService transportService) { + super(settings, PutPrivilegesAction.NAME, threadPool, transportService, actionFilters, indexNameExpressionResolver, + 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..1ab36c17f0027 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,25 @@ 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.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 +51,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); + HasPrivilegesRequest::new); this.authorizationService = authorizationService; + this.privilegeStore = privilegeStore; } @Override @@ -66,15 +74,31 @@ 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 checkPrivileges(HasPrivilegesRequest request, Role userRole, + 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 +110,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 +195,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/store/CompositeRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java index b5a20af8d30b9..afa112a91eac4 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 @@ -12,6 +12,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; @@ -28,6 +29,7 @@ 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.ApplicationPrivilege; 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; @@ -51,6 +53,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; @@ -80,6 +83,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; @@ -88,7 +92,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); @@ -98,6 +102,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); @@ -117,31 +122,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)); } @@ -238,13 +245,20 @@ 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<>(); 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()); @@ -256,7 +270,7 @@ public static Role buildRoleFromDescriptors(Set roleDescriptors, } 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 = @@ -274,11 +288,22 @@ 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) + final Role.Builder builder = Role.builder(roleNames.toArray(new String[roleNames.size()]), fieldPermissionsCache) .cluster(ClusterPrivilege.get(clusterPrivs)) .runAs(runAsPrivilege); indicesPrivilegesMap.entrySet().forEach((entry) -> { @@ -286,7 +311,22 @@ public static Role buildRoleFromDescriptors(Set roleDescriptors, 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() { @@ -340,11 +380,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..8260c5418bdbd --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativePrivilegeStore.java @@ -0,0 +1,281 @@ +/* + * 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.ApplicationPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege.Fields; +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.ApplicationPrivilege.DOC_TYPE_VALUE; +import static org.elasticsearch.xpack.security.support.SecurityIndexManager.SECURITY_INDEX_NAME; + +/** + * {@code NativePrivilegeStore} is a store that reads {@link ApplicationPrivilege} 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(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(Fields.APPLICATION.getPreferredName(), applications)); + } else if (isEmpty(applications)) { + query = QueryBuilders.boolQuery().filter(typeQuery) + .filter(QueryBuilders.termsQuery(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 (ApplicationPrivilege privilege : privileges) { + innerPutPrivilege(privilege, refreshPolicy, groupListener); + } + }); + } + + private void innerPutPrivilege(ApplicationPrivilege privilege, WriteRequest.RefreshPolicy refreshPolicy, + ActionListener listener) { + if (privilege.name().size() != 1) { + listener.onFailure(new IllegalArgumentException("Cannot store application privileges with multivariate names")); + } else { + try { + final String name = privilege.getPrivilegeName(); + final XContentBuilder xContentBuilder = privilege.toIndexContent(jsonBuilder()); + 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 ApplicationPrivilege 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 ApplicationPrivilege privilege = ApplicationPrivilege.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.name().size() == 1 && privilege.getPrivilegeName().equals(name.v2()) + : "Incorrect name for application privilege. Expected [" + name.v2() + "] but was " + privilege.name(); + 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 9093b6a66739e..e578a4005c4ee 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; @@ -63,6 +64,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 @@ -173,15 +175,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); } @@ -234,7 +238,6 @@ public void onResponse(MultiSearchResponse items) { } else { usageStats.put("size", responses[0].getResponse().getHits().getTotalHits()); } - if (responses[1].isFailure()) { usageStats.put("fls", false); } else { @@ -289,7 +292,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..9c44b874661cd --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetPrivilegesAction.java @@ -0,0 +1,94 @@ +/* + * 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.iterable.Iterables; +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.ApplicationPrivilege; +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 (ApplicationPrivilege privilege : privsByApp.get(app)) { + assert privilege.name().size() == 1 + : "Stored privileges should have a single name (got: " + privilege.name() + ")"; + builder.field(privilege.getPrivilegeName(), 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(ApplicationPrivilege[] privileges) { + return Arrays.stream(privileges).collect(Collectors.toMap( + ApplicationPrivilege::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..dd4168047728c --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestPutPrivilegesAction.java @@ -0,0 +1,81 @@ +/* + * 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.util.iterable.Iterables; +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.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 java.util.function.Function; +import java.util.stream.Collectors; + +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(ApplicationPrivilege::getApplication) + .distinct() + .forEach(a -> result.put(a, new HashMap<>())); + privileges.forEach(privilege -> { + assert privilege.name().size() == 1 : "Privilege name [" + privilege.name() + "] should have a single value"; + String name = privilege.getPrivilegeName(); + 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..e1dbe9a27200c 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 @@ -24,6 +24,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; @@ -84,10 +86,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 +99,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/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..1abac2076a11e --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesRequestBuilderTests.java @@ -0,0 +1,123 @@ +/* + * 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.xcontent.XContentType; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; + +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( + new ApplicationPrivilege("foo", "read", "data:/read/*", "admin:/read/*"), + new ApplicationPrivilege("foo", "write", "data:/write/*", "admin:*"), + new ApplicationPrivilege("foo", "all", "*"), + new ApplicationPrivilege("bar", "read", "read/*"), + new ApplicationPrivilege("bar", "write", "write/*"), + new ApplicationPrivilege("bar", "all", "*") + )); + } + + 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( + new ApplicationPrivilege("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 (ApplicationPrivilege p : builder.request().getPrivileges()) { + assertThat(p.getApplication(), equalTo("foo")); + assertThat(p.getPrivilegeName(), notNullValue()); + } + assertThat(builder.request().getPrivileges().get(0).getPrivilegeName(), equalTo("read")); + assertThat(builder.request().getPrivileges().get(1).getPrivilegeName(), equalTo("write")); + assertThat(builder.request().getPrivileges().get(2).getPrivilegeName(), 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 d4a256b8a0ca8..a9f3531c392fc 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,27 +16,35 @@ 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.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.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.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.List; +import static org.hamcrest.Matchers.arrayWithSize; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; @@ -46,11 +54,14 @@ 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 +86,18 @@ public void setup() { return null; }).when(authorizationService).roles(eq(user), any(ActionListener.class)); + applicationPrivileges = Collections.emptyList(); + 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); + mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), authorizationService, privilegeStore); } /** @@ -93,6 +114,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 +126,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 +151,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 +163,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 +201,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 +215,26 @@ 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 = new ApplicationPrivilege("kibana", "read", + "data:read/*", "action:login", "action:view/dashboard"); + final ApplicationPrivilege kibanaWrite = new ApplicationPrivilege("kibana", "write", + "data:write/*", "action:login", "action:view/dashboard"); + final ApplicationPrivilege kibanaAdmin = new ApplicationPrivilege("kibana", "admin", + "action:login", "action:manage/*"); + final ApplicationPrivilege kibanaViewSpace = new ApplicationPrivilege("kibana", "view-space", + "action:login", "space:view/*"); + applicationPrivileges = Arrays.asList( + kibanaRead, + kibanaWrite, + kibanaAdmin, + kibanaViewSpace + ); role = Role.builder("test3") .add(IndexPrivilege.ALL, "logstash-*", "foo?") .add(IndexPrivilege.READ, "abc*") .add(IndexPrivilege.WRITE, "*xyz") + .addApplicationPrivilege(kibanaRead, Collections.singleton("*")) + .addApplicationPrivilege(kibanaViewSpace, Sets.newHashSet("space/engineering/*", "space/builds")) .build(); final HasPrivilegesRequest request = new HasPrivilegesRequest(); @@ -231,6 +270,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,14 +292,22 @@ 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)) )); } @@ -263,27 +324,118 @@ 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 = new ApplicationPrivilege("app1", "read", "data:read/*"); + final ApplicationPrivilege app1Write = new ApplicationPrivilege("app1", "write", "data:write/*"); + final ApplicationPrivilege app1All = new ApplicationPrivilege("app1", "all", "*"); + final ApplicationPrivilege app2Read = new ApplicationPrivilege("app2", "read", "data:read/*"); + final ApplicationPrivilege app2Write = new ApplicationPrivilege("app2", "write", "data:write/*"); + final ApplicationPrivilege app2All = new ApplicationPrivilege("app2", "all", "*"); + applicationPrivileges = Arrays.asList(app1Read, app1Write, app1All, app2Read, app2Write, app2All); + + 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 testIsCompleteMatch() throws Exception { + final ApplicationPrivilege kibanaRead = new ApplicationPrivilege("kibana", "read", "data:read/*"); + final ApplicationPrivilege kibanaWrite = new ApplicationPrivilege("kibana", "write", "data:write/*"); + this.applicationPrivileges = Arrays.asList(kibanaRead, kibanaWrite); 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 +447,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(); @@ -310,4 +473,4 @@ private static MapBuilder mapBuilder() { return MapBuilder.newMapBuilder(); } -} \ No newline at end of file +} 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 c42353ee75232..e94cdff423274 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 @@ -47,7 +47,7 @@ protected MigrateUserOrRoles newMigrateUserOrRoles() { @Override protected Environment createEnv(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()); } @@ -75,9 +75,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/authz/AuthorizationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java index bcd31c32f7f78..0773db9bb951e 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 @@ -113,6 +113,7 @@ 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.store.ReservedRolesStore; import org.elasticsearch.xpack.core.security.user.AnonymousUser; import org.elasticsearch.xpack.core.security.user.ElasticUser; @@ -122,6 +123,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.plugin.SqlQueryAction; import org.elasticsearch.xpack.sql.plugin.SqlQueryRequest; import org.junit.Before; @@ -129,6 +131,8 @@ 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; @@ -173,8 +177,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 +186,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 +213,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 +241,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 +253,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 +263,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 +274,10 @@ 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); } @@ -270,8 +286,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 +313,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 +329,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,8 +340,8 @@ 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); } @@ -340,24 +356,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 +382,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 +406,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 +417,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 +430,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 +456,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 +497,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 +515,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 +533,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 +551,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 +571,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 +596,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 +617,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 +626,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 +695,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 +742,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 +779,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 +797,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 +815,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 +859,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 +876,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 +902,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 +921,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 +933,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 +946,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 +986,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 +1049,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 +1065,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 +1104,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 +1121,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 +1130,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 +1152,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 +1169,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 +1181,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 +1192,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 +1239,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 +1261,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 +1271,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 +1287,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 +1304,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 +1319,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 +1333,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..ab68c5ecebed8 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 @@ -6,7 +6,9 @@ package org.elasticsearch.xpack.security.authz; import org.elasticsearch.Version; +import org.elasticsearch.action.ActionListener; 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; @@ -23,7 +25,9 @@ import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; import org.elasticsearch.xpack.security.support.SecurityIndexManager; +import java.util.HashSet; import java.util.List; +import java.util.Set; import static org.hamcrest.Matchers.containsInAnyOrder; @@ -57,8 +61,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..8ee7c289d9d56 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 @@ -22,6 +22,11 @@ import java.util.Map; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.hamcrest.Matchers.arrayContaining; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.emptyArray; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.core.Is.is; public class RoleDescriptorTests extends ESTestCase { @@ -45,9 +50,18 @@ public void testToString() throws Exception { .query("{\"query\": {\"match_all\": {}}}") .build() }; - RoleDescriptor descriptor = new RoleDescriptor("test", new String[] { "all", "none" }, groups, new String[] { "sudo" }); + final RoleDescriptor.ApplicationResourcePrivileges[] applicationPrivileges = { + RoleDescriptor.ApplicationResourcePrivileges.builder() + .application("my_app") + .privileges("read", "write") + .resources("*") + .build() + }; + RoleDescriptor descriptor = new RoleDescriptor("test", new String[] { "all", "none" }, groups, applicationPrivileges, + new String[] { "sudo" }, Collections.emptyMap(), Collections.emptyMap()); 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\": {}}}],]" + + ", applicationPrivileges=[ApplicationResourcePrivileges[application=my_app, privileges=[read,write], resources=[*]],]" + ", runAs=[sudo], metadata=[{}]]")); } @@ -60,8 +74,16 @@ 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() + }; 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, + 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); @@ -113,6 +135,43 @@ 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\" }" + + "] }"; + 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")); + + 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")); + + 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 { 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 ff9d93b3ba818..31037192aa3ec 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 @@ -105,7 +105,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<>(); @@ -165,7 +166,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<>(); @@ -198,7 +200,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 @@ -276,8 +278,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<>(); @@ -330,7 +332,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") @@ -372,8 +376,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<>(); @@ -413,7 +417,7 @@ public void testCustomRolesProvidersLicensing() { // these licenses don't allow custom role providers xPackLicenseState.update(randomFrom(OperationMode.BASIC, OperationMode.GOLD, OperationMode.STANDARD), true); 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"); @@ -426,7 +430,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); @@ -441,7 +445,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); roleNames = Sets.newHashSet("roleA"); @@ -461,7 +465,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(); @@ -504,9 +509,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/NativePrivilegeStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/NativePrivilegeStoreTests.java new file mode 100644 index 0000000000000..58e693097455a --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/NativePrivilegeStoreTests.java @@ -0,0 +1,305 @@ +/* + * 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.ApplicationPrivilege; +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.Collections; +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 java.util.stream.Collectors; + +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 ApplicationPrivilege sourcePrivilege = new ApplicationPrivilege("myapp", "admin", + Arrays.asList("action:admin/*", "action:login", "data:read/*"), + Collections.singletonMap("myapp-version", "1.2.3")); + + 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), Collections.emptyMap()) + )); + final ApplicationPrivilege 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, Collections.emptyMap()) + )); + final ApplicationPrivilege getPrivilege = future.get(1, TimeUnit.SECONDS); + assertThat(getPrivilege, Matchers.nullValue()); + } + + public void testGetPrivilegesByApplicationName() throws Exception { + final List sourcePrivileges = Arrays.asList( + new ApplicationPrivilege("myapp", "admin", "action:admin/*", "action:login", "data:read/*"), + new ApplicationPrivilege("myapp", "user", "action:login", "data:read/*"), + new ApplicationPrivilege("myapp", "author", "action:login", "data:read/*", "data:write/*") + ); + + 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 ApplicationPrivilege("app1", "admin", "action:admin/*", "action:login", "data:read/*"), + new ApplicationPrivilege("app2", "user", "action:login", "data:read/*"), + new ApplicationPrivilege("app3", "all", "*") + ); + + 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 ApplicationPrivilege("app1", "admin", "action:admin/*", "action:login", "data:read/*"), + new ApplicationPrivilege("app1", "user", "action:login", "data:read/*"), + new ApplicationPrivilege("app2", "all", "*") + ); + + 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++) { + ApplicationPrivilege 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.getPrivilegeName() + )); + final XContentBuilder builder + = privilege.toIndexContent(XContentBuilder.builder(XContentType.JSON.xContent())); + assertThat(request.source(), equalTo(BytesReference.bytes(builder))); + final boolean created = privilege.name().contains("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 ApplicationPrivilege p = sourcePrivileges.get(i); + hits[i] = new SearchHit(i, "application-privilege_" + p.getApplication() + ":" + p.getPrivilegeName(), 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(getPrivileges.stream().flatMap(p -> p.name().stream()).collect(Collectors.toSet()), + equalTo(sourcePrivileges.stream().flatMap(p -> p.name().stream()).collect(Collectors.toSet()))); + assertThat(getPrivileges.stream().map(Strings::toString).collect(Collectors.toSet()), + equalTo(sourcePrivileges.stream().map(Strings::toString).collect(Collectors.toSet()))); + } +} 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 + } + } + } } +