diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/deprecation/DeprecationInfoAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/deprecation/DeprecationInfoAction.java index 72d3c402e3b1b..32c76139475c2 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/deprecation/DeprecationInfoAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/deprecation/DeprecationInfoAction.java @@ -7,16 +7,18 @@ import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.ActionResponse; -import org.elasticsearch.action.IndicesRequest; import org.elasticsearch.action.ActionType; +import org.elasticsearch.action.IndicesRequest; import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.action.support.master.MasterNodeReadOperationRequestBuilder; import org.elasticsearch.action.support.master.MasterNodeReadRequest; +import org.elasticsearch.action.support.nodes.BaseNodeResponse; import org.elasticsearch.client.ElasticsearchClient; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; @@ -24,6 +26,9 @@ import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.xpack.core.ml.datafeed.DatafeedConfig; +import org.elasticsearch.xpack.core.security.action.role.GetFileRolesResponse; +import org.elasticsearch.xpack.core.security.action.role.GetRolesResponse; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import java.io.IOException; import java.util.ArrayList; @@ -36,6 +41,7 @@ import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.elasticsearch.action.ValidateActions.addValidationError; @@ -60,23 +66,6 @@ public static List filterChecks(List checks, Function mergeNodeIssues(NodesDeprecationCheckResponse response) { - Map> issueListMap = new HashMap<>(); - for (NodesDeprecationCheckAction.NodeResponse resp : response.getNodes()) { - for (DeprecationIssue issue : resp.getDeprecationIssues()) { - issueListMap.computeIfAbsent(issue, (key) -> new ArrayList<>()).add(resp.getNode().getName()); - } - } - - return issueListMap.entrySet().stream() - .map(entry -> { - DeprecationIssue issue = entry.getKey(); - String details = issue.getDetails() != null ? issue.getDetails() + " " : ""; - return new DeprecationIssue(issue.getLevel(), issue.getMessage(), issue.getUrl(), - details + "(nodes impacted: " + entry.getValue() + ")"); - }).collect(Collectors.toList()); - } - public static class Response extends ActionResponse implements ToXContentObject { private List clusterSettingsIssues; private List nodeSettingsIssues; @@ -162,12 +151,15 @@ public int hashCode() { * @param indexNameExpressionResolver Used to resolve indices into their concrete names * @param indices The list of index expressions to evaluate using `indexNameExpressionResolver` * @param indicesOptions The options to use when resolving and filtering which indices to check - * @param datafeeds The ml datafeed configurations - * @param nodeDeprecationResponse The response containing the deprecation issues found on each node + * @param datafeeds The ml datafeed configurations, if ML is enabled + * @param rolesResponse The configured roles, if security is enabled + * @param fileRolesResponse The configured file-based roles, for each node, if security is enabled * @param indexSettingsChecks The list of index-level checks that will be run across all specified * concrete indices * @param clusterSettingsChecks The list of cluster-level checks * @param mlSettingsCheck The list of ml checks + * @param rolesChecks The list of security checks + * @param nodeDeprecationResponse The response containing the deprecation issues found on each node * @return The list of deprecation issues found in the cluster */ public static DeprecationInfoAction.Response from(ClusterState state, @@ -175,14 +167,16 @@ public static DeprecationInfoAction.Response from(ClusterState state, IndexNameExpressionResolver indexNameExpressionResolver, String[] indices, IndicesOptions indicesOptions, List datafeeds, - NodesDeprecationCheckResponse nodeDeprecationResponse, + GetRolesResponse rolesResponse, + GetFileRolesResponse fileRolesResponse, List> indexSettingsChecks, List> clusterSettingsChecks, List> - mlSettingsCheck) { + mlSettingsCheck, + List, DeprecationIssue>> rolesChecks, + NodesDeprecationCheckResponse nodeDeprecationResponse) { List clusterSettingsIssues = filterChecks(clusterSettingsChecks, (c) -> c.apply(state)); - List nodeSettingsIssues = mergeNodeIssues(nodeDeprecationResponse); List mlSettingsIssues = new ArrayList<>(); for (DatafeedConfig config : datafeeds) { mlSettingsIssues.addAll(filterChecks(mlSettingsCheck, (c) -> c.apply(config, xContentRegistry))); @@ -200,7 +194,50 @@ public static DeprecationInfoAction.Response from(ClusterState state, } } - return new DeprecationInfoAction.Response(clusterSettingsIssues, nodeSettingsIssues, indexSettingsIssues, mlSettingsIssues); + // Security stuff + final List rolesIssues = filterChecks(rolesChecks, c -> c.apply(Arrays.asList(rolesResponse.roles()))); + final List mergedClusterIssues = Stream.concat(clusterSettingsIssues.stream(), rolesIssues.stream()) + .collect(Collectors.toList()); + + final Map> rolesIssuesByNode = runNodeRoleChecks(fileRolesResponse, rolesChecks); + final Map> nodeIssues = nodeResponseToMap(nodeDeprecationResponse); + final List mergedNodeIssues = mergeNodeIssues(nodeIssues, rolesIssuesByNode); + + return new DeprecationInfoAction.Response(mergedClusterIssues, mergedNodeIssues, indexSettingsIssues, mlSettingsIssues); + } + + private static List mergeNodeIssues(Map> nodeIssues, + Map> rolesIssuesByNode) { + Map> issuesToNodeMap = new HashMap<>(); + Stream.concat(nodeIssues.entrySet().stream(), rolesIssuesByNode.entrySet().stream()) + .forEach(entry -> { + String nodeName = entry.getKey().getName(); + entry.getValue().stream() + .forEach(issue -> issuesToNodeMap.computeIfAbsent(issue, i -> new ArrayList<>()).add(nodeName)); + }); + return issuesToNodeMap.entrySet().stream() + .map(entry -> { + DeprecationIssue issue = entry.getKey(); + String details = issue.getDetails() != null ? issue.getDetails() + " " : ""; + return new DeprecationIssue(issue.getLevel(), issue.getMessage(), issue.getUrl(), + details + "(nodes impacted: " + entry.getValue() + ")"); + }).collect(Collectors.toList()); + } + + private static Map> runNodeRoleChecks(GetFileRolesResponse fileRolesResponse, + List, + DeprecationIssue>> rolesChecks) { + return fileRolesResponse.getNodes().stream() + .collect(Collectors.groupingBy(BaseNodeResponse::getNode, + Collectors.flatMapping(nodeResp -> filterChecks(rolesChecks, (c) -> c.apply(nodeResp.getRoles())).stream(), + Collectors.toList()))); + } + + private static Map> nodeResponseToMap(NodesDeprecationCheckResponse nodeDeprecationResponse) { + return nodeDeprecationResponse.getNodes().stream() + .collect(Collectors.groupingBy(BaseNodeResponse::getNode, + Collectors.flatMapping(nodeResp -> nodeResp.getDeprecationIssues().stream(), + Collectors.toList()))); } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/GetFileRolesAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/GetFileRolesAction.java new file mode 100644 index 0000000000000..d925cd6de50df --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/GetFileRolesAction.java @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.security.action.role; + +import org.elasticsearch.action.ActionType; +import org.elasticsearch.action.support.nodes.BaseNodeRequest; +import org.elasticsearch.action.support.nodes.BaseNodeResponse; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; + +public class GetFileRolesAction extends ActionType { + public static final GetFileRolesAction INSTANCE = new GetFileRolesAction(); + public static final String NAME = "cluster:admin/xpack/security/role/file/get"; + + + private GetFileRolesAction() { + super(NAME, GetFileRolesResponse::new); + } + + public static class NodeRequest extends BaseNodeRequest { + GetFileRolesRequest request; + + public NodeRequest(StreamInput in) throws IOException { + super(in); + request = new GetFileRolesRequest(in); + } + + public NodeRequest(GetFileRolesRequest request) { + this.request = request; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + request.writeTo(out); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + NodeRequest that = (NodeRequest) o; + return Objects.equals(request, that.request); + } + + @Override + public int hashCode() { + return Objects.hash(request); + } + } + + public static class NodeResponse extends BaseNodeResponse { + private final List roles; + + public NodeResponse(StreamInput in) throws IOException { + super(in); + roles = in.readList(RoleDescriptor::new); + } + + public NodeResponse(DiscoveryNode node, List roles) { + super(node); + this.roles = roles; + } + + public List getRoles() { + return roles; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeList(roles); + } + + @Override + public String toString() { + return "GetFileRolesNodeResponse[roles=" + roles.toString() + "]"; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + NodeResponse that = (NodeResponse) o; + return Objects.equals(this.getNode(), that.getNode()) + && Objects.equals(roles, that.roles); + } + + @Override + public int hashCode() { + return Objects.hash(getNode(), roles); + } + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/GetFileRolesRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/GetFileRolesRequest.java new file mode 100644 index 0000000000000..1475941201b26 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/GetFileRolesRequest.java @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.security.action.role; + +import org.elasticsearch.action.support.nodes.BaseNodesRequest; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Objects; + +public class GetFileRolesRequest extends BaseNodesRequest { + public GetFileRolesRequest(StreamInput in) throws IOException { + super(in); + } + + public GetFileRolesRequest(String... nodesIds) { + super(nodesIds); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + } + + @Override + public int hashCode() { + return Objects.hash((Object[]) this.nodesIds()); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + GetFileRolesRequest that = (GetFileRolesRequest) obj; + return Arrays.equals(this.nodesIds(), that.nodesIds()); + } + + @Override + public String toString() { + return "GetFileRolesRequest[nodes: " + Arrays.toString(this.nodesIds()) + "]"; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/GetFileRolesResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/GetFileRolesResponse.java new file mode 100644 index 0000000000000..f6cbb51773626 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/GetFileRolesResponse.java @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.security.action.role; + +import org.elasticsearch.action.FailedNodeException; +import org.elasticsearch.action.support.nodes.BaseNodesResponse; +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; + +public class GetFileRolesResponse extends BaseNodesResponse { + + public GetFileRolesResponse(StreamInput in) throws IOException { + super(in); + } + + public GetFileRolesResponse(ClusterName clusterName, List nodes, + List failures) { + super(clusterName, nodes, failures); + } + + @Override + protected List readNodesFrom(StreamInput in) throws IOException { + return in.readList(GetFileRolesAction.NodeResponse::new); + } + + @Override + protected void writeNodesTo(StreamOutput out, List nodes) throws IOException { + out.writeList(nodes); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GetFileRolesResponse that = (GetFileRolesResponse) o; + return Objects.equals(getClusterName(), that.getClusterName()) + && Objects.equals(getNodes(), that.getNodes()) + && Objects.equals(failures(), that.failures()); + } + + @Override + public int hashCode() { + return Objects.hash(getClusterName(), getNodes(), failures()); + } + + @Override + public String toString() { + return "GetFileRolesResponse[nodeResponses= " + this.getNodes().toString() + "]"; + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/deprecation/DeprecationInfoActionResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/deprecation/DeprecationInfoActionResponseTests.java index c33ea2fd7f2cb..fe8433829b948 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/deprecation/DeprecationInfoActionResponseTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/deprecation/DeprecationInfoActionResponseTests.java @@ -23,8 +23,13 @@ import org.elasticsearch.test.AbstractWireSerializingTestCase; import org.elasticsearch.xpack.core.ml.datafeed.DatafeedConfig; import org.elasticsearch.xpack.core.ml.datafeed.DatafeedConfigTests; +import org.elasticsearch.xpack.core.security.action.role.GetFileRolesAction; +import org.elasticsearch.xpack.core.security.action.role.GetFileRolesResponse; +import org.elasticsearch.xpack.core.security.action.role.GetRolesResponse; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -35,7 +40,7 @@ import java.util.stream.Stream; import static java.util.Collections.emptyList; -import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.core.IsEqual.equalTo; public class DeprecationInfoActionResponseTests extends AbstractWireSerializingTestCase { @@ -85,11 +90,15 @@ public void testFrom() throws IOException { boolean nodeIssueFound = randomBoolean(); boolean indexIssueFound = randomBoolean(); boolean mlIssueFound = randomBoolean(); + boolean rolesIssueFound = randomBoolean(); DeprecationIssue foundIssue = DeprecationIssueTests.createTestInstance(); + // We need to be able to differentiate the roles issue, because it gets merged in with the cluster and/or node issues + DeprecationIssue rolesIssue = randomValueOtherThan(foundIssue , DeprecationIssueTests::createTestInstance); List> clusterSettingsChecks = List.of((s) -> clusterIssueFound ? foundIssue : null); List> indexSettingsChecks = List.of((idx) -> indexIssueFound ? foundIssue : null); List> mlSettingsChecks = List.of((idx, unused) -> mlIssueFound ? foundIssue : null); + List, DeprecationIssue>> rolesChecks = List.of(roles -> rolesIssueFound ? rolesIssue : null); NodesDeprecationCheckResponse nodeDeprecationIssues = new NodesDeprecationCheckResponse( new ClusterName(randomAlphaOfLength(5)), @@ -99,24 +108,41 @@ public void testFrom() throws IOException { : emptyList(), emptyList()); + GetRolesResponse rolesResponse = new GetRolesResponse(new RoleDescriptor(randomAlphaOfLength(10), null, null, null)); + GetFileRolesResponse nodeRolesResponse = new GetFileRolesResponse(new ClusterName(randomAlphaOfLength(5)), + rolesIssueFound + ? Collections.singletonList( + new GetFileRolesAction.NodeResponse(discoveryNode, + Collections.singletonList(new RoleDescriptor(randomAlphaOfLength(10), null, null, null)))) + : emptyList(), + emptyList()); + + // Fix this this test vvv DeprecationInfoAction.Response response = DeprecationInfoAction.Response.from(state, NamedXContentRegistry.EMPTY, - resolver, Strings.EMPTY_ARRAY, indicesOptions, datafeeds, - nodeDeprecationIssues, indexSettingsChecks, clusterSettingsChecks, mlSettingsChecks); + resolver, Strings.EMPTY_ARRAY, indicesOptions, datafeeds, rolesResponse, nodeRolesResponse, indexSettingsChecks, + clusterSettingsChecks, mlSettingsChecks, rolesChecks, nodeDeprecationIssues); + List expectedClusterIssues = new ArrayList<>(); if (clusterIssueFound) { - assertThat(response.getClusterSettingsIssues(), equalTo(Collections.singletonList(foundIssue))); - } else { - assertThat(response.getClusterSettingsIssues(), empty()); + expectedClusterIssues.add(foundIssue); + } + if (rolesIssueFound) { + expectedClusterIssues.add(rolesIssue); } + assertThat(response.getClusterSettingsIssues(), containsInAnyOrder(expectedClusterIssues.toArray())); + List expectedNodeIssues = new ArrayList<>(); if (nodeIssueFound) { String details = foundIssue.getDetails() != null ? foundIssue.getDetails() + " " : ""; - DeprecationIssue mergedFoundIssue = new DeprecationIssue(foundIssue.getLevel(), foundIssue.getMessage(), foundIssue.getUrl(), - details + "(nodes impacted: [" + discoveryNode.getName() + "])"); - assertThat(response.getNodeSettingsIssues(), equalTo(Collections.singletonList(mergedFoundIssue))); - } else { - assertTrue(response.getNodeSettingsIssues().isEmpty()); + expectedNodeIssues.add(new DeprecationIssue(foundIssue.getLevel(), foundIssue.getMessage(), foundIssue.getUrl(), + details + "(nodes impacted: [" + discoveryNode.getName() + "])")); + } + if (rolesIssueFound) { + String rolesIssueDetails = rolesIssue.getDetails() != null ? rolesIssue.getDetails() + " " : ""; + expectedNodeIssues.add(new DeprecationIssue(rolesIssue.getLevel(), rolesIssue.getMessage(), rolesIssue.getUrl(), + rolesIssueDetails + "(nodes impacted: [" + discoveryNode.getName() + "])")); } + assertThat(response.getNodeSettingsIssues(), containsInAnyOrder(expectedNodeIssues.toArray())); if (indexIssueFound) { assertThat(response.getIndexSettingsIssues(), equalTo(Collections.singletonMap("test", diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/role/GetFileRolesRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/role/GetFileRolesRequestTests.java new file mode 100644 index 0000000000000..565aebde1f137 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/role/GetFileRolesRequestTests.java @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.security.action.role; + +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.test.AbstractWireSerializingTestCase; + +public class GetFileRolesRequestTests extends AbstractWireSerializingTestCase { + + @Override + protected GetFileRolesRequest createTestInstance() { + return new GetFileRolesRequest(generateRandomStringArray(randomIntBetween(1,10), randomIntBetween(5, 10), false, true)); + } + + @Override + protected Writeable.Reader instanceReader() { + return GetFileRolesRequest::new; + } + + @Override + protected GetFileRolesRequest mutateInstance(GetFileRolesRequest instance) { + boolean givenInstanceEmpty = instance.nodesIds().length == 0; + return new GetFileRolesRequest(randomValueOtherThan(instance.nodesIds(), + () -> generateRandomStringArray(randomIntBetween(1,10), randomIntBetween(5, 10), false, givenInstanceEmpty == false))); + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/role/GetFileRolesResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/role/GetFileRolesResponseTests.java new file mode 100644 index 0000000000000..06f1de2b65332 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/role/GetFileRolesResponseTests.java @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.security.action.role; + +import org.elasticsearch.Version; +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.test.AbstractWireSerializingTestCase; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; + +import java.net.InetAddress; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class GetFileRolesResponseTests extends AbstractWireSerializingTestCase { + + @Override + protected GetFileRolesResponse createTestInstance() { + List responses = Arrays.asList( + randomArray(1, 10, GetFileRolesAction.NodeResponse[]::new, GetFileRolesResponseTests::randomNodeResponse)); + return new GetFileRolesResponse(new ClusterName(randomAlphaOfLength(10)), responses, Collections.emptyList()); + } + + @Override + protected Writeable.Reader instanceReader() { + return GetFileRolesResponse::new; + } + + @Override + protected GetFileRolesResponse mutateInstance(GetFileRolesResponse instance) { + int mutate = randomIntBetween(1,2); + switch (mutate) { + case 1: + String newClusterName = randomValueOtherThan(instance.getClusterName().value(), () -> randomAlphaOfLength(10)); + return new GetFileRolesResponse(new ClusterName(newClusterName), instance.getNodes(), instance.failures()); + case 2: + List newResponses = randomValueOtherThan(instance.getNodes(), () -> Arrays.asList( + randomArray(1, 10, GetFileRolesAction.NodeResponse[]::new, GetFileRolesResponseTests::randomNodeResponse))); + return new GetFileRolesResponse(instance.getClusterName(), newResponses, instance.failures()); + default: + fail("invalid randomization"); + } + throw new IllegalArgumentException("invalid randomization, did not return from switch"); + } + + private static DiscoveryNode randomDiscoveryNode() throws Exception { + InetAddress inetAddress = InetAddress.getByAddress(randomAlphaOfLength(5), + new byte[] { (byte) 192, (byte) 168, (byte) 0, (byte) 1}); + TransportAddress transportAddress = new TransportAddress(inetAddress, randomIntBetween(0, 65535)); + + return new DiscoveryNode(randomAlphaOfLength(5), randomAlphaOfLength(5), transportAddress, + Collections.emptyMap(), Collections.emptySet(), Version.CURRENT); + } + + private static RoleDescriptor randomRoleDescriptor() { + String[] clusterPrivs = randomArray(1, 10, String[]::new, () -> randomAlphaOfLengthBetween(5, 15)); + return new RoleDescriptor(randomAlphaOfLengthBetween(5, 15), clusterPrivs, null, null); + } + + private static GetFileRolesAction.NodeResponse randomNodeResponse() { + DiscoveryNode node; + try { + node = randomDiscoveryNode(); + } catch (Exception e) { + throw new RuntimeException(e); + } + List roles = Arrays.asList( + randomArray(0, 10, RoleDescriptor[]::new, GetFileRolesResponseTests::randomRoleDescriptor)); + return new GetFileRolesAction.NodeResponse(node, roles); + } +} diff --git a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/DeprecationChecks.java b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/DeprecationChecks.java index 9a77d94fd0e5a..e64b5f525877b 100644 --- a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/DeprecationChecks.java +++ b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/DeprecationChecks.java @@ -13,6 +13,7 @@ import org.elasticsearch.xpack.core.deprecation.DeprecationInfoAction; import org.elasticsearch.xpack.core.deprecation.DeprecationIssue; import org.elasticsearch.xpack.core.ml.datafeed.DatafeedConfig; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import java.util.Collections; import java.util.List; @@ -41,6 +42,9 @@ private DeprecationChecks() { static List> ML_SETTINGS_CHECKS = List.of(MlDeprecationChecks::checkDataFeedAggregations, MlDeprecationChecks::checkDataFeedQuery); + static List, DeprecationIssue>> ROLE_CHECKS = + List.of(RoleDeprecationChecks::createPrivilegeCheck); + /** * helper utility function to reduce repeat of running a specific {@link List} of checks. * diff --git a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/RoleDeprecationChecks.java b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/RoleDeprecationChecks.java new file mode 100644 index 0000000000000..221e2a8599609 --- /dev/null +++ b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/RoleDeprecationChecks.java @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.deprecation; + +import org.elasticsearch.xpack.core.deprecation.DeprecationIssue; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; + +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + + +public class RoleDeprecationChecks { + + static DeprecationIssue createPrivilegeCheck(List roles) { + final List rolesWithDeprecatedPriv = roles.stream() + .filter(hasPrivilegeForAnyIndexPredicate("create")) + .map(RoleDescriptor::getName) + .sorted() // maintains a consistent ordering so the message is consistent + .collect(Collectors.toList()); + return new DeprecationIssue(DeprecationIssue.Level.WARNING, + "Some roles use deprecated privilege \"create\"", + "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-8.0.html", // TODO: Fix this link + "Roles which use the deprevated privilege \"create\": " + + rolesWithDeprecatedPriv.toString() + + ". Please use the privilege \"index_doc\" instead."); + + } + + private static Predicate hasPrivilegeForAnyIndexPredicate(String privilege) { + return role -> Stream.of(role.getIndicesPrivileges()) + .map(RoleDescriptor.IndicesPrivileges::getPrivileges) + .flatMap(Stream::of) + .anyMatch(privilege::equals); + } +} diff --git a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/TransportDeprecationInfoAction.java b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/TransportDeprecationInfoAction.java index bc16c98a2773c..bf8e4c8b3f315 100644 --- a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/TransportDeprecationInfoAction.java +++ b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/TransportDeprecationInfoAction.java @@ -32,8 +32,15 @@ import org.elasticsearch.xpack.core.deprecation.DeprecationInfoAction; import org.elasticsearch.xpack.core.deprecation.NodesDeprecationCheckAction; import org.elasticsearch.xpack.core.deprecation.NodesDeprecationCheckRequest; +import org.elasticsearch.xpack.core.deprecation.NodesDeprecationCheckResponse; import org.elasticsearch.xpack.core.ml.action.GetDatafeedsAction; import org.elasticsearch.xpack.core.ml.datafeed.DatafeedConfig; +import org.elasticsearch.xpack.core.security.action.role.GetFileRolesAction; +import org.elasticsearch.xpack.core.security.action.role.GetFileRolesRequest; +import org.elasticsearch.xpack.core.security.action.role.GetFileRolesResponse; +import org.elasticsearch.xpack.core.security.action.role.GetRolesAction; +import org.elasticsearch.xpack.core.security.action.role.GetRolesRequest; +import org.elasticsearch.xpack.core.security.action.role.GetRolesResponse; import java.io.IOException; import java.util.Collections; @@ -43,6 +50,7 @@ import static org.elasticsearch.xpack.deprecation.DeprecationChecks.CLUSTER_SETTINGS_CHECKS; import static org.elasticsearch.xpack.deprecation.DeprecationChecks.INDEX_SETTINGS_CHECKS; import static org.elasticsearch.xpack.deprecation.DeprecationChecks.ML_SETTINGS_CHECKS; +import static org.elasticsearch.xpack.deprecation.DeprecationChecks.ROLE_CHECKS; public class TransportDeprecationInfoAction extends TransportMasterNodeReadAction { @@ -88,11 +96,40 @@ protected ClusterBlockException checkBlock(DeprecationInfoAction.Request request protected final void masterOperation(Task task, final DeprecationInfoAction.Request request, ClusterState state, final ActionListener listener) { if (licenseState.isDeprecationAllowed()) { + getNodeLocalDeprecations(ActionListener.wrap(nodeLocalDeprecations -> { + getDatafeedConfigs(ActionListener.wrap(datafeeds -> { + getRoles(ActionListener.wrap(roles -> { + getFileRoles(ActionListener.wrap(fileRoles -> { + listener.onResponse( + DeprecationInfoAction.Response.from( + state, + xContentRegistry, + indexNameExpressionResolver, + request.indices(), + request.indicesOptions(), + datafeeds, + roles, + fileRoles, + INDEX_SETTINGS_CHECKS, + CLUSTER_SETTINGS_CHECKS, + ML_SETTINGS_CHECKS, + ROLE_CHECKS, + nodeLocalDeprecations + )); - NodesDeprecationCheckRequest nodeDepReq = new NodesDeprecationCheckRequest("_all"); - ClientHelper.executeAsyncWithOrigin(client, ClientHelper.DEPRECATION_ORIGIN, - NodesDeprecationCheckAction.INSTANCE, nodeDepReq, - ActionListener.wrap(response -> { + }, listener::onFailure)); + }, listener::onFailure)); + }, listener::onFailure)); + }, listener::onFailure)); + } else { + listener.onFailure(LicenseUtils.newComplianceException(XPackField.DEPRECATION)); + } + } + + private void getNodeLocalDeprecations(ActionListener listener) { + NodesDeprecationCheckRequest nodeDepReq = new NodesDeprecationCheckRequest("_all"); + ClientHelper.executeAsyncWithOrigin(client, ClientHelper.DEPRECATION_ORIGIN, + NodesDeprecationCheckAction.INSTANCE, nodeDepReq, ActionListener.wrap(response -> { if (response.hasFailures()) { List failedNodeIds = response.failures().stream() .map(failure -> failure.nodeId() + ": " + failure.getMessage()) @@ -102,21 +139,8 @@ protected final void masterOperation(Task task, final DeprecationInfoAction.Requ logger.debug("node {} failed to run deprecation checks: {}", failure.nodeId(), failure); } } - getDatafeedConfigs(ActionListener.wrap( - datafeeds -> { - listener.onResponse( - DeprecationInfoAction.Response.from(state, xContentRegistry, indexNameExpressionResolver, - request.indices(), request.indicesOptions(), datafeeds, - response, INDEX_SETTINGS_CHECKS, CLUSTER_SETTINGS_CHECKS, - ML_SETTINGS_CHECKS)); - }, - listener::onFailure - )); - + listener.onResponse(response); }, listener::onFailure)); - } else { - listener.onFailure(LicenseUtils.newComplianceException(XPackField.DEPRECATION)); - } } private void getDatafeedConfigs(ActionListener> listener) { @@ -130,4 +154,32 @@ private void getDatafeedConfigs(ActionListener> listener) { )); } } + + private void getRoles(ActionListener listener) { + if (XPackSettings.SECURITY_ENABLED.get(settings) == false) { + listener.onResponse(null); + } else { + ClientHelper.executeAsyncWithOrigin(client, ClientHelper.DEPRECATION_ORIGIN, GetRolesAction.INSTANCE, + new GetRolesRequest(), ActionListener.wrap(listener::onResponse, listener::onFailure)); + } + } + + private void getFileRoles(ActionListener listener) { + if (XPackSettings.SECURITY_ENABLED.get(settings) == false) { + listener.onResponse(null); + } else { + ClientHelper.executeAsyncWithOrigin(client, ClientHelper.DEPRECATION_ORIGIN, GetFileRolesAction.INSTANCE, + new GetFileRolesRequest(), ActionListener.wrap(response -> { + if (response.hasFailures()) { + List failedNodeIds = response.failures().stream() + .map(failure -> failure.nodeId() + ": " + failure.getMessage()) + .collect(Collectors.toList()); + logger.warn("failed to retrieve file-based roles from nodes: {}", failedNodeIds); + for (FailedNodeException failure : response.failures()) { + logger.debug("node {} failed to return file-based roles: {}", failure.nodeId(), failure); + } + } + }, listener::onFailure)); + } + } } diff --git a/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/RoleDeprecationChecksTests.java b/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/RoleDeprecationChecksTests.java new file mode 100644 index 0000000000000..7597b76589bbc --- /dev/null +++ b/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/RoleDeprecationChecksTests.java @@ -0,0 +1,46 @@ +/* + * 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.deprecation; + +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.deprecation.DeprecationIssue; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.elasticsearch.xpack.deprecation.DeprecationChecks.ROLE_CHECKS; + +public class RoleDeprecationChecksTests extends ESTestCase { + + public void testCreatePrivilegeCheck() { + RoleDescriptor.IndicesPrivileges badPriv = RoleDescriptor.IndicesPrivileges.builder() + .indices(randomAlphaOfLength(10)) + .privileges("create") + .build(); + RoleDescriptor.IndicesPrivileges fixedPriv = RoleDescriptor.IndicesPrivileges.builder() + .indices(randomAlphaOfLength(10)) + .privileges("index_doc") + .build(); + final String badRoleName = randomAlphaOfLength(10); + RoleDescriptor badRole = new RoleDescriptor(badRoleName, null, + new RoleDescriptor.IndicesPrivileges[]{badPriv}, null); + final String fixedRoleName = randomValueOtherThan(badRoleName, () -> randomAlphaOfLength(10)); + RoleDescriptor fixedRole = new RoleDescriptor(fixedRoleName, null, + new RoleDescriptor.IndicesPrivileges[]{fixedPriv}, null); + List roles = Arrays.asList(badRole, fixedRole); + + DeprecationIssue expected = new DeprecationIssue(DeprecationIssue.Level.WARNING, + "Some roles use deprecated privilege \"create\"", + "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-8.0.html", // TODO: Fix this + "Roles which use the deprevated privilege \"create\": [" + + badRoleName + + "]. Please use the privilege \"index_doc\" instead."); + List issues = DeprecationChecks.filterChecks(ROLE_CHECKS, c -> c.apply(roles)); + assertEquals(Collections.singletonList(expected), issues); + } +} 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 a0e07907a8712..6fd0d027a71af 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 @@ -91,6 +91,7 @@ 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; +import org.elasticsearch.xpack.core.security.action.role.GetFileRolesAction; import org.elasticsearch.xpack.core.security.action.role.GetRolesAction; import org.elasticsearch.xpack.core.security.action.role.PutRoleAction; import org.elasticsearch.xpack.core.security.action.rolemapping.DeleteRoleMappingAction; @@ -152,6 +153,7 @@ import org.elasticsearch.xpack.security.action.realm.TransportClearRealmCacheAction; import org.elasticsearch.xpack.security.action.role.TransportClearRolesCacheAction; import org.elasticsearch.xpack.security.action.role.TransportDeleteRoleAction; +import org.elasticsearch.xpack.security.action.role.TransportGetFileRolesAction; import org.elasticsearch.xpack.security.action.role.TransportGetRolesAction; import org.elasticsearch.xpack.security.action.role.TransportPutRoleAction; import org.elasticsearch.xpack.security.action.rolemapping.TransportDeleteRoleMappingAction; @@ -410,6 +412,8 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste final FieldPermissionsCache fieldPermissionsCache = new FieldPermissionsCache(settings); final FileRolesStore fileRolesStore = new FileRolesStore(settings, env, resourceWatcherService, getLicenseState(), xContentRegistry); + components.add(fileRolesStore); + final NativeRolesStore nativeRolesStore = new NativeRolesStore(settings, client, getLicenseState(), securityIndex.get()); final ReservedRolesStore reservedRolesStore = new ReservedRolesStore(); List, ActionListener>> rolesProviders = new ArrayList<>(); @@ -703,6 +707,7 @@ public void onIndexModule(IndexModule module) { new ActionHandler<>(PutUserAction.INSTANCE, TransportPutUserAction.class), new ActionHandler<>(DeleteUserAction.INSTANCE, TransportDeleteUserAction.class), new ActionHandler<>(GetRolesAction.INSTANCE, TransportGetRolesAction.class), + new ActionHandler<>(GetFileRolesAction.INSTANCE, TransportGetFileRolesAction.class), new ActionHandler<>(PutRoleAction.INSTANCE, TransportPutRoleAction.class), new ActionHandler<>(DeleteRoleAction.INSTANCE, TransportDeleteRoleAction.class), new ActionHandler<>(ChangePasswordAction.INSTANCE, TransportChangePasswordAction.class), diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/role/TransportGetFileRolesAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/role/TransportGetFileRolesAction.java new file mode 100644 index 0000000000000..a723eafac1769 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/role/TransportGetFileRolesAction.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.security.action.role; + +import org.elasticsearch.action.FailedNodeException; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.nodes.TransportNodesAction; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.security.action.role.GetFileRolesAction; +import org.elasticsearch.xpack.core.security.action.role.GetFileRolesRequest; +import org.elasticsearch.xpack.core.security.action.role.GetFileRolesResponse; +import org.elasticsearch.xpack.security.authz.store.FileRolesStore; + +import java.io.IOException; +import java.util.List; + +public class TransportGetFileRolesAction extends TransportNodesAction { + + private final FileRolesStore fileRolesStore; + + @Inject + public TransportGetFileRolesAction(ThreadPool threadPool, ClusterService clusterService, TransportService transportService, + ActionFilters actionFilters, FileRolesStore fileRolesStore) { + super(GetFileRolesAction.NAME, threadPool, clusterService, transportService, actionFilters, + GetFileRolesRequest::new, + GetFileRolesAction.NodeRequest::new, + ThreadPool.Names.GENERIC, //TODO: Is this the right threadpool? + GetFileRolesAction.NodeResponse.class); + this.fileRolesStore = fileRolesStore; + } + + @Override + protected GetFileRolesResponse newResponse(GetFileRolesRequest request, List nodeResponses, + List failures) { + return new GetFileRolesResponse(clusterService.getClusterName(), nodeResponses, failures); + } + + @Override + protected GetFileRolesAction.NodeRequest newNodeRequest(GetFileRolesRequest request) { + return new GetFileRolesAction.NodeRequest(request); + } + + @Override + protected GetFileRolesAction.NodeResponse newNodeResponse(StreamInput in) throws IOException { + return new GetFileRolesAction.NodeResponse(in); + } + + @Override + protected GetFileRolesAction.NodeResponse nodeOperation(GetFileRolesAction.NodeRequest request, Task task) { + return new GetFileRolesAction.NodeResponse(transportService.getLocalNode(), fileRolesStore.getAllRoleDescriptors()); + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/FileRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/FileRolesStore.java index b1059c46cc668..8e2c0c8572128 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/FileRolesStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/FileRolesStore.java @@ -107,6 +107,10 @@ Set roleDescriptors(Set roleNames) { return descriptors; } + public List getAllRoleDescriptors() { + return new ArrayList<>(permissions.values()); + } + public Map usageStats() { final Map localPermissions = permissions; Map usageStats = new HashMap<>(3);