Enhance security for alias and resize operations (elastic/x-pack-elasticsearch#3302)
This commit adds additional checks around resize operations and alias creation operations to add an extra layer of security around these APIs. Original commit: elastic/x-pack-elasticsearch@b79f16673c
This commit is contained in:
parent
2ca729afc2
commit
d48ad6dbd6
|
@ -82,7 +82,9 @@ import org.elasticsearch.xpack.extensions.XPackExtension;
|
||||||
import org.elasticsearch.xpack.extensions.XPackExtensionsService;
|
import org.elasticsearch.xpack.extensions.XPackExtensionsService;
|
||||||
import org.elasticsearch.xpack.security.action.filter.SecurityActionFilter;
|
import org.elasticsearch.xpack.security.action.filter.SecurityActionFilter;
|
||||||
import org.elasticsearch.xpack.security.action.interceptor.BulkShardRequestInterceptor;
|
import org.elasticsearch.xpack.security.action.interceptor.BulkShardRequestInterceptor;
|
||||||
|
import org.elasticsearch.xpack.security.action.interceptor.IndicesAliasesRequestInterceptor;
|
||||||
import org.elasticsearch.xpack.security.action.interceptor.RequestInterceptor;
|
import org.elasticsearch.xpack.security.action.interceptor.RequestInterceptor;
|
||||||
|
import org.elasticsearch.xpack.security.action.interceptor.ResizeRequestInterceptor;
|
||||||
import org.elasticsearch.xpack.security.action.interceptor.SearchRequestInterceptor;
|
import org.elasticsearch.xpack.security.action.interceptor.SearchRequestInterceptor;
|
||||||
import org.elasticsearch.xpack.security.action.interceptor.UpdateRequestInterceptor;
|
import org.elasticsearch.xpack.security.action.interceptor.UpdateRequestInterceptor;
|
||||||
import org.elasticsearch.xpack.security.action.realm.ClearRealmCacheAction;
|
import org.elasticsearch.xpack.security.action.realm.ClearRealmCacheAction;
|
||||||
|
@ -433,10 +435,12 @@ public class Security implements ActionPlugin, IngestPlugin, NetworkPlugin, Clus
|
||||||
|
|
||||||
final Set<RequestInterceptor> requestInterceptors;
|
final Set<RequestInterceptor> requestInterceptors;
|
||||||
if (XPackSettings.DLS_FLS_ENABLED.get(settings)) {
|
if (XPackSettings.DLS_FLS_ENABLED.get(settings)) {
|
||||||
requestInterceptors = Sets.newHashSet(
|
requestInterceptors = Collections.unmodifiableSet(Sets.newHashSet(
|
||||||
new SearchRequestInterceptor(settings, threadPool, licenseState),
|
new SearchRequestInterceptor(settings, threadPool, licenseState),
|
||||||
new UpdateRequestInterceptor(settings, threadPool, licenseState),
|
new UpdateRequestInterceptor(settings, threadPool, licenseState),
|
||||||
new BulkShardRequestInterceptor(settings, threadPool, licenseState));
|
new BulkShardRequestInterceptor(settings, threadPool, licenseState),
|
||||||
|
new ResizeRequestInterceptor(settings, threadPool, licenseState, auditTrailService),
|
||||||
|
new IndicesAliasesRequestInterceptor(threadPool.getThreadContext(), licenseState, auditTrailService)));
|
||||||
} else {
|
} else {
|
||||||
requestInterceptors = Collections.emptySet();
|
requestInterceptors = Collections.emptySet();
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,6 @@ import org.elasticsearch.action.support.ActionFilterChain;
|
||||||
import org.elasticsearch.action.support.ContextPreservingActionListener;
|
import org.elasticsearch.action.support.ContextPreservingActionListener;
|
||||||
import org.elasticsearch.action.support.DestructiveOperations;
|
import org.elasticsearch.action.support.DestructiveOperations;
|
||||||
import org.elasticsearch.common.component.AbstractComponent;
|
import org.elasticsearch.common.component.AbstractComponent;
|
||||||
import org.elasticsearch.common.inject.Inject;
|
|
||||||
import org.elasticsearch.common.settings.Settings;
|
import org.elasticsearch.common.settings.Settings;
|
||||||
import org.elasticsearch.common.util.concurrent.ThreadContext;
|
import org.elasticsearch.common.util.concurrent.ThreadContext;
|
||||||
import org.elasticsearch.license.LicenseUtils;
|
import org.elasticsearch.license.LicenseUtils;
|
||||||
|
@ -56,7 +55,6 @@ public class SecurityActionFilter extends AbstractComponent implements ActionFil
|
||||||
private final SecurityContext securityContext;
|
private final SecurityContext securityContext;
|
||||||
private final DestructiveOperations destructiveOperations;
|
private final DestructiveOperations destructiveOperations;
|
||||||
|
|
||||||
@Inject
|
|
||||||
public SecurityActionFilter(Settings settings, AuthenticationService authcService, AuthorizationService authzService,
|
public SecurityActionFilter(Settings settings, AuthenticationService authcService, AuthorizationService authzService,
|
||||||
XPackLicenseState licenseState, Set<RequestInterceptor> requestInterceptors, ThreadPool threadPool,
|
XPackLicenseState licenseState, Set<RequestInterceptor> requestInterceptors, ThreadPool threadPool,
|
||||||
SecurityContext securityContext, DestructiveOperations destructiveOperations) {
|
SecurityContext securityContext, DestructiveOperations destructiveOperations) {
|
||||||
|
@ -172,7 +170,7 @@ public class SecurityActionFilter extends AbstractComponent implements ActionFil
|
||||||
*/
|
*/
|
||||||
for (RequestInterceptor interceptor : requestInterceptors) {
|
for (RequestInterceptor interceptor : requestInterceptors) {
|
||||||
if (interceptor.supports(request)) {
|
if (interceptor.supports(request)) {
|
||||||
interceptor.intercept(request, user);
|
interceptor.intercept(request, user, runAsRoles != null ? runAsRoles : userRoles, securityAction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
listener.onResponse(null);
|
listener.onResponse(null);
|
||||||
|
|
|
@ -10,7 +10,6 @@ import org.elasticsearch.action.bulk.BulkItemRequest;
|
||||||
import org.elasticsearch.action.bulk.BulkShardRequest;
|
import org.elasticsearch.action.bulk.BulkShardRequest;
|
||||||
import org.elasticsearch.action.update.UpdateRequest;
|
import org.elasticsearch.action.update.UpdateRequest;
|
||||||
import org.elasticsearch.common.component.AbstractComponent;
|
import org.elasticsearch.common.component.AbstractComponent;
|
||||||
import org.elasticsearch.common.inject.Inject;
|
|
||||||
import org.elasticsearch.common.settings.Settings;
|
import org.elasticsearch.common.settings.Settings;
|
||||||
import org.elasticsearch.common.util.concurrent.ThreadContext;
|
import org.elasticsearch.common.util.concurrent.ThreadContext;
|
||||||
import org.elasticsearch.license.XPackLicenseState;
|
import org.elasticsearch.license.XPackLicenseState;
|
||||||
|
@ -19,6 +18,7 @@ import org.elasticsearch.threadpool.ThreadPool;
|
||||||
import org.elasticsearch.transport.TransportRequest;
|
import org.elasticsearch.transport.TransportRequest;
|
||||||
import org.elasticsearch.xpack.security.authz.AuthorizationService;
|
import org.elasticsearch.xpack.security.authz.AuthorizationService;
|
||||||
import org.elasticsearch.xpack.security.authz.accesscontrol.IndicesAccessControl;
|
import org.elasticsearch.xpack.security.authz.accesscontrol.IndicesAccessControl;
|
||||||
|
import org.elasticsearch.xpack.security.authz.permission.Role;
|
||||||
import org.elasticsearch.xpack.security.user.User;
|
import org.elasticsearch.xpack.security.user.User;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -29,14 +29,13 @@ public class BulkShardRequestInterceptor extends AbstractComponent implements Re
|
||||||
private final ThreadContext threadContext;
|
private final ThreadContext threadContext;
|
||||||
private final XPackLicenseState licenseState;
|
private final XPackLicenseState licenseState;
|
||||||
|
|
||||||
@Inject
|
|
||||||
public BulkShardRequestInterceptor(Settings settings, ThreadPool threadPool, XPackLicenseState licenseState) {
|
public BulkShardRequestInterceptor(Settings settings, ThreadPool threadPool, XPackLicenseState licenseState) {
|
||||||
super(settings);
|
super(settings);
|
||||||
this.threadContext = threadPool.getThreadContext();
|
this.threadContext = threadPool.getThreadContext();
|
||||||
this.licenseState = licenseState;
|
this.licenseState = licenseState;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void intercept(BulkShardRequest request, User user) {
|
public void intercept(BulkShardRequest request, User user, Role userPermissions, String action) {
|
||||||
if (licenseState.isDocumentAndFieldLevelSecurityAllowed() == false) {
|
if (licenseState.isDocumentAndFieldLevelSecurityAllowed() == false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import org.elasticsearch.common.util.concurrent.ThreadContext;
|
||||||
import org.elasticsearch.license.XPackLicenseState;
|
import org.elasticsearch.license.XPackLicenseState;
|
||||||
import org.elasticsearch.xpack.security.authz.AuthorizationService;
|
import org.elasticsearch.xpack.security.authz.AuthorizationService;
|
||||||
import org.elasticsearch.xpack.security.authz.accesscontrol.IndicesAccessControl;
|
import org.elasticsearch.xpack.security.authz.accesscontrol.IndicesAccessControl;
|
||||||
|
import org.elasticsearch.xpack.security.authz.permission.Role;
|
||||||
import org.elasticsearch.xpack.security.user.User;
|
import org.elasticsearch.xpack.security.user.User;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -31,7 +32,7 @@ abstract class FieldAndDocumentLevelSecurityRequestInterceptor<Request extends I
|
||||||
this.licenseState = licenseState;
|
this.licenseState = licenseState;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void intercept(Request request, User user) {
|
public void intercept(Request request, User user, Role userPermissions, String action) {
|
||||||
if (licenseState.isDocumentAndFieldLevelSecurityAllowed() == false) {
|
if (licenseState.isDocumentAndFieldLevelSecurityAllowed() == false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
/*
|
||||||
|
* 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.interceptor;
|
||||||
|
|
||||||
|
import org.apache.lucene.util.automaton.Automaton;
|
||||||
|
import org.apache.lucene.util.automaton.Operations;
|
||||||
|
import org.elasticsearch.ElasticsearchSecurityException;
|
||||||
|
import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest;
|
||||||
|
import org.elasticsearch.common.util.concurrent.ThreadContext;
|
||||||
|
import org.elasticsearch.license.XPackLicenseState;
|
||||||
|
import org.elasticsearch.rest.RestStatus;
|
||||||
|
import org.elasticsearch.transport.TransportRequest;
|
||||||
|
import org.elasticsearch.xpack.security.audit.AuditTrailService;
|
||||||
|
import org.elasticsearch.xpack.security.authz.AuthorizationService;
|
||||||
|
import org.elasticsearch.xpack.security.authz.accesscontrol.IndicesAccessControl;
|
||||||
|
import org.elasticsearch.xpack.security.authz.permission.Role;
|
||||||
|
import org.elasticsearch.xpack.security.support.Exceptions;
|
||||||
|
import org.elasticsearch.xpack.security.user.User;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class IndicesAliasesRequestInterceptor implements RequestInterceptor<IndicesAliasesRequest> {
|
||||||
|
|
||||||
|
private final ThreadContext threadContext;
|
||||||
|
private final XPackLicenseState licenseState;
|
||||||
|
private final AuditTrailService auditTrailService;
|
||||||
|
|
||||||
|
public IndicesAliasesRequestInterceptor(ThreadContext threadContext, XPackLicenseState licenseState,
|
||||||
|
AuditTrailService auditTrailService) {
|
||||||
|
this.threadContext = threadContext;
|
||||||
|
this.licenseState = licenseState;
|
||||||
|
this.auditTrailService = auditTrailService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void intercept(IndicesAliasesRequest request, User user, Role userPermissions, String action) {
|
||||||
|
if (licenseState.isDocumentAndFieldLevelSecurityAllowed()) {
|
||||||
|
IndicesAccessControl indicesAccessControl = threadContext.getTransient(AuthorizationService.INDICES_PERMISSIONS_KEY);
|
||||||
|
for (IndicesAliasesRequest.AliasActions aliasAction : request.getAliasActions()) {
|
||||||
|
if (aliasAction.actionType() == IndicesAliasesRequest.AliasActions.Type.ADD) {
|
||||||
|
for (String index : aliasAction.indices()) {
|
||||||
|
IndicesAccessControl.IndexAccessControl indexAccessControl = indicesAccessControl.getIndexPermissions(index);
|
||||||
|
if (indexAccessControl != null) {
|
||||||
|
final boolean fls = indexAccessControl.getFieldPermissions().hasFieldLevelSecurity();
|
||||||
|
final boolean dls = indexAccessControl.getQueries() != null;
|
||||||
|
if (fls || dls) {
|
||||||
|
throw new ElasticsearchSecurityException("Alias requests are not allowed for users who have " +
|
||||||
|
"field or document level security enabled on one of the indices", RestStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Automaton> permissionsMap = new HashMap<>();
|
||||||
|
for (IndicesAliasesRequest.AliasActions aliasAction : request.getAliasActions()) {
|
||||||
|
if (aliasAction.actionType() == IndicesAliasesRequest.AliasActions.Type.ADD) {
|
||||||
|
for (String index : aliasAction.indices()) {
|
||||||
|
Automaton indexPermissions = permissionsMap.computeIfAbsent(index, userPermissions.indices()::allowedActionsMatcher);
|
||||||
|
for (String alias : aliasAction.aliases()) {
|
||||||
|
Automaton aliasPermissions =
|
||||||
|
permissionsMap.computeIfAbsent(alias, userPermissions.indices()::allowedActionsMatcher);
|
||||||
|
if (Operations.subsetOf(aliasPermissions, indexPermissions) == false) {
|
||||||
|
// TODO we've already audited a access granted event so this is going to look ugly
|
||||||
|
auditTrailService.accessDenied(user, action, request, userPermissions.names());
|
||||||
|
throw Exceptions.authorizationError("Adding an alias is not allowed when the alias " +
|
||||||
|
"has more permissions than any of the indices");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supports(TransportRequest request) {
|
||||||
|
return request instanceof IndicesAliasesRequest;
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@
|
||||||
*/
|
*/
|
||||||
package org.elasticsearch.xpack.security.action.interceptor;
|
package org.elasticsearch.xpack.security.action.interceptor;
|
||||||
|
|
||||||
|
import org.elasticsearch.xpack.security.authz.permission.Role;
|
||||||
import org.elasticsearch.xpack.security.user.User;
|
import org.elasticsearch.xpack.security.user.User;
|
||||||
import org.elasticsearch.transport.TransportRequest;
|
import org.elasticsearch.transport.TransportRequest;
|
||||||
|
|
||||||
|
@ -17,7 +18,7 @@ public interface RequestInterceptor<Request> {
|
||||||
* If {@link #supports(TransportRequest)} returns <code>true</code> this interceptor will introspect the request
|
* If {@link #supports(TransportRequest)} returns <code>true</code> this interceptor will introspect the request
|
||||||
* and potentially modify it.
|
* and potentially modify it.
|
||||||
*/
|
*/
|
||||||
void intercept(Request request, User user);
|
void intercept(Request request, User user, Role userPermissions, String action);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether this request interceptor should intercept the specified request.
|
* Returns whether this request interceptor should intercept the specified request.
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
/*
|
||||||
|
* 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.interceptor;
|
||||||
|
|
||||||
|
import org.apache.lucene.util.automaton.Automaton;
|
||||||
|
import org.apache.lucene.util.automaton.Operations;
|
||||||
|
import org.elasticsearch.ElasticsearchSecurityException;
|
||||||
|
import org.elasticsearch.action.admin.indices.shrink.ResizeRequest;
|
||||||
|
import org.elasticsearch.common.component.AbstractComponent;
|
||||||
|
import org.elasticsearch.common.settings.Settings;
|
||||||
|
import org.elasticsearch.common.util.concurrent.ThreadContext;
|
||||||
|
import org.elasticsearch.license.XPackLicenseState;
|
||||||
|
import org.elasticsearch.rest.RestStatus;
|
||||||
|
import org.elasticsearch.threadpool.ThreadPool;
|
||||||
|
import org.elasticsearch.transport.TransportRequest;
|
||||||
|
import org.elasticsearch.xpack.security.audit.AuditTrailService;
|
||||||
|
import org.elasticsearch.xpack.security.authz.AuthorizationService;
|
||||||
|
import org.elasticsearch.xpack.security.authz.accesscontrol.IndicesAccessControl;
|
||||||
|
import org.elasticsearch.xpack.security.authz.permission.Role;
|
||||||
|
import org.elasticsearch.xpack.security.support.Exceptions;
|
||||||
|
import org.elasticsearch.xpack.security.user.User;
|
||||||
|
|
||||||
|
public final class ResizeRequestInterceptor extends AbstractComponent implements RequestInterceptor<ResizeRequest> {
|
||||||
|
|
||||||
|
private final ThreadContext threadContext;
|
||||||
|
private final XPackLicenseState licenseState;
|
||||||
|
private final AuditTrailService auditTrailService;
|
||||||
|
|
||||||
|
public ResizeRequestInterceptor(Settings settings, ThreadPool threadPool, XPackLicenseState licenseState,
|
||||||
|
AuditTrailService auditTrailService) {
|
||||||
|
super(settings);
|
||||||
|
this.threadContext = threadPool.getThreadContext();
|
||||||
|
this.licenseState = licenseState;
|
||||||
|
this.auditTrailService = auditTrailService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void intercept(ResizeRequest request, User user, Role userPermissions, String action) {
|
||||||
|
if (licenseState.isDocumentAndFieldLevelSecurityAllowed()) {
|
||||||
|
IndicesAccessControl indicesAccessControl = threadContext.getTransient(AuthorizationService.INDICES_PERMISSIONS_KEY);
|
||||||
|
IndicesAccessControl.IndexAccessControl indexAccessControl = indicesAccessControl.getIndexPermissions(request.getSourceIndex());
|
||||||
|
if (indexAccessControl != null) {
|
||||||
|
final boolean fls = indexAccessControl.getFieldPermissions().hasFieldLevelSecurity();
|
||||||
|
final boolean dls = indexAccessControl.getQueries() != null;
|
||||||
|
if (fls || dls) {
|
||||||
|
throw new ElasticsearchSecurityException("Resize requests are not allowed for users when " +
|
||||||
|
"field or document level security is enabled on the source index", RestStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure that the user would have the same level of access OR less on the target index
|
||||||
|
final Automaton sourceIndexPermissions = userPermissions.indices().allowedActionsMatcher(request.getSourceIndex());
|
||||||
|
final Automaton targetIndexPermissions = userPermissions.indices().allowedActionsMatcher(request.getTargetIndexRequest().index());
|
||||||
|
if (Operations.subsetOf(targetIndexPermissions, sourceIndexPermissions) == false) {
|
||||||
|
// TODO we've already audited a access granted event so this is going to look ugly
|
||||||
|
auditTrailService.accessDenied(user, action, request, userPermissions.names());
|
||||||
|
throw Exceptions.authorizationError("Resizing an index is not allowed when the target index " +
|
||||||
|
"has more permissions than the source index");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supports(TransportRequest request) {
|
||||||
|
return request instanceof ResizeRequest;
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,7 +7,6 @@ package org.elasticsearch.xpack.security.action.interceptor;
|
||||||
|
|
||||||
import org.elasticsearch.ElasticsearchSecurityException;
|
import org.elasticsearch.ElasticsearchSecurityException;
|
||||||
import org.elasticsearch.action.search.SearchRequest;
|
import org.elasticsearch.action.search.SearchRequest;
|
||||||
import org.elasticsearch.common.inject.Inject;
|
|
||||||
import org.elasticsearch.common.settings.Settings;
|
import org.elasticsearch.common.settings.Settings;
|
||||||
import org.elasticsearch.license.XPackLicenseState;
|
import org.elasticsearch.license.XPackLicenseState;
|
||||||
import org.elasticsearch.rest.RestStatus;
|
import org.elasticsearch.rest.RestStatus;
|
||||||
|
@ -19,7 +18,6 @@ import org.elasticsearch.transport.TransportRequest;
|
||||||
*/
|
*/
|
||||||
public class SearchRequestInterceptor extends FieldAndDocumentLevelSecurityRequestInterceptor<SearchRequest> {
|
public class SearchRequestInterceptor extends FieldAndDocumentLevelSecurityRequestInterceptor<SearchRequest> {
|
||||||
|
|
||||||
@Inject
|
|
||||||
public SearchRequestInterceptor(Settings settings, ThreadPool threadPool, XPackLicenseState licenseState) {
|
public SearchRequestInterceptor(Settings settings, ThreadPool threadPool, XPackLicenseState licenseState) {
|
||||||
super(settings, threadPool.getThreadContext(), licenseState);
|
super(settings, threadPool.getThreadContext(), licenseState);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ package org.elasticsearch.xpack.security.action.interceptor;
|
||||||
|
|
||||||
import org.elasticsearch.ElasticsearchSecurityException;
|
import org.elasticsearch.ElasticsearchSecurityException;
|
||||||
import org.elasticsearch.action.update.UpdateRequest;
|
import org.elasticsearch.action.update.UpdateRequest;
|
||||||
import org.elasticsearch.common.inject.Inject;
|
|
||||||
import org.elasticsearch.common.settings.Settings;
|
import org.elasticsearch.common.settings.Settings;
|
||||||
import org.elasticsearch.license.XPackLicenseState;
|
import org.elasticsearch.license.XPackLicenseState;
|
||||||
import org.elasticsearch.rest.RestStatus;
|
import org.elasticsearch.rest.RestStatus;
|
||||||
|
@ -23,7 +22,6 @@ import org.elasticsearch.transport.TransportRequest;
|
||||||
*/
|
*/
|
||||||
public class UpdateRequestInterceptor extends FieldAndDocumentLevelSecurityRequestInterceptor<UpdateRequest> {
|
public class UpdateRequestInterceptor extends FieldAndDocumentLevelSecurityRequestInterceptor<UpdateRequest> {
|
||||||
|
|
||||||
@Inject
|
|
||||||
public UpdateRequestInterceptor(Settings settings, ThreadPool threadPool, XPackLicenseState licenseState) {
|
public UpdateRequestInterceptor(Settings settings, ThreadPool threadPool, XPackLicenseState licenseState) {
|
||||||
super(settings, threadPool.getThreadContext(), licenseState);
|
super(settings, threadPool.getThreadContext(), licenseState);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
*/
|
*/
|
||||||
package org.elasticsearch.xpack.security.authz.permission;
|
package org.elasticsearch.xpack.security.authz.permission;
|
||||||
|
|
||||||
|
import org.apache.lucene.util.automaton.Automaton;
|
||||||
import org.elasticsearch.cluster.metadata.AliasOrIndex;
|
import org.elasticsearch.cluster.metadata.AliasOrIndex;
|
||||||
import org.elasticsearch.cluster.metadata.IndexMetaData;
|
import org.elasticsearch.cluster.metadata.IndexMetaData;
|
||||||
import org.elasticsearch.cluster.metadata.MetaData;
|
import org.elasticsearch.cluster.metadata.MetaData;
|
||||||
|
@ -90,6 +91,16 @@ public final class IndicesPermission implements Iterable<IndicesPermission.Group
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Automaton allowedActionsMatcher(String index) {
|
||||||
|
List<Automaton> automatonList = new ArrayList<>();
|
||||||
|
for (Group group : groups) {
|
||||||
|
if (group.indexNameMatcher.test(index)) {
|
||||||
|
automatonList.add(group.privilege.getAutomaton());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return automatonList.isEmpty() ? Automatons.EMPTY : Automatons.unionAndMinimize(automatonList);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authorizes the provided action against the provided indices, given the current cluster metadata
|
* Authorizes the provided action against the provided indices, given the current cluster metadata
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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.interceptor;
|
||||||
|
|
||||||
|
import org.elasticsearch.ElasticsearchSecurityException;
|
||||||
|
import org.elasticsearch.action.admin.indices.alias.IndicesAliasesAction;
|
||||||
|
import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest;
|
||||||
|
import org.elasticsearch.common.bytes.BytesArray;
|
||||||
|
import org.elasticsearch.common.bytes.BytesReference;
|
||||||
|
import org.elasticsearch.common.settings.Settings;
|
||||||
|
import org.elasticsearch.common.util.concurrent.ThreadContext;
|
||||||
|
import org.elasticsearch.license.XPackLicenseState;
|
||||||
|
import org.elasticsearch.test.ESTestCase;
|
||||||
|
import org.elasticsearch.xpack.security.audit.AuditTrailService;
|
||||||
|
import org.elasticsearch.xpack.security.authz.AuthorizationService;
|
||||||
|
import org.elasticsearch.xpack.security.authz.accesscontrol.IndicesAccessControl;
|
||||||
|
import org.elasticsearch.xpack.security.authz.permission.FieldPermissions;
|
||||||
|
import org.elasticsearch.xpack.security.authz.permission.FieldPermissionsDefinition;
|
||||||
|
import org.elasticsearch.xpack.security.authz.permission.Role;
|
||||||
|
import org.elasticsearch.xpack.security.authz.privilege.IndexPrivilege;
|
||||||
|
import org.elasticsearch.xpack.security.user.User;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class IndicesAliasesRequestInterceptorTests extends ESTestCase {
|
||||||
|
|
||||||
|
public void testInterceptorThrowsWhenFLSDLSEnabled() {
|
||||||
|
XPackLicenseState licenseState = mock(XPackLicenseState.class);
|
||||||
|
when(licenseState.isAuditingAllowed()).thenReturn(true);
|
||||||
|
when(licenseState.isDocumentAndFieldLevelSecurityAllowed()).thenReturn(true);
|
||||||
|
ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
|
||||||
|
AuditTrailService auditTrailService = new AuditTrailService(Settings.EMPTY, Collections.emptyList(), licenseState);
|
||||||
|
User user = new User("john", "role");
|
||||||
|
final FieldPermissions fieldPermissions;
|
||||||
|
final boolean useFls = randomBoolean();
|
||||||
|
if (useFls) {
|
||||||
|
fieldPermissions = new FieldPermissions(new FieldPermissionsDefinition(new String[] { "foo" }, null));
|
||||||
|
} else {
|
||||||
|
fieldPermissions = new FieldPermissions();
|
||||||
|
}
|
||||||
|
final boolean useDls = (useFls == false) || randomBoolean();
|
||||||
|
final Set<BytesReference> queries;
|
||||||
|
if (useDls) {
|
||||||
|
queries = Collections.singleton(new BytesArray(randomAlphaOfLengthBetween(2, 8)));
|
||||||
|
} else {
|
||||||
|
queries = null;
|
||||||
|
}
|
||||||
|
Role role = Role.builder().add(fieldPermissions, queries, IndexPrivilege.ALL, "foo").build();
|
||||||
|
final String action = IndicesAliasesAction.NAME;
|
||||||
|
IndicesAccessControl accessControl = new IndicesAccessControl(true, Collections.singletonMap("foo",
|
||||||
|
new IndicesAccessControl.IndexAccessControl(true, fieldPermissions, queries)));
|
||||||
|
threadContext.putTransient(AuthorizationService.INDICES_PERMISSIONS_KEY, accessControl);
|
||||||
|
|
||||||
|
IndicesAliasesRequestInterceptor interceptor =
|
||||||
|
new IndicesAliasesRequestInterceptor(threadContext, licenseState, auditTrailService);
|
||||||
|
|
||||||
|
IndicesAliasesRequest indicesAliasesRequest = new IndicesAliasesRequest();
|
||||||
|
if (randomBoolean()) {
|
||||||
|
indicesAliasesRequest.addAliasAction(IndicesAliasesRequest.AliasActions.remove().index("bar").alias(randomAlphaOfLength(4)));
|
||||||
|
}
|
||||||
|
indicesAliasesRequest.addAliasAction(IndicesAliasesRequest.AliasActions.add().index("foo").alias(randomAlphaOfLength(4)));
|
||||||
|
if (randomBoolean()) {
|
||||||
|
indicesAliasesRequest.addAliasAction(IndicesAliasesRequest.AliasActions.removeIndex().index("foofoo"));
|
||||||
|
}
|
||||||
|
ElasticsearchSecurityException securityException = expectThrows(ElasticsearchSecurityException.class,
|
||||||
|
() -> interceptor.intercept(indicesAliasesRequest, user, role, action));
|
||||||
|
assertEquals("Alias requests are not allowed for users who have field or document level security enabled on one of the indices",
|
||||||
|
securityException.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testInterceptorThrowsWhenTargetHasGreaterPermissions() throws Exception {
|
||||||
|
XPackLicenseState licenseState = mock(XPackLicenseState.class);
|
||||||
|
when(licenseState.isAuditingAllowed()).thenReturn(true);
|
||||||
|
when(licenseState.isDocumentAndFieldLevelSecurityAllowed()).thenReturn(true);
|
||||||
|
ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
|
||||||
|
AuditTrailService auditTrailService = new AuditTrailService(Settings.EMPTY, Collections.emptyList(), licenseState);
|
||||||
|
User user = new User("john", "role");
|
||||||
|
Role role = Role.builder()
|
||||||
|
.add(IndexPrivilege.ALL, "alias")
|
||||||
|
.add(IndexPrivilege.READ, "index")
|
||||||
|
.build();
|
||||||
|
final String action = IndicesAliasesAction.NAME;
|
||||||
|
IndicesAccessControl accessControl = new IndicesAccessControl(true, Collections.emptyMap());
|
||||||
|
threadContext.putTransient(AuthorizationService.INDICES_PERMISSIONS_KEY, accessControl);
|
||||||
|
IndicesAliasesRequestInterceptor interceptor =
|
||||||
|
new IndicesAliasesRequestInterceptor(threadContext, licenseState, auditTrailService);
|
||||||
|
|
||||||
|
final IndicesAliasesRequest indicesAliasesRequest = new IndicesAliasesRequest();
|
||||||
|
if (randomBoolean()) {
|
||||||
|
indicesAliasesRequest.addAliasAction(IndicesAliasesRequest.AliasActions.remove().index("bar").alias(randomAlphaOfLength(4)));
|
||||||
|
}
|
||||||
|
indicesAliasesRequest.addAliasAction(IndicesAliasesRequest.AliasActions.add().index("index").alias("alias"));
|
||||||
|
if (randomBoolean()) {
|
||||||
|
indicesAliasesRequest.addAliasAction(IndicesAliasesRequest.AliasActions.removeIndex().index("foofoo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
ElasticsearchSecurityException securityException = expectThrows(ElasticsearchSecurityException.class,
|
||||||
|
() -> interceptor.intercept(indicesAliasesRequest, user, role, action));
|
||||||
|
assertEquals("Adding an alias is not allowed when the alias has more permissions than any of the indices",
|
||||||
|
securityException.getMessage());
|
||||||
|
|
||||||
|
// swap target and source for success
|
||||||
|
final IndicesAliasesRequest successRequest = new IndicesAliasesRequest();
|
||||||
|
if (randomBoolean()) {
|
||||||
|
successRequest.addAliasAction(IndicesAliasesRequest.AliasActions.remove().index("bar").alias(randomAlphaOfLength(4)));
|
||||||
|
}
|
||||||
|
successRequest.addAliasAction(IndicesAliasesRequest.AliasActions.add().index("alias").alias("index"));
|
||||||
|
if (randomBoolean()) {
|
||||||
|
successRequest.addAliasAction(IndicesAliasesRequest.AliasActions.removeIndex().index("foofoo"));
|
||||||
|
}
|
||||||
|
interceptor.intercept(successRequest, user, role, action);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
/*
|
||||||
|
* 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.interceptor;
|
||||||
|
|
||||||
|
import org.elasticsearch.ElasticsearchSecurityException;
|
||||||
|
import org.elasticsearch.action.admin.indices.shrink.ResizeAction;
|
||||||
|
import org.elasticsearch.action.admin.indices.shrink.ResizeRequest;
|
||||||
|
import org.elasticsearch.action.admin.indices.shrink.ShrinkAction;
|
||||||
|
import org.elasticsearch.common.bytes.BytesArray;
|
||||||
|
import org.elasticsearch.common.bytes.BytesReference;
|
||||||
|
import org.elasticsearch.common.settings.Settings;
|
||||||
|
import org.elasticsearch.common.util.concurrent.ThreadContext;
|
||||||
|
import org.elasticsearch.license.XPackLicenseState;
|
||||||
|
import org.elasticsearch.test.ESTestCase;
|
||||||
|
import org.elasticsearch.threadpool.ThreadPool;
|
||||||
|
import org.elasticsearch.xpack.security.audit.AuditTrailService;
|
||||||
|
import org.elasticsearch.xpack.security.authz.AuthorizationService;
|
||||||
|
import org.elasticsearch.xpack.security.authz.accesscontrol.IndicesAccessControl;
|
||||||
|
import org.elasticsearch.xpack.security.authz.permission.FieldPermissions;
|
||||||
|
import org.elasticsearch.xpack.security.authz.permission.FieldPermissionsDefinition;
|
||||||
|
import org.elasticsearch.xpack.security.authz.permission.Role;
|
||||||
|
import org.elasticsearch.xpack.security.authz.privilege.IndexPrivilege;
|
||||||
|
import org.elasticsearch.xpack.security.user.User;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class ResizeRequestInterceptorTests extends ESTestCase {
|
||||||
|
|
||||||
|
public void testResizeRequestInterceptorThrowsWhenFLSDLSEnabled() {
|
||||||
|
XPackLicenseState licenseState = mock(XPackLicenseState.class);
|
||||||
|
when(licenseState.isAuditingAllowed()).thenReturn(true);
|
||||||
|
when(licenseState.isDocumentAndFieldLevelSecurityAllowed()).thenReturn(true);
|
||||||
|
ThreadPool threadPool = mock(ThreadPool.class);
|
||||||
|
ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
|
||||||
|
when(threadPool.getThreadContext()).thenReturn(threadContext);
|
||||||
|
AuditTrailService auditTrailService = new AuditTrailService(Settings.EMPTY, Collections.emptyList(), licenseState);
|
||||||
|
User user = new User("john", "role");
|
||||||
|
final FieldPermissions fieldPermissions;
|
||||||
|
final boolean useFls = randomBoolean();
|
||||||
|
if (useFls) {
|
||||||
|
fieldPermissions = new FieldPermissions(new FieldPermissionsDefinition(new String[] { "foo" }, null));
|
||||||
|
} else {
|
||||||
|
fieldPermissions = new FieldPermissions();
|
||||||
|
}
|
||||||
|
final boolean useDls = (useFls == false) || randomBoolean();
|
||||||
|
final Set<BytesReference> queries;
|
||||||
|
if (useDls) {
|
||||||
|
queries = Collections.singleton(new BytesArray(randomAlphaOfLengthBetween(2, 8)));
|
||||||
|
} else {
|
||||||
|
queries = null;
|
||||||
|
}
|
||||||
|
Role role = Role.builder().add(fieldPermissions, queries, IndexPrivilege.ALL, "foo").build();
|
||||||
|
final String action = randomFrom(ShrinkAction.NAME, ResizeAction.NAME);
|
||||||
|
IndicesAccessControl accessControl = new IndicesAccessControl(true, Collections.singletonMap("foo",
|
||||||
|
new IndicesAccessControl.IndexAccessControl(true, fieldPermissions, queries)));
|
||||||
|
threadContext.putTransient(AuthorizationService.INDICES_PERMISSIONS_KEY, accessControl);
|
||||||
|
|
||||||
|
ResizeRequestInterceptor resizeRequestInterceptor =
|
||||||
|
new ResizeRequestInterceptor(Settings.EMPTY, threadPool, licenseState, auditTrailService);
|
||||||
|
|
||||||
|
ElasticsearchSecurityException securityException = expectThrows(ElasticsearchSecurityException.class,
|
||||||
|
() -> resizeRequestInterceptor.intercept(new ResizeRequest("bar", "foo"), user, role, action));
|
||||||
|
assertEquals("Resize requests are not allowed for users when field or document level security is enabled on the source index",
|
||||||
|
securityException.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testResizeRequestInterceptorThrowsWhenTargetHasGreaterPermissions() throws Exception {
|
||||||
|
XPackLicenseState licenseState = mock(XPackLicenseState.class);
|
||||||
|
when(licenseState.isAuditingAllowed()).thenReturn(true);
|
||||||
|
when(licenseState.isDocumentAndFieldLevelSecurityAllowed()).thenReturn(true);
|
||||||
|
ThreadPool threadPool = mock(ThreadPool.class);
|
||||||
|
ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
|
||||||
|
when(threadPool.getThreadContext()).thenReturn(threadContext);
|
||||||
|
AuditTrailService auditTrailService = new AuditTrailService(Settings.EMPTY, Collections.emptyList(), licenseState);
|
||||||
|
User user = new User("john", "role");
|
||||||
|
Role role = Role.builder()
|
||||||
|
.add(IndexPrivilege.ALL, "target")
|
||||||
|
.add(IndexPrivilege.READ, "source")
|
||||||
|
.build();
|
||||||
|
final String action = randomFrom(ShrinkAction.NAME, ResizeAction.NAME);
|
||||||
|
IndicesAccessControl accessControl = new IndicesAccessControl(true, Collections.emptyMap());
|
||||||
|
threadContext.putTransient(AuthorizationService.INDICES_PERMISSIONS_KEY, accessControl);
|
||||||
|
ResizeRequestInterceptor resizeRequestInterceptor =
|
||||||
|
new ResizeRequestInterceptor(Settings.EMPTY, threadPool, licenseState, auditTrailService);
|
||||||
|
ElasticsearchSecurityException securityException = expectThrows(ElasticsearchSecurityException.class,
|
||||||
|
() -> resizeRequestInterceptor.intercept(new ResizeRequest("target", "source"), user, role, action));
|
||||||
|
assertEquals("Resizing an index is not allowed when the target index has more permissions than the source index",
|
||||||
|
securityException.getMessage());
|
||||||
|
|
||||||
|
// swap target and source for success
|
||||||
|
resizeRequestInterceptor.intercept(new ResizeRequest("source", "target"), user, role, action);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue