From d48ad6dbd67114e79f0830838d9452312501607b Mon Sep 17 00:00:00 2001 From: Jay Modi Date: Tue, 12 Dec 2017 12:01:27 -0700 Subject: [PATCH] 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@b79f16673c6262d4ba6fb27679fe9e2740186a23 --- .../xpack/security/Security.java | 8 +- .../action/filter/SecurityActionFilter.java | 12 +- .../BulkShardRequestInterceptor.java | 5 +- ...cumentLevelSecurityRequestInterceptor.java | 3 +- .../IndicesAliasesRequestInterceptor.java | 84 ++++++++++++ .../interceptor/RequestInterceptor.java | 3 +- .../interceptor/ResizeRequestInterceptor.java | 70 ++++++++++ .../interceptor/SearchRequestInterceptor.java | 2 - .../interceptor/UpdateRequestInterceptor.java | 2 - .../authz/permission/IndicesPermission.java | 11 ++ ...IndicesAliasesRequestInterceptorTests.java | 120 ++++++++++++++++++ .../ResizeRequestInterceptorTests.java | 100 +++++++++++++++ 12 files changed, 402 insertions(+), 18 deletions(-) create mode 100644 plugin/src/main/java/org/elasticsearch/xpack/security/action/interceptor/IndicesAliasesRequestInterceptor.java create mode 100644 plugin/src/main/java/org/elasticsearch/xpack/security/action/interceptor/ResizeRequestInterceptor.java create mode 100644 plugin/src/test/java/org/elasticsearch/xpack/security/action/interceptor/IndicesAliasesRequestInterceptorTests.java create mode 100644 plugin/src/test/java/org/elasticsearch/xpack/security/action/interceptor/ResizeRequestInterceptorTests.java diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/Security.java b/plugin/src/main/java/org/elasticsearch/xpack/security/Security.java index 92d2754a36d..48146b89fe7 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -82,7 +82,9 @@ import org.elasticsearch.xpack.extensions.XPackExtension; import org.elasticsearch.xpack.extensions.XPackExtensionsService; import org.elasticsearch.xpack.security.action.filter.SecurityActionFilter; 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.ResizeRequestInterceptor; import org.elasticsearch.xpack.security.action.interceptor.SearchRequestInterceptor; import org.elasticsearch.xpack.security.action.interceptor.UpdateRequestInterceptor; import org.elasticsearch.xpack.security.action.realm.ClearRealmCacheAction; @@ -433,10 +435,12 @@ public class Security implements ActionPlugin, IngestPlugin, NetworkPlugin, Clus final Set requestInterceptors; if (XPackSettings.DLS_FLS_ENABLED.get(settings)) { - requestInterceptors = Sets.newHashSet( + requestInterceptors = Collections.unmodifiableSet(Sets.newHashSet( new SearchRequestInterceptor(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 { requestInterceptors = Collections.emptySet(); } diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilter.java b/plugin/src/main/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilter.java index a106a21aa13..aacd19981c3 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilter.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilter.java @@ -18,7 +18,6 @@ import org.elasticsearch.action.support.ActionFilterChain; import org.elasticsearch.action.support.ContextPreservingActionListener; import org.elasticsearch.action.support.DestructiveOperations; import org.elasticsearch.common.component.AbstractComponent; -import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.license.LicenseUtils; @@ -56,7 +55,6 @@ public class SecurityActionFilter extends AbstractComponent implements ActionFil private final SecurityContext securityContext; private final DestructiveOperations destructiveOperations; - @Inject public SecurityActionFilter(Settings settings, AuthenticationService authcService, AuthorizationService authzService, XPackLicenseState licenseState, Set requestInterceptors, ThreadPool threadPool, SecurityContext securityContext, DestructiveOperations destructiveOperations) { @@ -166,13 +164,13 @@ public class SecurityActionFilter extends AbstractComponent implements ActionFil authzService.authorize(authentication, securityAction, request, userRoles, runAsRoles); final User user = authentication.getUser(); - /* - * We use a separate concept for code that needs to be run after authentication and authorization that could - * affect the running of the action. This is done to make it more clear of the state of the request. - */ + /* + * We use a separate concept for code that needs to be run after authentication and authorization that could + * affect the running of the action. This is done to make it more clear of the state of the request. + */ for (RequestInterceptor interceptor : requestInterceptors) { if (interceptor.supports(request)) { - interceptor.intercept(request, user); + interceptor.intercept(request, user, runAsRoles != null ? runAsRoles : userRoles, securityAction); } } listener.onResponse(null); diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/action/interceptor/BulkShardRequestInterceptor.java b/plugin/src/main/java/org/elasticsearch/xpack/security/action/interceptor/BulkShardRequestInterceptor.java index 51419772fdd..5c205f388f9 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/action/interceptor/BulkShardRequestInterceptor.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/action/interceptor/BulkShardRequestInterceptor.java @@ -10,7 +10,6 @@ import org.elasticsearch.action.bulk.BulkItemRequest; import org.elasticsearch.action.bulk.BulkShardRequest; import org.elasticsearch.action.update.UpdateRequest; import org.elasticsearch.common.component.AbstractComponent; -import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.license.XPackLicenseState; @@ -19,6 +18,7 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportRequest; 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.user.User; /** @@ -29,14 +29,13 @@ public class BulkShardRequestInterceptor extends AbstractComponent implements Re private final ThreadContext threadContext; private final XPackLicenseState licenseState; - @Inject public BulkShardRequestInterceptor(Settings settings, ThreadPool threadPool, XPackLicenseState licenseState) { super(settings); this.threadContext = threadPool.getThreadContext(); 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) { return; } diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/action/interceptor/FieldAndDocumentLevelSecurityRequestInterceptor.java b/plugin/src/main/java/org/elasticsearch/xpack/security/action/interceptor/FieldAndDocumentLevelSecurityRequestInterceptor.java index d1381493920..6cae676eeff 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/action/interceptor/FieldAndDocumentLevelSecurityRequestInterceptor.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/action/interceptor/FieldAndDocumentLevelSecurityRequestInterceptor.java @@ -12,6 +12,7 @@ import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.license.XPackLicenseState; 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.user.User; /** @@ -31,7 +32,7 @@ abstract class FieldAndDocumentLevelSecurityRequestInterceptor { + + 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 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; + } +} diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/action/interceptor/RequestInterceptor.java b/plugin/src/main/java/org/elasticsearch/xpack/security/action/interceptor/RequestInterceptor.java index d6549d928ab..5c1d137f88f 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/action/interceptor/RequestInterceptor.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/action/interceptor/RequestInterceptor.java @@ -5,6 +5,7 @@ */ 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.transport.TransportRequest; @@ -17,7 +18,7 @@ public interface RequestInterceptor { * If {@link #supports(TransportRequest)} returns true this interceptor will introspect the request * 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. diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/action/interceptor/ResizeRequestInterceptor.java b/plugin/src/main/java/org/elasticsearch/xpack/security/action/interceptor/ResizeRequestInterceptor.java new file mode 100644 index 00000000000..d774e1efe1b --- /dev/null +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/action/interceptor/ResizeRequestInterceptor.java @@ -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 { + + 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; + } +} diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/action/interceptor/SearchRequestInterceptor.java b/plugin/src/main/java/org/elasticsearch/xpack/security/action/interceptor/SearchRequestInterceptor.java index 068cd733b92..3ceaa02ee72 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/action/interceptor/SearchRequestInterceptor.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/action/interceptor/SearchRequestInterceptor.java @@ -7,7 +7,6 @@ package org.elasticsearch.xpack.security.action.interceptor; import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.action.search.SearchRequest; -import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.rest.RestStatus; @@ -19,7 +18,6 @@ import org.elasticsearch.transport.TransportRequest; */ public class SearchRequestInterceptor extends FieldAndDocumentLevelSecurityRequestInterceptor { - @Inject public SearchRequestInterceptor(Settings settings, ThreadPool threadPool, XPackLicenseState licenseState) { super(settings, threadPool.getThreadContext(), licenseState); } diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/action/interceptor/UpdateRequestInterceptor.java b/plugin/src/main/java/org/elasticsearch/xpack/security/action/interceptor/UpdateRequestInterceptor.java index f3fb814bc72..40b63d943d8 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/action/interceptor/UpdateRequestInterceptor.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/action/interceptor/UpdateRequestInterceptor.java @@ -7,7 +7,6 @@ package org.elasticsearch.xpack.security.action.interceptor; import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.action.update.UpdateRequest; -import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.rest.RestStatus; @@ -23,7 +22,6 @@ import org.elasticsearch.transport.TransportRequest; */ public class UpdateRequestInterceptor extends FieldAndDocumentLevelSecurityRequestInterceptor { - @Inject public UpdateRequestInterceptor(Settings settings, ThreadPool threadPool, XPackLicenseState licenseState) { super(settings, threadPool.getThreadContext(), licenseState); } diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authz/permission/IndicesPermission.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authz/permission/IndicesPermission.java index 8a0824d43c6..b1f13a64122 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/authz/permission/IndicesPermission.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authz/permission/IndicesPermission.java @@ -5,6 +5,7 @@ */ 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.IndexMetaData; import org.elasticsearch.cluster.metadata.MetaData; @@ -90,6 +91,16 @@ public final class IndicesPermission implements Iterable 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 */ diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/action/interceptor/IndicesAliasesRequestInterceptorTests.java b/plugin/src/test/java/org/elasticsearch/xpack/security/action/interceptor/IndicesAliasesRequestInterceptorTests.java new file mode 100644 index 00000000000..9fa224a909f --- /dev/null +++ b/plugin/src/test/java/org/elasticsearch/xpack/security/action/interceptor/IndicesAliasesRequestInterceptorTests.java @@ -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 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); + } +} diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/action/interceptor/ResizeRequestInterceptorTests.java b/plugin/src/test/java/org/elasticsearch/xpack/security/action/interceptor/ResizeRequestInterceptorTests.java new file mode 100644 index 00000000000..ed03d8d928a --- /dev/null +++ b/plugin/src/test/java/org/elasticsearch/xpack/security/action/interceptor/ResizeRequestInterceptorTests.java @@ -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 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); + } +}