From 9200e15b74d92593aea0c157741739c12247f6c4 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Thu, 18 Oct 2018 14:09:04 +1100 Subject: [PATCH] Add get-user-privileges API (#33928) This API is intended as a companion to the _has_privileges API. It returns the list of privileges that are held by the current user. This information is difficult to reason about, and consumers should avoid making direct security decisions based solely on this data. For example, each of the following index privileges (as well as many more) would grant a user access to index a new document into the "metrics-2018-08-30" index, but clients should not try and deduce that information from this API. - "all" on "*" - "all" on "metrics-*" - "write" on "metrics-2018-*" - "write" on "metrics-2018-08-30" Rather, if a client wished to know if a user had "index" access to _any_ index, it would be possible to use this API to determine whether the user has any index privileges, and on which index patterns, and then feed those index patterns into _has_privileges in order to determine whether the "index" privilege had been granted. The result JSON is modelled on the Role API, with a few small changes to reflect how privileges are modelled when multiple roles are merged together (multiple DLS queries, multiple FLS grants, multiple global conditions, etc). --- .../action/user/GetUserPrivilegesAction.java | 26 ++ .../action/user/GetUserPrivilegesRequest.java | 73 +++++ .../user/GetUserPrivilegesRequestBuilder.java | 28 ++ .../user/GetUserPrivilegesResponse.java | 242 ++++++++++++++ .../core/security/authz/RoleDescriptor.java | 6 +- .../permission/ApplicationPermission.java | 78 ++++- .../authz/permission/ClusterPermission.java | 34 +- .../authz/permission/FieldPermissions.java | 2 +- .../FieldPermissionsDefinition.java | 10 + .../core/security/authz/permission/Role.java | 2 +- .../authz/permission/RunAsPermission.java | 6 + .../ConditionalClusterPrivileges.java | 11 +- .../core/security/client/SecurityClient.java | 12 + .../user/GetUserPrivilegesRequestTests.java | 42 +++ .../user/GetUserPrivilegesResponseTests.java | 170 ++++++++++ .../ApplicationPermissionTests.java | 31 +- .../xpack/security/Security.java | 5 + .../TransportGetUserPrivilegesAction.java | 133 ++++++++ .../security/authz/AuthorizationService.java | 6 +- .../user/RestGetUserPrivilegesAction.java | 85 +++++ ...TransportGetUserPrivilegesActionTests.java | 87 +++++ .../RestGetUserPrivilegesActionTests.java | 91 ++++++ .../xpack.security.get_user_privileges.json | 13 + .../test/privileges/30_superuser.yml | 24 +- .../test/privileges/40_get_user_privs.yml | 304 ++++++++++++++++++ 25 files changed, 1486 insertions(+), 35 deletions(-) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/GetUserPrivilegesAction.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/GetUserPrivilegesRequest.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/GetUserPrivilegesRequestBuilder.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/GetUserPrivilegesResponse.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/GetUserPrivilegesRequestTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/GetUserPrivilegesResponseTests.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportGetUserPrivilegesAction.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestGetUserPrivilegesAction.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportGetUserPrivilegesActionTests.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/user/RestGetUserPrivilegesActionTests.java create mode 100644 x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.get_user_privileges.json create mode 100644 x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/40_get_user_privs.yml diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/GetUserPrivilegesAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/GetUserPrivilegesAction.java new file mode 100644 index 00000000000..6d51d74d899 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/GetUserPrivilegesAction.java @@ -0,0 +1,26 @@ +/* + * 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.action.Action; + +/** + * Action that lists the set of privileges held by a user. + */ +public final class GetUserPrivilegesAction extends Action { + + public static final GetUserPrivilegesAction INSTANCE = new GetUserPrivilegesAction(); + public static final String NAME = "cluster:admin/xpack/security/user/list_privileges"; + + private GetUserPrivilegesAction() { + super(NAME); + } + + @Override + public GetUserPrivilegesResponse newResponse() { + return new GetUserPrivilegesResponse(); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/GetUserPrivilegesRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/GetUserPrivilegesRequest.java new file mode 100644 index 00000000000..972e881cc38 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/GetUserPrivilegesRequest.java @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.security.action.user; + +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 java.io.IOException; + +/** + * A request for checking a user's privileges + */ +public final class GetUserPrivilegesRequest extends ActionRequest implements UserRequest { + + private String username; + + /** + * Package level access for {@link GetUserPrivilegesRequestBuilder}. + */ + GetUserPrivilegesRequest() { + } + + public GetUserPrivilegesRequest(StreamInput in) throws IOException { + super(in); + this.username = in.readString(); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + /** + * @return the username that this request applies to. + */ + public String username() { + return username; + } + + /** + * Set the username that the request applies to. Must not be {@code null} + */ + public void username(String username) { + this.username = username; + } + + @Override + public String[] usernames() { + return new String[] { username }; + } + + /** + * Always throws {@link UnsupportedOperationException} as this object should be deserialized using + * the {@link #GetUserPrivilegesRequest(StreamInput)} constructor instead. + */ + @Override + @Deprecated + public void readFrom(StreamInput in) throws IOException { + throw new UnsupportedOperationException("Use " + getClass() + " as Writeable not Streamable"); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(username); + } + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/GetUserPrivilegesRequestBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/GetUserPrivilegesRequestBuilder.java new file mode 100644 index 00000000000..0ac26af8bc6 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/GetUserPrivilegesRequestBuilder.java @@ -0,0 +1,28 @@ +/* + * 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.action.ActionRequestBuilder; +import org.elasticsearch.client.ElasticsearchClient; + +/** + * Request builder for checking a user's privileges + */ +public class GetUserPrivilegesRequestBuilder + extends ActionRequestBuilder { + + public GetUserPrivilegesRequestBuilder(ElasticsearchClient client) { + super(client, GetUserPrivilegesAction.INSTANCE, new GetUserPrivilegesRequest()); + } + + /** + * Set the username of the user whose privileges should be retrieved. Must not be {@code null} + */ + public GetUserPrivilegesRequestBuilder username(String username) { + request.username(username); + return this; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/GetUserPrivilegesResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/GetUserPrivilegesResponse.java new file mode 100644 index 00000000000..d14e513e2e7 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/GetUserPrivilegesResponse.java @@ -0,0 +1,242 @@ +/* + * 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.action.ActionResponse; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsDefinition; +import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivileges; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; + +/** + * Response for a {@link GetUserPrivilegesRequest} + */ +public final class GetUserPrivilegesResponse extends ActionResponse { + + private Set cluster; + private Set conditionalCluster; + private Set index; + private Set application; + private Set runAs; + + public GetUserPrivilegesResponse() { + this(Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet()); + } + + public GetUserPrivilegesResponse(Set cluster, Set conditionalCluster, + Set index, + Set application, + Set runAs) { + this.cluster = Collections.unmodifiableSet(cluster); + this.conditionalCluster = Collections.unmodifiableSet(conditionalCluster); + this.index = Collections.unmodifiableSet(index); + this.application = Collections.unmodifiableSet(application); + this.runAs = Collections.unmodifiableSet(runAs); + } + + public Set getClusterPrivileges() { + return cluster; + } + + public Set getConditionalClusterPrivileges() { + return conditionalCluster; + } + + public Set getIndexPrivileges() { + return index; + } + + public Set getApplicationPrivileges() { + return application; + } + + public Set getRunAs() { + return runAs; + } + + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + cluster = Collections.unmodifiableSet(in.readSet(StreamInput::readString)); + conditionalCluster = Collections.unmodifiableSet(in.readSet(ConditionalClusterPrivileges.READER)); + index = Collections.unmodifiableSet(in.readSet(Indices::new)); + application = Collections.unmodifiableSet(in.readSet(RoleDescriptor.ApplicationResourcePrivileges::createFrom)); + runAs = Collections.unmodifiableSet(in.readSet(StreamInput::readString)); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeCollection(cluster, StreamOutput::writeString); + out.writeCollection(conditionalCluster, ConditionalClusterPrivileges.WRITER); + out.writeCollection(index, (o, p) -> p.writeTo(o)); + out.writeCollection(application, (o, p) -> p.writeTo(o)); + out.writeCollection(runAs, StreamOutput::writeString); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + final GetUserPrivilegesResponse that = (GetUserPrivilegesResponse) other; + return Objects.equals(cluster, that.cluster) && + Objects.equals(conditionalCluster, that.conditionalCluster) && + Objects.equals(index, that.index) && + Objects.equals(application, that.application) && + Objects.equals(runAs, that.runAs); + } + + @Override + public int hashCode() { + return Objects.hash(cluster, conditionalCluster, index, application, runAs); + } + + /** + * This is modelled on {@link RoleDescriptor.IndicesPrivileges}, with support for multiple DLS and FLS field sets. + */ + public static class Indices implements ToXContentObject, Writeable { + + private final Set indices; + private final Set privileges; + private final Set fieldSecurity; + private final Set queries; + + public Indices(Collection indices, Collection privileges, + Set fieldSecurity, Set queries) { + // The use of TreeSet is to provide a consistent order that can be relied upon in tests + this.indices = Collections.unmodifiableSet(new TreeSet<>(Objects.requireNonNull(indices))); + this.privileges = Collections.unmodifiableSet(new TreeSet<>(Objects.requireNonNull(privileges))); + this.fieldSecurity = Collections.unmodifiableSet(Objects.requireNonNull(fieldSecurity)); + this.queries = Collections.unmodifiableSet(Objects.requireNonNull(queries)); + } + + public Indices(StreamInput in) throws IOException { + indices = Collections.unmodifiableSet(in.readSet(StreamInput::readString)); + privileges = Collections.unmodifiableSet(in.readSet(StreamInput::readString)); + fieldSecurity = Collections.unmodifiableSet(in.readSet(input -> { + final String[] grant = input.readOptionalStringArray(); + final String[] exclude = input.readOptionalStringArray(); + return new FieldPermissionsDefinition.FieldGrantExcludeGroup(grant, exclude); + })); + queries = Collections.unmodifiableSet(in.readSet(StreamInput::readBytesReference)); + } + + public Set getIndices() { + return indices; + } + + public Set getPrivileges() { + return privileges; + } + + public Set getFieldSecurity() { + return fieldSecurity; + } + + public Set getQueries() { + return queries; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(getClass().getSimpleName()) + .append("[") + .append("indices=[").append(Strings.collectionToCommaDelimitedString(indices)) + .append("], privileges=[").append(Strings.collectionToCommaDelimitedString(privileges)) + .append("]"); + if (fieldSecurity.isEmpty() == false) { + sb.append(", fls=[").append(Strings.collectionToCommaDelimitedString(fieldSecurity)).append("]"); + } + if (queries.isEmpty() == false) { + sb.append(", dls=[") + .append(queries.stream().map(BytesReference::utf8ToString).collect(Collectors.joining(","))) + .append("]"); + } + sb.append("]"); + return sb.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Indices that = (Indices) o; + + return this.indices.equals(that.indices) + && this.privileges.equals(that.privileges) + && this.fieldSecurity.equals(that.fieldSecurity) + && this.queries.equals(that.queries); + } + + @Override + public int hashCode() { + return Objects.hash(indices, privileges, fieldSecurity, queries); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(RoleDescriptor.Fields.NAMES.getPreferredName(), indices); + builder.field(RoleDescriptor.Fields.PRIVILEGES.getPreferredName(), privileges); + if (fieldSecurity.stream().anyMatch(g -> nonEmpty(g.getGrantedFields()) || nonEmpty(g.getExcludedFields()))) { + builder.startArray(RoleDescriptor.Fields.FIELD_PERMISSIONS.getPreferredName()); + for (FieldPermissionsDefinition.FieldGrantExcludeGroup group : this.fieldSecurity) { + builder.startObject(); + if (nonEmpty(group.getGrantedFields())) { + builder.array(RoleDescriptor.Fields.GRANT_FIELDS.getPreferredName(), group.getGrantedFields()); + } + if (nonEmpty(group.getExcludedFields())) { + builder.array(RoleDescriptor.Fields.EXCEPT_FIELDS.getPreferredName(), group.getExcludedFields()); + } + builder.endObject(); + } + builder.endArray(); + } + if (queries.isEmpty() == false) { + builder.startArray(RoleDescriptor.Fields.QUERY.getPreferredName()); + for (BytesReference q : queries) { + builder.value(q.utf8ToString()); + } + builder.endArray(); + } + return builder.endObject(); + } + + private boolean nonEmpty(String[] grantedFields) { + return grantedFields != null && grantedFields.length != 0; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeCollection(indices, StreamOutput::writeString); + out.writeCollection(privileges, StreamOutput::writeString); + out.writeCollection(fieldSecurity, (output, fields) -> { + output.writeOptionalStringArray(fields.getGrantedFields()); + output.writeOptionalStringArray(fields.getExcludedFields()); + }); + out.writeCollection(queries, StreamOutput::writeBytesReference); + } + } +} 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 69712a6f33d..9af4698b828 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 @@ -763,6 +763,10 @@ public class RoleDescriptor implements ToXContentObject { return this; } + public Builder privileges(Collection privileges) { + return privileges(privileges.toArray(new String[privileges.size()])); + } + public Builder grantedFields(String... grantedFields) { indicesPrivileges.grantedFields = grantedFields; return this; @@ -919,7 +923,7 @@ public class RoleDescriptor implements ToXContentObject { return this; } - public Builder resources(List resources) { + public Builder resources(Collection resources) { return resources(resources.toArray(new String[resources.size()])); } 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 8f1e78a4663..073e92f7faf 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 @@ -5,11 +5,12 @@ */ package org.elasticsearch.xpack.core.security.authz.permission; +import org.apache.logging.log4j.LogManager; 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.common.util.set.Sets; import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; import org.elasticsearch.xpack.core.security.support.Automatons; @@ -21,6 +22,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Predicate; +import java.util.stream.Collectors; /** * A permission that is based on privileges for application (non elasticsearch) capabilities @@ -38,14 +40,16 @@ public final class ApplicationPermission { * applied. The resources are treated as a wildcard {@link Automatons#pattern}. */ ApplicationPermission(List>> privilegesAndResources) { - this.logger = Loggers.getLogger(getClass()); + this.logger = LogManager.getLogger(getClass()); Map permissionsByPrivilege = new HashMap<>(); - privilegesAndResources.forEach(tup -> permissionsByPrivilege.compute(tup.v1(), (k, existing) -> { - final Automaton patterns = Automatons.patterns(tup.v2()); + privilegesAndResources.forEach(tup -> permissionsByPrivilege.compute(tup.v1(), (appPriv, existing) -> { + final Set resourceNames = tup.v2(); + final Automaton patterns = Automatons.patterns(resourceNames); if (existing == null) { - return new PermissionEntry(k, patterns); + return new PermissionEntry(appPriv, resourceNames, patterns); } else { - return new PermissionEntry(k, Automatons.unionAndMinimize(Arrays.asList(existing.resources, patterns))); + return new PermissionEntry(appPriv, Sets.union(existing.resourceNames, resourceNames), + Automatons.unionAndMinimize(Arrays.asList(existing.resourceAutomaton, patterns))); } })); this.permissions = Collections.unmodifiableList(new ArrayList<>(permissionsByPrivilege.values())); @@ -84,27 +88,73 @@ public final class ApplicationPermission { return getClass().getSimpleName() + "{privileges=" + permissions + "}"; } + public Set getApplicationNames() { + return permissions.stream() + .map(e -> e.privilege.getApplication()) + .collect(Collectors.toSet()); + } + + public Set getPrivileges(String application) { + return permissions.stream() + .filter(e -> application.equals(e.privilege.getApplication())) + .map(e -> e.privilege) + .collect(Collectors.toSet()); + } + + /** + * Returns a set of resource patterns that are permitted for the provided privilege. + * The returned set may include patterns that overlap (e.g. "object/*" and "object/1") and may + * also include patterns that are defined again a more permissive privilege. + * e.g. If a permission grants + *
    + *
  • "my-app", "read", [ "user/*" ]
  • + *
  • "my-app", "all", [ "user/kimchy", "config/*" ]
  • + *
+ * Then getResourcePatterns( myAppRead ) would return "user/*", "user/kimchy", "config/*". + */ + public Set getResourcePatterns(ApplicationPrivilege privilege) { + return permissions.stream() + .filter(e -> e.matchesPrivilege(privilege)) + .map(e -> e.resourceNames) + .flatMap(Set::stream) + .collect(Collectors.toSet()); + } + private static class PermissionEntry { private final ApplicationPrivilege privilege; private final Predicate application; - private final Automaton resources; + private final Set resourceNames; + private final Automaton resourceAutomaton; - private PermissionEntry(ApplicationPrivilege privilege, Automaton resources) { + private PermissionEntry(ApplicationPrivilege privilege, Set resourceNames, Automaton resourceAutomaton) { this.privilege = privilege; this.application = Automatons.predicate(privilege.getApplication()); - this.resources = resources; + this.resourceNames = resourceNames; + this.resourceAutomaton = resourceAutomaton; } private boolean grants(ApplicationPrivilege other, Automaton resource) { - return this.application.test(other.getApplication()) - && Operations.isEmpty(privilege.getAutomaton()) == false - && Operations.subsetOf(other.getAutomaton(), privilege.getAutomaton()) - && Operations.subsetOf(resource, this.resources); + return matchesPrivilege(other) && Operations.subsetOf(resource, this.resourceAutomaton); + } + + private boolean matchesPrivilege(ApplicationPrivilege other) { + if (this.privilege.equals(other)) { + return true; + } + if (this.application.test(other.getApplication()) == false) { + return false; + } + if (Operations.isTotal(privilege.getAutomaton())) { + return true; + } + return Operations.isEmpty(privilege.getAutomaton()) == false + && Operations.isEmpty(other.getAutomaton()) == false + && Operations.subsetOf(other.getAutomaton(), privilege.getAutomaton()); } @Override public String toString() { - return privilege.toString(); + return privilege.toString() + ":" + resourceNames; } } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermission.java index 370fd70b169..3af016959d4 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermission.java @@ -5,11 +5,14 @@ */ package org.elasticsearch.xpack.core.security.authz.permission; +import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege; import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -30,6 +33,8 @@ public abstract class ClusterPermission { public abstract boolean check(String action, TransportRequest request); + public abstract List> privileges(); + /** * A permission that is based solely on cluster privileges and does not consider request state */ @@ -48,28 +53,32 @@ public abstract class ClusterPermission { public boolean check(String action, TransportRequest request) { return predicate.test(action); } + + @Override + public List> privileges() { + return Collections.singletonList(new Tuple<>(super.privilege, null)); + } } /** * A permission that makes use of both cluster privileges and request inspection */ public static class ConditionalClusterPermission extends ClusterPermission { - private final Predicate actionPredicate; - private final Predicate requestPredicate; + private final ConditionalClusterPrivilege conditionalPrivilege; public ConditionalClusterPermission(ConditionalClusterPrivilege conditionalPrivilege) { - this(conditionalPrivilege.getPrivilege(), conditionalPrivilege.getRequestPredicate()); - } - - public ConditionalClusterPermission(ClusterPrivilege privilege, Predicate requestPredicate) { - super(privilege); - this.actionPredicate = privilege.predicate(); - this.requestPredicate = requestPredicate; + super(conditionalPrivilege.getPrivilege()); + this.conditionalPrivilege = conditionalPrivilege; } @Override public boolean check(String action, TransportRequest request) { - return actionPredicate.test(action) && requestPredicate.test(request); + return super.privilege.predicate().test(action) && conditionalPrivilege.getRequestPredicate().test(request); + } + + @Override + public List> privileges() { + return Collections.singletonList(new Tuple<>(super.privilege, conditionalPrivilege)); } } @@ -93,6 +102,11 @@ public abstract class ClusterPermission { return ClusterPrivilege.get(names); } + @Override + public List> privileges() { + return children.stream().map(ClusterPermission::privileges).flatMap(List::stream).collect(Collectors.toList()); + } + @Override public boolean check(String action, TransportRequest request) { return children.stream().anyMatch(p -> p.check(action, request)); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/FieldPermissions.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/FieldPermissions.java index 53d6c328f5d..7e45b893fed 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/FieldPermissions.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/FieldPermissions.java @@ -161,7 +161,7 @@ public final class FieldPermissions implements Accountable { return permittedFieldsAutomatonIsTotal || permittedFieldsAutomaton.run(fieldName); } - FieldPermissionsDefinition getFieldPermissionsDefinition() { + public FieldPermissionsDefinition getFieldPermissionsDefinition() { return fieldPermissionsDefinition; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/FieldPermissionsDefinition.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/FieldPermissionsDefinition.java index ad340c0f239..bcbee9f84b3 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/FieldPermissionsDefinition.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/FieldPermissionsDefinition.java @@ -5,6 +5,8 @@ */ package org.elasticsearch.xpack.core.security.authz.permission; +import org.elasticsearch.common.Strings; + import java.util.Arrays; import java.util.Collections; import java.util.Set; @@ -81,5 +83,13 @@ public final class FieldPermissionsDefinition { result = 31 * result + Arrays.hashCode(excludedFields); return result; } + + @Override + public String toString() { + return getClass().getSimpleName() + + "[grant=" + Strings.arrayToCommaDelimitedString(grantedFields) + + "; exclude=" + Strings.arrayToCommaDelimitedString(excludedFields) + + "]"; + } } } 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 fd7eb486419..8a68e71d0b9 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 @@ -201,7 +201,7 @@ public final class Role { 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, + Sets.newHashSet(arp.getPrivileges()), arp.getPrivileges() ), Sets.newHashSet(arp.getResources())); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/RunAsPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/RunAsPermission.java index a8b2f7bfaef..ef4a43db827 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/RunAsPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/RunAsPermission.java @@ -17,12 +17,18 @@ public final class RunAsPermission { public static final RunAsPermission NONE = new RunAsPermission(Privilege.NONE); + private final Privilege privilege; private final Predicate predicate; RunAsPermission(Privilege privilege) { + this.privilege = privilege; this.predicate = privilege.predicate(); } + public Privilege getPrivilege() { + return privilege; + } + /** * Checks if this permission grants run as to the specified user */ diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConditionalClusterPrivileges.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConditionalClusterPrivileges.java index e204d89b1c0..e5cfd2448aa 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConditionalClusterPrivileges.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConditionalClusterPrivileges.java @@ -10,6 +10,7 @@ import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParseException; @@ -37,6 +38,11 @@ public final class ConditionalClusterPrivileges { public static final ConditionalClusterPrivilege[] EMPTY_ARRAY = new ConditionalClusterPrivilege[0]; + public static final Writeable.Reader READER = + in1 -> in1.readNamedWriteable(ConditionalClusterPrivilege.class); + public static final Writeable.Writer WRITER = + (out1, value) -> out1.writeNamedWriteable(value); + private ConditionalClusterPrivileges() { } @@ -44,15 +50,14 @@ public final class ConditionalClusterPrivileges { * Utility method to read an array of {@link ConditionalClusterPrivilege} objects from a {@link StreamInput} */ public static ConditionalClusterPrivilege[] readArray(StreamInput in) throws IOException { - return in.readArray(in1 -> - in1.readNamedWriteable(ConditionalClusterPrivilege.class), ConditionalClusterPrivilege[]::new); + return in.readArray(READER, ConditionalClusterPrivilege[]::new); } /** * Utility method to write an array of {@link ConditionalClusterPrivilege} objects to a {@link StreamOutput} */ public static void writeArray(StreamOutput out, ConditionalClusterPrivilege[] privileges) throws IOException { - out.writeArray((out1, value) -> out1.writeNamedWriteable(value), privileges); + out.writeArray(WRITER, privileges); } /** 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 d3cc60194f2..ef59f870c68 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 @@ -74,6 +74,10 @@ import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesAction; import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesRequest; import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesRequestBuilder; import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesResponse; +import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesAction; +import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesRequest; +import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesRequestBuilder; +import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse; import org.elasticsearch.xpack.core.security.action.user.PutUserAction; import org.elasticsearch.xpack.core.security.action.user.PutUserRequest; import org.elasticsearch.xpack.core.security.action.user.PutUserRequestBuilder; @@ -173,6 +177,14 @@ public class SecurityClient { client.execute(HasPrivilegesAction.INSTANCE, request, listener); } + public GetUserPrivilegesRequestBuilder prepareGetUserPrivileges(String username) { + return new GetUserPrivilegesRequestBuilder(client).username(username); + } + + public void listUserPrivileges(GetUserPrivilegesRequest request, ActionListener listener) { + client.execute(GetUserPrivilegesAction.INSTANCE, request, listener); + } + /** * User Management */ diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/GetUserPrivilegesRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/GetUserPrivilegesRequestTests.java new file mode 100644 index 00000000000..4521b35c7dc --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/GetUserPrivilegesRequestTests.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.user; + +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.io.stream.ByteBufferStreamInput; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.NamedWriteableAwareStreamInput; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.XPackClientPlugin; + +import java.io.IOException; + +import static org.hamcrest.Matchers.equalTo; + +public class GetUserPrivilegesRequestTests extends ESTestCase { + + public void testSerialization() throws IOException { + final String user = randomAlphaOfLengthBetween(3, 12); + + final GetUserPrivilegesRequest original = new GetUserPrivilegesRequest(); + original.username(user); + + final BytesStreamOutput out = new BytesStreamOutput(); + original.writeTo(out); + + final NamedWriteableRegistry registry = new NamedWriteableRegistry(new XPackClientPlugin(Settings.EMPTY).getNamedWriteables()); + StreamInput in = new NamedWriteableAwareStreamInput(ByteBufferStreamInput.wrap(BytesReference.toBytes(out.bytes())), registry); + final GetUserPrivilegesRequest copy = new GetUserPrivilegesRequest(in); + + assertThat(copy.username(), equalTo(original.username())); + assertThat(copy.usernames(), equalTo(original.usernames())); + } + +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/GetUserPrivilegesResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/GetUserPrivilegesResponseTests.java new file mode 100644 index 00000000000..4ce017c45b4 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/GetUserPrivilegesResponseTests.java @@ -0,0 +1,170 @@ +/* + * 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.common.bytes.BytesArray; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.io.stream.ByteBufferStreamInput; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.NamedWriteableAwareStreamInput; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.CollectionUtils; +import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.EqualsHashCodeTestUtils; +import org.elasticsearch.xpack.core.XPackClientPlugin; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.ApplicationResourcePrivileges; +import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsDefinition.FieldGrantExcludeGroup; +import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivileges.ManageApplicationPrivileges; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import static java.util.Collections.emptySet; +import static org.hamcrest.Matchers.equalTo; + +public class GetUserPrivilegesResponseTests extends ESTestCase { + + public void testSerialization() throws IOException { + final GetUserPrivilegesResponse original = randomResponse(); + + final BytesStreamOutput out = new BytesStreamOutput(); + original.writeTo(out); + + final GetUserPrivilegesResponse copy = new GetUserPrivilegesResponse(); + final NamedWriteableRegistry registry = new NamedWriteableRegistry(new XPackClientPlugin(Settings.EMPTY).getNamedWriteables()); + StreamInput in = new NamedWriteableAwareStreamInput(ByteBufferStreamInput.wrap(BytesReference.toBytes(out.bytes())), registry); + copy.readFrom(in); + + assertThat(copy.getClusterPrivileges(), equalTo(original.getClusterPrivileges())); + assertThat(copy.getConditionalClusterPrivileges(), equalTo(original.getConditionalClusterPrivileges())); + assertThat(sorted(copy.getIndexPrivileges()), equalTo(sorted(original.getIndexPrivileges()))); + assertThat(copy.getApplicationPrivileges(), equalTo(original.getApplicationPrivileges())); + assertThat(copy.getRunAs(), equalTo(original.getRunAs())); + } + + public void testEqualsAndHashCode() throws IOException { + final GetUserPrivilegesResponse response = randomResponse(); + final EqualsHashCodeTestUtils.CopyFunction copy = original -> new GetUserPrivilegesResponse( + original.getClusterPrivileges(), + original.getConditionalClusterPrivileges(), + original.getIndexPrivileges(), + original.getApplicationPrivileges(), + original.getRunAs() + ); + final EqualsHashCodeTestUtils.MutateFunction mutate = + new EqualsHashCodeTestUtils.MutateFunction() { + @Override + public GetUserPrivilegesResponse mutate(GetUserPrivilegesResponse original) { + final int random = randomIntBetween(1, 0b11111); + final Set cluster = maybeMutate(random, 1, original.getClusterPrivileges(), () -> randomAlphaOfLength(5)); + final Set conditionalCluster = maybeMutate(random, 2, + original.getConditionalClusterPrivileges(), () -> new ManageApplicationPrivileges(randomStringSet(3))); + final Set index = maybeMutate(random, 3, original.getIndexPrivileges(), + () -> new GetUserPrivilegesResponse.Indices(randomStringSet(1), randomStringSet(1), emptySet(), emptySet())); + final Set application = maybeMutate(random, 4, original.getApplicationPrivileges(), + () -> ApplicationResourcePrivileges.builder().resources(generateRandomStringArray(3, 3, false, false)) + .application(randomAlphaOfLength(5)).privileges(generateRandomStringArray(3, 5, false, false)).build()); + final Set runAs = maybeMutate(random, 5, original.getRunAs(), () -> randomAlphaOfLength(8)); + return new GetUserPrivilegesResponse(cluster, conditionalCluster, index, application, runAs); + } + + private Set maybeMutate(int random, int index, Set original, Supplier supplier) { + if ((random & (1 << index)) == 0) { + return original; + } + if (original.isEmpty()) { + return Collections.singleton(supplier.get()); + } else { + return emptySet(); + } + } + }; + EqualsHashCodeTestUtils.checkEqualsAndHashCode(response, copy, mutate); + } + + private GetUserPrivilegesResponse randomResponse() { + final Set cluster = randomStringSet(5); + final Set conditionalCluster = Sets.newHashSet(randomArray(3, ConditionalClusterPrivilege[]::new, + () -> new ManageApplicationPrivileges( + randomStringSet(3) + ))); + final Set index = Sets.newHashSet(randomArray(5, GetUserPrivilegesResponse.Indices[]::new, + () -> new GetUserPrivilegesResponse.Indices(randomStringSet(6), randomStringSet(8), + Sets.newHashSet(randomArray(3, FieldGrantExcludeGroup[]::new, () -> new FieldGrantExcludeGroup( + generateRandomStringArray(3, 5, false, false), generateRandomStringArray(3, 5, false, false)))), + randomStringSet(3).stream().map(BytesArray::new).collect(Collectors.toSet()) + )) + ); + final Set application = Sets.newHashSet(randomArray(5, ApplicationResourcePrivileges[]::new, + () -> ApplicationResourcePrivileges.builder().resources(generateRandomStringArray(3, 3, false, false)) + .application(randomAlphaOfLength(5)).privileges(generateRandomStringArray(3, 5, false, false)).build() + )); + final Set runAs = randomStringSet(3); + return new GetUserPrivilegesResponse(cluster, conditionalCluster, index, application, runAs); + } + + private List sorted(Collection indices) { + final ArrayList list = CollectionUtils.iterableAsArrayList(indices); + Collections.sort(list, (a, b) -> { + int cmp = compareCollection(a.getIndices(), b.getIndices(), String::compareTo); + if (cmp != 0) { + return cmp; + } + cmp = compareCollection(a.getPrivileges(), b.getPrivileges(), String::compareTo); + if (cmp != 0) { + return cmp; + } + cmp = compareCollection(a.getQueries(), b.getQueries(), BytesReference::compareTo); + if (cmp != 0) { + return cmp; + } + cmp = compareCollection(a.getFieldSecurity(), b.getFieldSecurity(), (f1, f2) -> { + int c = compareCollection(Arrays.asList(f1.getGrantedFields()), Arrays.asList(f2.getGrantedFields()), String::compareTo); + if (c == 0) { + c = compareCollection(Arrays.asList(f1.getExcludedFields()), Arrays.asList(f2.getExcludedFields()), String::compareTo); + } + return c; + }); + return cmp; + }); + return list; + } + + private int compareCollection(Collection a, Collection b, Comparator comparator) { + int cmp = Integer.compare(a.size(), b.size()); + if (cmp != 0) { + return cmp; + } + Iterator i1 = a.iterator(); + Iterator i2 = b.iterator(); + while (i1.hasNext()) { + cmp = comparator.compare(i1.next(), i2.next()); + if (cmp != 0) { + return cmp; + } + } + return cmp; + } + + private HashSet randomStringSet(int maxSize) { + return Sets.newHashSet(generateRandomStringArray(maxSize, randomIntBetween(3, 6), false, false)); + } +} 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 47a189b41f1..992ca8db1b0 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 @@ -15,8 +15,12 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; import static java.util.Collections.singletonList; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; public class ApplicationPermissionTests extends ESTestCase { @@ -101,7 +105,7 @@ public class ApplicationPermissionTests extends ESTestCase { } public void testMergedPermissionChecking() { - final ApplicationPrivilege app1ReadWrite = ApplicationPrivilege.get("app1", Sets.union(app1Read.name(), app1Write.name()), store); + final ApplicationPrivilege app1ReadWrite = compositePrivilege("app1", app1Read, app1Write); final ApplicationPermission hasPermission = buildPermission(app1ReadWrite, "allow/*"); assertThat(hasPermission.grants(app1Read, "allow/1"), equalTo(true)); @@ -114,10 +118,35 @@ public class ApplicationPermissionTests extends ESTestCase { assertThat(hasPermission.grants(app2Read, "allow/1"), equalTo(false)); } + public void testInspectPermissionContents() { + final ApplicationPrivilege app1ReadWrite = compositePrivilege("app1", app1Read, app1Write); + ApplicationPermission perm = new ApplicationPermission(Arrays.asList( + new Tuple<>(app1Read, Sets.newHashSet("obj/1", "obj/2")), + new Tuple<>(app1Write, Sets.newHashSet("obj/3", "obj/4")), + new Tuple<>(app1ReadWrite, Sets.newHashSet("obj/5")), + new Tuple<>(app1All, Sets.newHashSet("obj/6", "obj/7")), + new Tuple<>(app2Read, Sets.newHashSet("obj/1", "obj/8")) + )); + assertThat(perm.getApplicationNames(), containsInAnyOrder("app1", "app2")); + assertThat(perm.getPrivileges("app1"), containsInAnyOrder(app1Read, app1Write, app1ReadWrite, app1All)); + assertThat(perm.getPrivileges("app2"), containsInAnyOrder(app2Read)); + assertThat(perm.getResourcePatterns(app1Read), containsInAnyOrder("obj/1", "obj/2", "obj/5", "obj/6", "obj/7")); + assertThat(perm.getResourcePatterns(app1Write), containsInAnyOrder("obj/3", "obj/4", "obj/5", "obj/6", "obj/7")); + assertThat(perm.getResourcePatterns(app1ReadWrite), containsInAnyOrder("obj/5", "obj/6", "obj/7")); + assertThat(perm.getResourcePatterns(app1All), containsInAnyOrder("obj/6", "obj/7")); + assertThat(perm.getResourcePatterns(app2Read), containsInAnyOrder("obj/1", "obj/8")); + } + private ApplicationPrivilege actionPrivilege(String appName, String... actions) { return ApplicationPrivilege.get(appName, Sets.newHashSet(actions), Collections.emptyList()); } + private ApplicationPrivilege compositePrivilege(String application, ApplicationPrivilege... children) { + Set names = Stream.of(children).map(ApplicationPrivilege::name).flatMap(Set::stream).collect(Collectors.toSet()); + return ApplicationPrivilege.get(application, names, store); + } + + private ApplicationPermission buildPermission(ApplicationPrivilege privilege, String... resources) { return new ApplicationPermission(singletonList(new Tuple<>(privilege, Sets.newHashSet(resources)))); } 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 eca4fdc0c2a..995be17cbd5 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 @@ -107,6 +107,7 @@ import org.elasticsearch.xpack.core.security.action.user.ChangePasswordAction; import org.elasticsearch.xpack.core.security.action.user.DeleteUserAction; import org.elasticsearch.xpack.core.security.action.user.GetUsersAction; import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesAction; +import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesAction; import org.elasticsearch.xpack.core.security.action.user.PutUserAction; import org.elasticsearch.xpack.core.security.action.user.SetEnabledAction; import org.elasticsearch.xpack.core.security.authc.AuthenticationFailureHandler; @@ -162,6 +163,7 @@ import org.elasticsearch.xpack.security.action.user.TransportChangePasswordActio import org.elasticsearch.xpack.security.action.user.TransportDeleteUserAction; import org.elasticsearch.xpack.security.action.user.TransportGetUsersAction; import org.elasticsearch.xpack.security.action.user.TransportHasPrivilegesAction; +import org.elasticsearch.xpack.security.action.user.TransportGetUserPrivilegesAction; import org.elasticsearch.xpack.security.action.user.TransportPutUserAction; import org.elasticsearch.xpack.security.action.user.TransportSetEnabledAction; import org.elasticsearch.xpack.security.audit.AuditTrail; @@ -206,6 +208,7 @@ import org.elasticsearch.xpack.security.rest.action.saml.RestSamlLogoutAction; import org.elasticsearch.xpack.security.rest.action.saml.RestSamlPrepareAuthenticationAction; import org.elasticsearch.xpack.security.rest.action.user.RestChangePasswordAction; import org.elasticsearch.xpack.security.rest.action.user.RestDeleteUserAction; +import org.elasticsearch.xpack.security.rest.action.user.RestGetUserPrivilegesAction; import org.elasticsearch.xpack.security.rest.action.user.RestGetUsersAction; import org.elasticsearch.xpack.security.rest.action.user.RestHasPrivilegesAction; import org.elasticsearch.xpack.security.rest.action.user.RestPutUserAction; @@ -704,6 +707,7 @@ public class Security extends Plugin implements ActionPlugin, IngestPlugin, Netw new ActionHandler<>(AuthenticateAction.INSTANCE, TransportAuthenticateAction.class), new ActionHandler<>(SetEnabledAction.INSTANCE, TransportSetEnabledAction.class), new ActionHandler<>(HasPrivilegesAction.INSTANCE, TransportHasPrivilegesAction.class), + new ActionHandler<>(GetUserPrivilegesAction.INSTANCE, TransportGetUserPrivilegesAction.class), new ActionHandler<>(GetRoleMappingsAction.INSTANCE, TransportGetRoleMappingsAction.class), new ActionHandler<>(PutRoleMappingAction.INSTANCE, TransportPutRoleMappingAction.class), new ActionHandler<>(DeleteRoleMappingAction.INSTANCE, TransportDeleteRoleMappingAction.class), @@ -753,6 +757,7 @@ public class Security extends Plugin implements ActionPlugin, IngestPlugin, Netw new RestChangePasswordAction(settings, restController, securityContext.get(), getLicenseState()), new RestSetEnabledAction(settings, restController, getLicenseState()), new RestHasPrivilegesAction(settings, restController, securityContext.get(), getLicenseState()), + new RestGetUserPrivilegesAction(settings, restController, securityContext.get(), getLicenseState()), new RestGetRoleMappingsAction(settings, restController, getLicenseState()), new RestPutRoleMappingAction(settings, restController, getLicenseState()), new RestDeleteRoleMappingAction(settings, restController, getLicenseState()), diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportGetUserPrivilegesAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportGetUserPrivilegesAction.java new file mode 100644 index 00000000000..7a86842eb52 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportGetUserPrivilegesAction.java @@ -0,0 +1,133 @@ +/* + * 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.user; + +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.apache.lucene.util.automaton.Operations; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesAction; +import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesRequest; +import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsDefinition; +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.ConditionalClusterPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.Privilege; +import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.security.authz.AuthorizationService; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.TreeSet; + +import static org.elasticsearch.common.Strings.arrayToCommaDelimitedString; + +/** + * Transport action for {@link GetUserPrivilegesAction} + */ +public class TransportGetUserPrivilegesAction extends HandledTransportAction { + + private final ThreadPool threadPool; + private final AuthorizationService authorizationService; + + @Inject + public TransportGetUserPrivilegesAction(Settings settings, ThreadPool threadPool, TransportService transportService, + ActionFilters actionFilters, AuthorizationService authorizationService) { + super(settings, GetUserPrivilegesAction.NAME, transportService, actionFilters, GetUserPrivilegesRequest::new); + this.threadPool = threadPool; + this.authorizationService = authorizationService; + } + + @Override + protected void doExecute(Task task, GetUserPrivilegesRequest request, ActionListener listener) { + final String username = request.username(); + + final User user = Authentication.getAuthentication(threadPool.getThreadContext()).getUser(); + if (user.principal().equals(username) == false) { + listener.onFailure(new IllegalArgumentException("users may only list the privileges of their own account")); + return; + } + + authorizationService.roles(user, ActionListener.wrap( + role -> listener.onResponse(buildResponseObject(role)), + listener::onFailure)); + } + + // package protected for testing + GetUserPrivilegesResponse buildResponseObject(Role userRole) { + logger.trace(() -> new ParameterizedMessage("List privileges for role [{}]", arrayToCommaDelimitedString(userRole.names()))); + + // We use sorted sets for Strings because they will typically be small, and having a predictable order allows for simpler testing + final Set cluster = new TreeSet<>(); + // But we don't have a meaningful ordering for objects like ConditionalClusterPrivilege, so the tests work with "random" ordering + final Set conditionalCluster = new HashSet<>(); + for (Tuple tup : userRole.cluster().privileges()) { + if (tup.v2() == null) { + if (ClusterPrivilege.NONE.equals(tup.v1()) == false) { + cluster.addAll(tup.v1().name()); + } + } else { + conditionalCluster.add(tup.v2()); + } + } + + final Set indices = new LinkedHashSet<>(); + for (IndicesPermission.Group group : userRole.indices()) { + final Set queries = group.getQuery() == null ? Collections.emptySet() : group.getQuery(); + final Set fieldSecurity = group.getFieldPermissions().hasFieldLevelSecurity() + ? group.getFieldPermissions().getFieldPermissionsDefinition().getFieldGrantExcludeGroups() : Collections.emptySet(); + indices.add(new GetUserPrivilegesResponse.Indices( + Arrays.asList(group.indices()), + group.privilege().name(), + fieldSecurity, + queries + )); + } + + final Set application = new LinkedHashSet<>(); + for (String applicationName : userRole.application().getApplicationNames()) { + for (ApplicationPrivilege privilege : userRole.application().getPrivileges(applicationName)) { + final Set resources = userRole.application().getResourcePatterns(privilege); + if (resources.isEmpty()) { + logger.trace("No resources defined in application privilege {}", privilege); + } else { + application.add(RoleDescriptor.ApplicationResourcePrivileges.builder() + .application(applicationName) + .privileges(privilege.name()) + .resources(resources) + .build()); + } + } + } + + final Privilege runAsPrivilege = userRole.runAs().getPrivilege(); + final Set runAs; + if (Operations.isEmpty(runAsPrivilege.getAutomaton())) { + runAs = Collections.emptySet(); + } else { + runAs = runAsPrivilege.name(); + } + + return new GetUserPrivilegesResponse(cluster, conditionalCluster, indices, application, runAs); + } + +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java index f9fe2b7eaa7..8db69ff4e47 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java @@ -41,6 +41,7 @@ import org.elasticsearch.transport.TransportActionProxy; import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.xpack.core.security.action.user.AuthenticateAction; import org.elasticsearch.xpack.core.security.action.user.ChangePasswordAction; +import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesAction; import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesAction; import org.elasticsearch.xpack.core.security.action.user.UserRequest; import org.elasticsearch.xpack.core.security.authc.Authentication; @@ -86,7 +87,7 @@ public class AuthorizationService extends AbstractComponent { private static final Predicate MONITOR_INDEX_PREDICATE = IndexPrivilege.MONITOR.predicate(); private static final Predicate SAME_USER_PRIVILEGE = Automatons.predicate( - ChangePasswordAction.NAME, AuthenticateAction.NAME, HasPrivilegesAction.NAME); + ChangePasswordAction.NAME, AuthenticateAction.NAME, HasPrivilegesAction.NAME, GetUserPrivilegesAction.NAME); private static final String INDEX_SUB_REQUEST_PRIMARY = IndexAction.NAME + "[p]"; private static final String INDEX_SUB_REQUEST_REPLICA = IndexAction.NAME + "[r]"; @@ -522,7 +523,8 @@ public class AuthorizationService extends AbstractComponent { return checkChangePasswordAction(authentication); } - assert AuthenticateAction.NAME.equals(action) || HasPrivilegesAction.NAME.equals(action) || sameUsername == false + assert AuthenticateAction.NAME.equals(action) || HasPrivilegesAction.NAME.equals(action) + || GetUserPrivilegesAction.NAME.equals(action) || sameUsername == false : "Action '" + action + "' should not be possible when sameUsername=" + sameUsername; return sameUsername; } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestGetUserPrivilegesAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestGetUserPrivilegesAction.java new file mode 100644 index 00000000000..75c790d861b --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestGetUserPrivilegesAction.java @@ -0,0 +1,85 @@ +/* + * 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.user; + +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.rest.BytesRestResponse; +import org.elasticsearch.rest.RestChannel; +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.SecurityContext; +import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesRequestBuilder; +import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivileges; +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 static org.elasticsearch.rest.RestRequest.Method.GET; + +/** + * REST handler that list the privileges held by a user. + */ +public class RestGetUserPrivilegesAction extends SecurityBaseRestHandler { + + private final SecurityContext securityContext; + + public RestGetUserPrivilegesAction(Settings settings, RestController controller, SecurityContext securityContext, + XPackLicenseState licenseState) { + super(settings, licenseState); + this.securityContext = securityContext; + controller.registerHandler(GET, "/_xpack/security/user/_privileges", this); + } + + @Override + public String getName() { + return "xpack_security_user_privileges_action"; + } + + @Override + public RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException { + final String username = securityContext.getUser().principal(); + final GetUserPrivilegesRequestBuilder requestBuilder = new SecurityClient(client).prepareGetUserPrivileges(username); + return channel -> requestBuilder.execute(new RestListener(channel)); + } + + // Package protected for testing + static class RestListener extends RestBuilderListener { + RestListener(RestChannel channel) { + super(channel); + } + + @Override + public RestResponse buildResponse(GetUserPrivilegesResponse response, XContentBuilder builder) throws Exception { + builder.startObject(); + + builder.field(RoleDescriptor.Fields.CLUSTER.getPreferredName(), response.getClusterPrivileges()); + builder.startArray(RoleDescriptor.Fields.GLOBAL.getPreferredName()); + for (ConditionalClusterPrivilege ccp : response.getConditionalClusterPrivileges()) { + ConditionalClusterPrivileges.toXContent(builder, ToXContent.EMPTY_PARAMS, Collections.singleton(ccp)); + } + builder.endArray(); + + builder.field(RoleDescriptor.Fields.INDICES.getPreferredName(), response.getIndexPrivileges()); + builder.field(RoleDescriptor.Fields.APPLICATIONS.getPreferredName(), response.getApplicationPrivileges()); + builder.field(RoleDescriptor.Fields.RUN_AS.getPreferredName(), response.getRunAs()); + + builder.endObject(); + return new BytesRestResponse(RestStatus.OK, builder); + } + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportGetUserPrivilegesActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportGetUserPrivilegesActionTests.java new file mode 100644 index 00000000000..81616341a88 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportGetUserPrivilegesActionTests.java @@ -0,0 +1,87 @@ +/* + * 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.user; + +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions; +import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsDefinition; +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.ConditionalClusterPrivileges.ManageApplicationPrivileges; +import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.Privilege; +import org.elasticsearch.xpack.security.authz.AuthorizationService; + +import java.util.Collections; +import java.util.Set; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.emptyIterable; +import static org.hamcrest.Matchers.iterableWithSize; +import static org.mockito.Mockito.mock; + +public class TransportGetUserPrivilegesActionTests extends ESTestCase { + + public void testBuildResponseObject() { + final ManageApplicationPrivileges manageApplicationPrivileges = new ManageApplicationPrivileges(Sets.newHashSet("app01", "app02")); + final BytesArray query = new BytesArray("{\"term\":{\"public\":true}}"); + final Role role = Role.builder("test", "role") + .cluster(Sets.newHashSet("monitor", "manage_watcher"), Collections.singleton(manageApplicationPrivileges)) + .add(IndexPrivilege.get(Sets.newHashSet("read", "write")), "index-1") + .add(IndexPrivilege.ALL, "index-2", "index-3") + .add( + new FieldPermissions(new FieldPermissionsDefinition(new String[]{ "public.*" }, new String[0])), + Collections.singleton(query), + IndexPrivilege.READ, "index-4", "index-5") + .addApplicationPrivilege(new ApplicationPrivilege("app01", "read", "data:read"), Collections.singleton("*")) + .runAs(new Privilege(Sets.newHashSet("user01", "user02"), "user01", "user02")) + .build(); + + final TransportGetUserPrivilegesAction action = new TransportGetUserPrivilegesAction(Settings.EMPTY, + mock(ThreadPool.class), mock(TransportService.class), mock(ActionFilters.class), mock(AuthorizationService.class)); + final GetUserPrivilegesResponse response = action.buildResponseObject(role); + + assertThat(response.getClusterPrivileges(), containsInAnyOrder("monitor", "manage_watcher")); + assertThat(response.getConditionalClusterPrivileges(), containsInAnyOrder(manageApplicationPrivileges)); + + assertThat(response.getIndexPrivileges(), iterableWithSize(3)); + final GetUserPrivilegesResponse.Indices index1 = findIndexPrivilege(response.getIndexPrivileges(), "index-1"); + assertThat(index1.getIndices(), containsInAnyOrder("index-1")); + assertThat(index1.getPrivileges(), containsInAnyOrder("read", "write")); + assertThat(index1.getFieldSecurity(), emptyIterable()); + assertThat(index1.getQueries(), emptyIterable()); + final GetUserPrivilegesResponse.Indices index2 = findIndexPrivilege(response.getIndexPrivileges(), "index-2"); + assertThat(index2.getIndices(), containsInAnyOrder("index-2", "index-3")); + assertThat(index2.getPrivileges(), containsInAnyOrder("all")); + assertThat(index2.getFieldSecurity(), emptyIterable()); + assertThat(index2.getQueries(), emptyIterable()); + final GetUserPrivilegesResponse.Indices index4 = findIndexPrivilege(response.getIndexPrivileges(), "index-4"); + assertThat(index4.getIndices(), containsInAnyOrder("index-4", "index-5")); + assertThat(index4.getPrivileges(), containsInAnyOrder("read")); + assertThat(index4.getFieldSecurity(), containsInAnyOrder( + new FieldPermissionsDefinition.FieldGrantExcludeGroup(new String[]{ "public.*" }, new String[0]))); + assertThat(index4.getQueries(), containsInAnyOrder(query)); + + assertThat(response.getApplicationPrivileges(), containsInAnyOrder( + RoleDescriptor.ApplicationResourcePrivileges.builder().application("app01").privileges("read").resources("*").build()) + ); + + assertThat(response.getRunAs(), containsInAnyOrder("user01", "user02")); + } + + private GetUserPrivilegesResponse.Indices findIndexPrivilege(Set indices, String name) { + return indices.stream().filter(i -> i.getIndices().contains(name)).findFirst().get(); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/user/RestGetUserPrivilegesActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/user/RestGetUserPrivilegesActionTests.java new file mode 100644 index 00000000000..ed75cec3243 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/user/RestGetUserPrivilegesActionTests.java @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.security.rest.action.user; + +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.ApplicationResourcePrivileges; +import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsDefinition; +import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivileges; +import org.hamcrest.Matchers; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; + +public class RestGetUserPrivilegesActionTests extends ESTestCase { + + public void testBuildResponse() throws Exception { + final RestGetUserPrivilegesAction.RestListener listener = new RestGetUserPrivilegesAction.RestListener(null); + + + final Set cluster = new LinkedHashSet<>(Arrays.asList("monitor", "manage_ml", "manage_watcher")); + final Set conditionalCluster = Collections.singleton( + new ConditionalClusterPrivileges.ManageApplicationPrivileges(new LinkedHashSet<>(Arrays.asList("app01", "app02")))); + final Set index = new LinkedHashSet<>(Arrays.asList( + new GetUserPrivilegesResponse.Indices(Arrays.asList("index-1", "index-2", "index-3-*"), Arrays.asList("read", "write"), + new LinkedHashSet<>(Arrays.asList( + new FieldPermissionsDefinition.FieldGrantExcludeGroup(new String[]{ "public.*" }, new String[0]), + new FieldPermissionsDefinition.FieldGrantExcludeGroup(new String[]{ "*" }, new String[]{ "private.*" }) + )), + new LinkedHashSet<>(Arrays.asList( + new BytesArray("{ \"term\": { \"access\": \"public\" } }"), + new BytesArray("{ \"term\": { \"access\": \"standard\" } }") + )) + ), + new GetUserPrivilegesResponse.Indices(Arrays.asList("index-4"), Collections.singleton("all"), + Collections.emptySet(), Collections.emptySet() + ) + )); + final Set application = Sets.newHashSet( + ApplicationResourcePrivileges.builder().application("app01").privileges("read", "write").resources("*").build(), + ApplicationResourcePrivileges.builder().application("app01").privileges("admin").resources("department/1").build(), + ApplicationResourcePrivileges.builder().application("app02").privileges("all").resources("tenant/42", "tenant/99").build() + ); + final Set runAs = new LinkedHashSet<>(Arrays.asList("app-user-*", "backup-user")); + final GetUserPrivilegesResponse response = new GetUserPrivilegesResponse(cluster, conditionalCluster, index, application, runAs); + XContentBuilder builder = jsonBuilder(); + listener.buildResponse(response, builder); + + String json = Strings.toString(builder); + assertThat(json, Matchers.equalTo("{" + + "\"cluster\":[\"monitor\",\"manage_ml\",\"manage_watcher\"]," + + "\"global\":[" + + "{\"application\":{\"manage\":{\"applications\":[\"app01\",\"app02\"]}}}" + + "]," + + "\"indices\":[" + + "{\"names\":[\"index-1\",\"index-2\",\"index-3-*\"]," + + "\"privileges\":[\"read\",\"write\"]," + + "\"field_security\":[" + + "{\"grant\":[\"public.*\"]}," + + "{\"grant\":[\"*\"],\"except\":[\"private.*\"]}" + + "]," + + "\"query\":[" + + "\"{ \\\"term\\\": { \\\"access\\\": \\\"public\\\" } }\"," + + "\"{ \\\"term\\\": { \\\"access\\\": \\\"standard\\\" } }\"" + + "]}," + + "{\"names\":[\"index-4\"],\"privileges\":[\"all\"]}" + + "]," + + "\"applications\":[" + + "{\"application\":\"app01\",\"privileges\":[\"read\",\"write\"],\"resources\":[\"*\"]}," + + "{\"application\":\"app01\",\"privileges\":[\"admin\"],\"resources\":[\"department/1\"]}," + + "{\"application\":\"app02\",\"privileges\":[\"all\"],\"resources\":[\"tenant/42\",\"tenant/99\"]}" + + "]," + + "\"run_as\":[\"app-user-*\",\"backup-user\"]" + + "}" + )); + } + +} diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.get_user_privileges.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.get_user_privileges.json new file mode 100644 index 00000000000..56b9609c264 --- /dev/null +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.get_user_privileges.json @@ -0,0 +1,13 @@ +{ + "xpack.security.get_user_privileges": { + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-get-user-privileges.html", + "methods": [ "GET" ], + "url": { + "path": "/_xpack/security/user/_privileges", + "paths": [ "/_xpack/security/user/_privileges" ], + "parts": { }, + "params": {} + }, + "body": null + } +} 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 index cbf08e94d59..912a27884bf 100644 --- 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 @@ -109,6 +109,16 @@ teardown: "application" : "app02", "resources" : [ "thing/1" ], "privileges" : [ "data:write/thing" ] + }, + { + "application" : "app01", + "resources" : [ "foo" ], + "privileges" : [ "dne", "data:dne" ] + }, + { + "application" : "app-dne", + "resources" : [ "bar" ], + "privileges" : [ "anything", "action:anything" ] } ] } @@ -120,12 +130,22 @@ teardown: "*" : { "action:login" : true, "data:read/secrets" : true + }, + "foo" : { + "dne" : true, + "data:dne" : true } }, "app02" : { "thing/1" : { "data:write/thing" : true } + }, + "app-dne" : { + "bar" : { + "anything" : true, + "action:anything" : true + } } - } } - + } + } diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/40_get_user_privs.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/40_get_user_privs.yml new file mode 100644 index 00000000000..eccd37565c7 --- /dev/null +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/40_get_user_privs.yml @@ -0,0 +1,304 @@ +--- +setup: + - skip: + features: headers + + - do: + cluster.health: + wait_for_status: yellow + + # Create some privileges + - do: + xpack.security.put_privileges: + body: > + { + "test-app": { + "user": { + "actions": [ "action:login", "version:1.0.*" ] + }, + "read": { + "actions": [ "data:read/*" ] + }, + "write": { + "actions": [ "data:write/*" ] + } + } + } + + # Store 2 test roles + - do: + xpack.security.put_role: + name: "test-role-1" + body: > + { + "cluster": [ "monitor" ], + "global": { + "application": { + "manage": { + "applications": [ "test-*" ] + } + } + }, + "indices": [ + { + "names": ["test-1-*"], + "privileges": ["read" ] + }, + { + "names": ["test-2-*"], + "privileges": ["read"], + "field_security": { + "grant" : ["*"], + "except" : [ "secret-*", "private-*" ] + }, + "query" : { "term" : { "test": true } } + }, + { + "names": ["test-3-*", "test-4-*", "test-5-*" ], + "privileges": ["read"], + "field_security": { + "grant" : ["test-*"] + } + }, + { + "names": ["test-6-*", "test-7-*" ], + "privileges": ["read"], + "query" : { "term" : { "test": true } } + } + ], + "applications": [ + { + "application": "test-app", + "privileges": ["user"], + "resources": ["*"] + }, + { + "application": "test-app", + "privileges": ["read"], + "resources": ["object/1"] + } + ], + "run_as": [ "test-*" ] + } + + - do: + xpack.security.put_role: + name: "test-role-2" + body: > + { + "cluster": [ "manage", "manage_security" ], + "global": { + "application": { + "manage": { + "applications": [ "apps-*" ] + } + } + }, + "indices": [ + { + "names": [ "test-1-*", "test-9-*" ], + "privileges": ["all" ] + }, + { + "names": ["test-2-*"], + "privileges": ["read"], + "field_security": { + "grant" : ["apps-*"] + }, + "query" : { "term" : { "apps": true } } + }, + { + "names": ["test-3-*", "test-6-*" ], + "privileges": ["read", "write" ] + }, + { + "names": ["test-4-*"], + "privileges": ["read" ], + "field_security": { + "grant" : ["*"], + "except" : [ "private-*" ] + } + } + ], + "applications": [ + { + "application": "app-dne", + "privileges": ["all"], + "resources": ["*"] + }, + { + "application": "test-app", + "privileges": ["dne"], + "resources": ["*"] + }, + { + "application": "test-app", + "privileges": ["read"], + "resources": ["object/2"] + } + ], + "run_as": [ "app-*" ] + } + + # And a user for each role combination + - do: + xpack.security.put_user: + username: "test-1" + body: > + { + "password": "12345678", + "roles" : [ "test-role-1" ] + } + - do: + xpack.security.put_user: + username: "test-2" + body: > + { + "password": "12345678", + "roles" : [ "test-role-2" ] + } + - do: + xpack.security.put_user: + username: "test-3" + body: > + { + "password": "12345678", + "roles" : [ "test-role-1", "test-role-2" ] + } + +--- +teardown: + - do: + xpack.security.delete_privileges: + application: test-app + name: "user,read,write" + ignore: 404 + + - do: + xpack.security.delete_user: + username: "test-1" + ignore: 404 + + - do: + xpack.security.delete_user: + username: "test-2" + ignore: 404 + + - do: + xpack.security.delete_user: + username: "test-3" + ignore: 404 + + - do: + xpack.security.delete_role: + name: "test-role-1" + ignore: 404 + + - do: + xpack.security.delete_role: + name: "test-role-2" + ignore: 404 + +--- + +"Test get_user_privileges for single role": + - do: + headers: { Authorization: "Basic dGVzdC0xOjEyMzQ1Njc4" } # test-1 + xpack.security.get_user_privileges: {} + + - match: { "cluster" : [ "monitor" ] } + + - length: { "global" : 1 } + - match: { "global.0.application.manage.applications" : [ "test-*" ]} + + - length: { "indices" : 4 } + - contains: { "indices" : { "names" : [ "test-1-*" ], "privileges" : [ "read" ] } } + - contains: { "indices" : { "names" : [ "test-2-*" ], "privileges" : [ "read" ], + "field_security" : [ { "grant" : [ "*" ], "except" : [ "secret-*", "private-*" ] } ], + "query" : [ "{\"term\":{\"test\":true}}" ] } + } + - contains: { "indices" : { "names" : [ "test-3-*" , "test-4-*", "test-5-*" ], "privileges" : ["read"], + "field_security" : [ { "grant" : [ "test-*" ] } ] } + } + - contains: { "indices" : { "names" : [ "test-6-*" , "test-7-*" ], "privileges" : ["read"], + "query" : [ "{\"term\":{\"test\":true}}" ] } + } + + - length: { "applications" : 2 } + - contains: { "applications" : { "application" : "test-app", "privileges" : [ "user" ], "resources" : [ "*" ] } } + - contains: { "applications" : { "application" : "test-app", "privileges" : [ "read" ], "resources" : [ "object/1" ] } } + + - match: { "run_as" : [ "test-*" ] } + + - do: + headers: { Authorization: "Basic dGVzdC0yOjEyMzQ1Njc4" } # test-2 + xpack.security.get_user_privileges: + username: null + + - match: { "cluster" : [ "manage", "manage_security" ] } + + - length: { "global" : 1 } + - match: { "global.0.application.manage.applications" : [ "apps-*" ]} + + - length: { "indices" : 4 } + - contains: { "indices" : { "names" : [ "test-1-*", "test-9-*" ], "privileges" : [ "all" ] } } + - contains: { "indices" : { "names" : [ "test-2-*" ], "privileges" : [ "read" ], + "field_security" : [ { "grant" : [ "apps-*" ] } ], + "query" : [ "{\"term\":{\"apps\":true}}" ] } + } + - contains: { "indices" : { "names" : [ "test-3-*", "test-6-*" ], "privileges" : ["read","write"] } } + - contains: { "indices" : { "names" : [ "test-4-*" ], "privileges" : ["read"], + "field_security" : [ { "grant" : [ "*" ], "except" : [ "private-*" ] } ] } + } + + - length: { "applications" : 3 } + - contains: { "applications" : { "application" : "app-dne", "privileges" : [ "all" ], "resources" : [ "*" ] } } + - contains: { "applications" : { "application" : "test-app", "privileges" : [ "read" ], "resources" : [ "object/2" ] } } + - contains: { "applications" : { "application" : "test-app", "privileges" : [ "dne" ], "resources" : [ "*" ] } } + + - match: { "run_as" : [ "app-*" ] } + +--- + +"Test get_user_privileges for merged roles": + - do: + headers: { Authorization: "Basic dGVzdC0zOjEyMzQ1Njc4" } # test-3 + xpack.security.get_user_privileges: {} + + - match: { "cluster" : [ "manage", "manage_security", "monitor" ] } + + - length: { "global" : 2 } + - contains: { "global" : { "application" : { "manage" : { "applications" : [ "test-*" ]} } } } + - contains: { "global" : { "application" : { "manage" : { "applications" : [ "apps-*" ]} } } } + + - length: { "indices" : 7 } + - contains: { "indices" : { "names" : [ "test-1-*" ], "privileges" : [ "read" ] } } + - contains: { "indices" : { "names" : [ "test-2-*" ], "privileges" : [ "read" ], + "field_security" : [ + { "grant" : [ "*" ], "except" : [ "secret-*", "private-*" ] }, + { "grant" : [ "apps-*" ] } + ], + "query" : [ + "{\"term\":{\"test\":true}}", + "{\"term\":{\"apps\":true}}" + ] + } } + - contains: { "indices" : { "names" : [ "test-3-*" , "test-4-*", "test-5-*" ], "privileges" : ["read"], + "field_security" : [ { "grant" : [ "test-*" ] } ] } + } + - contains: { "indices" : { "names" : [ "test-6-*" , "test-7-*" ], "privileges" : ["read"], + "query" : [ "{\"term\":{\"test\":true}}" ] } + } + - contains: { "indices" : { "names" : [ "test-1-*", "test-9-*" ], "privileges" : [ "all" ] } } + - contains: { "indices" : { "names" : [ "test-3-*", "test-6-*" ], "privileges" : ["read","write"] } } + - contains: { "indices" : { "names" : [ "test-4-*" ], "privileges" : ["read"], + "field_security" : [ { "grant" : [ "*" ], "except" : [ "private-*" ] } ] } + } + + - length: { "applications" : 3 } + - contains: { "applications" : { "application" : "app-dne", "privileges" : [ "all" ], "resources" : [ "*" ] } } + - contains: { "applications" : { "application" : "test-app", "privileges" : [ "user", "dne" ], "resources" : [ "*" ] } } + - contains: { "applications" : { "application" : "test-app", "privileges" : [ "read" ], "resources" : [ "object/1", "object/2" ] } } + + - match: { "run_as" : [ "app-*", "test-*" ] } +