From fdf628ba69778fd242a87da773519c3fdcf8363d Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Thu, 26 Apr 2018 14:01:06 +1000 Subject: [PATCH 01/20] Support application (custom) privileges Adds - CRUD actions for defining application level privileges - application privileges in role definitions - application privileges in the has_privileges API --- .../privilege/DeletePrivilegesAction.java | 32 ++ .../privilege/DeletePrivilegesRequest.java | 90 +++++ .../DeletePrivilegesRequestBuilder.java | 38 ++ .../privilege/DeletePrivilegesResponse.java | 66 ++++ .../action/privilege/GetPrivilegesAction.java | 32 ++ .../privilege/GetPrivilegesRequest.java | 69 ++++ .../GetPrivilegesRequestBuilder.java | 34 ++ .../privilege/GetPrivilegesResponse.java | 47 +++ .../action/privilege/PutPrivilegesAction.java | 32 ++ .../privilege/PutPrivilegesRequest.java | 96 ++++++ .../PutPrivilegesRequestBuilder.java | 143 ++++++++ .../privilege/PutPrivilegesResponse.java | 57 +++ .../security/action/role/PutRoleRequest.java | 19 +- .../action/role/PutRoleRequestBuilder.java | 1 + .../action/user/HasPrivilegesRequest.java | 29 +- .../user/HasPrivilegesRequestBuilder.java | 1 + .../action/user/HasPrivilegesResponse.java | 74 ++-- .../core/security/authz/RoleDescriptor.java | 258 +++++++++++++- .../permission/ApplicationPermission.java | 60 ++++ .../core/security/authz/permission/Role.java | 20 +- .../authz/privilege/ApplicationPrivilege.java | 262 ++++++++++++++ .../core/security/client/SecurityClient.java | 35 +- .../resources/security-index-template.json | 20 ++ .../ApplicationPermissionTests.java | 107 ++++++ .../ml/action/TransportPutDatafeedAction.java | 4 +- .../xpack/security/Security.java | 27 +- .../TransportDeletePrivilegesAction.java | 54 +++ .../TransportGetPrivilegesAction.java | 60 ++++ .../TransportPutPrivilegesAction.java | 54 +++ .../user/TransportHasPrivilegesAction.java | 89 ++++- .../authz/store/CompositeRolesStore.java | 95 +++-- .../authz/store/NativePrivilegeStore.java | 257 ++++++++++++++ .../authz/store/NativeRolesStore.java | 24 +- .../rest/action/SecurityBaseRestHandler.java | 2 +- .../privilege/RestDeletePrivilegesAction.java | 74 ++++ .../privilege/RestGetPrivilegesAction.java | 94 +++++ .../privilege/RestPutPrivilegeAction.java | 49 +++ .../privilege/RestPutPrivilegesAction.java | 81 +++++ .../action/user/RestHasPrivilegesAction.java | 22 +- .../PutPrivilegesRequestBuilderTests.java | 104 ++++++ .../HasPrivilegesRequestBuilderTests.java | 2 +- .../TransportHasPrivilegesActionTests.java | 207 +++++++++-- .../ESNativeRealmMigrateToolTests.java | 10 +- .../authz/AuthorizationServiceTests.java | 5 +- .../authz/AuthorizedIndicesTests.java | 10 +- .../authz/IndicesAndAliasesResolverTests.java | 5 +- .../security/authz/RoleDescriptorTests.java | 44 ++- .../authz/store/CompositeRolesStoreTests.java | 36 +- .../store/NativePrivilegeStoreTests.java | 284 +++++++++++++++ .../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 ++++++++++ 57 files changed, 3815 insertions(+), 168 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/authz/permission/ApplicationPermissionTests.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 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..0bf67743e6323 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/DeletePrivilegesAction.java @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.security.action.privilege; + +import org.elasticsearch.action.Action; +import org.elasticsearch.client.ElasticsearchClient; + +/** + * Action for deleting application privileges. + */ +public class DeletePrivilegesAction extends Action { + + public static final DeletePrivilegesAction INSTANCE = new DeletePrivilegesAction(); + public static final String NAME = "cluster:admin/xpack/security/privilege/delete"; + + protected DeletePrivilegesAction() { + super(NAME); + } + + @Override + public DeletePrivilegesRequestBuilder newRequestBuilder(ElasticsearchClient client) { + return new DeletePrivilegesRequestBuilder(client, this); + } + + @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..1229a7e9482d4 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/DeletePrivilegesRequest.java @@ -0,0 +1,90 @@ +/* + * 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 static org.elasticsearch.action.ValidateActions.addValidationError; + +/** + * A request to delete an application privilege. + */ +public 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) { + application(application); + 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) { + 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); + privileges = in.readStringArray(); + refreshPolicy = RefreshPolicy.readFrom(in); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + 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..cb65f16e5be88 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/DeletePrivilegesRequestBuilder.java @@ -0,0 +1,38 @@ +/* + * 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 class DeletePrivilegesRequestBuilder + extends ActionRequestBuilder + implements WriteRequestBuilder { + + public DeletePrivilegesRequestBuilder(ElasticsearchClient client) { + this(client, DeletePrivilegesAction.INSTANCE); + } + + 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..54d9c2d5398cc --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/DeletePrivilegesResponse.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.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.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Response when deleting application privileges. + * Returns a collection of privileges that were successfully found and deleted. + */ +public class DeletePrivilegesResponse extends ActionResponse implements ToXContentObject { + + private Set found; + + public DeletePrivilegesResponse() { + } + + public DeletePrivilegesResponse(Collection found) { + this.found = 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 Collections.unmodifiableSet(this.found); + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + final int size = in.readVInt(); + found = new HashSet<>(size); + for (int i = 0; i < size; i++) { + found.add(in.readString()); + } + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeVInt(found.size()); + for (String s : found) { + out.writeString(s); + } + } + +} 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..f4ac0a44d56de --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetPrivilegesAction.java @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.security.action.privilege; + +import org.elasticsearch.action.Action; +import org.elasticsearch.client.ElasticsearchClient; + +/** + * Action for retrieving one or more application privileges from the security index + */ +public class GetPrivilegesAction extends Action { + + public static final GetPrivilegesAction INSTANCE = new GetPrivilegesAction(); + public static final String NAME = "cluster:admin/xpack/security/privilege/get"; + + protected GetPrivilegesAction() { + super(NAME); + } + + @Override + public GetPrivilegesRequestBuilder newRequestBuilder(ElasticsearchClient client) { + return new GetPrivilegesRequestBuilder(client, this); + } + + @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..ec22006878cda --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetPrivilegesRequest.java @@ -0,0 +1,69 @@ +/* + * 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 class GetPrivilegesRequest extends ActionRequest { + + private @Nullable + 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); + privileges = in.readStringArray(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + 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..56cf47d8dd771 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetPrivilegesRequestBuilder.java @@ -0,0 +1,34 @@ +/* + * 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 class GetPrivilegesRequestBuilder + extends ActionRequestBuilder { + + public GetPrivilegesRequestBuilder(ElasticsearchClient client) { + this(client, GetPrivilegesAction.INSTANCE); + } + + 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..ce8fefb181fb7 --- /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 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); + 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..1bffa55ba2bdb --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesAction.java @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.security.action.privilege; + +import org.elasticsearch.action.Action; +import org.elasticsearch.client.ElasticsearchClient; + +/** + * Action for putting (adding/updating) one or more application privileges. + */ +public class PutPrivilegesAction extends Action { + + public static final PutPrivilegesAction INSTANCE = new PutPrivilegesAction(); + public static final String NAME = "cluster:admin/xpack/security/privilege/put"; + + protected PutPrivilegesAction() { + super(NAME); + } + + @Override + public PutPrivilegesRequestBuilder newRequestBuilder(ElasticsearchClient client) { + return new PutPrivilegesRequestBuilder(client, this); + } + + @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..16bf22e5ee675 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesRequest.java @@ -0,0 +1,96 @@ +/* + * 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 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) { + 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 Collections.unmodifiableList(privileges); + } + + public void setPrivileges(Collection privileges) { + this.privileges = 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 = 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..ac46720344ec1 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesRequestBuilder.java @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.security.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.logging.Loggers; +import org.elasticsearch.common.util.iterable.Iterables; +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 class PutPrivilegesRequestBuilder + extends ActionRequestBuilder + implements WriteRequestBuilder { + + public PutPrivilegesRequestBuilder(ElasticsearchClient client) { + this(client, PutPrivilegesAction.INSTANCE); + } + + 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, 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 = Iterables.get(privilege.name(), 0); + 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..ac4cf44ef039a --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesResponse.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.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 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 class PutPrivilegesResponse extends ActionResponse implements ToXContentObject { + + private Map> created; + + public PutPrivilegesResponse() { + this(Collections.emptyMap()); + } + + public PutPrivilegesResponse(Map> created) { + this.created = Collections.unmodifiableMap(created); + } + + 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 = 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 79142a7b5758a..2eb5c38459857 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..effbbd91629f4 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,13 @@ */ 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 java.io.IOException; @@ -23,6 +25,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 +36,13 @@ 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); + } + 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; } @@ -56,7 +63,7 @@ public void username(String username) { @Override public String[] usernames() { - return new String[] { username }; + return new String[]{username}; } public RoleDescriptor.IndicesPrivileges[] indexPrivileges() { @@ -67,6 +74,10 @@ public String[] clusterPrivileges() { return clusterPrivileges; } + public ApplicationResourcePrivileges[] applicationPrivileges() { + return applicationPrivileges; + } + public void indexPrivileges(RoleDescriptor.IndicesPrivileges... privileges) { this.indexPrivileges = privileges; } @@ -75,6 +86,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 +100,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 +114,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 2bf2bdb4d876e..af28777abd374 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesRequestBuilder.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesRequestBuilder.java @@ -39,6 +39,7 @@ public HasPrivilegesRequestBuilder source(String username, BytesReference source request.username(username); request.indexPrivileges(role.getIndicesPrivileges()); request.clusterPrivileges(role.getClusterPrivileges()); + request.applicationPrivileges(role.getApplicationPrivileges()); return this; } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponse.java index dcc34d75ddbaf..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..6979491d47773 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; @@ -66,16 +68,26 @@ public RoleDescriptor(String name, this(name, clusterPrivileges, indicesPrivileges, runAs, metadata, null); } + 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 +106,10 @@ public IndicesPrivileges[] getIndicesPrivileges() { return this.indicesPrivileges; } + public ApplicationResourcePrivileges[] getApplicationPrivileges() { + return this.applicationPrivileges; + } + public String[] getRunAs() { return this.runAs; } @@ -119,6 +135,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 +156,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 +166,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 +180,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 +192,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 +222,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 +245,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 +256,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 +278,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 +291,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 +305,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 +313,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 +329,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 +339,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 +347,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 +368,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 +403,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 +516,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 +768,172 @@ 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("[resources=") + .append(Strings.arrayToCommaDelimitedString(resources)) + .append("], privileges=[") + .append(Strings.arrayToCommaDelimitedString(privileges)) + .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..e3d332d77b274 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ApplicationPermission.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.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.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * 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 Map privileges; + + public ApplicationPermission(List>> tuples) { + this.logger = Loggers.getLogger(getClass()); + this.privileges = new HashMap<>(); + tuples.forEach(tup -> privileges.compute(tup.v1(), (k, existing) -> { + final Automaton patterns = Automatons.patterns(tup.v2()); + if (existing == null) { + return patterns; + } else { + return Automatons.unionAndMinimize(Arrays.asList(existing, patterns)); + } + })); + } + + public boolean check(ApplicationPrivilege other, String resource) { + Automaton resourceAutomaton = Automatons.patterns(resource); + logger.debug("Checking permission [{}] against [{} , {}]", this, other, resource); + return privileges.entrySet().stream() + .anyMatch(entry -> Objects.equals(other.getApplication(), entry.getKey().getApplication()) + && Operations.subsetOf(other.getAutomaton(), entry.getKey().getAutomaton()) + && Operations.subsetOf(resourceAutomaton, entry.getValue())); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{privileges=" + privileges.keySet() + "}"; + } +} 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..9b7fb83c497da 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,10 +8,12 @@ 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.ClusterPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; 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; } @@ -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; @@ -136,10 +145,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); + final ApplicationPermission applicationPermission + = applicationPrivs.isEmpty() ? ApplicationPermission.NONE : new ApplicationPermission(applicationPrivs); + return new Role(names, cluster, indices, applicationPermission, runAs); } static List convertFromIndicesPrivileges(RoleDescriptor.IndicesPrivileges[] indicesPrivileges, 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..0c87768a8c3ef --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ApplicationPrivilege.java @@ -0,0 +1,262 @@ +/* + * 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.collect.Tuple; +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.util.iterable.Iterables; +import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +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.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.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * An application privilege has a name (e.g. {@code "my-app:admin"}) that starts with a prefix that identifies the app, + * followed by a suffix that is meaningful to the application. + * It also has zero or more "action patterns" that start with the same prefix and contain at least 1 '/' character (e.g. + * {@code "my-app:/admin/user/*", "my-app:/admin/team/*"}. + * 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 ConstructingObjectParser PARSER = new ConstructingObjectParser<>(DOC_TYPE_VALUE, + arr -> new ApplicationPrivilege((String) arr[0], (String) arr[1], (List) arr[2], (Map) arr[3]) + ); + + static { + PARSER.declareString(ConstructingObjectParser.constructorArg(), Fields.APPLICATION); + PARSER.declareString(ConstructingObjectParser.constructorArg(), Fields.NAME); + PARSER.declareStringArray(ConstructingObjectParser.constructorArg(), Fields.ACTIONS); + PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), (parser, context) -> parser.map(), Fields.METADATA); + PARSER.declareField((priv, fieldValue) -> { + if (DOC_TYPE_VALUE.equals(fieldValue) == false) { + throw new IllegalStateException("XContent has wrong " + Fields.TYPE.getPreferredName() + " field " + fieldValue); + } + }, (parser, acceptType) -> { + if (acceptType == false) { + throw new IllegalStateException("Field " + Fields.TYPE.getPreferredName() + " cannot be specified here"); + } + return parser.text(); + }, 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_NAME = Pattern.compile("^[a-z][a-zA-Z0-9_.-]*$"); + private static final ConcurrentHashMap>, ApplicationPrivilege> CACHE = new ConcurrentHashMap<>(); + + 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, metadata, true); + } + + public ApplicationPrivilege(String application, String privilegeName, String... patterns) { + this(application, Sets.newHashSet(privilegeName), patterns, Collections.emptyMap(), true); + } + + private ApplicationPrivilege(String application, Set name, Collection patterns, Map metadata, + boolean validateNames) { + this(application, name, patterns.toArray(new String[patterns.size()]), metadata, validateNames); + } + + 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 ? Collections.emptyMap() : metadata); + validate(validateNames); + } + + public String getApplication() { + return application; + } + + public Map getMetadata() { + return Collections.unmodifiableMap(metadata); + } + + private void validate(boolean validateNames) { + if (VALID_APPLICATION.matcher(application).matches() == false) { + throw new IllegalArgumentException("Application names must match /" + + VALID_APPLICATION.pattern() + "/ (but was '" + application + "')"); + } + + for (String name : super.name()) { + if (validateNames && isPrivilegeName(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 [ '/' , '*' , ':' ]"); + } + } + } + + public static boolean isPrivilegeName(String name) { + return VALID_NAME.matcher(name).matches(); + } + + public static ApplicationPrivilege get(String application, Set name, Collection stored) { + Tuple> key = new Tuple<>(application, name); + return CACHE.computeIfAbsent(key, (ignore) -> { + 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(cp -> Iterables.get(cp.name, 0), 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 (isPrivilegeName(name) != false) { + 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, Collections.emptyMap(), true); + } else { + patterns.addAll(actions); + return new ApplicationPrivilege(application, names, patterns, Collections.emptyMap(), false); + } + } + + @Override + public String toString() { + return application + ":" + super.toString(); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + Objects.hashCode(application); + return result; + } + + @Override + public boolean equals(Object o) { + return super.equals(o) && Objects.equals(this.application, ((ApplicationPrivilege) o).application); + } + + public XContentBuilder toIndexContent(XContentBuilder builder, Params params) 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 { + assert name.size() == 1; + + builder.startObject() + .field(Fields.APPLICATION.getPreferredName(), application) + .field(Fields.NAME.getPreferredName(), Iterables.get(name, 0)) + .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(); + } + + public static ApplicationPrivilege parse(XContentParser parser, boolean allowType) throws IOException { + return PARSER.parse(parser, allowType); + } + + public static ApplicationPrivilege readFrom(StreamInput in) throws IOException { + final String application = in.readString(); + int nameSize = in.readVInt(); + Set names = new HashSet<>(nameSize); + for (int i = 0; i < nameSize; i++) { + names.add(in.readString()); + } + String[] patterns = in.readStringArray(); + Map metadata = in.readBoolean() ? in.readMap() : Collections.emptyMap(); + return new ApplicationPrivilege(application, names, patterns, metadata, false); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(application); + out.writeVInt(name.size()); + for (String n : name) { + out.writeString(n); + } + out.writeStringArray(patterns); + if (metadata.isEmpty()) { + out.writeBoolean(false); + } else { + out.writeBoolean(true); + out.writeMap(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/client/SecurityClient.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/client/SecurityClient.java index af1cfe0579e03..aebf1cae7fc6d 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,9 @@ 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.DeletePrivilegesRequestBuilder; +import org.elasticsearch.xpack.core.security.action.privilege.GetPrivilegesRequestBuilder; +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 +172,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 +285,24 @@ public DeleteRoleMappingRequestBuilder prepareDeleteRoleMapping(String name) { .name(name); } + /* -- Application Privileges -- */ + public GetPrivilegesRequestBuilder prepareGetPrivileges(String applicationName, String[] privileges) { + return new GetPrivilegesRequestBuilder(client).application(applicationName).privileges(privileges); + } + + public PutPrivilegesRequestBuilder preparePutPrivilege(String applicationName, String privilegeName, + BytesReference bytesReference, XContentType xContentType) throws IOException { + return new PutPrivilegesRequestBuilder(client).source(applicationName, privilegeName, bytesReference, xContentType); + } + + public PutPrivilegesRequestBuilder preparePutPrivileges(BytesReference bytesReference, XContentType xContentType) throws IOException { + return new PutPrivilegesRequestBuilder(client).source(bytesReference, xContentType); + } + + public DeletePrivilegesRequestBuilder prepareDeletePrivileges(String applicationName, String[] privileges) { + return new DeletePrivilegesRequestBuilder(client).application(applicationName).privileges(privileges); + } + public CreateTokenRequestBuilder prepareCreateToken() { return new CreateTokenRequestBuilder(client, CreateTokenAction.INSTANCE); } @@ -299,7 +326,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/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..62ea62dfa74f8 --- /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.check(app1Write, "*"), equalTo(true)); + assertThat(hasPermission.check(app1Write, "foo"), equalTo(true)); + assertThat(hasPermission.check(app1Delete, "*"), equalTo(true)); + assertThat(hasPermission.check(app1Create, "foo"), equalTo(true)); + + assertThat(hasPermission.check(app1Read, "*"), equalTo(false)); + assertThat(hasPermission.check(app1Read, "foo"), equalTo(false)); + assertThat(hasPermission.check(app1All, "*"), equalTo(false)); + assertThat(hasPermission.check(app1All, "foo"), equalTo(false)); + } + + public void testResourceMatching() { + final ApplicationPermission hasPermission = buildPermission(app1All, "dashboard/*", "audit/*", "user/12345"); + + assertThat(hasPermission.check(app1Write, "*"), equalTo(false)); + assertThat(hasPermission.check(app1Write, "dashboard"), equalTo(false)); + assertThat(hasPermission.check(app1Write, "dashboard/999"), equalTo(true)); + + assertThat(hasPermission.check(app1Create, "audit/2018-02-21"), equalTo(true)); + assertThat(hasPermission.check(app1Create, "report/2018-02-21"), equalTo(false)); + + assertThat(hasPermission.check(app1Read, "user/12345"), equalTo(true)); + assertThat(hasPermission.check(app1Read, "user/67890"), equalTo(false)); + + assertThat(hasPermission.check(app1All, "dashboard/999"), equalTo(true)); + assertThat(hasPermission.check(app1All, "audit/2018-02-21"), equalTo(true)); + assertThat(hasPermission.check(app1All, "user/12345"), equalTo(true)); + } + + public void testActionMatching() { + final ApplicationPermission hasPermission = buildPermission(app1Write, "allow/*"); + + final ApplicationPrivilege update = actionPrivilege("app1", "write/update"); + assertThat(hasPermission.check(update, "allow/1"), equalTo(true)); + assertThat(hasPermission.check(update, "deny/1"), equalTo(false)); + + final ApplicationPrivilege updateCreate = actionPrivilege("app1", "write/update", "write/create"); + assertThat(hasPermission.check(updateCreate, "allow/1"), equalTo(true)); + assertThat(hasPermission.check(updateCreate, "deny/1"), equalTo(false)); + + final ApplicationPrivilege manage = actionPrivilege("app1", "admin/manage"); + assertThat(hasPermission.check(manage, "allow/1"), equalTo(false)); + assertThat(hasPermission.check(manage, "deny/1"), equalTo(false)); + } + + public void testDoesNotMatchAcrossApplications() { + assertThat(buildPermission(app1Read, "*").check(app1Read, "123"), equalTo(true)); + assertThat(buildPermission(app1All, "*").check(app1Read, "123"), equalTo(true)); + + assertThat(buildPermission(app1Read, "*").check(app2Read, "123"), equalTo(false)); + assertThat(buildPermission(app1All, "*").check(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.check(app1Read, "allow/1"), equalTo(true)); + assertThat(hasPermission.check(app1Write, "allow/1"), equalTo(true)); + + assertThat(hasPermission.check(app1Read, "deny/1"), equalTo(false)); + assertThat(hasPermission.check(app1Write, "deny/1"), equalTo(false)); + + assertThat(hasPermission.check(app1All, "allow/1"), equalTo(false)); + assertThat(hasPermission.check(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/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 0c492a0817c98..ee6667a0b260d 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 @@ -106,8 +106,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 4e9c6d1e5d754..67689493ebe35 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; @@ -119,6 +122,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; @@ -174,11 +180,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; @@ -449,6 +460,9 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste anonymousUser, tokenService)); components.add(authcService.get()); + final NativePrivilegeStore privilegeStore = new NativePrivilegeStore(settings, client, securityLifecycleService); + components.add(privilegeStore); + final FileRolesStore fileRolesStore = new FileRolesStore(settings, env, resourceWatcherService, getLicenseState()); final NativeRolesStore nativeRolesStore = new NativeRolesStore(settings, client, getLicenseState(), securityLifecycleService); final ReservedRolesStore reservedRolesStore = new ReservedRolesStore(); @@ -457,7 +471,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()); securityLifecycleService.addSecurityIndexHealthChangeListener(allRolesStore::onSecurityIndexHealthChange); securityLifecycleService.addSecurityIndexOutOfDateListener(allRolesStore::onSecurityIndexOutOfDateChange); // to keep things simple, just invalidate all cached entries on license change. this happens so rarely that the impact should be @@ -668,7 +682,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) ); } @@ -713,7 +730,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..fb0618846de07 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/privilege/TransportPutPrivilegesAction.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.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.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.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * 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())); + return; + } + 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..ba552ed100008 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); this.authorizationService = authorizationService; + this.privilegeStore = privilegeStore; } @Override @@ -66,15 +74,32 @@ protected void doExecute(HasPrivilegesRequest request, ActionListener checkPrivileges(request, role, listener), + 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) + .distinct() + .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.debug(() -> 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 +111,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 [{}]", + 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 [{}]", + 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)); } } - listener.onResponse(new HasPrivilegesResponse(allMatch, cluster, indices.values())); + + 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(), privilegesByApplication)); } private boolean testIndexMatch(String checkIndex, String checkPrivilegeName, Role userRole, @@ -139,4 +196,12 @@ 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); + return userRole.application().check(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 1f604406a7120..519247b509e5d 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java @@ -13,6 +13,7 @@ import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.cache.Cache; import org.elasticsearch.common.cache.CacheBuilder; +import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.component.AbstractComponent; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Setting.Property; @@ -29,11 +30,11 @@ 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; import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; -import org.elasticsearch.xpack.security.SecurityLifecycleService; import java.util.ArrayList; import java.util.Arrays; @@ -52,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.SecurityLifecycleService.isIndexDeleted; import static org.elasticsearch.xpack.security.SecurityLifecycleService.isMoveFromRedToNonRed; @@ -81,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; @@ -89,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); @@ -99,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); @@ -118,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)); } @@ -239,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()); @@ -257,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 = @@ -275,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) -> { @@ -287,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() { @@ -345,11 +384,11 @@ private static class MergeableIndicesPrivilege { MergeableIndicesPrivilege(String[] indices, String[] privileges, @Nullable String[] grantedFields, @Nullable String[] deniedFields, @Nullable BytesReference query) { - this.indices = Sets.newHashSet(Objects.requireNonNull(indices)); - this.privileges = Sets.newHashSet(Objects.requireNonNull(privileges)); + this.indices = newHashSet(Objects.requireNonNull(indices)); + this.privileges = newHashSet(Objects.requireNonNull(privileges)); this.fieldPermissionsDefinition = new FieldPermissionsDefinition(grantedFields, deniedFields); if (query != null) { - this.query = Sets.newHashSet(query); + this.query = newHashSet(query); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativePrivilegeStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativePrivilegeStore.java new file mode 100644 index 0000000000000..49e2b7fdb6172 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativePrivilegeStore.java @@ -0,0 +1,257 @@ +/* + * 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.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.ToXContent; +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.authz.privilege.ApplicationPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege.Fields; +import org.elasticsearch.xpack.security.SecurityLifecycleService; + +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.SecurityLifecycleService.SECURITY_INDEX_NAME; + +/** + * {@code NativePrivilegeStore} is a store that reads {@link ApplicationPrivilege} objects, + * from an Elasticsearch index. + */ +public class NativePrivilegeStore extends AbstractComponent { + + private final Client client; + private final SecurityLifecycleService securityLifecycleService; + + public NativePrivilegeStore(Settings settings, Client client, SecurityLifecycleService securityLifecycleService) { + super(settings); + this.client = client; + this.securityLifecycleService = securityLifecycleService; + } + + 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 { + securityLifecycleService.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(SecurityLifecycleService.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) { + securityLifecycleService.prepareIndexIfNeededThenExecute(listener::onFailure, + () -> executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN, + client.prepareGet(SecurityLifecycleService.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) { + securityLifecycleService.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(tuplesToMap()); + listener.onResponse(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")); + return; + } + try { + final String name = Iterables.get(privilege.name(), 0); + final XContentBuilder xContentBuilder = privilege.toIndexContent(jsonBuilder(), ToXContent.EMPTY_PARAMS); + 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) { + securityLifecycleService.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(tuplesToMap()); + listener.onResponse(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); + } + }); + } + + Collector, ?, Map>> tuplesToMap() { + return Collectors.toMap(Tuple::v1, t -> CollectionUtils.newSingletonArrayList(t.v2()), (a, b) -> { + a.addAll(b); + return a; + }); + } + + 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, true); + assert privilege.getApplication().equals(name.v1()) + : "Incorrect application name for privilege. Expected [" + name.v1() + "] but was " + privilege.getApplication(); + assert privilege.name().size() == 1 && Iterables.get(privilege.name(), 0).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 4f0bb5b2e3c32..19faf096732d0 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.SecurityLifecycleService.SECURITY_INDEX_NAME; /** * NativeRolesStore is a {@code RolesStore} that, instead of reading from a @@ -118,7 +120,7 @@ public void getRoleDescriptors(String[] names, final ActionListener supplier = client.threadPool().getThreadContext().newRestorableContext(false); try (ThreadContext.StoredContext ignore = stashWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN)) { - SearchRequest request = client.prepareSearch(SecurityLifecycleService.SECURITY_INDEX_NAME) + SearchRequest request = client.prepareSearch(SECURITY_INDEX_NAME) .setScroll(TimeValue.timeValueSeconds(10L)) .setQuery(query) .setSize(1000) @@ -134,7 +136,7 @@ public void getRoleDescriptors(String[] names, final ActionListener listener) { securityLifecycleService.prepareIndexIfNeededThenExecute(listener::onFailure, () -> { - DeleteRequest request = client.prepareDelete(SecurityLifecycleService.SECURITY_INDEX_NAME, + DeleteRequest request = client.prepareDelete(SECURITY_INDEX_NAME, ROLE_DOC_TYPE, getIdForUser(deleteRoleRequest.name())).request(); request.setRefreshPolicy(deleteRoleRequest.getRefreshPolicy()); executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN, request, @@ -174,15 +176,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(SecurityLifecycleService.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); } @@ -206,10 +210,10 @@ public void usageStats(ActionListener> listener) { securityLifecycleService.prepareIndexIfNeededThenExecute(listener::onFailure, () -> executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN, client.prepareMultiSearch() - .add(client.prepareSearch(SecurityLifecycleService.SECURITY_INDEX_NAME) + .add(client.prepareSearch(SECURITY_INDEX_NAME) .setQuery(QueryBuilders.termQuery(RoleDescriptor.Fields.TYPE.getPreferredName(), ROLE_TYPE)) .setSize(0)) - .add(client.prepareSearch(SecurityLifecycleService.SECURITY_INDEX_NAME) + .add(client.prepareSearch(SECURITY_INDEX_NAME) .setQuery(QueryBuilders.boolQuery() .must(QueryBuilders.termQuery(RoleDescriptor.Fields.TYPE.getPreferredName(), ROLE_TYPE)) .must(QueryBuilders.boolQuery() @@ -219,7 +223,7 @@ public void usageStats(ActionListener> listener) { .should(existsQuery("indices.fields")))) .setSize(0) .setTerminateAfter(1)) - .add(client.prepareSearch(SecurityLifecycleService.SECURITY_INDEX_NAME) + .add(client.prepareSearch(SECURITY_INDEX_NAME) .setQuery(QueryBuilders.boolQuery() .must(QueryBuilders.termQuery(RoleDescriptor.Fields.TYPE.getPreferredName(), ROLE_TYPE)) .filter(existsQuery("indices.query"))) @@ -290,7 +294,7 @@ public void onFailure(Exception e) { private void executeGetRoleRequest(String role, ActionListener listener) { securityLifecycleService.prepareIndexIfNeededThenExecute(listener::onFailure, () -> executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN, - client.prepareGet(SecurityLifecycleService.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..22d50739a735f --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestDeletePrivilegesAction.java @@ -0,0 +1,74 @@ +/* + * 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.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 = getPrivileges(request); + 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); + } + }); + } + + public String[] getPrivileges(RestRequest request) { + final String[] privilegeArray = request.paramAsStringArray("privilege", null); + if (privilegeArray == null || (privilegeArray.length == 1 && Strings.isNullOrEmpty(privilegeArray[0]))) { + throw new IllegalArgumentException("The \"privilege\" parameter is required"); + } + return privilegeArray; + } +} 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..49d5067581f16 --- /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(Iterables.get(privilege.name(), 0), 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..4a2699142b0d1 --- /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 = Iterables.get(privilege.name(), 0); + 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..6bebac42f01da --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesRequestBuilderTests.java @@ -0,0 +1,104 @@ +/* + * 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.iterableWithSize; + +public class PutPrivilegesRequestBuilderTests extends ESTestCase { + + public void testBuildRequestWithMultipleElements() throws Exception { + final PutPrivilegesRequestBuilder builder = new PutPrivilegesRequestBuilder(null); + 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); + 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); + 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); + 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); + 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); + 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")); + } + +} 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..74df875d06a75 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]; + 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(); 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 3013a7c41c2ac..c80f85ab51da4 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 @@ -198,8 +198,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/AuthorizedIndicesTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java index 4bb8af96ca8c8..0e4cb9ab04f37 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.SecurityLifecycleService; import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; +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 17d8c754e1642..d8ce516af375c 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 @@ -168,8 +168,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..0d500c4472926 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 { @@ -48,7 +53,7 @@ public void testToString() throws Exception { RoleDescriptor descriptor = new RoleDescriptor("test", new String[] { "all", "none" }, groups, new String[] { "sudo" }); assertThat(descriptor.toString(), is("Role[name=test, cluster=[all,none], indicesPrivileges=[IndicesPrivileges[indices=[i1,i2], " + "privileges=[read], field_security=[grant=[body,title], except=null], query={\"query\": {\"match_all\": {}}}],]" + - ", runAs=[sudo], metadata=[{}]]")); + ", applicationPrivileges=[], runAs=[sudo], metadata=[{}]]")); } public void testToXContent() throws Exception { @@ -113,6 +118,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 7c732cd7c52f9..b503637289eec 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 @@ -104,7 +104,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<>(); @@ -164,7 +165,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<>(); @@ -197,7 +199,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 @@ -275,8 +277,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<>(); @@ -329,7 +331,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") @@ -371,8 +375,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<>(); @@ -412,7 +416,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"); @@ -425,7 +429,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); @@ -440,7 +444,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"); @@ -456,7 +460,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(); @@ -499,9 +504,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..891e93d6df08d --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/NativePrivilegeStoreTests.java @@ -0,0 +1,284 @@ +/* + * 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.util.iterable.Iterables; +import org.elasticsearch.common.xcontent.ToXContent; +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.authz.privilege.ApplicationPrivilege; +import org.elasticsearch.xpack.security.SecurityLifecycleService; +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 SecurityLifecycleService lifecycleService = mock(SecurityLifecycleService.class); + when(lifecycleService.isSecurityIndexAvailable()).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(lifecycleService).prepareIndexIfNeededThenExecute(any(Consumer.class), any(Runnable.class)); + store = new NativePrivilegeStore(Settings.EMPTY, client, lifecycleService); + } + + @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(SecurityLifecycleService.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(SecurityLifecycleService.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(SecurityLifecycleService.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(SecurityLifecycleService.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 String uuid = UUIDs.randomBase64UUID(random()); + for (int i = 0; i < putPrivileges.size(); i++) { + ApplicationPrivilege privilege = putPrivileges.get(i); + IndexRequest request = (IndexRequest) requests.get(i); + assertThat(request.indices(), arrayContaining(SecurityLifecycleService.SECURITY_INDEX_NAME)); + assertThat(request.type(), equalTo("doc")); + assertThat(request.id(), equalTo( + "application-privilege_" + privilege.getApplication() + ":" + Iterables.get(privilege.name(), 0) + )); + final XContentBuilder builder + = privilege.toIndexContent(XContentBuilder.builder(XContentType.JSON.xContent()), ToXContent.EMPTY_PARAMS); + assertThat(request.source(), equalTo(BytesReference.bytes(builder))); + final boolean created = privilege.name().contains("user") == false; + listener.get().onResponse(new IndexResponse( + new ShardId(SecurityLifecycleService.SECURITY_INDEX_NAME, uuid, i), + request.type(), request.id(), 1, 1, 1, created + )); + } + + 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 String uuid = UUIDs.randomBase64UUID(random()); + for (int i = 0; i < privilegeNames.size(); i++) { + String name = privilegeNames.get(i); + DeleteRequest request = (DeleteRequest) requests.get(i); + assertThat(request.indices(), arrayContaining(SecurityLifecycleService.SECURITY_INDEX_NAME)); + assertThat(request.type(), equalTo("doc")); + assertThat(request.id(), equalTo("application-privilege_app1:" + name)); + final boolean found = name.equals("p2") == false; + listener.get().onResponse(new DeleteResponse( + new ShardId(SecurityLifecycleService.SECURITY_INDEX_NAME, uuid, i), + request.type(), request.id(), 1, 1, 1, found + )); + } + + 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() + ":" + Iterables.get(p.name(), 0), 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 + } + } + } } + From f7463bc8840c60ed5db79c05c2169430147e7917 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Thu, 26 Apr 2018 16:30:26 +1000 Subject: [PATCH 02/20] Static caches are evil --- .../permission/ApplicationPermission.java | 13 ++-- .../authz/privilege/ApplicationPrivilege.java | 42 +++++------ .../ApplicationPermissionTests.java | 70 +++++++++---------- .../user/TransportHasPrivilegesAction.java | 39 ++++++----- .../TransportHasPrivilegesActionTests.java | 4 +- 5 files changed, 85 insertions(+), 83 deletions(-) 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 index e3d332d77b274..2d02d4435c16e 100644 --- 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 @@ -44,13 +44,14 @@ public ApplicationPermission(List>> tupl })); } - public boolean check(ApplicationPrivilege other, String resource) { + public boolean grants(ApplicationPrivilege other, String resource) { Automaton resourceAutomaton = Automatons.patterns(resource); - logger.debug("Checking permission [{}] against [{} , {}]", this, other, resource); - return privileges.entrySet().stream() - .anyMatch(entry -> Objects.equals(other.getApplication(), entry.getKey().getApplication()) - && Operations.subsetOf(other.getAutomaton(), entry.getKey().getAutomaton()) - && Operations.subsetOf(resourceAutomaton, entry.getValue())); + final boolean matched = privileges.entrySet().stream() + .anyMatch(entry -> Objects.equals(other.getApplication(), entry.getKey().getApplication()) + && Operations.subsetOf(other.getAutomaton(), entry.getKey().getAutomaton()) + && Operations.subsetOf(resourceAutomaton, entry.getValue())); + logger.debug("Permission [{}] {} grant [{} , {}]", this, matched ? "does" : "does not", other, resource); + return matched; } @Override 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 index 0c87768a8c3ef..8895b3bd394d8 100644 --- 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 @@ -6,6 +6,7 @@ package org.elasticsearch.xpack.core.security.authz.privilege; import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; @@ -29,7 +30,6 @@ import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -47,7 +47,7 @@ public final class ApplicationPrivilege extends Privilege implements ToXContentO public static final String DOC_TYPE_VALUE = "application-privilege"; private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(DOC_TYPE_VALUE, - arr -> new ApplicationPrivilege((String) arr[0], (String) arr[1], (List) arr[2], (Map) arr[3]) + arr -> new ApplicationPrivilege((String) arr[0], (String) arr[1], (List) arr[2], (Map) arr[3]) ); static { @@ -69,7 +69,6 @@ public final class ApplicationPrivilege extends Privilege implements ToXContentO private static final Pattern VALID_APPLICATION = Pattern.compile("^[a-z][A-Za-z0-9_-]{2,}$"); private static final Pattern VALID_NAME = Pattern.compile("^[a-z][a-zA-Z0-9_.-]*$"); - private static final ConcurrentHashMap>, ApplicationPrivilege> CACHE = new ConcurrentHashMap<>(); public static final Function NONE = app -> new ApplicationPrivilege(app, "none", new String[0]); @@ -110,19 +109,19 @@ public Map getMetadata() { private void validate(boolean validateNames) { if (VALID_APPLICATION.matcher(application).matches() == false) { throw new IllegalArgumentException("Application names must match /" - + VALID_APPLICATION.pattern() + "/ (but was '" + application + "')"); + + VALID_APPLICATION.pattern() + "/ (but was '" + application + "')"); } for (String name : super.name()) { if (validateNames && isPrivilegeName(name) == false) { throw new IllegalArgumentException("Application privilege names must match the pattern /" + VALID_NAME.pattern() - + "/ (found '" + name + "')"); + + "/ (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 [ '/' , '*' , ':' ]"); + "The application privilege pattern [" + pattern + "] must contain one of [ '/' , '*' , ':' ]"); } } } @@ -132,18 +131,15 @@ public static boolean isPrivilegeName(String name) { } public static ApplicationPrivilege get(String application, Set name, Collection stored) { - Tuple> key = new Tuple<>(application, name); - return CACHE.computeIfAbsent(key, (ignore) -> { - 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(cp -> Iterables.get(cp.name, 0), Function.identity())); - return resolve(application, name, lookup); - } - }); + 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(cp -> Iterables.get(cp.name, 0), Function.identity())); + return resolve(application, name, lookup); + } } private static ApplicationPrivilege resolve(String application, Set names, Map lookup) { @@ -180,7 +176,7 @@ private static ApplicationPrivilege resolve(String application, Set name @Override public String toString() { - return application + ":" + super.toString(); + return application + ":" + super.toString() + "(" + Strings.arrayToCommaDelimitedString(patterns) + ")"; } @Override @@ -208,10 +204,10 @@ private XContentBuilder writeXContent(XContentBuilder builder, boolean includeTy assert name.size() == 1; builder.startObject() - .field(Fields.APPLICATION.getPreferredName(), application) - .field(Fields.NAME.getPreferredName(), Iterables.get(name, 0)) - .array(Fields.ACTIONS.getPreferredName(), this.patterns) - .field(Fields.METADATA.getPreferredName(), this.metadata); + .field(Fields.APPLICATION.getPreferredName(), application) + .field(Fields.NAME.getPreferredName(), Iterables.get(name, 0)) + .array(Fields.ACTIONS.getPreferredName(), this.patterns) + .field(Fields.METADATA.getPreferredName(), this.metadata); if (includeType) { builder.field(Fields.TYPE.getPreferredName(), DOC_TYPE_VALUE); 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 index 62ea62dfa74f8..16f512cf75f44 100644 --- 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 @@ -30,71 +30,71 @@ public class ApplicationPermissionTests extends ESTestCase { public void testCheckSimplePermission() { final ApplicationPermission hasPermission = buildPermission(app1Write, "*"); - assertThat(hasPermission.check(app1Write, "*"), equalTo(true)); - assertThat(hasPermission.check(app1Write, "foo"), equalTo(true)); - assertThat(hasPermission.check(app1Delete, "*"), equalTo(true)); - assertThat(hasPermission.check(app1Create, "foo"), equalTo(true)); - - assertThat(hasPermission.check(app1Read, "*"), equalTo(false)); - assertThat(hasPermission.check(app1Read, "foo"), equalTo(false)); - assertThat(hasPermission.check(app1All, "*"), equalTo(false)); - assertThat(hasPermission.check(app1All, "foo"), equalTo(false)); + 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.check(app1Write, "*"), equalTo(false)); - assertThat(hasPermission.check(app1Write, "dashboard"), equalTo(false)); - assertThat(hasPermission.check(app1Write, "dashboard/999"), equalTo(true)); + assertThat(hasPermission.grants(app1Write, "*"), equalTo(false)); + assertThat(hasPermission.grants(app1Write, "dashboard"), equalTo(false)); + assertThat(hasPermission.grants(app1Write, "dashboard/999"), equalTo(true)); - assertThat(hasPermission.check(app1Create, "audit/2018-02-21"), equalTo(true)); - assertThat(hasPermission.check(app1Create, "report/2018-02-21"), equalTo(false)); + assertThat(hasPermission.grants(app1Create, "audit/2018-02-21"), equalTo(true)); + assertThat(hasPermission.grants(app1Create, "report/2018-02-21"), equalTo(false)); - assertThat(hasPermission.check(app1Read, "user/12345"), equalTo(true)); - assertThat(hasPermission.check(app1Read, "user/67890"), equalTo(false)); + assertThat(hasPermission.grants(app1Read, "user/12345"), equalTo(true)); + assertThat(hasPermission.grants(app1Read, "user/67890"), equalTo(false)); - assertThat(hasPermission.check(app1All, "dashboard/999"), equalTo(true)); - assertThat(hasPermission.check(app1All, "audit/2018-02-21"), equalTo(true)); - assertThat(hasPermission.check(app1All, "user/12345"), equalTo(true)); + 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.check(update, "allow/1"), equalTo(true)); - assertThat(hasPermission.check(update, "deny/1"), equalTo(false)); + 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.check(updateCreate, "allow/1"), equalTo(true)); - assertThat(hasPermission.check(updateCreate, "deny/1"), equalTo(false)); + 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.check(manage, "allow/1"), equalTo(false)); - assertThat(hasPermission.check(manage, "deny/1"), equalTo(false)); + assertThat(hasPermission.grants(manage, "allow/1"), equalTo(false)); + assertThat(hasPermission.grants(manage, "deny/1"), equalTo(false)); } public void testDoesNotMatchAcrossApplications() { - assertThat(buildPermission(app1Read, "*").check(app1Read, "123"), equalTo(true)); - assertThat(buildPermission(app1All, "*").check(app1Read, "123"), equalTo(true)); + assertThat(buildPermission(app1Read, "*").grants(app1Read, "123"), equalTo(true)); + assertThat(buildPermission(app1All, "*").grants(app1Read, "123"), equalTo(true)); - assertThat(buildPermission(app1Read, "*").check(app2Read, "123"), equalTo(false)); - assertThat(buildPermission(app1All, "*").check(app2Read, "123"), equalTo(false)); + 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.check(app1Read, "allow/1"), equalTo(true)); - assertThat(hasPermission.check(app1Write, "allow/1"), equalTo(true)); + assertThat(hasPermission.grants(app1Read, "allow/1"), equalTo(true)); + assertThat(hasPermission.grants(app1Write, "allow/1"), equalTo(true)); - assertThat(hasPermission.check(app1Read, "deny/1"), equalTo(false)); - assertThat(hasPermission.check(app1Write, "deny/1"), equalTo(false)); + assertThat(hasPermission.grants(app1Read, "deny/1"), equalTo(false)); + assertThat(hasPermission.grants(app1Write, "deny/1"), equalTo(false)); - assertThat(hasPermission.check(app1All, "allow/1"), equalTo(false)); - assertThat(hasPermission.check(app2Read, "allow/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) { 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 ba552ed100008..2c87e59113243 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 @@ -58,7 +58,7 @@ public TransportHasPrivilegesAction(Settings settings, ThreadPool threadPool, Tr ActionFilters actionFilters, IndexNameExpressionResolver indexNameExpressionResolver, AuthorizationService authorizationService, NativePrivilegeStore privilegeStore) { super(settings, HasPrivilegesAction.NAME, threadPool, transportService, actionFilters, indexNameExpressionResolver, - HasPrivilegesRequest::new); + HasPrivilegesRequest::new); this.authorizationService = authorizationService; this.privilegeStore = privilegeStore; } @@ -74,10 +74,10 @@ protected void doExecute(HasPrivilegesRequest request, ActionListener resolveApplicationPrivileges(request, ActionListener.wrap( - applicationPrivilegeLookup -> checkPrivileges(request, role, applicationPrivilegeLookup, listener), - listener::onFailure)), - listener::onFailure)); + role -> resolveApplicationPrivileges(request, ActionListener.wrap( + applicationPrivilegeLookup -> checkPrivileges(request, role, applicationPrivilegeLookup, listener), + listener::onFailure)), + listener::onFailure)); } private void resolveApplicationPrivileges(HasPrivilegesRequest request, ActionListener> listener) { @@ -87,18 +87,18 @@ private void resolveApplicationPrivileges(HasPrivilegesRequest request, ActionLi private Set getApplicationNames(HasPrivilegesRequest request) { return Arrays.stream(request.applicationPrivileges()) - .map(RoleDescriptor.ApplicationResourcePrivileges::getApplication) - .distinct() - .collect(Collectors.toSet()); + .map(RoleDescriptor.ApplicationResourcePrivileges::getApplication) + .distinct() + .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=[{}] application=[{}]", - Strings.arrayToCommaDelimitedString(userRole.names()), - Strings.arrayToCommaDelimitedString(request.clusterPrivileges()), - Strings.arrayToCommaDelimitedString(request.indexPrivileges()), - Strings.arrayToCommaDelimitedString(request.applicationPrivileges()) + Strings.arrayToCommaDelimitedString(userRole.names()), + Strings.arrayToCommaDelimitedString(request.clusterPrivileges()), + Strings.arrayToCommaDelimitedString(request.indexPrivileges()), + Strings.arrayToCommaDelimitedString(request.applicationPrivileges()) )); Map cluster = new HashMap<>(); @@ -122,11 +122,11 @@ private void checkPrivileges(HasPrivilegesRequest request, Role userRole, Collec for (String privilege : check.getPrivileges()) { if (testIndexMatch(index, privilege, userRole, predicateCache)) { logger.debug(() -> new ParameterizedMessage("Role [{}] has [{}] on index [{}]", - Strings.arrayToCommaDelimitedString(userRole.names()), privilege, index)); + Strings.arrayToCommaDelimitedString(userRole.names()), privilege, index)); privileges.put(privilege, true); } else { logger.debug(() -> new ParameterizedMessage("Role [{}] does not have [{}] on index [{}]", - Strings.arrayToCommaDelimitedString(userRole.names()), privilege, index)); + Strings.arrayToCommaDelimitedString(userRole.names()), privilege, index)); privileges.put(privilege, false); allMatch = false; } @@ -150,11 +150,11 @@ private void checkPrivileges(HasPrivilegesRequest request, Role userRole, Collec 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)); + 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)); + Strings.arrayToCommaDelimitedString(userRole.names()), applicationName, privilege, resource)); privileges.put(privilege, false); allMatch = false; } @@ -201,7 +201,12 @@ private boolean testResourceMatch(String application, String checkResource, Stri Collection privileges) { final Set nameSet = Collections.singleton(checkPrivilegeName); final ApplicationPrivilege checkPrivilege = ApplicationPrivilege.get(application, nameSet, privileges); - return userRole.application().check(checkPrivilege, checkResource); + 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/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 74df875d06a75..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 @@ -91,11 +91,11 @@ public void setup() { Mockito.doAnswer(inv -> { assertThat(inv.getArguments(), arrayWithSize(3)); ActionListener> listener = (ActionListener>) inv.getArguments()[2]; + logger.info("Privileges for ({}) are {}", Arrays.toString(inv.getArguments()), applicationPrivileges); listener.onResponse(applicationPrivileges); return null; }).when(privilegeStore).getPrivileges(any(Collection.class), any(Collection.class), any(ActionListener.class)); - action = new TransportHasPrivilegesAction(settings, threadPool, transportService, mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), authorizationService, privilegeStore); } @@ -473,4 +473,4 @@ private static MapBuilder mapBuilder() { return MapBuilder.newMapBuilder(); } -} \ No newline at end of file +} From 9b1ff6e39af5e1adcdc816c35ea6ad260e40393c Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Mon, 30 Apr 2018 16:35:40 +1000 Subject: [PATCH 03/20] Grant all application privileges to superuser --- .../privilege/PutPrivilegesRequest.java | 11 +- .../action/user/HasPrivilegesRequest.java | 12 +- .../core/security/authz/RoleDescriptor.java | 4 +- .../permission/ApplicationPermission.java | 44 +- .../core/security/authz/permission/Role.java | 32 +- .../authz/privilege/ApplicationPrivilege.java | 28 +- .../authz/store/ReservedRolesStore.java | 5 +- .../authz/AuthorizationServiceTests.java | 499 +++++++++--------- .../test/privileges/30_superuser.yml | 131 +++++ 9 files changed, 488 insertions(+), 278 deletions(-) create mode 100644 x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/30_superuser.yml 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 index 16bf22e5ee675..cfebc99373fa6 100644 --- 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 @@ -39,13 +39,18 @@ public PutPrivilegesRequest() { 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); + privilege.name() + ")", validationException); } if (MetadataUtils.containsReservedMetadata(privilege.getMetadata())) { validationException = addValidationError("metadata keys may not start with [" + MetadataUtils.RESERVED_PREFIX - + "] (in privilege " + privilege.name() + ")", validationException); + + "] (in privilege " + privilege.name() + ")", validationException); } } return validationException; @@ -77,7 +82,7 @@ public void setPrivileges(Collection privileges) { @Override public String toString() { return getClass().getSimpleName() + "{[" + privileges.stream().map(Strings::toString).collect(Collectors.joining(",")) - + "];" + refreshPolicy + "}"; + + "];" + refreshPolicy + "}"; } @Override 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 effbbd91629f4..10defecb4a75e 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 @@ -12,6 +12,7 @@ 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; @@ -38,10 +39,17 @@ public ActionRequestValidationException validate() { } if (applicationPrivileges == null) { validationException = addValidationError("applicationPrivileges must not be null", validationException); + 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) { + && indexPrivileges != null && indexPrivileges.length == 0 + && applicationPrivileges != null && applicationPrivileges.length == 0) { validationException = addValidationError("must specify at least one privilege", validationException); } return validationException; 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 6979491d47773..a24b0614bb7d5 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 @@ -806,7 +806,9 @@ public String[] getPrivileges() { @Override public String toString() { StringBuilder sb = new StringBuilder(getClass().getSimpleName()) - .append("[resources=") + .append("[application=") + .append(application) + .append("resources=") .append(Strings.arrayToCommaDelimitedString(resources)) .append("], privileges=[") .append(Strings.arrayToCommaDelimitedString(privileges)) 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 index 2d02d4435c16e..e8c96953dbbc0 100644 --- 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 @@ -13,13 +13,14 @@ 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.Objects; import java.util.Set; +import java.util.function.Predicate; /** * A permission that is based on privileges for application (non elasticsearch) capabilities @@ -29,33 +30,54 @@ public final class ApplicationPermission { public static final ApplicationPermission NONE = new ApplicationPermission(Collections.emptyList()); private final Logger logger; - private final Map privileges; + private final List permissions; public ApplicationPermission(List>> tuples) { this.logger = Loggers.getLogger(getClass()); - this.privileges = new HashMap<>(); - tuples.forEach(tup -> privileges.compute(tup.v1(), (k, existing) -> { + Map permissionsByPrivilege = new HashMap<>(); + tuples.forEach(tup -> permissionsByPrivilege.compute(tup.v1(), (k, existing) -> { final Automaton patterns = Automatons.patterns(tup.v2()); if (existing == null) { - return patterns; + return new PermissionEntry(k, patterns); } else { - return Automatons.unionAndMinimize(Arrays.asList(existing, patterns)); + return new PermissionEntry(k, Automatons.unionAndMinimize(Arrays.asList(existing.resources, patterns))); } })); + this.permissions = new ArrayList<>(permissionsByPrivilege.values()); } public boolean grants(ApplicationPrivilege other, String resource) { Automaton resourceAutomaton = Automatons.patterns(resource); - final boolean matched = privileges.entrySet().stream() - .anyMatch(entry -> Objects.equals(other.getApplication(), entry.getKey().getApplication()) - && Operations.subsetOf(other.getAutomaton(), entry.getKey().getAutomaton()) - && Operations.subsetOf(resourceAutomaton, entry.getValue())); + final boolean matched = permissions.stream().anyMatch(e -> e.grants(other, resourceAutomaton)); logger.debug("Permission [{}] {} grant [{} , {}]", this, matched ? "does" : "does not", other, resource); return matched; } @Override public String toString() { - return getClass().getSimpleName() + "{privileges=" + privileges.keySet() + "}"; + 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; + } + + public 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 9b7fb83c497da..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 @@ -12,8 +12,8 @@ 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.ClusterPrivilege; 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; @@ -82,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: @@ -111,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; @@ -119,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)); @@ -152,9 +158,9 @@ public Builder addApplicationPrivilege(ApplicationPrivilege privilege, Set 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 index 8895b3bd394d8..d88ce04b3fdc0 100644 --- 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 @@ -7,10 +7,10 @@ import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; -import org.elasticsearch.common.collect.Tuple; 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.util.iterable.Iterables; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.common.xcontent.ConstructingObjectParser; @@ -67,8 +67,8 @@ public final class ApplicationPrivilege extends Privilege implements ToXContentO }, 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_NAME = Pattern.compile("^[a-z][a-zA-Z0-9_.-]*$"); + public static final Pattern VALID_APPLICATION = Pattern.compile("^[a-z][A-Za-z0-9_-]{2,}$"); + public 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]); @@ -107,13 +107,12 @@ public Map getMetadata() { } private void validate(boolean validateNames) { - if (VALID_APPLICATION.matcher(application).matches() == false) { - throw new IllegalArgumentException("Application names must match /" - + VALID_APPLICATION.pattern() + "/ (but was '" + application + "')"); + if (Regex.isSimpleMatchPattern(application) == false) { + validateApplicationName(application); } for (String name : super.name()) { - if (validateNames && isPrivilegeName(name) == false) { + if (validateNames && isValidPrivilegeName(name) == false) { throw new IllegalArgumentException("Application privilege names must match the pattern /" + VALID_NAME.pattern() + "/ (found '" + name + "')"); } @@ -126,7 +125,18 @@ private void validate(boolean validateNames) { } } - public static boolean isPrivilegeName(String name) { + /** + * Validate that the provided application name is valid, and throws an exception otherwise + * @thorw IllegalArgumentException if the name is not valid + */ + public static void validateApplicationName(String application) { + if (VALID_APPLICATION.matcher(application).matches() == false) { + throw new IllegalArgumentException("Application names must match /" + + VALID_APPLICATION.pattern() + "/ (but was '" + application + "')"); + } + } + + public static boolean isValidPrivilegeName(String name) { return VALID_NAME.matcher(name).matches(); } @@ -152,7 +162,7 @@ private static ApplicationPrivilege resolve(String application, Set name Set patterns = new HashSet<>(); for (String name : names) { name = name.toLowerCase(Locale.ROOT); - if (isPrivilegeName(name) != false) { + if (isValidPrivilegeName(name) != false) { ApplicationPrivilege privilege = lookup.get(name); if (privilege != null && size == 1) { return privilege; 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/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 c80f85ab51da4..02469d6cf5ea8 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,23 +213,23 @@ public void setup() { if (roleDescriptors.isEmpty()) { callback.onResponse(Role.EMPTY); } else { - CompositeRolesStore.buildRoleFromDescriptors(roleDescriptors, fieldPermissionsCache, null, - ActionListener.wrap(r -> callback.onResponse(r), callback::onFailure) + 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(); } @@ -226,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); } @@ -238,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); } @@ -248,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); } @@ -259,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); } @@ -271,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); } @@ -298,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); } @@ -314,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); } @@ -325,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); } @@ -341,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); } @@ -367,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")); @@ -391,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); } @@ -402,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(); @@ -415,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()); } @@ -441,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); } @@ -482,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(); @@ -500,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(); @@ -518,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(); @@ -536,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(); @@ -556,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(); @@ -581,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(); @@ -602,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); } @@ -611,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()); } @@ -680,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]", @@ -727,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"); @@ -764,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))); @@ -782,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()}); } } @@ -800,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(); @@ -844,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"); @@ -861,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 @@ -887,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); @@ -906,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); } @@ -918,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); } @@ -931,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")); } @@ -971,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(); @@ -1034,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); } @@ -1050,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)); @@ -1089,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)); @@ -1106,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(); @@ -1115,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); @@ -1137,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); @@ -1154,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)); @@ -1166,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; @@ -1177,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)); @@ -1224,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()); } @@ -1246,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")); } @@ -1256,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() { @@ -1272,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(); @@ -1289,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(); @@ -1304,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); @@ -1318,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/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 + } + } + } } + From 662fd50d26c3e40cd989952c9079b6968a7ded6a Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Tue, 1 May 2018 15:49:07 +1000 Subject: [PATCH 04/20] Address review feedback --- .../privilege/DeletePrivilegesRequest.java | 4 +- .../DeletePrivilegesRequestBuilder.java | 2 +- .../privilege/DeletePrivilegesResponse.java | 9 +++-- .../privilege/GetPrivilegesRequest.java | 6 ++- .../GetPrivilegesRequestBuilder.java | 2 +- .../privilege/PutPrivilegesRequest.java | 6 +-- .../PutPrivilegesRequestBuilder.java | 4 +- .../privilege/PutPrivilegesResponse.java | 2 +- .../action/user/HasPrivilegesRequest.java | 2 +- .../permission/ApplicationPermission.java | 25 +++++++++++- .../authz/privilege/ApplicationPrivilege.java | 39 +++++++++++-------- .../authz/store/NativePrivilegeStore.java | 4 +- .../privilege/RestGetPrivilegesAction.java | 2 +- .../privilege/RestPutPrivilegesAction.java | 2 +- .../store/NativePrivilegeStoreTests.java | 4 +- 15 files changed, 72 insertions(+), 41 deletions(-) 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 index 1229a7e9482d4..d382351906bf6 100644 --- 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 @@ -30,8 +30,8 @@ public DeletePrivilegesRequest() { } public DeletePrivilegesRequest(String application, String[] privileges) { - application(application); - privileges(privileges); + this.application = application; + this.privileges = privileges; } @Override 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 index cb65f16e5be88..11c43f7acf9c7 100644 --- 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 @@ -22,7 +22,7 @@ public DeletePrivilegesRequestBuilder(ElasticsearchClient client) { this(client, DeletePrivilegesAction.INSTANCE); } - public DeletePrivilegesRequestBuilder(ElasticsearchClient client, DeletePrivilegesAction action) { + DeletePrivilegesRequestBuilder(ElasticsearchClient client, DeletePrivilegesAction action) { super(client, action, new DeletePrivilegesRequest()); } 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 index 54d9c2d5398cc..39872d397905e 100644 --- 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 @@ -31,7 +31,7 @@ public DeletePrivilegesResponse() { } public DeletePrivilegesResponse(Collection found) { - this.found = new HashSet<>(found); + this.found = Collections.unmodifiableSet(new HashSet<>(found)); } @Override @@ -41,17 +41,18 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws } public Set found() { - return Collections.unmodifiableSet(this.found); + return this.found; } @Override public void readFrom(StreamInput in) throws IOException { super.readFrom(in); final int size = in.readVInt(); - found = new HashSet<>(size); + final HashSet foundSet = new HashSet<>(size); for (int i = 0; i < size; i++) { - found.add(in.readString()); + foundSet.add(in.readString()); } + this.found = Collections.unmodifiableSet(foundSet); } @Override 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 index ec22006878cda..00f14d577fcfc 100644 --- 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 @@ -21,8 +21,8 @@ */ public class GetPrivilegesRequest extends ActionRequest { - private @Nullable - String application; + @Nullable + private String application; private String[] privileges; public GetPrivilegesRequest() { @@ -57,12 +57,14 @@ public String[] privileges() { @Override public void readFrom(StreamInput in) throws IOException { super.readFrom(in); + application = in.readString(); privileges = in.readStringArray(); } @Override public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); + out.writeString(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 index 56cf47d8dd771..17332e93c7c62 100644 --- 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 @@ -18,7 +18,7 @@ public GetPrivilegesRequestBuilder(ElasticsearchClient client) { this(client, GetPrivilegesAction.INSTANCE); } - public GetPrivilegesRequestBuilder(ElasticsearchClient client, GetPrivilegesAction action) { + GetPrivilegesRequestBuilder(ElasticsearchClient client, GetPrivilegesAction action) { super(client, action, new GetPrivilegesRequest()); } 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 index cfebc99373fa6..5708a504f9853 100644 --- 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 @@ -72,11 +72,11 @@ public PutPrivilegesRequest setRefreshPolicy(RefreshPolicy refreshPolicy) { } public List getPrivileges() { - return Collections.unmodifiableList(privileges); + return privileges; } public void setPrivileges(Collection privileges) { - this.privileges = new ArrayList<>(privileges); + this.privileges = Collections.unmodifiableList(new ArrayList<>(privileges)); } @Override @@ -88,7 +88,7 @@ public String toString() { @Override public void readFrom(StreamInput in) throws IOException { super.readFrom(in); - privileges = in.readList(ApplicationPrivilege::readFrom); + privileges = Collections.unmodifiableList(in.readList(ApplicationPrivilege::readFrom)); refreshPolicy = RefreshPolicy.readFrom(in); } 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 index ac46720344ec1..a1cc2ad842131 100644 --- 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 @@ -37,7 +37,7 @@ public PutPrivilegesRequestBuilder(ElasticsearchClient client) { this(client, PutPrivilegesAction.INSTANCE); } - public PutPrivilegesRequestBuilder(ElasticsearchClient client, PutPrivilegesAction action) { + PutPrivilegesRequestBuilder(ElasticsearchClient client, PutPrivilegesAction action) { super(client, action, new PutPrivilegesRequest()); } @@ -129,7 +129,7 @@ private String checkPrivilegeName(ApplicationPrivilege privilege, String applica throw new IllegalArgumentException("privilege name [" + privilege.name() + "] in source must contain exactly 1 value"); } - final String privilegeName = Iterables.get(privilege.name(), 0); + 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 + "]"); 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 index ac4cf44ef039a..abdbb758bdbc7 100644 --- 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 @@ -52,6 +52,6 @@ public void writeTo(StreamOutput out) throws IOException { @Override public void readFrom(StreamInput in) throws IOException { super.readFrom(in); - this.created = in.readMap(StreamInput::readString, si -> si.readList(StreamInput::readString)); + 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/user/HasPrivilegesRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesRequest.java index 10defecb4a75e..2857191f3801c 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 @@ -71,7 +71,7 @@ public void username(String username) { @Override public String[] usernames() { - return new String[]{username}; + return new String[] { username }; } public RoleDescriptor.IndicesPrivileges[] indexPrivileges() { 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 index e8c96953dbbc0..e91e28ed2c3c6 100644 --- 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 @@ -43,13 +43,34 @@ public ApplicationPermission(List>> tupl return new PermissionEntry(k, Automatons.unionAndMinimize(Arrays.asList(existing.resources, patterns))); } })); - this.permissions = new ArrayList<>(permissionsByPrivilege.values()); + 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.debug("Permission [{}] {} grant [{} , {}]", this, matched ? "does" : "does not", other, resource); + logger.trace("Permission [{}] {} grant [{} , {}]", this, matched ? "does" : "does not", other, resource); return matched; } 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 index d88ce04b3fdc0..6458c50135aaa 100644 --- 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 @@ -11,8 +11,6 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.regex.Regex; -import org.elasticsearch.common.util.iterable.Iterables; -import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.ToXContentObject; @@ -34,6 +32,8 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; +import static java.util.Collections.emptyMap; + /** * An application privilege has a name (e.g. {@code "my-app:admin"}) that starts with a prefix that identifies the app, * followed by a suffix that is meaningful to the application. @@ -77,16 +77,11 @@ public final class ApplicationPrivilege extends Privilege implements ToXContentO private final Map metadata; public ApplicationPrivilege(String application, String privilegeName, Collection patterns, Map metadata) { - this(application, Collections.singleton(privilegeName), patterns, metadata, true); + this(application, Collections.singleton(privilegeName), patterns.toArray(new String[patterns.size()]), metadata, true); } public ApplicationPrivilege(String application, String privilegeName, String... patterns) { - this(application, Sets.newHashSet(privilegeName), patterns, Collections.emptyMap(), true); - } - - private ApplicationPrivilege(String application, Set name, Collection patterns, Map metadata, - boolean validateNames) { - this(application, name, patterns.toArray(new String[patterns.size()]), metadata, validateNames); + this(application, Collections.singleton(privilegeName), patterns, emptyMap(), true); } private ApplicationPrivilege(String application, Set name, String[] patterns, Map metadata, @@ -94,7 +89,7 @@ private ApplicationPrivilege(String application, Set name, String[] patt super(name, patterns); this.application = application; this.patterns = patterns; - this.metadata = new HashMap<>(metadata == null ? Collections.emptyMap() : metadata); + this.metadata = new HashMap<>(metadata == null ? emptyMap() : metadata); validate(validateNames); } @@ -102,6 +97,18 @@ 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); } @@ -127,7 +134,7 @@ private void validate(boolean validateNames) { /** * Validate that the provided application name is valid, and throws an exception otherwise - * @thorw IllegalArgumentException if the name is not valid + * @throws IllegalArgumentException if the name is not valid */ public static void validateApplicationName(String application) { if (VALID_APPLICATION.matcher(application).matches() == false) { @@ -147,7 +154,7 @@ public static ApplicationPrivilege get(String application, Set name, Col Map lookup = stored.stream() .filter(cp -> cp.application.equals(application)) .filter(cp -> cp.name.size() == 1) - .collect(Collectors.toMap(cp -> Iterables.get(cp.name, 0), Function.identity())); + .collect(Collectors.toMap(ApplicationPrivilege::getPrivilegeName, Function.identity())); return resolve(application, name, lookup); } } @@ -177,10 +184,10 @@ private static ApplicationPrivilege resolve(String application, Set name } if (actions.isEmpty()) { - return new ApplicationPrivilege(application, names, patterns, Collections.emptyMap(), true); + return new ApplicationPrivilege(application, names, patterns.toArray(new String[patterns.size()]), emptyMap(), true); } else { patterns.addAll(actions); - return new ApplicationPrivilege(application, names, patterns, Collections.emptyMap(), false); + return new ApplicationPrivilege(application, names, patterns.toArray(new String[patterns.size()]), emptyMap(), false); } } @@ -215,7 +222,7 @@ private XContentBuilder writeXContent(XContentBuilder builder, boolean includeTy builder.startObject() .field(Fields.APPLICATION.getPreferredName(), application) - .field(Fields.NAME.getPreferredName(), Iterables.get(name, 0)) + .field(Fields.NAME.getPreferredName(), getPrivilegeName()) .array(Fields.ACTIONS.getPreferredName(), this.patterns) .field(Fields.METADATA.getPreferredName(), this.metadata); @@ -237,7 +244,7 @@ public static ApplicationPrivilege readFrom(StreamInput in) throws IOException { names.add(in.readString()); } String[] patterns = in.readStringArray(); - Map metadata = in.readBoolean() ? in.readMap() : Collections.emptyMap(); + Map metadata = in.readBoolean() ? in.readMap() : emptyMap(); return new ApplicationPrivilege(application, names, patterns, metadata, false); } 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 index 49e2b7fdb6172..399e21e7ca72d 100644 --- 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 @@ -176,7 +176,7 @@ private void innerPutPrivilege(ApplicationPrivilege privilege, WriteRequest.Refr return; } try { - final String name = Iterables.get(privilege.name(), 0); + final String name = privilege.getPrivilegeName(); final XContentBuilder xContentBuilder = privilege.toIndexContent(jsonBuilder(), ToXContent.EMPTY_PARAMS); ClientHelper.executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN, client.prepareIndex(SECURITY_INDEX_NAME, "doc", toDocId(privilege.getApplication(), name)) @@ -232,7 +232,7 @@ private ApplicationPrivilege buildPrivilege(String docId, BytesReference source) final ApplicationPrivilege privilege = ApplicationPrivilege.parse(parser, true); assert privilege.getApplication().equals(name.v1()) : "Incorrect application name for privilege. Expected [" + name.v1() + "] but was " + privilege.getApplication(); - assert privilege.name().size() == 1 && Iterables.get(privilege.name(), 0).equals(name.v2()) + assert privilege.name().size() == 1 && privilege.getPrivilegeName().equals(name.v2()) : "Incorrect name for application privilege. Expected [" + name.v2() + "] but was " + privilege.name(); return privilege; } 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 index 49d5067581f16..9c44b874661cd 100644 --- 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 @@ -65,7 +65,7 @@ public RestResponse buildResponse(GetPrivilegesResponse response, XContentBuilde for (ApplicationPrivilege privilege : privsByApp.get(app)) { assert privilege.name().size() == 1 : "Stored privileges should have a single name (got: " + privilege.name() + ")"; - builder.field(Iterables.get(privilege.name(), 0), privilege); + builder.field(privilege.getPrivilegeName(), privilege); } builder.endObject(); } 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 index 4a2699142b0d1..dd4168047728c 100644 --- 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 @@ -68,7 +68,7 @@ public RestResponse buildResponse(PutPrivilegesResponse response, XContentBuilde .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 = Iterables.get(privilege.name(), 0); + 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)); }); 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 index 891e93d6df08d..717e8fc641f44 100644 --- 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 @@ -214,7 +214,7 @@ public void testPutPrivileges() throws Exception { assertThat(request.indices(), arrayContaining(SecurityLifecycleService.SECURITY_INDEX_NAME)); assertThat(request.type(), equalTo("doc")); assertThat(request.id(), equalTo( - "application-privilege_" + privilege.getApplication() + ":" + Iterables.get(privilege.name(), 0) + "application-privilege_" + privilege.getApplication() + ":" + privilege.getPrivilegeName() )); final XContentBuilder builder = privilege.toIndexContent(XContentBuilder.builder(XContentType.JSON.xContent()), ToXContent.EMPTY_PARAMS); @@ -266,7 +266,7 @@ 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() + ":" + Iterables.get(p.name(), 0), null, null); + hits[i] = new SearchHit(i, "application-privilege_" + p.getApplication() + ":" + p.getPrivilegeName(), null, null); hits[i].sourceRef(new BytesArray(Strings.toString(p))); } return hits; From ef621618867939aa92441fac8f5503db49583d83 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Wed, 9 May 2018 15:26:22 +1000 Subject: [PATCH 05/20] Fix merge --- .../xpack/security/authz/store/NativePrivilegeStore.java | 8 ++++---- .../security/authz/store/NativePrivilegeStoreTests.java | 7 +++++-- 2 files changed, 9 insertions(+), 6 deletions(-) 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 index 399e21e7ca72d..1eec6867239a3 100644 --- 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 @@ -83,7 +83,7 @@ public void getPrivileges(Collection applications, Collection na listener.onResponse(privilege == null ? Collections.emptyList() : Collections.singletonList(privilege)), listener::onFailure)); } else { - securityLifecycleService.prepareIndexIfNeededThenExecute(listener::onFailure, () -> { + securityLifecycleService.securityIndex().prepareIndexIfNeededThenExecute(listener::onFailure, () -> { final QueryBuilder query; final TermQueryBuilder typeQuery = QueryBuilders.termQuery(Fields.TYPE.getPreferredName(), DOC_TYPE_VALUE); if (isEmpty(applications) && isEmpty(names)) { @@ -123,7 +123,7 @@ private static boolean isEmpty(Collection collection) { } public void getPrivilege(String application, String name, ActionListener listener) { - securityLifecycleService.prepareIndexIfNeededThenExecute(listener::onFailure, + securityLifecycleService.securityIndex().prepareIndexIfNeededThenExecute(listener::onFailure, () -> executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN, client.prepareGet(SecurityLifecycleService.SECURITY_INDEX_NAME, "doc", toDocId(application, name)).request(), new ActionListener() { @@ -153,7 +153,7 @@ public void onFailure(Exception e) { public void putPrivileges(Collection privileges, WriteRequest.RefreshPolicy refreshPolicy, ActionListener>> listener) { - securityLifecycleService.prepareIndexIfNeededThenExecute(listener::onFailure, () -> { + securityLifecycleService.securityIndex().prepareIndexIfNeededThenExecute(listener::onFailure, () -> { ActionListener groupListener = new GroupedActionListener<>( ActionListener.wrap((Collection responses) -> { final Map> createdNames = responses.stream() @@ -191,7 +191,7 @@ private void innerPutPrivilege(ApplicationPrivilege privilege, WriteRequest.Refr public void deletePrivileges(String application, Collection names, WriteRequest.RefreshPolicy refreshPolicy, ActionListener>> listener) { - securityLifecycleService.prepareIndexIfNeededThenExecute(listener::onFailure, () -> { + securityLifecycleService.securityIndex().prepareIndexIfNeededThenExecute(listener::onFailure, () -> { ActionListener groupListener = new GroupedActionListener<>( ActionListener.wrap(responses -> { final Map> deletedNames = responses.stream() 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 index 717e8fc641f44..2a4a03bf77afc 100644 --- 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 @@ -40,6 +40,7 @@ import org.elasticsearch.test.junit.annotations.TestLogging; import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; import org.elasticsearch.xpack.security.SecurityLifecycleService; +import org.elasticsearch.xpack.security.support.SecurityIndexManager; import org.hamcrest.Matchers; import org.junit.After; import org.junit.Before; @@ -91,13 +92,15 @@ void doExecute(Action action, Request request } }; final SecurityLifecycleService lifecycleService = mock(SecurityLifecycleService.class); - when(lifecycleService.isSecurityIndexAvailable()).thenReturn(true); + final SecurityIndexManager securityIndex = mock(SecurityIndexManager.class); + when(lifecycleService.securityIndex()).thenReturn(securityIndex); + 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(lifecycleService).prepareIndexIfNeededThenExecute(any(Consumer.class), any(Runnable.class)); + }).when(securityIndex).prepareIndexIfNeededThenExecute(any(Consumer.class), any(Runnable.class)); store = new NativePrivilegeStore(Settings.EMPTY, client, lifecycleService); } From 39c1222afa68741cab9cb6b201850e239273aa03 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Thu, 10 May 2018 15:10:13 +1000 Subject: [PATCH 06/20] Clear role cache on app privilege changes --- .../authz/store/NativePrivilegeStore.java | 154 ++++++++++-------- .../store/NativePrivilegeStoreTests.java | 80 +++++---- 2 files changed, 141 insertions(+), 93 deletions(-) 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 index 1eec6867239a3..fc8b272117db8 100644 --- 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 @@ -6,6 +6,7 @@ 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; @@ -39,8 +40,11 @@ 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.SecurityLifecycleService; import java.io.IOException; @@ -67,11 +71,13 @@ public class NativePrivilegeStore extends AbstractComponent { private final Client client; + private final SecurityClient securityClient; private final SecurityLifecycleService securityLifecycleService; public NativePrivilegeStore(Settings settings, Client client, SecurityLifecycleService securityLifecycleService) { super(settings); this.client = client; + this.securityClient = new SecurityClient(client); this.securityLifecycleService = securityLifecycleService; } @@ -79,9 +85,9 @@ public void getPrivileges(Collection applications, Collection na 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)); + ActionListener.wrap(privilege -> + listener.onResponse(privilege == null ? Collections.emptyList() : Collections.singletonList(privilege)), + listener::onFailure)); } else { securityLifecycleService.securityIndex().prepareIndexIfNeededThenExecute(listener::onFailure, () -> { final QueryBuilder query; @@ -90,29 +96,29 @@ public void getPrivileges(Collection applications, Collection na query = typeQuery; } else if (isEmpty(names)) { query = QueryBuilders.boolQuery().filter(typeQuery) - .filter(QueryBuilders.termsQuery(Fields.APPLICATION.getPreferredName(), applications)); + .filter(QueryBuilders.termsQuery(Fields.APPLICATION.getPreferredName(), applications)); } else if (isEmpty(applications)) { query = QueryBuilders.boolQuery().filter(typeQuery) - .filter(QueryBuilders.termsQuery(Fields.NAME.getPreferredName(), names)); + .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); + .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(SecurityLifecycleService.SECURITY_INDEX_NAME) - .setScroll(TimeValue.timeValueSeconds(10L)) - .setQuery(query) - .setSize(1000) - .setFetchSource(true) - .request(); + .setScroll(TimeValue.timeValueSeconds(10L)) + .setQuery(query) + .setSize(1000) + .setFetchSource(true) + .request(); logger.trace(() -> - new ParameterizedMessage("Searching for privileges [{}] with query [{}]", names, Strings.toString(query))); + 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())); + hit -> buildPrivilege(hit.getId(), hit.getSourceRef())); } }); } @@ -124,45 +130,45 @@ private static boolean isEmpty(Collection collection) { public void getPrivilege(String application, String name, ActionListener listener) { securityLifecycleService.securityIndex().prepareIndexIfNeededThenExecute(listener::onFailure, - () -> executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN, - client.prepareGet(SecurityLifecycleService.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); - } - } + () -> executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN, + client.prepareGet(SecurityLifecycleService.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)); + @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) { securityLifecycleService.securityIndex().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(tuplesToMap()); - listener.onResponse(createdNames); - }, listener::onFailure), privileges.size(), Collections.emptyList()); + ActionListener.wrap((Collection responses) -> { + final Map> createdNames = responses.stream() + .filter(r -> r.getResult() == DocWriteResponse.Result.CREATED) + .map(r -> r.getId()) + .map(NativePrivilegeStore::nameFromDocId) + .collect(tuplesToMap()); + clearRolesCache(listener, createdNames); + }, listener::onFailure), privileges.size(), Collections.emptyList()); for (ApplicationPrivilege privilege : privileges) { innerPutPrivilege(privilege, refreshPolicy, groupListener); } @@ -179,10 +185,10 @@ private void innerPutPrivilege(ApplicationPrivilege privilege, WriteRequest.Refr final String name = privilege.getPrivilegeName(); final XContentBuilder xContentBuilder = privilege.toIndexContent(jsonBuilder(), ToXContent.EMPTY_PARAMS); 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); + 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); @@ -193,23 +199,43 @@ public void deletePrivileges(String application, Collection names, Write ActionListener>> listener) { securityLifecycleService.securityIndex().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(tuplesToMap()); - listener.onResponse(deletedNames); - }, listener::onFailure), names.size(), Collections.emptyList()); + ActionListener.wrap(responses -> { + final Map> deletedNames = responses.stream() + .filter(r -> r.getResult() == DocWriteResponse.Result.DELETED) + .map(r -> r.getId()) + .map(NativePrivilegeStore::nameFromDocId) + .collect(tuplesToMap()); + 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); + 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); + } + + Collector, ?, Map>> tuplesToMap() { return Collectors.toMap(Tuple::v1, t -> CollectionUtils.newSingletonArrayList(t.v2()), (a, b) -> { a.addAll(b); @@ -228,12 +254,12 @@ private ApplicationPrivilege buildPrivilege(String docId, BytesReference source) try (StreamInput input = source.streamInput(); XContentParser parser = XContentType.JSON.xContent().createParser(NamedXContentRegistry.EMPTY, - LoggingDeprecationHandler.INSTANCE, input)) { + LoggingDeprecationHandler.INSTANCE, input)) { final ApplicationPrivilege privilege = ApplicationPrivilege.parse(parser, true); assert privilege.getApplication().equals(name.v1()) - : "Incorrect application name for privilege. Expected [" + name.v1() + "] but was " + privilege.getApplication(); + : "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(); + : "Incorrect name for application privilege. Expected [" + name.v2() + "] but was " + privilege.name(); return privilege; } } catch (IOException | XContentParseException e) { 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 index 2a4a03bf77afc..917b0016e83a5 100644 --- 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 @@ -27,7 +27,6 @@ import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.util.iterable.Iterables; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentType; @@ -38,6 +37,7 @@ 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.SecurityLifecycleService; import org.elasticsearch.xpack.security.support.SecurityIndexManager; @@ -85,7 +85,7 @@ public void setup() { client = new NoOpClient(getTestName()) { @Override protected > + RequestBuilder extends ActionRequestBuilder> void doExecute(Action action, Request request, ActionListener listener) { NativePrivilegeStoreTests.this.requests.add(request); NativePrivilegeStoreTests.this.listener.set(listener); @@ -111,8 +111,8 @@ public void cleanup() { 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")); + 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); @@ -125,7 +125,7 @@ public void testGetSinglePrivilegeByName() throws Exception { 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()) + 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)); @@ -142,7 +142,7 @@ public void testGetMissingPrivilege() throws Exception { 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()) + 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()); @@ -150,9 +150,9 @@ public void testGetMissingPrivilege() throws Exception { 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/*") + 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<>(); @@ -168,16 +168,16 @@ public void testGetPrivilegesByApplicationName() throws Exception { 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)); + 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", "*") + 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<>(); @@ -193,16 +193,16 @@ public void testGetAllPrivileges() throws Exception { 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)); + 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", "*") + 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<>(); @@ -210,25 +210,36 @@ public void testPutPrivileges() throws Exception { 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 = (IndexRequest) requests.get(i); + IndexRequest request = indexRequests.get(i); assertThat(request.indices(), arrayContaining(SecurityLifecycleService.SECURITY_INDEX_NAME)); assertThat(request.type(), equalTo("doc")); assertThat(request.id(), equalTo( - "application-privilege_" + privilege.getApplication() + ":" + privilege.getPrivilegeName() + "application-privilege_" + privilege.getApplication() + ":" + privilege.getPrivilegeName() )); final XContentBuilder builder - = privilege.toIndexContent(XContentBuilder.builder(XContentType.JSON.xContent()), ToXContent.EMPTY_PARAMS); + = privilege.toIndexContent(XContentBuilder.builder(XContentType.JSON.xContent()), ToXContent.EMPTY_PARAMS); assertThat(request.source(), equalTo(BytesReference.bytes(builder))); final boolean created = privilege.name().contains("user") == false; - listener.get().onResponse(new IndexResponse( - new ShardId(SecurityLifecycleService.SECURITY_INDEX_NAME, uuid, i), - request.type(), request.id(), 1, 1, 1, created + indexListener.onResponse(new IndexResponse( + new ShardId(SecurityLifecycleService.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)); @@ -245,20 +256,31 @@ public void testDeletePrivileges() throws Exception { 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 = (DeleteRequest) requests.get(i); + DeleteRequest request = deletes.get(i); assertThat(request.indices(), arrayContaining(SecurityLifecycleService.SECURITY_INDEX_NAME)); assertThat(request.type(), equalTo("doc")); assertThat(request.id(), equalTo("application-privilege_app1:" + name)); final boolean found = name.equals("p2") == false; - listener.get().onResponse(new DeleteResponse( - new ShardId(SecurityLifecycleService.SECURITY_INDEX_NAME, uuid, i), - request.type(), request.id(), 1, 1, 1, found + deleteListener.onResponse(new DeleteResponse( + new ShardId(SecurityLifecycleService.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)); @@ -280,8 +302,8 @@ private void assertResult(List sourcePrivileges, PlainActi 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()))); + 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()))); + equalTo(sourcePrivileges.stream().map(Strings::toString).collect(Collectors.toSet()))); } } From 4526db1041ef11907dff938e2e3e118f83b77552 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Tue, 22 May 2018 21:43:52 +1000 Subject: [PATCH 07/20] Address feedback --- .../common/io/stream/StreamInput.java | 19 +- .../common/io/stream/StreamOutput.java | 11 + .../common/io/stream/StreamTests.java | 50 +++-- .../privilege/DeletePrivilegesAction.java | 5 +- .../privilege/DeletePrivilegesRequest.java | 4 +- .../DeletePrivilegesRequestBuilder.java | 6 +- .../privilege/DeletePrivilegesResponse.java | 16 +- .../action/privilege/GetPrivilegesAction.java | 4 +- .../privilege/GetPrivilegesRequest.java | 4 +- .../GetPrivilegesRequestBuilder.java | 8 +- .../core/security/authz/RoleDescriptor.java | 4 +- .../authz/privilege/ApplicationPrivilege.java | 88 +++++--- .../core/security/client/SecurityClient.java | 8 +- .../DeletePrivilegesRequestTests.java | 61 ++++++ .../DeletePrivilegesResponseTests.java | 32 +++ .../privilege/GetPrivilegesRequestTests.java | 61 ++++++ .../privilege/ApplicationPrivilegeTests.java | 199 ++++++++++++++++++ .../authz/store/NativePrivilegeStore.java | 3 +- .../security/authz/RoleDescriptorTests.java | 23 +- .../store/NativePrivilegeStoreTests.java | 4 +- 20 files changed, 516 insertions(+), 94 deletions(-) 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/authz/privilege/ApplicationPrivilegeTests.java 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 list 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/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 index 0bf67743e6323..d32cc0989ebb0 100644 --- 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 @@ -11,12 +11,13 @@ /** * Action for deleting application privileges. */ -public class DeletePrivilegesAction extends Action { +public final class DeletePrivilegesAction + extends Action { public static final DeletePrivilegesAction INSTANCE = new DeletePrivilegesAction(); public static final String NAME = "cluster:admin/xpack/security/privilege/delete"; - protected DeletePrivilegesAction() { + private DeletePrivilegesAction() { super(NAME); } 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 index d382351906bf6..e259ee23badb4 100644 --- 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 @@ -19,7 +19,7 @@ /** * A request to delete an application privilege. */ -public class DeletePrivilegesRequest extends ActionRequest implements WriteRequest { +public final class DeletePrivilegesRequest extends ActionRequest implements WriteRequest { private String application; private String[] privileges; @@ -76,6 +76,7 @@ public void privileges(String[] privileges) { @Override public void readFrom(StreamInput in) throws IOException { super.readFrom(in); + application = in.readString(); privileges = in.readStringArray(); refreshPolicy = RefreshPolicy.readFrom(in); } @@ -83,6 +84,7 @@ public void readFrom(StreamInput in) throws IOException { @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 index 11c43f7acf9c7..97362db96acbe 100644 --- 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 @@ -18,11 +18,7 @@ public class DeletePrivilegesRequestBuilder extends ActionRequestBuilder implements WriteRequestBuilder { - public DeletePrivilegesRequestBuilder(ElasticsearchClient client) { - this(client, DeletePrivilegesAction.INSTANCE); - } - - DeletePrivilegesRequestBuilder(ElasticsearchClient client, DeletePrivilegesAction action) { + public DeletePrivilegesRequestBuilder(ElasticsearchClient client, DeletePrivilegesAction action) { super(client, action, new DeletePrivilegesRequest()); } 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 index 39872d397905e..18efb2ac5fac3 100644 --- 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 @@ -14,16 +14,14 @@ import java.io.IOException; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; -import java.util.Map; import java.util.Set; /** * Response when deleting application privileges. * Returns a collection of privileges that were successfully found and deleted. */ -public class DeletePrivilegesResponse extends ActionResponse implements ToXContentObject { +public final class DeletePrivilegesResponse extends ActionResponse implements ToXContentObject { private Set found; @@ -47,21 +45,13 @@ public Set found() { @Override public void readFrom(StreamInput in) throws IOException { super.readFrom(in); - final int size = in.readVInt(); - final HashSet foundSet = new HashSet<>(size); - for (int i = 0; i < size; i++) { - foundSet.add(in.readString()); - } - this.found = Collections.unmodifiableSet(foundSet); + this.found = Collections.unmodifiableSet(in.readSet(StreamInput::readString)); } @Override public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); - out.writeVInt(found.size()); - for (String s : found) { - out.writeString(s); - } + 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 index f4ac0a44d56de..76f23a49a7f1a 100644 --- 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 @@ -11,12 +11,12 @@ /** * Action for retrieving one or more application privileges from the security index */ -public class GetPrivilegesAction extends Action { +public final class GetPrivilegesAction extends Action { public static final GetPrivilegesAction INSTANCE = new GetPrivilegesAction(); public static final String NAME = "cluster:admin/xpack/security/privilege/get"; - protected GetPrivilegesAction() { + private GetPrivilegesAction() { super(NAME); } 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 index 00f14d577fcfc..08eacca592e43 100644 --- 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 @@ -57,14 +57,14 @@ public String[] privileges() { @Override public void readFrom(StreamInput in) throws IOException { super.readFrom(in); - application = in.readString(); + application = in.readOptionalString(); privileges = in.readStringArray(); } @Override public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); - out.writeString(application); + 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 index 17332e93c7c62..adad3968a0611 100644 --- 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 @@ -12,13 +12,9 @@ * Builder for {@link GetPrivilegesRequest} */ public class GetPrivilegesRequestBuilder - extends ActionRequestBuilder { + extends ActionRequestBuilder { - public GetPrivilegesRequestBuilder(ElasticsearchClient client) { - this(client, GetPrivilegesAction.INSTANCE); - } - - GetPrivilegesRequestBuilder(ElasticsearchClient client, GetPrivilegesAction action) { + public GetPrivilegesRequestBuilder(ElasticsearchClient client, GetPrivilegesAction action) { super(client, action, new GetPrivilegesRequest()); } 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 a24b0614bb7d5..870f54700ffa9 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 @@ -808,10 +808,10 @@ public String toString() { StringBuilder sb = new StringBuilder(getClass().getSimpleName()) .append("[application=") .append(application) - .append("resources=") - .append(Strings.arrayToCommaDelimitedString(resources)) .append("], privileges=[") .append(Strings.arrayToCommaDelimitedString(privileges)) + .append("], resources=") + .append(Strings.arrayToCommaDelimitedString(resources)) .append("]]"); return sb.toString(); } 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 index 6458c50135aaa..48c6d020871f2 100644 --- 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 @@ -11,6 +11,7 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.regex.Regex; +import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.ToXContentObject; @@ -35,10 +36,11 @@ import static java.util.Collections.emptyMap; /** - * An application privilege has a name (e.g. {@code "my-app:admin"}) that starts with a prefix that identifies the app, - * followed by a suffix that is meaningful to the application. - * It also has zero or more "action patterns" that start with the same prefix and contain at least 1 '/' character (e.g. - * {@code "my-app:/admin/user/*", "my-app:/admin/team/*"}. + * 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. */ @@ -67,8 +69,9 @@ public final class ApplicationPrivilege extends Privilege implements ToXContentO }, Fields.TYPE, ObjectParser.ValueType.STRING); } - public static final Pattern VALID_APPLICATION = Pattern.compile("^[a-z][A-Za-z0-9_-]{2,}$"); - public static final Pattern VALID_NAME = Pattern.compile("^[a-z][a-zA-Z0-9_.-]*$"); + 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]); @@ -99,6 +102,7 @@ public String getApplication() { /** * If this privilege has a single name, returns that name. Otherwise throws {@link IllegalStateException}. + * * @see #name() */ public String getPrivilegeName() { @@ -113,15 +117,23 @@ public Map getMetadata() { return Collections.unmodifiableMap(metadata); } + // Package level for testing + String[] getPatterns() { + return patterns; + } + private void validate(boolean validateNames) { - if (Regex.isSimpleMatchPattern(application) == false) { - validateApplicationName(application); + // 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 + "')"); + throw new IllegalArgumentException("Application privilege names must match the pattern " + VALID_NAME.pattern() + + " (found '" + name + "')"); } } for (String pattern : patterns) { @@ -134,19 +146,28 @@ private void validate(boolean validateNames) { /** * 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) { - if (VALID_APPLICATION.matcher(application).matches() == false) { - throw new IllegalArgumentException("Application names must match /" - + VALID_APPLICATION.pattern() + "/ (but was '" + 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 + "')"); } } - public static boolean isValidPrivilegeName(String name) { + 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); @@ -200,15 +221,24 @@ public String toString() { 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); + 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); } - public XContentBuilder toIndexContent(XContentBuilder builder, Params params) throws IOException { + /** + * 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); } @@ -218,8 +248,6 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws } private XContentBuilder writeXContent(XContentBuilder builder, boolean includeType) throws IOException { - assert name.size() == 1; - builder.startObject() .field(Fields.APPLICATION.getPreferredName(), application) .field(Fields.NAME.getPreferredName(), getPrivilegeName()) @@ -232,36 +260,28 @@ private XContentBuilder writeXContent(XContentBuilder builder, boolean includeTy 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, boolean allowType) throws IOException { return PARSER.parse(parser, allowType); } public static ApplicationPrivilege readFrom(StreamInput in) throws IOException { final String application = in.readString(); - int nameSize = in.readVInt(); - Set names = new HashSet<>(nameSize); - for (int i = 0; i < nameSize; i++) { - names.add(in.readString()); - } + Set names = in.readSet(StreamInput::readString); String[] patterns = in.readStringArray(); - Map metadata = in.readBoolean() ? in.readMap() : emptyMap(); + 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.writeVInt(name.size()); - for (String n : name) { - out.writeString(n); - } + out.writeCollection(name, StreamOutput::writeString); out.writeStringArray(patterns); - if (metadata.isEmpty()) { - out.writeBoolean(false); - } else { - out.writeBoolean(true); - out.writeMap(metadata); - } + out.writeGenericValue(metadata); } public interface Fields { 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 aebf1cae7fc6d..55f4c75c84a31 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,7 +10,9 @@ 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.PutPrivilegesRequestBuilder; import org.elasticsearch.xpack.core.security.action.realm.ClearRealmCacheAction; @@ -287,7 +289,7 @@ public DeleteRoleMappingRequestBuilder prepareDeleteRoleMapping(String name) { /* -- Application Privileges -- */ public GetPrivilegesRequestBuilder prepareGetPrivileges(String applicationName, String[] privileges) { - return new GetPrivilegesRequestBuilder(client).application(applicationName).privileges(privileges); + return new GetPrivilegesRequestBuilder(client, GetPrivilegesAction.INSTANCE).application(applicationName).privileges(privileges); } public PutPrivilegesRequestBuilder preparePutPrivilege(String applicationName, String privilegeName, @@ -300,7 +302,9 @@ public PutPrivilegesRequestBuilder preparePutPrivileges(BytesReference bytesRefe } public DeletePrivilegesRequestBuilder prepareDeletePrivileges(String applicationName, String[] privileges) { - return new DeletePrivilegesRequestBuilder(client).application(applicationName).privileges(privileges); + return new DeletePrivilegesRequestBuilder(client, DeletePrivilegesAction.INSTANCE) + .application(applicationName) + .privileges(privileges); } public CreateTokenRequestBuilder prepareCreateToken() { 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..c767b5172513d --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/DeletePrivilegesRequestTests.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.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"); + + 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..96f4fd6e598c6 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/DeletePrivilegesResponseTests.java @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.security.action.privilege; + +import org.elasticsearch.common.Strings; +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())); + assertThat(Strings.toString(copy), equalTo(Strings.toString(original))); + } +} 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/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..2aca3ab9a718f --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/ApplicationPrivilegeTests.java @@ -0,0 +1,199 @@ +/* + * 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.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, includeTypeField); + assertThat(clone, Matchers.equalTo(original)); + assertThat(original, Matchers.equalTo(clone)); + } + } + } + + 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/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 index 776ea40d127b2..7ec85380b7ada 100644 --- 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 @@ -30,7 +30,6 @@ import org.elasticsearch.common.util.iterable.Iterables; import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; import org.elasticsearch.common.xcontent.NamedXContentRegistry; -import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParseException; import org.elasticsearch.common.xcontent.XContentParser; @@ -183,7 +182,7 @@ private void innerPutPrivilege(ApplicationPrivilege privilege, WriteRequest.Refr } try { final String name = privilege.getPrivilegeName(); - final XContentBuilder xContentBuilder = privilege.toIndexContent(jsonBuilder(), ToXContent.EMPTY_PARAMS); + 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) 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 0d500c4472926..761f93a43d0e6 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 @@ -50,10 +50,19 @@ 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=[], runAs=[sudo], metadata=[{}]]")); + ", applicationPrivileges=[ApplicationResourcePrivileges[application=my_app, privileges=[read,write], resources=[*]]" + + ", runAs=[sudo], metadata=[{}]]")); } public void testToXContent() throws Exception { @@ -65,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); 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 index 22b416aeac525..70b0197f0193d 100644 --- 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 @@ -27,7 +27,6 @@ import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.get.GetResult; @@ -40,7 +39,6 @@ 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.elasticsearch.xpack.security.support.SecurityIndexManager; import org.hamcrest.Matchers; import org.junit.After; import org.junit.Before; @@ -223,7 +221,7 @@ public void testPutPrivileges() throws Exception { "application-privilege_" + privilege.getApplication() + ":" + privilege.getPrivilegeName() )); final XContentBuilder builder - = privilege.toIndexContent(XContentBuilder.builder(XContentType.JSON.xContent()), ToXContent.EMPTY_PARAMS); + = 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( From a2bad110bb6f02c4a9b866d2abcee430d9cd43b1 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Fri, 25 May 2018 13:06:49 +1000 Subject: [PATCH 08/20] Address feedback --- .../DeletePrivilegesRequestBuilder.java | 2 +- .../privilege/GetPrivilegesRequest.java | 2 +- .../GetPrivilegesRequestBuilder.java | 2 +- .../privilege/GetPrivilegesResponse.java | 4 +- .../action/privilege/PutPrivilegesAction.java | 4 +- .../privilege/PutPrivilegesRequest.java | 2 +- .../PutPrivilegesRequestBuilder.java | 32 ++--- .../privilege/PutPrivilegesResponse.java | 9 +- .../action/user/HasPrivilegesRequest.java | 1 + .../core/security/client/SecurityClient.java | 5 +- .../privilege/GetPrivilegesResponseTests.java | 49 +++++++ .../privilege/PutPrivilegesRequestTests.java | 102 ++++++++++++++ .../privilege/PutPrivilegesResponseTests.java | 42 ++++++ .../user/HasPrivilegesRequestTests.java | 126 ++++++++++++++++++ .../PutPrivilegesRequestBuilderTests.java | 12 +- 15 files changed, 356 insertions(+), 38 deletions(-) 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 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 index 97362db96acbe..a50eb2cd03ab5 100644 --- 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 @@ -14,7 +14,7 @@ /** * Builder for {@link DeletePrivilegesRequest} */ -public class DeletePrivilegesRequestBuilder +public final class DeletePrivilegesRequestBuilder extends ActionRequestBuilder implements WriteRequestBuilder { 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 index 08eacca592e43..78a245ad5bab0 100644 --- 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 @@ -19,7 +19,7 @@ /** * Request to retrieve one or more application privileges. */ -public class GetPrivilegesRequest extends ActionRequest { +public final class GetPrivilegesRequest extends ActionRequest { @Nullable private String application; 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 index adad3968a0611..dcde839e40786 100644 --- 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 @@ -11,7 +11,7 @@ /** * Builder for {@link GetPrivilegesRequest} */ -public class GetPrivilegesRequestBuilder +public final class GetPrivilegesRequestBuilder extends ActionRequestBuilder { public GetPrivilegesRequestBuilder(ElasticsearchClient client, GetPrivilegesAction action) { 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 index ce8fefb181fb7..0f989c747afbf 100644 --- 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 @@ -16,7 +16,7 @@ /** * Response containing one or more application privileges retrieved from the security index */ -public class GetPrivilegesResponse extends ActionResponse { +public final class GetPrivilegesResponse extends ActionResponse { private ApplicationPrivilege[] privileges; @@ -35,7 +35,7 @@ public ApplicationPrivilege[] privileges() { @Override public void readFrom(StreamInput in) throws IOException { super.readFrom(in); - in.readArray(ApplicationPrivilege::readFrom, ApplicationPrivilege[]::new); + this.privileges = in.readArray(ApplicationPrivilege::readFrom, ApplicationPrivilege[]::new); } @Override 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 index 1bffa55ba2bdb..a1912c32da324 100644 --- 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 @@ -11,12 +11,12 @@ /** * Action for putting (adding/updating) one or more application privileges. */ -public class PutPrivilegesAction extends Action { +public final class PutPrivilegesAction extends Action { public static final PutPrivilegesAction INSTANCE = new PutPrivilegesAction(); public static final String NAME = "cluster:admin/xpack/security/privilege/put"; - protected PutPrivilegesAction() { + private PutPrivilegesAction() { super(NAME); } 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 index 5708a504f9853..26810c2feb971 100644 --- 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 @@ -26,7 +26,7 @@ /** * Request object to put a one or more application privileges. */ -public class PutPrivilegesRequest extends ActionRequest implements WriteRequest { +public final class PutPrivilegesRequest extends ActionRequest implements WriteRequest { private List privileges; private RefreshPolicy refreshPolicy = RefreshPolicy.IMMEDIATE; 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 index a1cc2ad842131..c78beaa0e1112 100644 --- 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 @@ -11,8 +11,6 @@ import org.elasticsearch.client.ElasticsearchClient; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesReference; -import org.elasticsearch.common.logging.Loggers; -import org.elasticsearch.common.util.iterable.Iterables; import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.XContentParser; @@ -29,15 +27,11 @@ /** * Request builder for {@link PutPrivilegesRequest} */ -public class PutPrivilegesRequestBuilder - extends ActionRequestBuilder - implements WriteRequestBuilder { +public final class PutPrivilegesRequestBuilder + extends ActionRequestBuilder + implements WriteRequestBuilder { - public PutPrivilegesRequestBuilder(ElasticsearchClient client) { - this(client, PutPrivilegesAction.INSTANCE); - } - - PutPrivilegesRequestBuilder(ElasticsearchClient client, PutPrivilegesAction action) { + public PutPrivilegesRequestBuilder(ElasticsearchClient client, PutPrivilegesAction action) { super(client, action, new PutPrivilegesRequest()); } @@ -47,12 +41,12 @@ public PutPrivilegesRequestBuilder(ElasticsearchClient client) { */ public PutPrivilegesRequestBuilder source(String applicationName, String expectedName, BytesReference source, XContentType xContentType) - throws IOException { + 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)) { + .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, stream)) { XContentParser.Token token = parser.currentToken(); if (token == null) { token = parser.nextToken(); @@ -80,12 +74,12 @@ ApplicationPrivilege parsePrivilege(XContentParser parser, String applicationNam * 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 { + 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)) { + .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, stream)) { XContentParser.Token token = parser.currentToken(); if (token == null) { token = parser.nextToken(); @@ -103,7 +97,7 @@ public PutPrivilegesRequestBuilder source(BytesReference source, XContentType xC 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); + applicationName, token); } while (parser.nextToken() != XContentParser.Token.END_OBJECT) { @@ -114,7 +108,7 @@ public PutPrivilegesRequestBuilder source(BytesReference source, XContentType xC 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); + applicationName, token); } privileges.add(parsePrivilege(parser, applicationName, privilegeName)); } @@ -127,16 +121,16 @@ public PutPrivilegesRequestBuilder source(BytesReference source, XContentType xC 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"); + + "] 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 + "]"); + + "] 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 + "]"); + + "] 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 index abdbb758bdbc7..6d4a3f1ad44d0 100644 --- 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 @@ -15,17 +15,16 @@ import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Set; /** * 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 class PutPrivilegesResponse extends ActionResponse implements ToXContentObject { +public final class PutPrivilegesResponse extends ActionResponse implements ToXContentObject { private Map> created; - public PutPrivilegesResponse() { + PutPrivilegesResponse() { this(Collections.emptyMap()); } @@ -33,6 +32,10 @@ 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; } 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 2857191f3801c..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 @@ -39,6 +39,7 @@ public ActionRequestValidationException validate() { } if (applicationPrivileges == null) { validationException = addValidationError("applicationPrivileges must not be null", validationException); + } else { for (ApplicationResourcePrivileges applicationPrivilege : applicationPrivileges) { try { ApplicationPrivilege.validateApplicationName(applicationPrivilege.getApplication()); 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 55f4c75c84a31..fae5fa7eb80c0 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 @@ -14,6 +14,7 @@ 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; @@ -294,11 +295,11 @@ public GetPrivilegesRequestBuilder prepareGetPrivileges(String applicationName, public PutPrivilegesRequestBuilder preparePutPrivilege(String applicationName, String privilegeName, BytesReference bytesReference, XContentType xContentType) throws IOException { - return new PutPrivilegesRequestBuilder(client).source(applicationName, privilegeName, bytesReference, xContentType); + return new PutPrivilegesRequestBuilder(client, PutPrivilegesAction.INSTANCE).source(applicationName, privilegeName, bytesReference, xContentType); } public PutPrivilegesRequestBuilder preparePutPrivileges(BytesReference bytesReference, XContentType xContentType) throws IOException { - return new PutPrivilegesRequestBuilder(client).source(bytesReference, xContentType); + return new PutPrivilegesRequestBuilder(client, PutPrivilegesAction.INSTANCE).source(bytesReference, xContentType); } public DeletePrivilegesRequestBuilder prepareDeletePrivileges(String applicationName, String[] privileges) { 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..67d7e742580c7 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/GetPrivilegesResponseTests.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.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())); + } + + private T[] randomArray(int max, IntFunction arrayConstructor, Supplier valueConstructor) { + int size = randomInt(max); + T[] array = arrayConstructor.apply(size); + for (int i = 0; i < array.length; i++) { + array[i] = valueConstructor.get(); + } + return array; + } + +} 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..4f2c25780fe6e --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesRequestTests.java @@ -0,0 +1,102 @@ +/* + * 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; + } + + private T[] randomArray(int max, IntFunction arrayConstructor, Supplier valueConstructor) { + int size = randomInt(max); + T[] array = arrayConstructor.apply(size); + for (int i = 0; i < array.length; i++) { + array[i] = valueConstructor.get(); + } + return array; + } + +} 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/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 index 6bebac42f01da..959b6a9b035d9 100644 --- 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 @@ -19,7 +19,7 @@ public class PutPrivilegesRequestBuilderTests extends ESTestCase { public void testBuildRequestWithMultipleElements() throws Exception { - final PutPrivilegesRequestBuilder builder = new PutPrivilegesRequestBuilder(null); + final PutPrivilegesRequestBuilder builder = new PutPrivilegesRequestBuilder(null, PutPrivilegesAction.INSTANCE); builder.source(new BytesArray("{ " + "\"foo\":{" + " \"read\":{ \"application\":\"foo\", \"name\":\"read\", \"actions\":[ \"data:/read/*\", \"admin:/read/*\" ] }," @@ -45,7 +45,7 @@ public void testBuildRequestWithMultipleElements() throws Exception { } public void testBuildRequestFromJsonObject() throws Exception { - final PutPrivilegesRequestBuilder builder = new PutPrivilegesRequestBuilder(null); + 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); @@ -57,7 +57,7 @@ public void testBuildRequestFromJsonObject() throws Exception { } public void testPrivilegeNameValidationOfSingleElement() throws Exception { - final PutPrivilegesRequestBuilder builder = new PutPrivilegesRequestBuilder(null); + 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/*\" ] }" @@ -67,7 +67,7 @@ public void testPrivilegeNameValidationOfSingleElement() throws Exception { } public void testApplicationNameValidationOfSingleElement() throws Exception { - final PutPrivilegesRequestBuilder builder = new PutPrivilegesRequestBuilder(null); + 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/*\" ] }" @@ -77,7 +77,7 @@ public void testApplicationNameValidationOfSingleElement() throws Exception { } public void testPrivilegeNameValidationOfMultipleElement() throws Exception { - final PutPrivilegesRequestBuilder builder = new PutPrivilegesRequestBuilder(null); + 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/*\"] }," @@ -89,7 +89,7 @@ public void testPrivilegeNameValidationOfMultipleElement() throws Exception { } public void testApplicationNameValidationOfMultipleElement() throws Exception { - final PutPrivilegesRequestBuilder builder = new PutPrivilegesRequestBuilder(null); + 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/*\" ] }," From e0a20132d3015a11e32be4c12dc8c6f1713b25b5 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Fri, 25 May 2018 13:21:28 +1000 Subject: [PATCH 09/20] Fix checkstyle --- .../xpack/core/security/client/SecurityClient.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 fae5fa7eb80c0..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 @@ -295,7 +295,8 @@ public GetPrivilegesRequestBuilder prepareGetPrivileges(String applicationName, public PutPrivilegesRequestBuilder preparePutPrivilege(String applicationName, String privilegeName, BytesReference bytesReference, XContentType xContentType) throws IOException { - return new PutPrivilegesRequestBuilder(client, PutPrivilegesAction.INSTANCE).source(applicationName, privilegeName, bytesReference, xContentType); + return new PutPrivilegesRequestBuilder(client, PutPrivilegesAction.INSTANCE) + .source(applicationName, privilegeName, bytesReference, xContentType); } public PutPrivilegesRequestBuilder preparePutPrivileges(BytesReference bytesReference, XContentType xContentType) throws IOException { From 22418cc3d81b81d64a9f80bcf3947d338728c4a3 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Fri, 25 May 2018 14:14:24 +1000 Subject: [PATCH 10/20] Address feedback --- .../privilege/DeletePrivilegesRequest.java | 3 +- .../core/security/authz/RoleDescriptor.java | 8 ++++ .../permission/ApplicationPermission.java | 11 +++-- .../DeletePrivilegesRequestTests.java | 1 + .../TransportPutPrivilegesAction.java | 12 ++--- .../user/TransportHasPrivilegesAction.java | 3 +- .../authz/store/NativePrivilegeStore.java | 45 +++++++++---------- .../privilege/RestDeletePrivilegesAction.java | 10 +---- .../authz/AuthorizationServiceTests.java | 2 +- 9 files changed, 48 insertions(+), 47 deletions(-) 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 index e259ee23badb4..92199f11d06aa 100644 --- 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 @@ -13,6 +13,7 @@ import org.elasticsearch.common.io.stream.StreamOutput; import java.io.IOException; +import java.util.Arrays; import static org.elasticsearch.action.ValidateActions.addValidationError; @@ -51,7 +52,7 @@ public ActionRequestValidationException validate() { if (Strings.isNullOrEmpty(application)) { validationException = addValidationError("application name is missing", validationException); } - if (privileges == null || privileges.length == 0) { + if (privileges == null || privileges.length == 0 || Arrays.stream(privileges).allMatch(Strings::isNullOrEmpty)) { validationException = addValidationError("privileges are missing", validationException); } return validationException; 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 870f54700ffa9..654e3519e81a6 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 @@ -60,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, @@ -68,6 +72,10 @@ 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, 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 index e91e28ed2c3c6..4447cccc51d82 100644 --- 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 @@ -32,10 +32,15 @@ public final class ApplicationPermission { private final Logger logger; private final List permissions; - public ApplicationPermission(List>> tuples) { + /** + * @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<>(); - tuples.forEach(tup -> permissionsByPrivilege.compute(tup.v1(), (k, existing) -> { + privilegesAndResources.forEach(tup -> permissionsByPrivilege.compute(tup.v1(), (k, existing) -> { final Automaton patterns = Automatons.patterns(tup.v2()); if (existing == null) { return new PermissionEntry(k, patterns); @@ -90,7 +95,7 @@ private PermissionEntry(ApplicationPrivilege privilege, Automaton resources) { this.resources = resources; } - public boolean grants(ApplicationPrivilege other, Automaton resource) { + private boolean grants(ApplicationPrivilege other, Automaton resource) { return this.application.test(other.getApplication()) && Operations.subsetOf(other.getAutomaton(), privilege.getAutomaton()) && Operations.subsetOf(resource, this.resources); 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 index c767b5172513d..03232181f930e 100644 --- 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 @@ -45,6 +45,7 @@ public void testValidation() { 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()); 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 index fb0618846de07..51a5e7099d038 100644 --- 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 @@ -9,7 +9,6 @@ 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; @@ -19,10 +18,7 @@ import org.elasticsearch.xpack.core.security.action.privilege.PutPrivilegesResponse; import org.elasticsearch.xpack.security.authz.store.NativePrivilegeStore; -import java.util.Arrays; import java.util.Collections; -import java.util.HashSet; -import java.util.Set; /** * Transport action to retrieve one or more application privileges from the security index @@ -44,11 +40,11 @@ public TransportPutPrivilegesAction(Settings settings, ThreadPool threadPool, Ac protected void doExecute(final PutPrivilegesRequest request, final ActionListener listener) { if (request.getPrivileges() == null || request.getPrivileges().size() == 0) { listener.onResponse(new PutPrivilegesResponse(Collections.emptyMap())); - return; - } - this.privilegeStore.putPrivileges(request.getPrivileges(), request.getRefreshPolicy(), ActionListener.wrap( + } 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 2c87e59113243..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 @@ -88,13 +88,12 @@ private void resolveApplicationPrivileges(HasPrivilegesRequest request, ActionLi private Set getApplicationNames(HasPrivilegesRequest request) { return Arrays.stream(request.applicationPrivileges()) .map(RoleDescriptor.ApplicationResourcePrivileges::getApplication) - .distinct() .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=[{}] application=[{}]", + logger.trace(() -> new ParameterizedMessage("Check whether role [{}] has privileges cluster=[{}] index=[{}] application=[{}]", Strings.arrayToCommaDelimitedString(userRole.names()), Strings.arrayToCommaDelimitedString(request.clusterPrivileges()), Strings.arrayToCommaDelimitedString(request.indexPrivileges()), 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 index 7ec85380b7ada..2ab13b0855de3 100644 --- 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 @@ -69,6 +69,13 @@ */ 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; @@ -165,7 +172,7 @@ public void putPrivileges(Collection privileges, WriteRequ .filter(r -> r.getResult() == DocWriteResponse.Result.CREATED) .map(r -> r.getId()) .map(NativePrivilegeStore::nameFromDocId) - .collect(tuplesToMap()); + .collect(TUPLES_TO_MAP); clearRolesCache(listener, createdNames); }, listener::onFailure), privileges.size(), Collections.emptyList()); for (ApplicationPrivilege privilege : privileges) { @@ -178,19 +185,19 @@ private void innerPutPrivilege(ApplicationPrivilege privilege, WriteRequest.Refr ActionListener listener) { if (privilege.name().size() != 1) { listener.onFailure(new IllegalArgumentException("Cannot store application privileges with multivariate names")); - return; - } - 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); + } 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); + } } } @@ -203,7 +210,7 @@ public void deletePrivileges(String application, Collection names, Write .filter(r -> r.getResult() == DocWriteResponse.Result.DELETED) .map(r -> r.getId()) .map(NativePrivilegeStore::nameFromDocId) - .collect(tuplesToMap()); + .collect(TUPLES_TO_MAP); clearRolesCache(listener, deletedNames); }, listener::onFailure), names.size(), Collections.emptyList()); for (String name : names) { @@ -234,14 +241,6 @@ public void onFailure(Exception e) { }, securityClient::clearRolesCache); } - - Collector, ?, Map>> tuplesToMap() { - return Collectors.toMap(Tuple::v1, t -> CollectionUtils.newSingletonArrayList(t.v2()), (a, b) -> { - a.addAll(b); - return a; - }); - } - private ApplicationPrivilege buildPrivilege(String docId, BytesReference source) { logger.trace("Building privilege from [{}] [{}]", docId, source == null ? "<>" : source.utf8ToString()); if (source == null) { 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 index 22d50739a735f..d0cee0dd6b902 100644 --- 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 @@ -6,7 +6,6 @@ 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.xcontent.XContentBuilder; import org.elasticsearch.license.XPackLicenseState; @@ -45,7 +44,7 @@ public String getName() { @Override public RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException { final String application = request.param("application"); - final String[] privileges = getPrivileges(request); + final String[] privileges = request.paramAsStringArray("privilege", null); final String refresh = request.param("refresh"); return channel -> new SecurityClient(client).prepareDeletePrivileges(application, privileges) .setRefreshPolicy(refresh) @@ -64,11 +63,4 @@ public RestResponse buildResponse(DeletePrivilegesResponse response, XContentBui }); } - public String[] getPrivileges(RestRequest request) { - final String[] privilegeArray = request.paramAsStringArray("privilege", null); - if (privilegeArray == null || (privilegeArray.length == 1 && Strings.isNullOrEmpty(privilegeArray[0]))) { - throw new IllegalArgumentException("The \"privilege\" parameter is required"); - } - return privilegeArray; - } } 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 2eb8d9c404369..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 @@ -554,7 +554,7 @@ public void testDenialForAnonymousUser() { 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); From e201c9176e86767a52a19f206f891f9156bdd3a4 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Fri, 25 May 2018 16:02:04 +1000 Subject: [PATCH 11/20] Address feedback --- .../core/security/authz/privilege/ApplicationPrivilege.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 48c6d020871f2..9e924dcb25ed0 100644 --- 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 @@ -190,7 +190,7 @@ private static ApplicationPrivilege resolve(String application, Set name Set patterns = new HashSet<>(); for (String name : names) { name = name.toLowerCase(Locale.ROOT); - if (isValidPrivilegeName(name) != false) { + if (isValidPrivilegeName(name)) { ApplicationPrivilege privilege = lookup.get(name); if (privilege != null && size == 1) { return privilege; From 48820a22542a96b69cb2f94a678ae943d8ed7813 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Fri, 25 May 2018 18:05:36 +1000 Subject: [PATCH 12/20] Infer app/privilege name in PutPrivileges request --- .../PutPrivilegesRequestBuilder.java | 2 +- .../authz/privilege/ApplicationPrivilege.java | 81 ++++++++++++++----- .../privilege/ApplicationPrivilegeTests.java | 38 ++++++++- .../authz/store/NativePrivilegeStore.java | 2 +- .../PutPrivilegesRequestBuilderTests.java | 19 +++++ 5 files changed, 117 insertions(+), 25 deletions(-) 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 index c78beaa0e1112..5dc91c6322f36 100644 --- 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 @@ -62,7 +62,7 @@ public PutPrivilegesRequestBuilder source(String applicationName, String expecte } ApplicationPrivilege parsePrivilege(XContentParser parser, String applicationName, String privilegeName) throws IOException { - final ApplicationPrivilege privilege = ApplicationPrivilege.parse(parser, false); + final ApplicationPrivilege privilege = ApplicationPrivilege.parse(parser, applicationName, privilegeName, false); checkPrivilegeName(privilege, applicationName, privilegeName); return privilege; } 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 index 9e924dcb25ed0..8a071de980a10 100644 --- 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 @@ -11,14 +11,13 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.regex.Regex; -import org.elasticsearch.common.util.set.Sets; -import org.elasticsearch.common.xcontent.ConstructingObjectParser; 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; @@ -48,25 +47,15 @@ public final class ApplicationPrivilege extends Privilege implements ToXContentO public static final String DOC_TYPE_VALUE = "application-privilege"; - private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(DOC_TYPE_VALUE, - arr -> new ApplicationPrivilege((String) arr[0], (String) arr[1], (List) arr[2], (Map) arr[3]) - ); + private static final ObjectParser PARSER = new ObjectParser<>(DOC_TYPE_VALUE, Builder::new); static { - PARSER.declareString(ConstructingObjectParser.constructorArg(), Fields.APPLICATION); - PARSER.declareString(ConstructingObjectParser.constructorArg(), Fields.NAME); - PARSER.declareStringArray(ConstructingObjectParser.constructorArg(), Fields.ACTIONS); - PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), (parser, context) -> parser.map(), Fields.METADATA); - PARSER.declareField((priv, fieldValue) -> { - if (DOC_TYPE_VALUE.equals(fieldValue) == false) { - throw new IllegalStateException("XContent has wrong " + Fields.TYPE.getPreferredName() + " field " + fieldValue); - } - }, (parser, acceptType) -> { - if (acceptType == false) { - throw new IllegalStateException("Field " + Fields.TYPE.getPreferredName() + " cannot be specified here"); - } - return parser.text(); - }, Fields.TYPE, ObjectParser.ValueType.STRING); + 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,}$"); @@ -262,10 +251,19 @@ private XContentBuilder writeXContent(XContentBuilder builder, boolean includeTy /** * 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, boolean allowType) throws IOException { - return PARSER.parse(parser, allowType); + 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 { @@ -284,6 +282,47 @@ public void writeTo(StreamOutput out) throws IOException { 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"); 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 index 2aca3ab9a718f..f4affdcb994c7 100644 --- 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 @@ -36,6 +36,7 @@ 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 { @@ -163,13 +164,46 @@ public void testXContentGenerationAndParsing() throws IOException { final byte[] bytes = out.toByteArray(); try (XContentParser parser = xContent.createParser(NamedXContentRegistry.EMPTY, THROW_UNSUPPORTED_OPERATION, bytes)) { - final ApplicationPrivilege clone = ApplicationPrivilege.parse(parser, includeTypeField); + 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)); @@ -179,7 +213,7 @@ private ApplicationPrivilege randomPrivilege() { final String applicationName; if (randomBoolean()) { applicationName = "*"; - } else { + } else { applicationName = randomAlphaOfLength(1).toLowerCase(Locale.ROOT) + randomAlphaOfLengthBetween(2, 10); } final String privilegeName = randomAlphaOfLength(1).toLowerCase(Locale.ROOT) + randomAlphaOfLengthBetween(2, 8); 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 index 2ab13b0855de3..8260c5418bdbd 100644 --- 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 @@ -253,7 +253,7 @@ private ApplicationPrivilege buildPrivilege(String docId, BytesReference source) try (StreamInput input = source.streamInput(); XContentParser parser = XContentType.JSON.xContent().createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, input)) { - final ApplicationPrivilege privilege = ApplicationPrivilege.parse(parser, true); + 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()) 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 index 959b6a9b035d9..69805ca4726bb 100644 --- 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 @@ -14,7 +14,9 @@ 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 { @@ -101,4 +103,21 @@ public void testApplicationNameValidationOfMultipleElement() throws Exception { 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")); + } + } From 502dd4b0d2f3ef41e37100cd346054ab4156bee6 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Mon, 28 May 2018 12:54:15 +1000 Subject: [PATCH 13/20] Fix fragile test --- .../security/action/privilege/DeletePrivilegesResponseTests.java | 1 - 1 file changed, 1 deletion(-) 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 index 96f4fd6e598c6..924fdd72523aa 100644 --- 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 @@ -27,6 +27,5 @@ public void testSerialization() throws IOException { final DeletePrivilegesResponse copy = new DeletePrivilegesResponse(); copy.readFrom(output.bytes().streamInput()); assertThat(copy.found(), equalTo(original.found())); - assertThat(Strings.toString(copy), equalTo(Strings.toString(original))); } } From 2bcdadbe25fc4ae2960887ab36fc38584c45648f Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Mon, 28 May 2018 13:36:59 +1000 Subject: [PATCH 14/20] Fix import --- .../security/action/privilege/DeletePrivilegesResponseTests.java | 1 - 1 file changed, 1 deletion(-) 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 index 924fdd72523aa..d490177c0cec4 100644 --- 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 @@ -6,7 +6,6 @@ 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; From f9a47a41a2c6987fd504b3df9f4802f73e047927 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Tue, 5 Jun 2018 16:49:14 +1000 Subject: [PATCH 15/20] Address feedback --- .../elasticsearch/common/io/stream/StreamOutput.java | 2 +- .../main/java/org/elasticsearch/test/ESTestCase.java | 11 +++++++++++ .../action/privilege/GetPrivilegesResponseTests.java | 9 --------- .../action/privilege/PutPrivilegesRequestTests.java | 10 ---------- 4 files changed, 12 insertions(+), 20 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/io/stream/StreamOutput.java b/server/src/main/java/org/elasticsearch/common/io/stream/StreamOutput.java index be53290034180..199b8872c5517 100644 --- a/server/src/main/java/org/elasticsearch/common/io/stream/StreamOutput.java +++ b/server/src/main/java/org/elasticsearch/common/io/stream/StreamOutput.java @@ -997,7 +997,7 @@ public void writeList(List list) throws IOException { } /** - * Writes a list of generic objects via a {@link Writer} + * Writes a collection of generic objects via a {@link Writer} */ public void writeCollection(Collection collection, Writer writer) throws IOException { writeVInt(collection.size()); 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/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 index 67d7e742580c7..2d901bc4105ef 100644 --- 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 @@ -37,13 +37,4 @@ public void testSerialization() throws IOException { assertThat(copy.privileges(), Matchers.equalTo(original.privileges())); } - private T[] randomArray(int max, IntFunction arrayConstructor, Supplier valueConstructor) { - int size = randomInt(max); - T[] array = arrayConstructor.apply(size); - for (int i = 0; i < array.length; i++) { - array[i] = valueConstructor.get(); - } - return array; - } - } 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 index 4f2c25780fe6e..3ae197dfe16bd 100644 --- 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 @@ -89,14 +89,4 @@ private PutPrivilegesRequest request(ApplicationPrivilege... privileges) { original.setPrivileges(Arrays.asList(privileges)); return original; } - - private T[] randomArray(int max, IntFunction arrayConstructor, Supplier valueConstructor) { - int size = randomInt(max); - T[] array = arrayConstructor.apply(size); - for (int i = 0; i < array.length; i++) { - array[i] = valueConstructor.get(); - } - return array; - } - } From 8098b960342a6b5e7da42b0130509cb51b606e3d Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Tue, 5 Jun 2018 17:15:04 +1000 Subject: [PATCH 16/20] Update for #30966 --- .../security/action/privilege/DeletePrivilegesAction.java | 8 +------- .../action/privilege/DeletePrivilegesRequestBuilder.java | 3 +-- .../security/action/privilege/GetPrivilegesAction.java | 7 +------ .../action/privilege/GetPrivilegesRequestBuilder.java | 3 +-- .../security/action/privilege/PutPrivilegesAction.java | 7 +------ .../action/privilege/PutPrivilegesRequestBuilder.java | 3 +-- .../security/authz/store/NativePrivilegeStoreTests.java | 4 ++-- 7 files changed, 8 insertions(+), 27 deletions(-) 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 index d32cc0989ebb0..395eafa878718 100644 --- 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 @@ -11,8 +11,7 @@ /** * Action for deleting application privileges. */ -public final class DeletePrivilegesAction - extends Action { +public final class DeletePrivilegesAction extends Action { public static final DeletePrivilegesAction INSTANCE = new DeletePrivilegesAction(); public static final String NAME = "cluster:admin/xpack/security/privilege/delete"; @@ -21,11 +20,6 @@ private DeletePrivilegesAction() { super(NAME); } - @Override - public DeletePrivilegesRequestBuilder newRequestBuilder(ElasticsearchClient client) { - return new DeletePrivilegesRequestBuilder(client, this); - } - @Override public DeletePrivilegesResponse newResponse() { return new DeletePrivilegesResponse(); 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 index a50eb2cd03ab5..a6f9a576a4719 100644 --- 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 @@ -14,8 +14,7 @@ /** * Builder for {@link DeletePrivilegesRequest} */ -public final class DeletePrivilegesRequestBuilder - extends ActionRequestBuilder +public final class DeletePrivilegesRequestBuilder extends ActionRequestBuilder implements WriteRequestBuilder { public DeletePrivilegesRequestBuilder(ElasticsearchClient client, DeletePrivilegesAction action) { 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 index 76f23a49a7f1a..8f503f73c1447 100644 --- 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 @@ -11,7 +11,7 @@ /** * Action for retrieving one or more application privileges from the security index */ -public final class GetPrivilegesAction extends Action { +public final class GetPrivilegesAction extends Action { public static final GetPrivilegesAction INSTANCE = new GetPrivilegesAction(); public static final String NAME = "cluster:admin/xpack/security/privilege/get"; @@ -20,11 +20,6 @@ private GetPrivilegesAction() { super(NAME); } - @Override - public GetPrivilegesRequestBuilder newRequestBuilder(ElasticsearchClient client) { - return new GetPrivilegesRequestBuilder(client, this); - } - @Override public GetPrivilegesResponse newResponse() { return new GetPrivilegesResponse(); 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 index dcde839e40786..305c8d1ff7946 100644 --- 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 @@ -11,8 +11,7 @@ /** * Builder for {@link GetPrivilegesRequest} */ -public final class GetPrivilegesRequestBuilder - extends ActionRequestBuilder { +public final class GetPrivilegesRequestBuilder extends ActionRequestBuilder { public GetPrivilegesRequestBuilder(ElasticsearchClient client, GetPrivilegesAction action) { super(client, action, new GetPrivilegesRequest()); 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 index a1912c32da324..457761efaeb2f 100644 --- 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 @@ -11,7 +11,7 @@ /** * Action for putting (adding/updating) one or more application privileges. */ -public final class PutPrivilegesAction extends Action { +public final class PutPrivilegesAction extends Action { public static final PutPrivilegesAction INSTANCE = new PutPrivilegesAction(); public static final String NAME = "cluster:admin/xpack/security/privilege/put"; @@ -20,11 +20,6 @@ private PutPrivilegesAction() { super(NAME); } - @Override - public PutPrivilegesRequestBuilder newRequestBuilder(ElasticsearchClient client) { - return new PutPrivilegesRequestBuilder(client, this); - } - @Override public PutPrivilegesResponse newResponse() { return new PutPrivilegesResponse(); 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 index 5dc91c6322f36..3e60c48d42ff5 100644 --- 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 @@ -27,8 +27,7 @@ /** * Request builder for {@link PutPrivilegesRequest} */ -public final class PutPrivilegesRequestBuilder - extends ActionRequestBuilder +public final class PutPrivilegesRequestBuilder extends ActionRequestBuilder implements WriteRequestBuilder { public PutPrivilegesRequestBuilder(ElasticsearchClient client, PutPrivilegesAction action) { 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 index 70b0197f0193d..58e693097455a 100644 --- 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 @@ -83,8 +83,8 @@ public void setup() { client = new NoOpClient(getTestName()) { @Override protected > - void doExecute(Action action, Request request, ActionListener listener) { + RequestBuilder extends ActionRequestBuilder> + void doExecute(Action action, Request request, ActionListener listener) { NativePrivilegeStoreTests.this.requests.add(request); NativePrivilegeStoreTests.this.listener.set(listener); } From 5d80b39e42b18b59691658aa174114880e94b04f Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Wed, 6 Jun 2018 14:06:44 +1000 Subject: [PATCH 17/20] Fix ML use of HasPrivileges --- .../xpack/ml/action/TransportPutDatafeedAction.java | 1 + 1 file changed, 1 insertion(+) 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 036ec07ad3509..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 { From 6f6bf4ce4cc3a0adfc28e2bdef45d3a04e9b50bf Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Wed, 6 Jun 2018 17:07:44 +1000 Subject: [PATCH 18/20] Update doc snippets for API changes --- x-pack/docs/en/rest-api/security/privileges.asciidoc | 3 ++- x-pack/docs/en/rest-api/security/roles.asciidoc | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) 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 From e8191a06491c5a27777ef19e26761ce24c7e81ed Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Wed, 6 Jun 2018 19:25:07 +1000 Subject: [PATCH 19/20] Fix RoleDescriptor.toString --- .../xpack/core/security/authz/RoleDescriptor.java | 4 ++-- .../xpack/security/authz/RoleDescriptorTests.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 654e3519e81a6..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 @@ -816,9 +816,9 @@ public String toString() { StringBuilder sb = new StringBuilder(getClass().getSimpleName()) .append("[application=") .append(application) - .append("], privileges=[") + .append(", privileges=[") .append(Strings.arrayToCommaDelimitedString(privileges)) - .append("], resources=") + .append("], resources=[") .append(Strings.arrayToCommaDelimitedString(resources)) .append("]]"); return sb.toString(); 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 761f93a43d0e6..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 @@ -61,7 +61,7 @@ public void testToString() throws Exception { 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=[*]]" + + ", applicationPrivileges=[ApplicationResourcePrivileges[application=my_app, privileges=[read,write], resources=[*]],]" + ", runAs=[sudo], metadata=[{}]]")); } From f43f04c930c28b722fa26944b030544f6bfbfc5e Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Thu, 7 Jun 2018 11:12:49 +1000 Subject: [PATCH 20/20] Fix broken test --- .../action/privilege/PutPrivilegesRequestBuilderTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 69805ca4726bb..1abac2076a11e 100644 --- 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 @@ -108,7 +108,7 @@ public void testInferApplicationNameAndPrivilegeName() throws Exception { builder.source(new BytesArray("{ \"foo\":{" + "\"read\":{ \"actions\":[ \"data:/read/*\", \"admin:/read/*\" ] }," + "\"write\":{ \"actions\":[ \"data:/write/*\", \"admin:/*\" ] }," - + "\"all\":{ \"actions\":[ \"/*\" ] }" + + "\"all\":{ \"actions\":[ \"*\" ] }" + "} }"), XContentType.JSON); assertThat(builder.request().getPrivileges(), iterableWithSize(3)); for (ApplicationPrivilege p : builder.request().getPrivileges()) {