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-*" ] } +