Add `has_privileges` API (elastic/x-pack-elasticsearch#604)
Security API to determine which (if any) of a specified set of index/cluster privileges are held by the current (runAs) user. Intended for use by Kibana to distinguish between read/write and read-only users, but should be applicable to other uses cases also. Closes: elastic/x-pack-elasticsearch#282 Original commit: elastic/x-pack-elasticsearch@8b4cfdb858
This commit is contained in:
parent
c92562e9d9
commit
b105118ef0
|
@ -74,12 +74,14 @@ import org.elasticsearch.xpack.security.action.user.AuthenticateAction;
|
|||
import org.elasticsearch.xpack.security.action.user.ChangePasswordAction;
|
||||
import org.elasticsearch.xpack.security.action.user.DeleteUserAction;
|
||||
import org.elasticsearch.xpack.security.action.user.GetUsersAction;
|
||||
import org.elasticsearch.xpack.security.action.user.HasPrivilegesAction;
|
||||
import org.elasticsearch.xpack.security.action.user.PutUserAction;
|
||||
import org.elasticsearch.xpack.security.action.user.SetEnabledAction;
|
||||
import org.elasticsearch.xpack.security.action.user.TransportAuthenticateAction;
|
||||
import org.elasticsearch.xpack.security.action.user.TransportChangePasswordAction;
|
||||
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.TransportPutUserAction;
|
||||
import org.elasticsearch.xpack.security.action.user.TransportSetEnabledAction;
|
||||
import org.elasticsearch.xpack.security.audit.AuditTrail;
|
||||
|
@ -122,6 +124,7 @@ import org.elasticsearch.xpack.security.rest.action.role.RestPutRoleAction;
|
|||
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.RestGetUsersAction;
|
||||
import org.elasticsearch.xpack.security.rest.action.user.RestHasPrivilegesAction;
|
||||
import org.elasticsearch.xpack.security.rest.action.user.RestPutUserAction;
|
||||
import org.elasticsearch.xpack.security.rest.action.user.RestSetEnabledAction;
|
||||
import org.elasticsearch.xpack.security.transport.SecurityServerTransportInterceptor;
|
||||
|
@ -515,7 +518,8 @@ public class Security implements ActionPlugin, IngestPlugin, NetworkPlugin {
|
|||
new ActionHandler<>(DeleteRoleAction.INSTANCE, TransportDeleteRoleAction.class),
|
||||
new ActionHandler<>(ChangePasswordAction.INSTANCE, TransportChangePasswordAction.class),
|
||||
new ActionHandler<>(AuthenticateAction.INSTANCE, TransportAuthenticateAction.class),
|
||||
new ActionHandler<>(SetEnabledAction.INSTANCE, TransportSetEnabledAction.class));
|
||||
new ActionHandler<>(SetEnabledAction.INSTANCE, TransportSetEnabledAction.class),
|
||||
new ActionHandler<>(HasPrivilegesAction.INSTANCE, TransportHasPrivilegesAction.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -548,7 +552,8 @@ public class Security implements ActionPlugin, IngestPlugin, NetworkPlugin {
|
|||
new RestPutRoleAction(settings, restController),
|
||||
new RestDeleteRoleAction(settings, restController),
|
||||
new RestChangePasswordAction(settings, restController, securityContext.get()),
|
||||
new RestSetEnabledAction(settings, restController));
|
||||
new RestSetEnabledAction(settings, restController),
|
||||
new RestHasPrivilegesAction(settings, restController, securityContext.get()));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 java.util.Collections;
|
||||
|
||||
import org.elasticsearch.action.Action;
|
||||
import org.elasticsearch.client.ElasticsearchClient;
|
||||
|
||||
/**
|
||||
* This action is testing whether a user has the specified
|
||||
* {@link org.elasticsearch.xpack.security.authz.RoleDescriptor.IndicesPrivileges privileges}
|
||||
*/
|
||||
public class HasPrivilegesAction extends Action<HasPrivilegesRequest, HasPrivilegesResponse, HasPrivilegesRequestBuilder> {
|
||||
|
||||
public static final HasPrivilegesAction INSTANCE = new HasPrivilegesAction();
|
||||
public static final String NAME = "cluster:admin/xpack/security/user/has_privileges";
|
||||
|
||||
private HasPrivilegesAction() {
|
||||
super(NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HasPrivilegesRequestBuilder newRequestBuilder(ElasticsearchClient client) {
|
||||
return new HasPrivilegesRequestBuilder(client);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HasPrivilegesResponse newResponse() {
|
||||
return new HasPrivilegesResponse();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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 java.io.IOException;
|
||||
|
||||
import org.elasticsearch.action.ActionRequest;
|
||||
import org.elasticsearch.action.ActionRequestValidationException;
|
||||
import org.elasticsearch.common.io.stream.StreamInput;
|
||||
import org.elasticsearch.common.io.stream.StreamOutput;
|
||||
import org.elasticsearch.xpack.security.authz.RoleDescriptor;
|
||||
|
||||
import static org.elasticsearch.action.ValidateActions.addValidationError;
|
||||
|
||||
/**
|
||||
* A request for checking a user's privileges
|
||||
*/
|
||||
public class HasPrivilegesRequest extends ActionRequest implements UserRequest {
|
||||
|
||||
private String username;
|
||||
private String[] clusterPrivileges;
|
||||
private RoleDescriptor.IndicesPrivileges[] indexPrivileges;
|
||||
|
||||
@Override
|
||||
public ActionRequestValidationException validate() {
|
||||
ActionRequestValidationException validationException = null;
|
||||
if (clusterPrivileges == null) {
|
||||
validationException = addValidationError("clusterPrivileges must not be null", validationException);
|
||||
}
|
||||
if (indexPrivileges == null) {
|
||||
validationException = addValidationError("indexPrivileges must not be null", validationException);
|
||||
}
|
||||
if (clusterPrivileges != null && clusterPrivileges.length == 0 && indexPrivileges != null && indexPrivileges.length == 0) {
|
||||
validationException = addValidationError("clusterPrivileges and indexPrivileges cannot both be empty",
|
||||
validationException);
|
||||
}
|
||||
return validationException;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 };
|
||||
}
|
||||
|
||||
public RoleDescriptor.IndicesPrivileges[] indexPrivileges() {
|
||||
return indexPrivileges;
|
||||
}
|
||||
|
||||
public String[] clusterPrivileges() {
|
||||
return clusterPrivileges;
|
||||
}
|
||||
|
||||
public void indexPrivileges(RoleDescriptor.IndicesPrivileges... privileges) {
|
||||
this.indexPrivileges = privileges;
|
||||
}
|
||||
|
||||
public void clusterPrivileges(String... privileges) {
|
||||
this.clusterPrivileges = privileges;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readFrom(StreamInput in) throws IOException {
|
||||
super.readFrom(in);
|
||||
this.username = in.readString();
|
||||
this.clusterPrivileges = in.readStringArray();
|
||||
int indexSize = in.readVInt();
|
||||
indexPrivileges = new RoleDescriptor.IndicesPrivileges[indexSize];
|
||||
for (int i = 0; i < indexSize; i++) {
|
||||
indexPrivileges[i] = RoleDescriptor.IndicesPrivileges.createFrom(in);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(StreamOutput out) throws IOException {
|
||||
super.writeTo(out);
|
||||
out.writeString(username);
|
||||
out.writeStringArray(clusterPrivileges);
|
||||
out.writeVInt(indexPrivileges.length);
|
||||
for (RoleDescriptor.IndicesPrivileges priv : indexPrivileges) {
|
||||
priv.writeTo(out);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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 java.io.IOException;
|
||||
|
||||
import org.elasticsearch.action.ActionRequestBuilder;
|
||||
import org.elasticsearch.client.ElasticsearchClient;
|
||||
import org.elasticsearch.common.bytes.BytesReference;
|
||||
import org.elasticsearch.common.xcontent.XContentType;
|
||||
import org.elasticsearch.xpack.security.authz.RoleDescriptor;
|
||||
import org.elasticsearch.xpack.security.authz.RoleDescriptor.IndicesPrivileges;
|
||||
|
||||
/**
|
||||
* Request builder for checking a user's privileges
|
||||
*/
|
||||
public class HasPrivilegesRequestBuilder
|
||||
extends ActionRequestBuilder<HasPrivilegesRequest, HasPrivilegesResponse, HasPrivilegesRequestBuilder> {
|
||||
|
||||
public HasPrivilegesRequestBuilder(ElasticsearchClient client) {
|
||||
super(client, HasPrivilegesAction.INSTANCE, new HasPrivilegesRequest());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the username of the user that should enabled or disabled. Must not be {@code null}
|
||||
*/
|
||||
public HasPrivilegesRequestBuilder username(String username) {
|
||||
request.username(username);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether the user should be enabled or not
|
||||
*/
|
||||
public HasPrivilegesRequestBuilder source(String username, BytesReference source, XContentType xContentType) throws IOException {
|
||||
final RoleDescriptor role = RoleDescriptor.parsePrivilegesCheck(username + "/has_privileges", source, xContentType);
|
||||
request.username(username);
|
||||
request.indexPrivileges(role.getIndicesPrivileges());
|
||||
request.clusterPrivileges(role.getClusterPrivileges());
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* 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 java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.elasticsearch.action.ActionResponse;
|
||||
import org.elasticsearch.common.io.stream.StreamInput;
|
||||
import org.elasticsearch.common.io.stream.StreamOutput;
|
||||
|
||||
/**
|
||||
* Response for a {@link HasPrivilegesRequest}
|
||||
*/
|
||||
public class HasPrivilegesResponse extends ActionResponse {
|
||||
private boolean completeMatch;
|
||||
private Map<String, Boolean> cluster;
|
||||
private List<IndexPrivileges> index;
|
||||
|
||||
public HasPrivilegesResponse() {
|
||||
this(true, Collections.emptyMap(), Collections.emptyList());
|
||||
}
|
||||
|
||||
public HasPrivilegesResponse(boolean completeMatch, Map<String, Boolean> cluster, Collection<IndexPrivileges> index) {
|
||||
super();
|
||||
this.completeMatch = completeMatch;
|
||||
this.cluster = new HashMap<>(cluster);
|
||||
this.index = new ArrayList<>(index);
|
||||
}
|
||||
|
||||
public boolean isCompleteMatch() {
|
||||
return completeMatch;
|
||||
}
|
||||
|
||||
public Map<String, Boolean> getClusterPrivileges() {
|
||||
return Collections.unmodifiableMap(cluster);
|
||||
}
|
||||
|
||||
public List<IndexPrivileges> getIndexPrivileges() {
|
||||
return Collections.unmodifiableList(index);
|
||||
}
|
||||
|
||||
public void readFrom(StreamInput in) throws IOException {
|
||||
super.readFrom(in);
|
||||
completeMatch = in.readBoolean();
|
||||
int count = in.readVInt();
|
||||
index = new ArrayList<>(count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
final String index = in.readString();
|
||||
final Map<String, Boolean> privileges = in.readMap(StreamInput::readString, StreamInput::readBoolean);
|
||||
this.index.add(new IndexPrivileges(index, privileges));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(StreamOutput out) throws IOException {
|
||||
super.writeTo(out);
|
||||
out.writeBoolean(completeMatch);
|
||||
out.writeVInt(index.size());
|
||||
for (IndexPrivileges index : index) {
|
||||
out.writeString(index.index);
|
||||
out.writeMap(index.privileges, StreamOutput::writeString, StreamOutput::writeBoolean);
|
||||
}
|
||||
}
|
||||
|
||||
public static class IndexPrivileges {
|
||||
private final String index;
|
||||
private final Map<String, Boolean> privileges;
|
||||
|
||||
public IndexPrivileges(String index, Map<String, Boolean> privileges) {
|
||||
this.index = Objects.requireNonNull(index);
|
||||
this.privileges = Collections.unmodifiableMap(privileges);
|
||||
}
|
||||
|
||||
public String getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
public Map<String, Boolean> getPrivileges() {
|
||||
return privileges;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getSimpleName() + "{" +
|
||||
"index='" + index + '\'' +
|
||||
", privileges=" + privileges +
|
||||
'}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = index.hashCode();
|
||||
result = 31 * result + privileges.hashCode();
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final IndexPrivileges other = (IndexPrivileges) o;
|
||||
return this.index.equals(other.index) && this.privileges.equals(other.privileges);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* 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 java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.apache.lucene.util.automaton.Automaton;
|
||||
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.cluster.metadata.IndexNameExpressionResolver;
|
||||
import org.elasticsearch.common.inject.Inject;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.threadpool.ThreadPool;
|
||||
import org.elasticsearch.transport.TransportService;
|
||||
import org.elasticsearch.xpack.security.authc.Authentication;
|
||||
import org.elasticsearch.xpack.security.authz.AuthorizationService;
|
||||
import org.elasticsearch.xpack.security.authz.RoleDescriptor;
|
||||
import org.elasticsearch.xpack.security.authz.permission.IndicesPermission;
|
||||
import org.elasticsearch.xpack.security.authz.permission.Role;
|
||||
import org.elasticsearch.xpack.security.authz.privilege.ClusterPrivilege;
|
||||
import org.elasticsearch.xpack.security.authz.privilege.IndexPrivilege;
|
||||
import org.elasticsearch.xpack.security.authz.privilege.Privilege;
|
||||
import org.elasticsearch.xpack.security.support.Automatons;
|
||||
import org.elasticsearch.xpack.security.user.User;
|
||||
|
||||
/**
|
||||
* Transport action that tests whether a user has the specified
|
||||
* {@link org.elasticsearch.xpack.security.authz.RoleDescriptor.IndicesPrivileges privileges}
|
||||
*/
|
||||
public class TransportHasPrivilegesAction extends HandledTransportAction<HasPrivilegesRequest, HasPrivilegesResponse> {
|
||||
|
||||
private final AuthorizationService authorizationService;
|
||||
|
||||
@Inject
|
||||
public TransportHasPrivilegesAction(Settings settings, ThreadPool threadPool, TransportService transportService,
|
||||
ActionFilters actionFilters, IndexNameExpressionResolver indexNameExpressionResolver,
|
||||
AuthorizationService authorizationService) {
|
||||
super(settings, HasPrivilegesAction.NAME, threadPool, transportService, actionFilters, indexNameExpressionResolver,
|
||||
HasPrivilegesRequest::new);
|
||||
this.authorizationService = authorizationService;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doExecute(HasPrivilegesRequest request, ActionListener<HasPrivilegesResponse> listener) {
|
||||
final String username = request.username();
|
||||
|
||||
final User runAsUser = Authentication.getAuthentication(threadPool.getThreadContext()).getRunAsUser();
|
||||
if (runAsUser.principal().equals(username) == false) {
|
||||
listener.onFailure(new IllegalArgumentException("users may only check the privileges of their own account"));
|
||||
return;
|
||||
}
|
||||
|
||||
authorizationService.roles(runAsUser, ActionListener.wrap(
|
||||
role -> checkPrivileges(request, role, listener),
|
||||
listener::onFailure));
|
||||
}
|
||||
|
||||
private void checkPrivileges(HasPrivilegesRequest request, Role userRole,
|
||||
ActionListener<HasPrivilegesResponse> listener) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Check whether role [{}] has privileges cluster=[{}] index=[{}]", userRole.name(),
|
||||
Arrays.toString(request.clusterPrivileges()), Arrays.toString(request.indexPrivileges()));
|
||||
}
|
||||
|
||||
Map<String, Boolean> cluster = new HashMap<>();
|
||||
for (String checkAction : request.clusterPrivileges()) {
|
||||
final ClusterPrivilege checkPrivilege = ClusterPrivilege.get(Collections.singleton(checkAction));
|
||||
final ClusterPrivilege rolePrivilege = userRole.cluster().privilege();
|
||||
cluster.put(checkAction, testPrivilege(checkPrivilege, rolePrivilege.getAutomaton()));
|
||||
}
|
||||
|
||||
final Map<IndicesPermission.Group, Predicate<String>> predicateCache = new HashMap<>();
|
||||
|
||||
final Map<String, HasPrivilegesResponse.IndexPrivileges> indices = new LinkedHashMap<>();
|
||||
boolean allMatch = true;
|
||||
for (RoleDescriptor.IndicesPrivileges check : request.indexPrivileges()) {
|
||||
for (String index : check.getIndices()) {
|
||||
final Map<String, Boolean> privileges = new HashMap<>();
|
||||
final HasPrivilegesResponse.IndexPrivileges existing = indices.get(index);
|
||||
if (existing != null) {
|
||||
privileges.putAll(existing.getPrivileges());
|
||||
}
|
||||
for (String privilege : check.getPrivileges()) {
|
||||
if (testIndexMatch(index, privilege, userRole, predicateCache)) {
|
||||
logger.debug("Role [{}] has [{}] on [{}]", userRole.name(), privilege, index);
|
||||
privileges.put(privilege, true);
|
||||
} else {
|
||||
logger.debug("Role [{}] does not have [{}] on [{}]", userRole.name(), privilege, index);
|
||||
privileges.put(privilege, false);
|
||||
allMatch = false;
|
||||
}
|
||||
}
|
||||
indices.put(index, new HasPrivilegesResponse.IndexPrivileges(index, privileges));
|
||||
}
|
||||
}
|
||||
listener.onResponse(new HasPrivilegesResponse(allMatch, cluster, indices.values()));
|
||||
}
|
||||
|
||||
private boolean testIndexMatch(String checkIndex, String checkPrivilegeName, Role userRole,
|
||||
Map<IndicesPermission.Group, Predicate<String>> predicateCache) {
|
||||
final IndexPrivilege checkPrivilege = IndexPrivilege.get(Collections.singleton(checkPrivilegeName));
|
||||
|
||||
List<Automaton> privilegeAutomatons = new ArrayList<>();
|
||||
for (IndicesPermission.Group group : userRole.indices().groups()) {
|
||||
final Predicate<String> predicate = predicateCache.computeIfAbsent(group, g -> Automatons.predicate(g.indices()));
|
||||
if (predicate.test(checkIndex)) {
|
||||
final IndexPrivilege rolePrivilege = group.privilege();
|
||||
if (rolePrivilege.name().contains(checkPrivilegeName)) {
|
||||
return true;
|
||||
}
|
||||
privilegeAutomatons.add(rolePrivilege.getAutomaton());
|
||||
}
|
||||
}
|
||||
return testPrivilege(checkPrivilege, Automatons.unionAndMinimize(privilegeAutomatons));
|
||||
}
|
||||
|
||||
private boolean testPrivilege(Privilege checkPrivilege, Automaton roleAutomaton) {
|
||||
return Operations.subsetOf(checkPrivilege.getAutomaton(), roleAutomaton);
|
||||
}
|
||||
}
|
|
@ -36,6 +36,7 @@ import org.elasticsearch.transport.TransportRequest;
|
|||
import org.elasticsearch.xpack.security.SecurityLifecycleService;
|
||||
import org.elasticsearch.xpack.security.action.user.AuthenticateAction;
|
||||
import org.elasticsearch.xpack.security.action.user.ChangePasswordAction;
|
||||
import org.elasticsearch.xpack.security.action.user.HasPrivilegesAction;
|
||||
import org.elasticsearch.xpack.security.action.user.UserRequest;
|
||||
import org.elasticsearch.xpack.security.audit.AuditTrailService;
|
||||
import org.elasticsearch.xpack.security.authc.Authentication;
|
||||
|
@ -74,7 +75,8 @@ public class AuthorizationService extends AbstractComponent {
|
|||
public static final String ORIGINATING_ACTION_KEY = "_originating_action_name";
|
||||
|
||||
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);
|
||||
private static final Predicate<String> SAME_USER_PRIVILEGE = Automatons.predicate(
|
||||
ChangePasswordAction.NAME, AuthenticateAction.NAME, HasPrivilegesAction.NAME);
|
||||
|
||||
private static final String INDEX_SUB_REQUEST_PRIMARY = IndexAction.NAME + "[p]";
|
||||
private static final String INDEX_SUB_REQUEST_REPLICA = IndexAction.NAME + "[r]";
|
||||
|
@ -374,7 +376,8 @@ public class AuthorizationService extends AbstractComponent {
|
|||
return checkChangePasswordAction(authentication);
|
||||
}
|
||||
|
||||
assert AuthenticateAction.NAME.equals(action) || sameUsername == false;
|
||||
assert AuthenticateAction.NAME.equals(action) || HasPrivilegesAction.NAME.equals(action) || sameUsername == false
|
||||
: "Action '" + action + "' should not be possible when sameUsername=" + sameUsername;
|
||||
return sameUsername;
|
||||
}
|
||||
return false;
|
||||
|
|
|
@ -25,6 +25,7 @@ import org.elasticsearch.common.xcontent.XContentParser;
|
|||
import org.elasticsearch.common.xcontent.XContentType;
|
||||
import org.elasticsearch.common.xcontent.json.JsonXContent;
|
||||
import org.elasticsearch.xpack.common.xcontent.XContentUtils;
|
||||
import org.elasticsearch.xpack.security.authz.privilege.ClusterPrivilege;
|
||||
import org.elasticsearch.xpack.security.support.MetadataUtils;
|
||||
import org.elasticsearch.xpack.security.support.Validation;
|
||||
|
||||
|
@ -231,7 +232,7 @@ public class RoleDescriptor implements ToXContentObject {
|
|||
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
|
||||
if (token == XContentParser.Token.FIELD_NAME) {
|
||||
currentFieldName = parser.currentName();
|
||||
} else if (Fields.INDICES.match(currentFieldName)) {
|
||||
} else if (Fields.INDEX.match(currentFieldName) || Fields.INDICES.match(currentFieldName)) {
|
||||
indicesPrivileges = parseIndices(name, parser, allow2xFormat);
|
||||
} else if (Fields.RUN_AS.match(currentFieldName)) {
|
||||
runAsUsers = readStringArray(name, parser, true);
|
||||
|
@ -267,6 +268,47 @@ public class RoleDescriptor implements ToXContentObject {
|
|||
}
|
||||
}
|
||||
|
||||
public static RoleDescriptor parsePrivilegesCheck(String description, BytesReference source, XContentType xContentType)
|
||||
throws IOException {
|
||||
try (XContentParser parser = xContentType.xContent().createParser(NamedXContentRegistry.EMPTY, source)) {
|
||||
// advance to the START_OBJECT token
|
||||
XContentParser.Token token = parser.nextToken();
|
||||
if (token != XContentParser.Token.START_OBJECT) {
|
||||
throw new ElasticsearchParseException("failed to parse privileges check [{}]. expected an object but found [{}] instead",
|
||||
description, token);
|
||||
}
|
||||
String currentFieldName = null;
|
||||
IndicesPrivileges[] indexPrivileges = null;
|
||||
String[] clusterPrivileges = null;
|
||||
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
|
||||
if (token == XContentParser.Token.FIELD_NAME) {
|
||||
currentFieldName = parser.currentName();
|
||||
} else if (Fields.INDEX.match(currentFieldName)) {
|
||||
indexPrivileges = parseIndices(description, parser, false);
|
||||
} else if (Fields.CLUSTER.match(currentFieldName)) {
|
||||
clusterPrivileges = readStringArray(description, parser, true);
|
||||
} else {
|
||||
throw new ElasticsearchParseException("failed to parse privileges check [{}]. unexpected field [{}]",
|
||||
description, currentFieldName);
|
||||
}
|
||||
}
|
||||
if (indexPrivileges == null && clusterPrivileges == null) {
|
||||
throw new ElasticsearchParseException("failed to parse privileges check [{}]. fields [{}] and [{}] are both missing",
|
||||
description, Fields.INDEX, Fields.CLUSTER);
|
||||
}
|
||||
if (indexPrivileges != null) {
|
||||
if (Arrays.stream(indexPrivileges).anyMatch(IndicesPrivileges::isUsingFieldLevelSecurity)) {
|
||||
throw new ElasticsearchParseException("Field [{}] is not supported in a has_privileges request",
|
||||
RoleDescriptor.Fields.FIELD_PERMISSIONS);
|
||||
}
|
||||
if (Arrays.stream(indexPrivileges).anyMatch(IndicesPrivileges::isUsingDocumentLevelSecurity)) {
|
||||
throw new ElasticsearchParseException("Field [{}] is not supported in a has_privileges request", Fields.QUERY);
|
||||
}
|
||||
}
|
||||
return new RoleDescriptor(description, clusterPrivileges, indexPrivileges, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static RoleDescriptor.IndicesPrivileges[] parseIndices(String roleName, XContentParser parser,
|
||||
boolean allow2xFormat) throws IOException {
|
||||
if (parser.currentToken() != XContentParser.Token.START_ARRAY) {
|
||||
|
@ -634,6 +676,7 @@ public class RoleDescriptor implements ToXContentObject {
|
|||
|
||||
public interface Fields {
|
||||
ParseField CLUSTER = new ParseField("cluster");
|
||||
ParseField INDEX = new ParseField("index");
|
||||
ParseField INDICES = new ParseField("indices");
|
||||
ParseField RUN_AS = new ParseField("run_as");
|
||||
ParseField NAMES = new ParseField("names");
|
||||
|
|
|
@ -124,7 +124,7 @@ public final class IndexPrivilege extends Privilege {
|
|||
} else if (indexPrivilege != null) {
|
||||
automata.add(indexPrivilege.automaton);
|
||||
} else {
|
||||
throw new IllegalArgumentException("unknown index privilege [" + name + "]. a privilege must be either " +
|
||||
throw new IllegalArgumentException("unknown index privilege [" + part + "]. a privilege must be either " +
|
||||
"one of the predefined fixed indices privileges [" +
|
||||
Strings.collectionToCommaDelimitedString(VALUES.entrySet()) + "] or a pattern over one of the available index" +
|
||||
" actions");
|
||||
|
@ -135,7 +135,7 @@ public final class IndexPrivilege extends Privilege {
|
|||
if (actions.isEmpty() == false) {
|
||||
automata.add(patterns(actions));
|
||||
}
|
||||
return new IndexPrivilege(name, Automatons.unionAndMinimize(automata));
|
||||
return new IndexPrivilege(name, unionAndMinimize(automata));
|
||||
}
|
||||
|
||||
static Map<String, IndexPrivilege> values() {
|
||||
|
|
|
@ -42,6 +42,10 @@ import org.elasticsearch.xpack.security.action.user.GetUsersAction;
|
|||
import org.elasticsearch.xpack.security.action.user.GetUsersRequest;
|
||||
import org.elasticsearch.xpack.security.action.user.GetUsersRequestBuilder;
|
||||
import org.elasticsearch.xpack.security.action.user.GetUsersResponse;
|
||||
import org.elasticsearch.xpack.security.action.user.HasPrivilegesAction;
|
||||
import org.elasticsearch.xpack.security.action.user.HasPrivilegesRequest;
|
||||
import org.elasticsearch.xpack.security.action.user.HasPrivilegesRequestBuilder;
|
||||
import org.elasticsearch.xpack.security.action.user.HasPrivilegesResponse;
|
||||
import org.elasticsearch.xpack.security.action.user.PutUserAction;
|
||||
import org.elasticsearch.xpack.security.action.user.PutUserRequest;
|
||||
import org.elasticsearch.xpack.security.action.user.PutUserRequestBuilder;
|
||||
|
@ -126,6 +130,22 @@ public class SecurityClient {
|
|||
return client.execute(ClearRolesCacheAction.INSTANCE, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Permissions / Privileges
|
||||
*/
|
||||
public HasPrivilegesRequestBuilder prepareHasPrivileges(String username) {
|
||||
return new HasPrivilegesRequestBuilder(client).username(username);
|
||||
}
|
||||
|
||||
public HasPrivilegesRequestBuilder prepareHasPrivileges(String username, BytesReference source, XContentType xContentType)
|
||||
throws IOException {
|
||||
return new HasPrivilegesRequestBuilder(client).source(username, source, xContentType);
|
||||
}
|
||||
|
||||
public void hasPrivileges(HasPrivilegesRequest request, ActionListener<HasPrivilegesResponse> listener) {
|
||||
client.execute(HasPrivilegesAction.INSTANCE, request, listener);
|
||||
}
|
||||
|
||||
/** User Management */
|
||||
|
||||
public GetUsersRequestBuilder prepareGetUsers(String... usernames) {
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* 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 java.io.IOException;
|
||||
|
||||
import org.elasticsearch.client.node.NodeClient;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.rest.BaseRestHandler;
|
||||
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.security.SecurityContext;
|
||||
import org.elasticsearch.xpack.security.action.user.HasPrivilegesRequestBuilder;
|
||||
import org.elasticsearch.xpack.security.action.user.HasPrivilegesResponse;
|
||||
import org.elasticsearch.xpack.security.client.SecurityClient;
|
||||
import org.elasticsearch.xpack.security.user.User;
|
||||
|
||||
import static org.elasticsearch.rest.RestRequest.Method.GET;
|
||||
import static org.elasticsearch.rest.RestRequest.Method.POST;
|
||||
|
||||
/**
|
||||
* REST handler that tests whether a user has the specified
|
||||
* {@link org.elasticsearch.xpack.security.authz.RoleDescriptor.IndicesPrivileges privileges}
|
||||
*/
|
||||
public class RestHasPrivilegesAction extends BaseRestHandler {
|
||||
|
||||
private final SecurityContext securityContext;
|
||||
|
||||
public RestHasPrivilegesAction(Settings settings, RestController controller, SecurityContext securityContext) {
|
||||
super(settings);
|
||||
this.securityContext = securityContext;
|
||||
controller.registerHandler(GET, "/_xpack/security/user/{username}/_has_privileges", this);
|
||||
controller.registerHandler(POST, "/_xpack/security/user/{username}/_has_privileges", this);
|
||||
controller.registerHandler(GET, "/_xpack/security/user/_has_privileges", this);
|
||||
controller.registerHandler(POST, "/_xpack/security/user/_has_privileges", this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException {
|
||||
final String username = getUsername(request);
|
||||
HasPrivilegesRequestBuilder requestBuilder = new SecurityClient(client)
|
||||
.prepareHasPrivileges(username, request.content(), request.getXContentType());
|
||||
return channel -> requestBuilder.execute(new HasPrivilegesRestResponseBuilder(username, channel));
|
||||
}
|
||||
|
||||
private String getUsername(RestRequest request) {
|
||||
final String username = request.param("username");
|
||||
if (username != null) {
|
||||
return username;
|
||||
}
|
||||
final User user = securityContext.getUser();
|
||||
if (user.runAs() != null) {
|
||||
return user.runAs().principal();
|
||||
} else {
|
||||
return user.principal();
|
||||
}
|
||||
}
|
||||
|
||||
static class HasPrivilegesRestResponseBuilder extends RestBuilderListener<HasPrivilegesResponse> {
|
||||
private String username;
|
||||
|
||||
HasPrivilegesRestResponseBuilder(String username, RestChannel channel) {
|
||||
super(channel);
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestResponse buildResponse(HasPrivilegesResponse response, XContentBuilder builder) throws Exception {
|
||||
builder.startObject()
|
||||
.field("username", username)
|
||||
.field("has_all_requested", response.isCompleteMatch());
|
||||
|
||||
builder.field("cluster");
|
||||
builder.map(response.getClusterPrivileges());
|
||||
|
||||
builder.startObject("index");
|
||||
for (HasPrivilegesResponse.IndexPrivileges index : response.getIndexPrivileges()) {
|
||||
builder.field(index.getIndex());
|
||||
builder.map(index.getPrivileges());
|
||||
}
|
||||
builder.endObject();
|
||||
|
||||
builder.endObject();
|
||||
return new BytesRestResponse(RestStatus.OK, builder);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* 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 java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.elasticsearch.ElasticsearchParseException;
|
||||
import org.elasticsearch.action.admin.cluster.health.ClusterHealthAction;
|
||||
import org.elasticsearch.action.admin.cluster.stats.ClusterStatsAction;
|
||||
import org.elasticsearch.client.Client;
|
||||
import org.elasticsearch.common.bytes.BytesArray;
|
||||
import org.elasticsearch.common.xcontent.XContentType;
|
||||
import org.elasticsearch.test.ESTestCase;
|
||||
import org.elasticsearch.xpack.security.authz.RoleDescriptor;
|
||||
|
||||
import static org.hamcrest.Matchers.arrayContaining;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
public class HasPrivilegesRequestBuilderTests extends ESTestCase {
|
||||
|
||||
public void testParseValidJsonWithClusterAndIndexPrivileges() throws Exception {
|
||||
String json = "{ "
|
||||
+ " \"cluster\":[ \"all\"],"
|
||||
+ " \"index\":[ "
|
||||
+ " { \"names\": [ \".kibana\", \".reporting\" ], "
|
||||
+ " \"privileges\" : [ \"read\", \"write\" ] }, "
|
||||
+ " { \"names\": [ \".security\" ], "
|
||||
+ " \"privileges\" : [ \"manage\" ] } "
|
||||
+ " ]"
|
||||
+ "}";
|
||||
|
||||
final HasPrivilegesRequestBuilder builder = new HasPrivilegesRequestBuilder(mock(Client.class));
|
||||
builder.source("elastic", new BytesArray(json.getBytes(StandardCharsets.UTF_8)), XContentType.JSON);
|
||||
|
||||
final HasPrivilegesRequest request = builder.request();
|
||||
assertThat(request.clusterPrivileges().length, equalTo(1));
|
||||
assertThat(request.clusterPrivileges()[0], equalTo("all"));
|
||||
|
||||
assertThat(request.indexPrivileges().length, equalTo(2));
|
||||
|
||||
final RoleDescriptor.IndicesPrivileges privileges0 = request.indexPrivileges()[0];
|
||||
assertThat(privileges0.getIndices(), arrayContaining(".kibana", ".reporting"));
|
||||
assertThat(privileges0.getPrivileges(), arrayContaining("read", "write"));
|
||||
|
||||
final RoleDescriptor.IndicesPrivileges privileges1 = request.indexPrivileges()[1];
|
||||
assertThat(privileges1.getIndices(), arrayContaining(".security"));
|
||||
assertThat(privileges1.getPrivileges(), arrayContaining("manage"));
|
||||
}
|
||||
|
||||
public void testParseValidJsonWithJustIndexPrivileges() throws Exception {
|
||||
String json = "{ \"index\":[ "
|
||||
+ "{ \"names\": [ \".kibana\", \".reporting\" ], "
|
||||
+ " \"privileges\" : [ \"read\", \"write\" ] }, "
|
||||
+ "{ \"names\": [ \".security\" ], "
|
||||
+ " \"privileges\" : [ \"manage\" ] } "
|
||||
+ "] }";
|
||||
|
||||
final HasPrivilegesRequestBuilder builder = new HasPrivilegesRequestBuilder(mock(Client.class));
|
||||
builder.source("elastic", new BytesArray(json.getBytes(StandardCharsets.UTF_8)), XContentType.JSON);
|
||||
|
||||
final HasPrivilegesRequest request = builder.request();
|
||||
assertThat(request.clusterPrivileges().length, equalTo(0));
|
||||
assertThat(request.indexPrivileges().length, equalTo(2));
|
||||
|
||||
final RoleDescriptor.IndicesPrivileges privileges0 = request.indexPrivileges()[0];
|
||||
assertThat(privileges0.getIndices(), arrayContaining(".kibana", ".reporting"));
|
||||
assertThat(privileges0.getPrivileges(), arrayContaining("read", "write"));
|
||||
|
||||
final RoleDescriptor.IndicesPrivileges privileges1 = request.indexPrivileges()[1];
|
||||
assertThat(privileges1.getIndices(), arrayContaining(".security"));
|
||||
assertThat(privileges1.getPrivileges(), arrayContaining("manage"));
|
||||
}
|
||||
|
||||
public void testParseValidJsonWithJustClusterPrivileges() throws Exception {
|
||||
String json = "{ \"cluster\":[ "
|
||||
+ "\"manage\","
|
||||
+ "\"" + ClusterHealthAction.NAME + "\","
|
||||
+ "\"" + ClusterStatsAction.NAME + "\""
|
||||
+ "] }";
|
||||
|
||||
final HasPrivilegesRequestBuilder builder = new HasPrivilegesRequestBuilder(mock(Client.class));
|
||||
builder.source("elastic", new BytesArray(json.getBytes(StandardCharsets.UTF_8)), XContentType.JSON);
|
||||
|
||||
final HasPrivilegesRequest request = builder.request();
|
||||
assertThat(request.indexPrivileges().length, equalTo(0));
|
||||
assertThat(request.clusterPrivileges(), arrayContaining("manage", ClusterHealthAction.NAME, ClusterStatsAction.NAME));
|
||||
}
|
||||
|
||||
public void testUseOfFieldLevelSecurityThrowsException() throws Exception {
|
||||
String json = "{ \"index\":[ "
|
||||
+ "{"
|
||||
+ " \"names\": [ \"employees\" ], "
|
||||
+ " \"privileges\" : [ \"read\", \"write\" ] ,"
|
||||
+ " \"field_security\": { \"grant\": [ \"name\", \"department\", \"title\" ] }"
|
||||
+ "} ] }";
|
||||
|
||||
final HasPrivilegesRequestBuilder builder = new HasPrivilegesRequestBuilder(mock(Client.class));
|
||||
final ElasticsearchParseException parseException = expectThrows(ElasticsearchParseException.class,
|
||||
() -> builder.source("elastic", new BytesArray(json.getBytes(StandardCharsets.UTF_8)), XContentType.JSON)
|
||||
);
|
||||
assertThat(parseException.getMessage(), containsString("[field_security]"));
|
||||
}
|
||||
|
||||
public void testMissingPrivilegesThrowsException() throws Exception {
|
||||
String json = "{ }";
|
||||
final HasPrivilegesRequestBuilder builder = new HasPrivilegesRequestBuilder(mock(Client.class));
|
||||
final ElasticsearchParseException parseException = expectThrows(ElasticsearchParseException.class,
|
||||
() -> builder.source("elastic", new BytesArray(json.getBytes(StandardCharsets.UTF_8)), XContentType.JSON)
|
||||
);
|
||||
assertThat(parseException.getMessage(), containsString("[index] and [cluster] are both missing"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,262 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.security.action.user;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
import org.elasticsearch.action.ActionListener;
|
||||
import org.elasticsearch.action.admin.cluster.health.ClusterHealthAction;
|
||||
import org.elasticsearch.action.delete.DeleteAction;
|
||||
import org.elasticsearch.action.index.IndexAction;
|
||||
import org.elasticsearch.action.support.ActionFilters;
|
||||
import org.elasticsearch.action.support.PlainActionFuture;
|
||||
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
|
||||
import org.elasticsearch.common.Strings;
|
||||
import org.elasticsearch.common.collect.MapBuilder;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.util.concurrent.ThreadContext;
|
||||
import org.elasticsearch.mock.orig.Mockito;
|
||||
import org.elasticsearch.test.ESTestCase;
|
||||
import org.elasticsearch.threadpool.ThreadPool;
|
||||
import org.elasticsearch.transport.TransportService;
|
||||
import org.elasticsearch.xpack.security.action.user.HasPrivilegesResponse.IndexPrivileges;
|
||||
import org.elasticsearch.xpack.security.authc.Authentication;
|
||||
import org.elasticsearch.xpack.security.authz.AuthorizationService;
|
||||
import org.elasticsearch.xpack.security.authz.RoleDescriptor;
|
||||
import org.elasticsearch.xpack.security.authz.permission.Role;
|
||||
import org.elasticsearch.xpack.security.authz.privilege.ClusterPrivilege;
|
||||
import org.elasticsearch.xpack.security.authz.privilege.IndexPrivilege;
|
||||
import org.elasticsearch.xpack.security.user.User;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Before;
|
||||
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class TransportHasPrivilegesActionTests extends ESTestCase {
|
||||
|
||||
private User user;
|
||||
private Role role;
|
||||
private TransportHasPrivilegesAction action;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
final Settings settings = Settings.builder().build();
|
||||
user = new User(randomAsciiOfLengthBetween(4, 12));
|
||||
final ThreadPool threadPool = mock(ThreadPool.class);
|
||||
final ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
|
||||
final TransportService transportService = new TransportService(Settings.EMPTY, null, null, TransportService
|
||||
.NOOP_TRANSPORT_INTERCEPTOR,
|
||||
x -> null, null);
|
||||
|
||||
final Authentication authentication = mock(Authentication.class);
|
||||
threadContext.putTransient(Authentication.AUTHENTICATION_KEY, authentication);
|
||||
when(threadPool.getThreadContext()).thenReturn(threadContext);
|
||||
|
||||
when(authentication.getRunAsUser()).thenReturn(user);
|
||||
|
||||
AuthorizationService authorizationService = mock(AuthorizationService.class);
|
||||
Mockito.doAnswer(invocationOnMock -> {
|
||||
ActionListener<Role> listener = (ActionListener<Role>) invocationOnMock.getArguments()[1];
|
||||
listener.onResponse(role);
|
||||
return null;
|
||||
}).when(authorizationService).roles(eq(user), any(ActionListener.class));
|
||||
|
||||
action = new TransportHasPrivilegesAction(settings, threadPool, transportService,
|
||||
mock(ActionFilters.class), mock(IndexNameExpressionResolver.class), authorizationService);
|
||||
}
|
||||
|
||||
/**
|
||||
* This tests that action names in the request are considered "matched" by the relevant named privilege
|
||||
* (in this case that {@link DeleteAction} and {@link IndexAction} are satisfied by {@link IndexPrivilege#WRITE}).
|
||||
*/
|
||||
public void testNamedIndexPrivilegesMatchApplicableActions() throws Exception {
|
||||
role = Role.builder("test1").cluster(ClusterPrivilege.ALL).add(IndexPrivilege.WRITE, "academy").build();
|
||||
|
||||
final HasPrivilegesRequest request = new HasPrivilegesRequest();
|
||||
request.username(user.principal());
|
||||
request.clusterPrivileges(ClusterHealthAction.NAME);
|
||||
request.indexPrivileges(RoleDescriptor.IndicesPrivileges.builder()
|
||||
.indices("academy")
|
||||
.privileges(DeleteAction.NAME, IndexAction.NAME)
|
||||
.build());
|
||||
final PlainActionFuture<HasPrivilegesResponse> future = new PlainActionFuture();
|
||||
action.doExecute(request, future);
|
||||
|
||||
final HasPrivilegesResponse response = future.get();
|
||||
assertThat(response, notNullValue());
|
||||
assertThat(response.isCompleteMatch(), is(true));
|
||||
|
||||
assertThat(response.getClusterPrivileges().size(), equalTo(1));
|
||||
assertThat(response.getClusterPrivileges().get(ClusterHealthAction.NAME), equalTo(true));
|
||||
|
||||
assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(1));
|
||||
final IndexPrivileges result = response.getIndexPrivileges().get(0);
|
||||
assertThat(result.getIndex(), equalTo("academy"));
|
||||
assertThat(result.getPrivileges().size(), equalTo(2));
|
||||
assertThat(result.getPrivileges().get(DeleteAction.NAME), equalTo(true));
|
||||
assertThat(result.getPrivileges().get(IndexAction.NAME), equalTo(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* This tests that the action responds correctly when the user/role has some, but not all
|
||||
* of the privileges being checked.
|
||||
*/
|
||||
public void testMatchSubsetOfPrivileges() throws Exception {
|
||||
role = Role.builder("test2")
|
||||
.cluster(ClusterPrivilege.MONITOR)
|
||||
.add(IndexPrivilege.INDEX, "academy")
|
||||
.add(IndexPrivilege.WRITE, "initiative")
|
||||
.build();
|
||||
|
||||
final HasPrivilegesRequest request = new HasPrivilegesRequest();
|
||||
request.username(user.principal());
|
||||
request.clusterPrivileges("monitor", "manage");
|
||||
request.indexPrivileges(RoleDescriptor.IndicesPrivileges.builder()
|
||||
.indices("academy", "initiative", "school")
|
||||
.privileges("delete", "index", "manage")
|
||||
.build());
|
||||
final PlainActionFuture<HasPrivilegesResponse> future = new PlainActionFuture();
|
||||
action.doExecute(request, future);
|
||||
|
||||
final HasPrivilegesResponse response = future.get();
|
||||
assertThat(response, notNullValue());
|
||||
assertThat(response.isCompleteMatch(), is(false));
|
||||
assertThat(response.getClusterPrivileges().size(), equalTo(2));
|
||||
assertThat(response.getClusterPrivileges().get("monitor"), equalTo(true));
|
||||
assertThat(response.getClusterPrivileges().get("manage"), equalTo(false));
|
||||
assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(3));
|
||||
|
||||
final IndexPrivileges academy = response.getIndexPrivileges().get(0);
|
||||
final IndexPrivileges initiative = response.getIndexPrivileges().get(1);
|
||||
final IndexPrivileges school = response.getIndexPrivileges().get(2);
|
||||
|
||||
assertThat(academy.getIndex(), equalTo("academy"));
|
||||
assertThat(academy.getPrivileges().size(), equalTo(3));
|
||||
assertThat(academy.getPrivileges().get("index"), equalTo(true)); // explicit
|
||||
assertThat(academy.getPrivileges().get("delete"), equalTo(false));
|
||||
assertThat(academy.getPrivileges().get("manage"), equalTo(false));
|
||||
|
||||
assertThat(initiative.getIndex(), equalTo("initiative"));
|
||||
assertThat(initiative.getPrivileges().size(), equalTo(3));
|
||||
assertThat(initiative.getPrivileges().get("index"), equalTo(true)); // implied by write
|
||||
assertThat(initiative.getPrivileges().get("delete"), equalTo(true)); // implied by write
|
||||
assertThat(initiative.getPrivileges().get("manage"), equalTo(false));
|
||||
|
||||
assertThat(school.getIndex(), equalTo("school"));
|
||||
assertThat(school.getPrivileges().size(), equalTo(3));
|
||||
assertThat(school.getPrivileges().get("index"), equalTo(false));
|
||||
assertThat(school.getPrivileges().get("delete"), equalTo(false));
|
||||
assertThat(school.getPrivileges().get("manage"), equalTo(false));
|
||||
}
|
||||
|
||||
/**
|
||||
* This tests that the action responds correctly when the user/role has none
|
||||
* of the privileges being checked.
|
||||
*/
|
||||
public void testMatchNothing() throws Exception {
|
||||
role = Role.builder("test3")
|
||||
.cluster(ClusterPrivilege.MONITOR)
|
||||
.build();
|
||||
|
||||
final HasPrivilegesRequest request = new HasPrivilegesRequest();
|
||||
request.username(user.principal());
|
||||
request.clusterPrivileges(Strings.EMPTY_ARRAY);
|
||||
request.indexPrivileges(RoleDescriptor.IndicesPrivileges.builder()
|
||||
.indices("academy")
|
||||
.privileges("read", "write")
|
||||
.build());
|
||||
final PlainActionFuture<HasPrivilegesResponse> future = new PlainActionFuture();
|
||||
action.doExecute(request, future);
|
||||
|
||||
final HasPrivilegesResponse response = future.get();
|
||||
assertThat(response, notNullValue());
|
||||
assertThat(response.isCompleteMatch(), is(false));
|
||||
assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(1));
|
||||
final IndexPrivileges result = response.getIndexPrivileges().get(0);
|
||||
assertThat(result.getIndex(), equalTo("academy"));
|
||||
assertThat(result.getPrivileges().size(), equalTo(2));
|
||||
assertThat(result.getPrivileges().get("read"), equalTo(false));
|
||||
assertThat(result.getPrivileges().get("write"), equalTo(false));
|
||||
}
|
||||
|
||||
/**
|
||||
* We intentionally ignore wildcards in the request. This tests that
|
||||
* <code>log*</code> in the request isn't granted by <code>logstash-*</code>
|
||||
* in the role, but <code>logstash-2016-*</code> is, because it's just
|
||||
* treated as the name of an index.
|
||||
*/
|
||||
public void testWildcardsInRequestAreIgnored() throws Exception {
|
||||
role = Role.builder("test3")
|
||||
.add(IndexPrivilege.ALL, "logstash-*")
|
||||
.build();
|
||||
|
||||
final HasPrivilegesRequest request = new HasPrivilegesRequest();
|
||||
request.username(user.principal());
|
||||
request.clusterPrivileges(Strings.EMPTY_ARRAY);
|
||||
request.indexPrivileges(
|
||||
RoleDescriptor.IndicesPrivileges.builder()
|
||||
.indices("logstash-2016-*")
|
||||
.privileges("write")
|
||||
.build(),
|
||||
RoleDescriptor.IndicesPrivileges.builder()
|
||||
.indices("log*")
|
||||
.privileges("read")
|
||||
.build()
|
||||
);
|
||||
final PlainActionFuture<HasPrivilegesResponse> future = new PlainActionFuture();
|
||||
action.doExecute(request, future);
|
||||
|
||||
final HasPrivilegesResponse response = future.get();
|
||||
assertThat(response, notNullValue());
|
||||
assertThat(response.isCompleteMatch(), is(false));
|
||||
assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(2));
|
||||
assertThat(response.getIndexPrivileges(), containsInAnyOrder(
|
||||
new IndexPrivileges("logstash-2016-*", Collections.singletonMap("write", true)),
|
||||
new IndexPrivileges("log*", Collections.singletonMap("read", false))
|
||||
));
|
||||
}
|
||||
|
||||
public void testCheckingIndexPermissionsDefinedOnDifferentPatterns() throws Exception {
|
||||
role = Role.builder("test-write")
|
||||
.add(IndexPrivilege.INDEX, "apache-*")
|
||||
.add(IndexPrivilege.DELETE, "apache-2016-*")
|
||||
.build();
|
||||
|
||||
final HasPrivilegesRequest request = new HasPrivilegesRequest();
|
||||
request.username(user.principal());
|
||||
request.clusterPrivileges(Strings.EMPTY_ARRAY);
|
||||
request.indexPrivileges(
|
||||
RoleDescriptor.IndicesPrivileges.builder()
|
||||
.indices("apache-2016-12", "apache-2017-01")
|
||||
.privileges("index", "delete")
|
||||
.build()
|
||||
);
|
||||
final PlainActionFuture<HasPrivilegesResponse> future = new PlainActionFuture();
|
||||
action.doExecute(request, future);
|
||||
|
||||
final HasPrivilegesResponse response = future.get();
|
||||
assertThat(response, notNullValue());
|
||||
assertThat(response.isCompleteMatch(), is(false));
|
||||
assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(2));
|
||||
assertThat(response.getIndexPrivileges(), containsInAnyOrder(
|
||||
new IndexPrivileges("apache-2016-12",
|
||||
MapBuilder.newMapBuilder(new LinkedHashMap<String, Boolean>())
|
||||
.put("index", true).put("delete", true).map()),
|
||||
new IndexPrivileges("apache-2017-01",
|
||||
MapBuilder.newMapBuilder(new LinkedHashMap<String, Boolean>())
|
||||
.put("index", true).put("delete", false).map()
|
||||
)
|
||||
));
|
||||
}
|
||||
}
|
|
@ -82,7 +82,7 @@ public class RoleDescriptorTests extends ESTestCase {
|
|||
assertEquals(0, rd.getIndicesPrivileges().length);
|
||||
assertArrayEquals(new String[] { "m", "n" }, rd.getRunAs());
|
||||
|
||||
q = "{\"cluster\":[\"a\", \"b\"], \"run_as\": [\"m\", \"n\"], \"indices\": [{\"names\": \"idx1\", \"privileges\": [\"p1\", " +
|
||||
q = "{\"cluster\":[\"a\", \"b\"], \"run_as\": [\"m\", \"n\"], \"index\": [{\"names\": \"idx1\", \"privileges\": [\"p1\", " +
|
||||
"\"p2\"]}, {\"names\": \"idx2\", \"privileges\": [\"p3\"], \"field_security\": " +
|
||||
"{\"grant\": [\"f1\", \"f2\"]}}, {\"names\": " +
|
||||
"\"idx2\", " +
|
||||
|
@ -93,7 +93,7 @@ public class RoleDescriptorTests extends ESTestCase {
|
|||
assertEquals(3, rd.getIndicesPrivileges().length);
|
||||
assertArrayEquals(new String[] { "m", "n" }, rd.getRunAs());
|
||||
|
||||
q = "{\"cluster\":[\"a\", \"b\"], \"run_as\": [\"m\", \"n\"], \"indices\": [{\"names\": [\"idx1\",\"idx2\"], \"privileges\": " +
|
||||
q = "{\"cluster\":[\"a\", \"b\"], \"run_as\": [\"m\", \"n\"], \"index\": [{\"names\": [\"idx1\",\"idx2\"], \"privileges\": " +
|
||||
"[\"p1\", \"p2\"]}]}";
|
||||
rd = RoleDescriptor.parse("test", new BytesArray(q), false, XContentType.JSON);
|
||||
assertEquals("test", rd.getName());
|
||||
|
@ -134,6 +134,18 @@ public class RoleDescriptorTests extends ESTestCase {
|
|||
}
|
||||
|
||||
public void testParseEmptyQuery() throws Exception {
|
||||
String json = "{\"cluster\":[\"a\", \"b\"], \"run_as\": [\"m\", \"n\"], \"index\": [{\"names\": [\"idx1\",\"idx2\"], " +
|
||||
"\"privileges\": [\"p1\", \"p2\"], \"query\": \"\"}]}";
|
||||
RoleDescriptor rd = RoleDescriptor.parse("test", new BytesArray(json), false, XContentType.JSON);
|
||||
assertEquals("test", rd.getName());
|
||||
assertArrayEquals(new String[] { "a", "b" }, rd.getClusterPrivileges());
|
||||
assertEquals(1, rd.getIndicesPrivileges().length);
|
||||
assertArrayEquals(new String[] { "idx1", "idx2" }, rd.getIndicesPrivileges()[0].getIndices());
|
||||
assertArrayEquals(new String[] { "m", "n" }, rd.getRunAs());
|
||||
assertNull(rd.getIndicesPrivileges()[0].getQuery());
|
||||
}
|
||||
|
||||
public void testParseEmptyQueryUsingDeprecatedIndicesField() throws Exception {
|
||||
String json = "{\"cluster\":[\"a\", \"b\"], \"run_as\": [\"m\", \"n\"], \"indices\": [{\"names\": [\"idx1\",\"idx2\"], " +
|
||||
"\"privileges\": [\"p1\", \"p2\"], \"query\": \"\"}]}";
|
||||
RoleDescriptor rd = RoleDescriptor.parse("test", new BytesArray(json), false, XContentType.JSON);
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
import org.elasticsearch.common.collect.MapBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentType;
|
||||
import org.elasticsearch.rest.BytesRestResponse;
|
||||
import org.elasticsearch.rest.RestChannel;
|
||||
import org.elasticsearch.rest.RestResponse;
|
||||
import org.elasticsearch.test.ESTestCase;
|
||||
import org.elasticsearch.xpack.security.action.user.HasPrivilegesResponse;
|
||||
import org.elasticsearch.xpack.security.rest.action.user.RestHasPrivilegesAction.HasPrivilegesRestResponseBuilder;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
public class HasPrivilegesRestResponseTests extends ESTestCase {
|
||||
|
||||
public void testBuildValidJsonResponse() throws Exception {
|
||||
final HasPrivilegesRestResponseBuilder response = new HasPrivilegesRestResponseBuilder("daredevil", mock(RestChannel.class));
|
||||
final HasPrivilegesResponse actionResponse = new HasPrivilegesResponse(false,
|
||||
Collections.singletonMap("manage", true),
|
||||
Arrays.asList(
|
||||
new HasPrivilegesResponse.IndexPrivileges("staff",
|
||||
MapBuilder.<String, Boolean>newMapBuilder(new LinkedHashMap<>())
|
||||
.put("read", true).put("index", true).put("delete", false).put("manage", false).map()),
|
||||
new HasPrivilegesResponse.IndexPrivileges("customers",
|
||||
MapBuilder.<String, Boolean>newMapBuilder(new LinkedHashMap<>())
|
||||
.put("read", true).put("index", true).put("delete", true).put("manage", false).map())
|
||||
));
|
||||
final XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent());
|
||||
final RestResponse rest = response.buildResponse(actionResponse, builder);
|
||||
|
||||
assertThat(rest, instanceOf(BytesRestResponse.class));
|
||||
|
||||
final String json = rest.content().utf8ToString();
|
||||
assertThat(json, equalTo("{" +
|
||||
"\"username\":\"daredevil\"," +
|
||||
"\"has_all_requested\":false," +
|
||||
"\"cluster\":{\"manage\":true}," +
|
||||
"\"index\":{" +
|
||||
"\"staff\":{\"read\":true,\"index\":true,\"delete\":false,\"manage\":false}," +
|
||||
"\"customers\":{\"read\":true,\"index\":true,\"delete\":true,\"manage\":false}" +
|
||||
"}}"));
|
||||
}
|
||||
}
|
|
@ -88,6 +88,7 @@ cluster:admin/xpack/security/user/put
|
|||
cluster:admin/xpack/security/user/delete
|
||||
cluster:admin/xpack/security/user/get
|
||||
cluster:admin/xpack/security/user/set_enabled
|
||||
cluster:admin/xpack/security/user/has_privileges
|
||||
cluster:admin/xpack/security/role/put
|
||||
cluster:admin/xpack/security/role/delete
|
||||
cluster:admin/xpack/security/role/get
|
||||
|
|
Loading…
Reference in New Issue