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).
This commit is contained in:
parent
d445785f1a
commit
9200e15b74
|
@ -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<GetUserPrivilegesResponse> {
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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<GetUserPrivilegesRequest, GetUserPrivilegesResponse> {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -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<String> cluster;
|
||||
private Set<ConditionalClusterPrivilege> conditionalCluster;
|
||||
private Set<Indices> index;
|
||||
private Set<RoleDescriptor.ApplicationResourcePrivileges> application;
|
||||
private Set<String> runAs;
|
||||
|
||||
public GetUserPrivilegesResponse() {
|
||||
this(Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet());
|
||||
}
|
||||
|
||||
public GetUserPrivilegesResponse(Set<String> cluster, Set<ConditionalClusterPrivilege> conditionalCluster,
|
||||
Set<Indices> index,
|
||||
Set<RoleDescriptor.ApplicationResourcePrivileges> application,
|
||||
Set<String> 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<String> getClusterPrivileges() {
|
||||
return cluster;
|
||||
}
|
||||
|
||||
public Set<ConditionalClusterPrivilege> getConditionalClusterPrivileges() {
|
||||
return conditionalCluster;
|
||||
}
|
||||
|
||||
public Set<Indices> getIndexPrivileges() {
|
||||
return index;
|
||||
}
|
||||
|
||||
public Set<RoleDescriptor.ApplicationResourcePrivileges> getApplicationPrivileges() {
|
||||
return application;
|
||||
}
|
||||
|
||||
public Set<String> 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<String> indices;
|
||||
private final Set<String> privileges;
|
||||
private final Set<FieldPermissionsDefinition.FieldGrantExcludeGroup> fieldSecurity;
|
||||
private final Set<BytesReference> queries;
|
||||
|
||||
public Indices(Collection<String> indices, Collection<String> privileges,
|
||||
Set<FieldPermissionsDefinition.FieldGrantExcludeGroup> fieldSecurity, Set<BytesReference> 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<String> getIndices() {
|
||||
return indices;
|
||||
}
|
||||
|
||||
public Set<String> getPrivileges() {
|
||||
return privileges;
|
||||
}
|
||||
|
||||
public Set<FieldPermissionsDefinition.FieldGrantExcludeGroup> getFieldSecurity() {
|
||||
return fieldSecurity;
|
||||
}
|
||||
|
||||
public Set<BytesReference> 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -763,6 +763,10 @@ public class RoleDescriptor implements ToXContentObject {
|
|||
return this;
|
||||
}
|
||||
|
||||
public Builder privileges(Collection<String> 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<String> resources) {
|
||||
public Builder resources(Collection<String> resources) {
|
||||
return resources(resources.toArray(new String[resources.size()]));
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Tuple<ApplicationPrivilege, Set<String>>> privilegesAndResources) {
|
||||
this.logger = Loggers.getLogger(getClass());
|
||||
this.logger = LogManager.getLogger(getClass());
|
||||
Map<ApplicationPrivilege, PermissionEntry> 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<String> 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<String> getApplicationNames() {
|
||||
return permissions.stream()
|
||||
.map(e -> e.privilege.getApplication())
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public Set<ApplicationPrivilege> 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
|
||||
* <ul>
|
||||
* <li>"my-app", "read", [ "user/*" ]</li>
|
||||
* <li>"my-app", "all", [ "user/kimchy", "config/*" ]</li>
|
||||
* </ul>
|
||||
* Then <code>getResourcePatterns( myAppRead )</code> would return <code>"user/*", "user/kimchy", "config/*"</code>.
|
||||
*/
|
||||
public Set<String> 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<String> application;
|
||||
private final Automaton resources;
|
||||
private final Set<String> resourceNames;
|
||||
private final Automaton resourceAutomaton;
|
||||
|
||||
private PermissionEntry(ApplicationPrivilege privilege, Automaton resources) {
|
||||
private PermissionEntry(ApplicationPrivilege privilege, Set<String> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Tuple<ClusterPrivilege, ConditionalClusterPrivilege>> 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<Tuple<ClusterPrivilege, ConditionalClusterPrivilege>> 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<String> actionPredicate;
|
||||
private final Predicate<TransportRequest> requestPredicate;
|
||||
private final ConditionalClusterPrivilege conditionalPrivilege;
|
||||
|
||||
public ConditionalClusterPermission(ConditionalClusterPrivilege conditionalPrivilege) {
|
||||
this(conditionalPrivilege.getPrivilege(), conditionalPrivilege.getRequestPredicate());
|
||||
}
|
||||
|
||||
public ConditionalClusterPermission(ClusterPrivilege privilege, Predicate<TransportRequest> 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<Tuple<ClusterPrivilege, ConditionalClusterPrivilege>> privileges() {
|
||||
return Collections.singletonList(new Tuple<>(super.privilege, conditionalPrivilege));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -93,6 +102,11 @@ public abstract class ClusterPermission {
|
|||
return ClusterPrivilege.get(names);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Tuple<ClusterPrivilege, ConditionalClusterPrivilege>> 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));
|
||||
|
|
|
@ -161,7 +161,7 @@ public final class FieldPermissions implements Accountable {
|
|||
return permittedFieldsAutomatonIsTotal || permittedFieldsAutomaton.run(fieldName);
|
||||
}
|
||||
|
||||
FieldPermissionsDefinition getFieldPermissionsDefinition() {
|
||||
public FieldPermissionsDefinition getFieldPermissionsDefinition() {
|
||||
return fieldPermissionsDefinition;
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
+ "]";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -201,7 +201,7 @@ public final class Role {
|
|||
static Tuple<ApplicationPrivilege, Set<String>> 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()));
|
||||
}
|
||||
|
|
|
@ -17,12 +17,18 @@ public final class RunAsPermission {
|
|||
|
||||
public static final RunAsPermission NONE = new RunAsPermission(Privilege.NONE);
|
||||
|
||||
private final Privilege privilege;
|
||||
private final Predicate<String> 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
|
||||
*/
|
||||
|
|
|
@ -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<ConditionalClusterPrivilege> READER =
|
||||
in1 -> in1.readNamedWriteable(ConditionalClusterPrivilege.class);
|
||||
public static final Writeable.Writer<ConditionalClusterPrivilege> 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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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<GetUserPrivilegesResponse> listener) {
|
||||
client.execute(GetUserPrivilegesAction.INSTANCE, request, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* User Management
|
||||
*/
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
|
||||
}
|
|
@ -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<GetUserPrivilegesResponse> copy = original -> new GetUserPrivilegesResponse(
|
||||
original.getClusterPrivileges(),
|
||||
original.getConditionalClusterPrivileges(),
|
||||
original.getIndexPrivileges(),
|
||||
original.getApplicationPrivileges(),
|
||||
original.getRunAs()
|
||||
);
|
||||
final EqualsHashCodeTestUtils.MutateFunction<GetUserPrivilegesResponse> mutate =
|
||||
new EqualsHashCodeTestUtils.MutateFunction<GetUserPrivilegesResponse>() {
|
||||
@Override
|
||||
public GetUserPrivilegesResponse mutate(GetUserPrivilegesResponse original) {
|
||||
final int random = randomIntBetween(1, 0b11111);
|
||||
final Set<String> cluster = maybeMutate(random, 1, original.getClusterPrivileges(), () -> randomAlphaOfLength(5));
|
||||
final Set<ConditionalClusterPrivilege> conditionalCluster = maybeMutate(random, 2,
|
||||
original.getConditionalClusterPrivileges(), () -> new ManageApplicationPrivileges(randomStringSet(3)));
|
||||
final Set<GetUserPrivilegesResponse.Indices> index = maybeMutate(random, 3, original.getIndexPrivileges(),
|
||||
() -> new GetUserPrivilegesResponse.Indices(randomStringSet(1), randomStringSet(1), emptySet(), emptySet()));
|
||||
final Set<ApplicationResourcePrivileges> 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<String> runAs = maybeMutate(random, 5, original.getRunAs(), () -> randomAlphaOfLength(8));
|
||||
return new GetUserPrivilegesResponse(cluster, conditionalCluster, index, application, runAs);
|
||||
}
|
||||
|
||||
private <T> Set<T> maybeMutate(int random, int index, Set<T> original, Supplier<T> 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<String> cluster = randomStringSet(5);
|
||||
final Set<ConditionalClusterPrivilege> conditionalCluster = Sets.newHashSet(randomArray(3, ConditionalClusterPrivilege[]::new,
|
||||
() -> new ManageApplicationPrivileges(
|
||||
randomStringSet(3)
|
||||
)));
|
||||
final Set<GetUserPrivilegesResponse.Indices> 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<ApplicationResourcePrivileges> 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<String> runAs = randomStringSet(3);
|
||||
return new GetUserPrivilegesResponse(cluster, conditionalCluster, index, application, runAs);
|
||||
}
|
||||
|
||||
private List<GetUserPrivilegesResponse.Indices> sorted(Collection<GetUserPrivilegesResponse.Indices> indices) {
|
||||
final ArrayList<GetUserPrivilegesResponse.Indices> 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 <T> int compareCollection(Collection<T> a, Collection<T> b, Comparator<T> comparator) {
|
||||
int cmp = Integer.compare(a.size(), b.size());
|
||||
if (cmp != 0) {
|
||||
return cmp;
|
||||
}
|
||||
Iterator<T> i1 = a.iterator();
|
||||
Iterator<T> i2 = b.iterator();
|
||||
while (i1.hasNext()) {
|
||||
cmp = comparator.compare(i1.next(), i2.next());
|
||||
if (cmp != 0) {
|
||||
return cmp;
|
||||
}
|
||||
}
|
||||
return cmp;
|
||||
}
|
||||
|
||||
private HashSet<String> randomStringSet(int maxSize) {
|
||||
return Sets.newHashSet(generateRandomStringArray(maxSize, randomIntBetween(3, 6), false, false));
|
||||
}
|
||||
}
|
|
@ -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<String> 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))));
|
||||
}
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -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<GetUserPrivilegesRequest, GetUserPrivilegesResponse> {
|
||||
|
||||
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<GetUserPrivilegesResponse> 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<String> cluster = new TreeSet<>();
|
||||
// But we don't have a meaningful ordering for objects like ConditionalClusterPrivilege, so the tests work with "random" ordering
|
||||
final Set<ConditionalClusterPrivilege> conditionalCluster = new HashSet<>();
|
||||
for (Tuple<ClusterPrivilege, ConditionalClusterPrivilege> 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<GetUserPrivilegesResponse.Indices> indices = new LinkedHashSet<>();
|
||||
for (IndicesPermission.Group group : userRole.indices()) {
|
||||
final Set<BytesReference> queries = group.getQuery() == null ? Collections.emptySet() : group.getQuery();
|
||||
final Set<FieldPermissionsDefinition.FieldGrantExcludeGroup> 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<RoleDescriptor.ApplicationResourcePrivileges> application = new LinkedHashSet<>();
|
||||
for (String applicationName : userRole.application().getApplicationNames()) {
|
||||
for (ApplicationPrivilege privilege : userRole.application().getPrivileges(applicationName)) {
|
||||
final Set<String> 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<String> runAs;
|
||||
if (Operations.isEmpty(runAsPrivilege.getAutomaton())) {
|
||||
runAs = Collections.emptySet();
|
||||
} else {
|
||||
runAs = runAsPrivilege.name();
|
||||
}
|
||||
|
||||
return new GetUserPrivilegesResponse(cluster, conditionalCluster, indices, application, runAs);
|
||||
}
|
||||
|
||||
}
|
|
@ -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<String> MONITOR_INDEX_PREDICATE = IndexPrivilege.MONITOR.predicate();
|
||||
private static final Predicate<String> 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;
|
||||
}
|
||||
|
|
|
@ -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<GetUserPrivilegesResponse> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<GetUserPrivilegesResponse.Indices> indices, String name) {
|
||||
return indices.stream().filter(i -> i.getIndices().contains(name)).findFirst().get();
|
||||
}
|
||||
}
|
|
@ -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<String> cluster = new LinkedHashSet<>(Arrays.asList("monitor", "manage_ml", "manage_watcher"));
|
||||
final Set<ConditionalClusterPrivilege> conditionalCluster = Collections.singleton(
|
||||
new ConditionalClusterPrivileges.ManageApplicationPrivileges(new LinkedHashSet<>(Arrays.asList("app01", "app02"))));
|
||||
final Set<GetUserPrivilegesResponse.Indices> 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<ApplicationResourcePrivileges> 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<String> 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\"]" +
|
||||
"}"
|
||||
));
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
} }
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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-*" ] }
|
||||
|
Loading…
Reference in New Issue