diff --git a/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java b/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java index 84f9fddf738da..c16e21b984d6f 100644 --- a/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java +++ b/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java @@ -104,6 +104,17 @@ public void loadAuthorizedIndices(RequestInfo requestInfo, AuthorizationInfo aut } } + @Override + public void validateIndexPermissionsAreSubset(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, + Map> indexNameToNewNames, + ActionListener listener) { + if (isSuperuser(requestInfo.getAuthentication().getUser())) { + listener.onResponse(AuthorizationResult.granted()); + } else { + listener.onResponse(AuthorizationResult.deny()); + } + } + public static class CustomAuthorizationInfo implements AuthorizationInfo { private final String[] roles; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java index a0d7a85f6a579..51e1f409771d8 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java @@ -127,6 +127,8 @@ void authorizeIndexAction(RequestInfo requestInfo, AuthorizationInfo authorizati ActionListener listener); /** + * Asynchronously loads a list of alias and index names for which the user is authorized + * to execute the requested action. * * @param requestInfo object contain the request and associated information such as the action * and associated user(s) @@ -139,6 +141,26 @@ void authorizeIndexAction(RequestInfo requestInfo, AuthorizationInfo authorizati void loadAuthorizedIndices(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, Map aliasAndIndexLookup, ActionListener> listener); + /** + * Asynchronously checks that the permissions a user would have for a given list of names do + * not exceed their permissions for a given name. This is used to ensure that a user cannot + * perform operations that would escalate their privileges over the data. Some examples include + * adding an alias to gain more permissions to a given index and/or resizing an index in order + * to gain more privileges on the data since the index name changes. + * + * @param requestInfo object contain the request and associated information such as the action + * and associated user(s) + * @param authorizationInfo information needed from authorization that was previously retrieved + * from {@link #resolveAuthorizationInfo(RequestInfo, ActionListener)} + * @param indexNameToNewNames A map of an existing index/alias name to a one or more names of + * an index/alias that the user is requesting to create. The method + * should validate that none of the names have more permissions than + * the name in the key would have. + * @param listener the listener to be notified of the authorization result + */ + void validateIndexPermissionsAreSubset(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, + Map> indexNameToNewNames, ActionListener listener); + /** * Interface for objects that contains the information needed to authorize a request */ diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 948b9b7143e96..b47c458b7f9af 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -129,12 +129,12 @@ import org.elasticsearch.xpack.core.ssl.action.TransportGetCertificateInfoAction; import org.elasticsearch.xpack.core.ssl.rest.RestGetCertificateInfoAction; 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.authz.interceptor.BulkShardRequestInterceptor; +import org.elasticsearch.xpack.security.authz.interceptor.IndicesAliasesRequestInterceptor; +import org.elasticsearch.xpack.security.authz.interceptor.RequestInterceptor; +import org.elasticsearch.xpack.security.authz.interceptor.ResizeRequestInterceptor; +import org.elasticsearch.xpack.security.authz.interceptor.SearchRequestInterceptor; +import org.elasticsearch.xpack.security.authz.interceptor.UpdateRequestInterceptor; import org.elasticsearch.xpack.security.action.privilege.TransportDeletePrivilegesAction; import org.elasticsearch.xpack.security.action.privilege.TransportGetPrivilegesAction; import org.elasticsearch.xpack.security.action.privilege.TransportPutPrivilegesAction; @@ -437,8 +437,24 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste // to keep things simple, just invalidate all cached entries on license change. this happens so rarely that the impact should be // minimal getLicenseState().addListener(allRolesStore::invalidateAll); + + final Set requestInterceptors; + if (XPackSettings.DLS_FLS_ENABLED.get(settings)) { + requestInterceptors = Collections.unmodifiableSet(Sets.newHashSet( + new SearchRequestInterceptor(threadPool, getLicenseState()), + new UpdateRequestInterceptor(threadPool, getLicenseState()), + new BulkShardRequestInterceptor(threadPool, getLicenseState()), + new ResizeRequestInterceptor(threadPool, getLicenseState(), auditTrailService), + new IndicesAliasesRequestInterceptor(threadPool.getThreadContext(), getLicenseState(), auditTrailService))); + } else { + requestInterceptors = Collections.unmodifiableSet(Sets.newHashSet( + new ResizeRequestInterceptor(threadPool, getLicenseState(), auditTrailService), + new IndicesAliasesRequestInterceptor(threadPool.getThreadContext(), getLicenseState(), auditTrailService))); + } + final AuthorizationService authzService = new AuthorizationService(settings, allRolesStore, clusterService, - auditTrailService, failureHandler, threadPool, anonymousUser, getAuthorizationEngine()); + auditTrailService, failureHandler, threadPool, anonymousUser, getAuthorizationEngine(), requestInterceptors); + components.add(nativeRolesStore); // used by roles actions components.add(reservedRolesStore); // used by roles actions components.add(allRolesStore); // for SecurityFeatureSet and clear roles cache @@ -450,20 +466,8 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste securityInterceptor.set(new SecurityServerTransportInterceptor(settings, threadPool, authcService.get(), authzService, getLicenseState(), getSslService(), securityContext.get(), destructiveOperations, clusterService)); - final Set requestInterceptors; - if (XPackSettings.DLS_FLS_ENABLED.get(settings)) { - requestInterceptors = Collections.unmodifiableSet(Sets.newHashSet( - new SearchRequestInterceptor(threadPool, getLicenseState()), - new UpdateRequestInterceptor(threadPool, getLicenseState()), - new BulkShardRequestInterceptor(threadPool, getLicenseState()), - new ResizeRequestInterceptor(threadPool, getLicenseState(), auditTrailService), - new IndicesAliasesRequestInterceptor(threadPool.getThreadContext(), getLicenseState(), auditTrailService))); - } else { - requestInterceptors = Collections.emptySet(); - } - securityActionFilter.set(new SecurityActionFilter(authcService.get(), authzService, getLicenseState(), - requestInterceptors, threadPool, securityContext.get(), destructiveOperations)); + threadPool, securityContext.get(), destructiveOperations)); return components; } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilter.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilter.java index 3ae21f21c71a8..06d6446057bf3 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilter.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilter.java @@ -28,20 +28,15 @@ import org.elasticsearch.xpack.core.XPackField; import org.elasticsearch.xpack.core.security.SecurityContext; import org.elasticsearch.xpack.core.security.authc.Authentication; -import org.elasticsearch.xpack.core.security.authz.permission.Role; import org.elasticsearch.xpack.core.security.authz.privilege.HealthAndStatsPrivilege; import org.elasticsearch.xpack.core.security.support.Automatons; import org.elasticsearch.xpack.core.security.user.SystemUser; import org.elasticsearch.xpack.security.action.SecurityActionMapper; -import org.elasticsearch.xpack.security.action.interceptor.RequestInterceptor; import org.elasticsearch.xpack.security.authc.AuthenticationService; -import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo; import org.elasticsearch.xpack.security.authz.AuthorizationService; import org.elasticsearch.xpack.security.authz.AuthorizationUtils; -import org.elasticsearch.xpack.security.authz.RBACEngine.RBACAuthorizationInfo; import java.io.IOException; -import java.util.Set; import java.util.function.Predicate; public class SecurityActionFilter implements ActionFilter { @@ -53,19 +48,17 @@ public class SecurityActionFilter implements ActionFilter { private final AuthenticationService authcService; private final AuthorizationService authzService; private final SecurityActionMapper actionMapper = new SecurityActionMapper(); - private final Set requestInterceptors; private final XPackLicenseState licenseState; private final ThreadContext threadContext; private final SecurityContext securityContext; private final DestructiveOperations destructiveOperations; public SecurityActionFilter(AuthenticationService authcService, AuthorizationService authzService, - XPackLicenseState licenseState, Set requestInterceptors, ThreadPool threadPool, + XPackLicenseState licenseState, ThreadPool threadPool, SecurityContext securityContext, DestructiveOperations destructiveOperations) { this.authcService = authcService; this.authzService = authzService; this.licenseState = licenseState; - this.requestInterceptors = requestInterceptors; this.threadContext = threadPool.getThreadContext(); this.securityContext = securityContext; this.destructiveOperations = destructiveOperations; @@ -167,24 +160,8 @@ private void authorizeRequest(Authentication aut if (authentication == null) { listener.onFailure(new IllegalArgumentException("authentication must be non null for authorization")); } else { - authzService.authorize(authentication, securityAction, request, ActionListener.wrap(ignore -> { - /* - * 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. - */ - // FIXME this needs to be done in a way that allows us to operate without a role - Role role = null; - AuthorizationInfo authorizationInfo = threadContext.getTransient("_authz_info"); - if (authorizationInfo instanceof RBACAuthorizationInfo) { - role = ((RBACAuthorizationInfo) authorizationInfo).getRole(); - } - for (RequestInterceptor interceptor : requestInterceptors) { - if (interceptor.supports(request)) { - interceptor.intercept(request, authentication, role, securityAction); - } - } - listener.onResponse(null); - }, listener::onFailure)); + authzService.authorize(authentication, securityAction, request, ActionListener.wrap(ignore -> listener.onResponse(null), + listener::onFailure)); } } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/FieldAndDocumentLevelSecurityRequestInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/FieldAndDocumentLevelSecurityRequestInterceptor.java deleted file mode 100644 index b9bf11aca3a8d..0000000000000 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/FieldAndDocumentLevelSecurityRequestInterceptor.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.elasticsearch.action.IndicesRequest; -import org.elasticsearch.common.util.concurrent.ThreadContext; -import org.elasticsearch.license.XPackLicenseState; -import org.elasticsearch.xpack.core.security.authc.Authentication; -import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField; -import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; -import org.elasticsearch.xpack.core.security.authz.permission.Role; - -/** - * Base class for interceptors that disables features when field level security is configured for indices a request - * is going to execute on. - */ -abstract class FieldAndDocumentLevelSecurityRequestInterceptor implements - RequestInterceptor { - - private final ThreadContext threadContext; - private final XPackLicenseState licenseState; - private final Logger logger; - - FieldAndDocumentLevelSecurityRequestInterceptor(ThreadContext threadContext, XPackLicenseState licenseState) { - this.threadContext = threadContext; - this.licenseState = licenseState; - this.logger = LogManager.getLogger(getClass()); - } - - @Override - public void intercept(Request request, Authentication authentication, Role userPermissions, String action) { - if (licenseState.isDocumentAndFieldLevelSecurityAllowed()) { - final IndicesAccessControl indicesAccessControl = threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY); - for (String index : request.indices()) { - IndicesAccessControl.IndexAccessControl indexAccessControl = indicesAccessControl.getIndexPermissions(index); - if (indexAccessControl != null) { - boolean fieldLevelSecurityEnabled = indexAccessControl.getFieldPermissions().hasFieldLevelSecurity(); - boolean documentLevelSecurityEnabled = indexAccessControl.getQueries() != null; - if (fieldLevelSecurityEnabled || documentLevelSecurityEnabled) { - if (fieldLevelSecurityEnabled || documentLevelSecurityEnabled) { - logger.trace("intercepted request for index [{}] with field level access controls [{}] document level access " + - "controls [{}]. disabling conflicting features", index, fieldLevelSecurityEnabled, - documentLevelSecurityEnabled); - } - disableFeatures(request, fieldLevelSecurityEnabled, documentLevelSecurityEnabled); - return; - } - } - logger.trace("intercepted request for index [{}] without field or document level access controls", index); - } - } - } - - protected abstract void disableFeatures(Request request, boolean fieldLevelSecurityEnabled, boolean documentLevelSecurityEnabled); - -} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/IndicesAliasesRequestInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/IndicesAliasesRequestInterceptor.java deleted file mode 100644 index acc51ba8dff09..0000000000000 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/IndicesAliasesRequestInterceptor.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * 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.core.security.authc.Authentication; -import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField; -import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; -import org.elasticsearch.xpack.core.security.authz.permission.Role; -import org.elasticsearch.xpack.core.security.support.Exceptions; -import org.elasticsearch.xpack.security.audit.AuditTrailService; -import org.elasticsearch.xpack.security.audit.AuditUtil; - -import java.util.HashMap; -import java.util.Map; - -import static org.elasticsearch.xpack.security.authz.AuthorizationService.AUTHORIZATION_INFO_KEY; - -public final class IndicesAliasesRequestInterceptor implements RequestInterceptor { - - 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, Authentication authentication, Role userPermissions, String action) { - final XPackLicenseState frozenLicenseState = licenseState.copyCurrentLicenseState(); - if (frozenLicenseState.isAuthAllowed()) { - if (frozenLicenseState.isDocumentAndFieldLevelSecurityAllowed()) { - IndicesAccessControl indicesAccessControl = threadContext.getTransient(AuthorizationServiceField.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(AuditUtil.extractRequestId(threadContext), authentication, action, request, - threadContext.getTransient(AUTHORIZATION_INFO_KEY)); - 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/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/RequestInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/RequestInterceptor.java deleted file mode 100644 index c994626a7f402..0000000000000 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/RequestInterceptor.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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.transport.TransportRequest; -import org.elasticsearch.xpack.core.security.authc.Authentication; -import org.elasticsearch.xpack.core.security.authz.permission.Role; - -/** - * A request interceptor can introspect a request and modify it. - */ -public interface RequestInterceptor { - - /** - * If {@link #supports(TransportRequest)} returns true this interceptor will introspect the request - * and potentially modify it. - */ - void intercept(Request request, Authentication authentication, Role userPermissions, String action); - - /** - * Returns whether this request interceptor should intercept the specified request. - */ - boolean supports(TransportRequest request); - -} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/ResizeRequestInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/ResizeRequestInterceptor.java deleted file mode 100644 index c0bf38bf8bf98..0000000000000 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/ResizeRequestInterceptor.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * 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.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.core.security.authc.Authentication; -import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField; -import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; -import org.elasticsearch.xpack.core.security.authz.permission.Role; -import org.elasticsearch.xpack.core.security.support.Exceptions; -import org.elasticsearch.xpack.security.audit.AuditTrailService; - -import static org.elasticsearch.xpack.security.audit.AuditUtil.extractRequestId; -import static org.elasticsearch.xpack.security.authz.AuthorizationService.AUTHORIZATION_INFO_KEY; - -public final class ResizeRequestInterceptor implements RequestInterceptor { - - private final ThreadContext threadContext; - private final XPackLicenseState licenseState; - private final AuditTrailService auditTrailService; - - public ResizeRequestInterceptor(ThreadPool threadPool, XPackLicenseState licenseState, - AuditTrailService auditTrailService) { - this.threadContext = threadPool.getThreadContext(); - this.licenseState = licenseState; - this.auditTrailService = auditTrailService; - } - - @Override - public void intercept(ResizeRequest request, Authentication authentication, Role userPermissions, String action) { - final XPackLicenseState frozenLicenseState = licenseState.copyCurrentLicenseState(); - if (frozenLicenseState.isAuthAllowed()) { - if (frozenLicenseState.isDocumentAndFieldLevelSecurityAllowed()) { - IndicesAccessControl indicesAccessControl = - threadContext.getTransient(AuthorizationServiceField.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(extractRequestId(threadContext), authentication, action, request, - threadContext.getTransient(AUTHORIZATION_INFO_KEY)); - 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/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java index 98186af0503ba..bd27bd24fdd85 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java @@ -11,6 +11,7 @@ import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.DocWriteRequest; +import org.elasticsearch.action.StepListener; import org.elasticsearch.action.admin.indices.alias.Alias; import org.elasticsearch.action.admin.indices.alias.IndicesAliasesAction; import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; @@ -38,6 +39,12 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationFailureHandler; import org.elasticsearch.xpack.core.security.authc.esnative.ClientReservedRealm; import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AsyncSupplier; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationResult; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.EmptyAuthorizationInfo; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.IndexAuthorizationResult; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.RequestInfo; import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField; import org.elasticsearch.xpack.core.security.authz.ResolvedIndices; import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; @@ -50,12 +57,7 @@ import org.elasticsearch.xpack.core.security.user.XPackUser; import org.elasticsearch.xpack.security.audit.AuditTrailService; import org.elasticsearch.xpack.security.audit.AuditUtil; -import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AsyncSupplier; -import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo; -import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationResult; -import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.EmptyAuthorizationInfo; -import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.IndexAuthorizationResult; -import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.RequestInfo; +import org.elasticsearch.xpack.security.authz.interceptor.RequestInterceptor; import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; import java.util.ArrayList; @@ -63,10 +65,12 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; import static org.elasticsearch.action.support.ContextPreservingActionListener.wrapPreservingContext; import static org.elasticsearch.xpack.core.security.SecurityField.setting; @@ -78,7 +82,7 @@ public class AuthorizationService { Setting.boolSetting(setting("authc.anonymous.authz_exception"), true, Property.NodeScope); public static final String ORIGINATING_ACTION_KEY = "_originating_action_name"; public static final String AUTHORIZATION_INFO_KEY = "_authz_info"; - static final AuthorizationInfo SYSTEM_AUTHZ_INFO = + private static final AuthorizationInfo SYSTEM_AUTHZ_INFO = () -> Collections.singletonMap(PRINCIPAL_ROLES_FIELD_NAME, new String[] { SystemUser.ROLE_NAME }); private static final Logger logger = LogManager.getLogger(AuthorizationService.class); @@ -92,12 +96,14 @@ public class AuthorizationService { private final AnonymousUser anonymousUser; private final AuthorizationEngine rbacEngine; private final AuthorizationEngine authorizationEngine; + private final Set requestInterceptors; private final boolean isAnonymousEnabled; private final boolean anonymousAuthzExceptionEnabled; public AuthorizationService(Settings settings, CompositeRolesStore rolesStore, ClusterService clusterService, AuditTrailService auditTrail, AuthenticationFailureHandler authcFailureHandler, - ThreadPool threadPool, AnonymousUser anonymousUser, @Nullable AuthorizationEngine authorizationEngine) { + ThreadPool threadPool, AnonymousUser anonymousUser, @Nullable AuthorizationEngine authorizationEngine, + Set requestInterceptors) { this.clusterService = clusterService; this.auditTrail = auditTrail; this.indicesAndAliasesResolver = new IndicesAndAliasesResolver(settings, clusterService); @@ -108,6 +114,7 @@ public AuthorizationService(Settings settings, CompositeRolesStore rolesStore, C this.anonymousAuthzExceptionEnabled = ANONYMOUS_AUTHORIZATION_EXCEPTION_SETTING.get(settings); this.rbacEngine = new RBACEngine(settings, rolesStore); this.authorizationEngine = authorizationEngine == null ? this.rbacEngine : authorizationEngine; + this.requestInterceptors = requestInterceptors; this.settings = settings; } @@ -175,14 +182,18 @@ private void maybeAuthorizeRunAs(final RequestInfo requestInfo, final String req } authorizeAction(requestInfo, requestId, authzInfo, listener); } else { - listener.onFailure(denyRunAs(requestId, authentication, action, request, - authzInfo.getAuthenticatedUserAuthorizationInfo())); + if (result.isAuditable()) { + auditTrail.runAsDenied(requestId, authentication, action, request, + authzInfo.getAuthenticatedUserAuthorizationInfo()); + } + listener.onFailure(denialException(authentication, action, null)); } }, e -> { - // TODO need a failure handler better than this! - listener.onFailure(denyRunAs(requestId, authentication, action, request, authzInfo, e)); + auditTrail.runAsDenied(requestId, authentication, action, request, + authzInfo.getAuthenticatedUserAuthorizationInfo()); + listener.onFailure(denialException(authentication, action, null)); }), threadContext); - authorizeRunAs(requestInfo, requestId, authzInfo, runAsListener); + authorizeRunAs(requestInfo, authzInfo, runAsListener); } else { authorizeAction(requestInfo, requestId, authzInfo, listener); } @@ -195,20 +206,11 @@ private void authorizeAction(final RequestInfo requestInfo, final String request final String action = requestInfo.getAction(); final AuthorizationEngine authzEngine = getAuthorizationEngine(authentication); if (ClusterPrivilege.ACTION_MATCHER.test(action)) { - final ActionListener clusterAuthzListener = wrapPreservingContext(ActionListener.wrap(result -> { - if (result.isGranted()) { - if (result.isAuditable()) { - auditTrail.accessGranted(requestId, authentication, action, request, authzInfo); - } - putTransientIfNonExisting(AuthorizationServiceField.INDICES_PERMISSIONS_KEY, IndicesAccessControl.ALLOW_ALL); - listener.onResponse(null); - } else { - listener.onFailure(denial(requestId, authentication, action, request, authzInfo)); - } - }, e -> { - // TODO need a failure handler better than this! - listener.onFailure(denial(requestId, authentication, action, request, authzInfo, e)); - }), threadContext); + final ActionListener clusterAuthzListener = + wrapPreservingContext(new AuthorizationResultListener<>(result -> { + putTransientIfNonExisting(AuthorizationServiceField.INDICES_PERMISSIONS_KEY, IndicesAccessControl.ALLOW_ALL); + listener.onResponse(null); + }, listener::onFailure, requestInfo, requestId, authzInfo), threadContext); authzEngine.authorizeClusterAction(requestInfo, authzInfo, clusterAuthzListener); } else if (IndexPrivilege.ACTION_MATCHER.test(action)) { final MetaData metaData = clusterService.state().metaData(); @@ -219,80 +221,98 @@ private void authorizeAction(final RequestInfo requestInfo, final String request authorizedIndicesSupplier.getAsync(ActionListener.wrap(authorizedIndices -> { resolveIndexNames(request, metaData, authorizedIndices, resolvedIndicesListener); }, e -> { + auditTrail.accessDenied(requestId, authentication, action, request, authzInfo); if (e instanceof IndexNotFoundException) { - auditTrail.accessDenied(requestId, authentication, action, request, authzInfo); listener.onFailure(e); } else { - listener.onFailure(denial(requestId, authentication, action, request, authzInfo, e)); + listener.onFailure(denialException(authentication, action, e)); } })); }); authzEngine.authorizeIndexAction(requestInfo, authzInfo, resolvedIndicesAsyncSupplier, - metaData.getAliasAndIndexLookup()::get, ActionListener.wrap(indexAuthorizationResult -> { - if (indexAuthorizationResult.isGranted()) { - if (indexAuthorizationResult.getIndicesAccessControl() != null) { - putTransientIfNonExisting(AuthorizationServiceField.INDICES_PERMISSIONS_KEY, - indexAuthorizationResult.getIndicesAccessControl()); - } - //if we are creating an index we need to authorize potential aliases created at the same time - if (IndexPrivilege.CREATE_INDEX_MATCHER.test(action)) { - assert request instanceof CreateIndexRequest; - Set aliases = ((CreateIndexRequest) request).aliases(); - if (aliases.isEmpty() == false) { - final RequestInfo aliasesRequestInfo = new RequestInfo(authentication, request, IndicesAliasesAction.NAME); - authzEngine.authorizeIndexAction(aliasesRequestInfo, authzInfo, - ril -> { - resolvedIndicesAsyncSupplier.getAsync(ActionListener.wrap(resolvedIndices -> { - List aliasesAndIndices = new ArrayList<>(resolvedIndices.getLocal()); - for (Alias alias : aliases) { - aliasesAndIndices.add(alias.name()); - } - ResolvedIndices withAliases = new ResolvedIndices(aliasesAndIndices, Collections.emptyList()); - ril.onResponse(withAliases); - }, ril::onFailure)); - }, - metaData.getAliasAndIndexLookup()::get, ActionListener.wrap(authorizationResult -> { - if (authorizationResult.isGranted()) { - if (authorizationResult.isAuditable()) { - auditTrail.accessGranted(requestId, authentication, IndicesAliasesAction.NAME, - request, authzInfo); - } - if (indexAuthorizationResult.isAuditable()) { - auditTrail.accessGranted(requestId, authentication, action, request, authzInfo); - } - listener.onResponse(null); - } else { - listener.onFailure(denial(requestId, authentication, IndicesAliasesAction.NAME, - request, authzInfo)); - } - }, listener::onFailure)); - } else { - listener.onResponse(null); - } - } else if (action.equals(TransportShardBulkAction.ACTION_NAME)) { - // if this is performing multiple actions on the index, then check each of those actions. - assert request instanceof BulkShardRequest - : "Action " + action + " requires " + BulkShardRequest.class + " but was " + request.getClass(); - - authorizeBulkItems(requestInfo, authzInfo, authzEngine, resolvedIndicesAsyncSupplier, authorizedIndicesSupplier, - metaData, requestId, ActionListener.wrap(ignore -> { - if (indexAuthorizationResult.isAuditable()) { - auditTrail.accessGranted(requestId, authentication, action, request, authzInfo); - } - listener.onResponse(null); - }, listener::onFailure)); - } else { - if (indexAuthorizationResult.isAuditable()) { - auditTrail.accessGranted(requestId, authentication, action, request, authzInfo); + metaData.getAliasAndIndexLookup()::get, wrapPreservingContext(new AuthorizationResultListener<>(result -> + handleIndexActionAuthorizationResult(result, requestInfo, requestId, authzInfo, authzEngine, authorizedIndicesSupplier, + resolvedIndicesAsyncSupplier, metaData, listener), + listener::onFailure, requestInfo, requestId, authzInfo), threadContext)); + } else { + logger.warn("denying access as action [{}] is not an index or cluster action", action); + auditTrail.accessDenied(requestId, authentication, action, request, authzInfo); + listener.onFailure(denialException(authentication, action, null)); + } + } + + private void handleIndexActionAuthorizationResult(final IndexAuthorizationResult result, final RequestInfo requestInfo, + final String requestId, final AuthorizationInfo authzInfo, + final AuthorizationEngine authzEngine, + final AsyncSupplier> authorizedIndicesSupplier, + final AsyncSupplier resolvedIndicesAsyncSupplier, + final MetaData metaData, + final ActionListener listener) { + final Authentication authentication = requestInfo.getAuthentication(); + final TransportRequest request = requestInfo.getRequest(); + final String action = requestInfo.getAction(); + if (result.getIndicesAccessControl() != null) { + putTransientIfNonExisting(AuthorizationServiceField.INDICES_PERMISSIONS_KEY, + result.getIndicesAccessControl()); + } + //if we are creating an index we need to authorize potential aliases created at the same time + if (IndexPrivilege.CREATE_INDEX_MATCHER.test(action)) { + assert request instanceof CreateIndexRequest; + Set aliases = ((CreateIndexRequest) request).aliases(); + if (aliases.isEmpty()) { + runRequestInterceptors(requestInfo, authzInfo, authorizationEngine, listener); + } else { + final RequestInfo aliasesRequestInfo = new RequestInfo(authentication, request, IndicesAliasesAction.NAME); + authzEngine.authorizeIndexAction(aliasesRequestInfo, authzInfo, + ril -> { + resolvedIndicesAsyncSupplier.getAsync(ActionListener.wrap(resolvedIndices -> { + List aliasesAndIndices = new ArrayList<>(resolvedIndices.getLocal()); + for (Alias alias : aliases) { + aliasesAndIndices.add(alias.name()); } - listener.onResponse(null); - } - } else { - listener.onFailure(denial(requestId, authentication, action, request, authzInfo)); - } - }, listener::onFailure)); + ResolvedIndices withAliases = new ResolvedIndices(aliasesAndIndices, Collections.emptyList()); + ril.onResponse(withAliases); + }, ril::onFailure)); + }, + metaData.getAliasAndIndexLookup()::get, + wrapPreservingContext(new AuthorizationResultListener<>( + authorizationResult -> runRequestInterceptors(requestInfo, authzInfo, authorizationEngine, listener), + listener::onFailure, aliasesRequestInfo, requestId, authzInfo), threadContext)); + } + } else if (action.equals(TransportShardBulkAction.ACTION_NAME)) { + // if this is performing multiple actions on the index, then check each of those actions. + assert request instanceof BulkShardRequest + : "Action " + action + " requires " + BulkShardRequest.class + " but was " + request.getClass(); + + authorizeBulkItems(requestInfo, authzInfo, authzEngine, resolvedIndicesAsyncSupplier, authorizedIndicesSupplier, + metaData, requestId, + ActionListener.wrap(ignore -> runRequestInterceptors(requestInfo, authzInfo, authorizationEngine, listener), + listener::onFailure)); } else { - listener.onFailure(denial(requestId, authentication, action, request, authzInfo)); + runRequestInterceptors(requestInfo, authzInfo, authorizationEngine, listener); + } + } + + private void runRequestInterceptors(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, + AuthorizationEngine authorizationEngine, ActionListener listener) { + if (requestInterceptors.isEmpty()) { + listener.onResponse(null); + } else { + Iterator requestInterceptorIterator = requestInterceptors.iterator(); + final StepListener firstStepListener = new StepListener<>(); + final RequestInterceptor first = requestInterceptorIterator.next(); + + StepListener prevListener = firstStepListener; + while (requestInterceptorIterator.hasNext()) { + final RequestInterceptor nextInterceptor = requestInterceptorIterator.next(); + final StepListener current = new StepListener<>(); + prevListener.whenComplete(v -> nextInterceptor.intercept(requestInfo, authorizationEngine, authorizationInfo, current), + listener::onFailure); + prevListener = current; + } + + prevListener.whenComplete(v -> listener.onResponse(null), listener::onFailure); + first.intercept(requestInfo, authorizationEngine, authorizationInfo, firstStepListener); } } @@ -322,7 +342,8 @@ private void authorizeSystemUser(final Authentication authentication, final Stri auditTrail.accessGranted(requestId, authentication, action, request, SYSTEM_AUTHZ_INFO); listener.onResponse(null); } else { - listener.onFailure(denial(requestId, authentication, action, request, SYSTEM_AUTHZ_INFO)); + auditTrail.accessDenied(requestId, authentication, action, request, SYSTEM_AUTHZ_INFO); + listener.onFailure(denialException(authentication, action, null)); } } @@ -339,12 +360,14 @@ private TransportRequest maybeUnwrapRequest(Authentication authentication, Trans if (isProxyAction && isOriginalRequestProxyRequest == false) { IllegalStateException cause = new IllegalStateException("originalRequest is not a proxy request: [" + originalRequest + "] but action: [" + action + "] is a proxy action"); - throw denial(requestId, authentication, action, request, EmptyAuthorizationInfo.INSTANCE, cause); + auditTrail.accessDenied(requestId, authentication, action, request, EmptyAuthorizationInfo.INSTANCE); + throw denialException(authentication, action, cause); } if (TransportActionProxy.isProxyRequest(originalRequest) && TransportActionProxy.isProxyAction(action) == false) { IllegalStateException cause = new IllegalStateException("originalRequest is a proxy request for: [" + request + "] but action: [" + action + "] isn't"); - throw denial(requestId, authentication, action, request, EmptyAuthorizationInfo.INSTANCE, cause); + auditTrail.accessDenied(requestId, authentication, action, request, EmptyAuthorizationInfo.INSTANCE); + throw denialException(authentication, action, cause); } } return request; @@ -354,15 +377,13 @@ private boolean isInternalUser(User user) { return SystemUser.is(user) || XPackUser.is(user) || XPackSecurityUser.is(user); } - private void authorizeRunAs(final RequestInfo requestInfo, final String requestId, final AuthorizationInfo authzInfo, + private void authorizeRunAs(final RequestInfo requestInfo, final AuthorizationInfo authzInfo, final ActionListener listener) { final Authentication authentication = requestInfo.getAuthentication(); - final TransportRequest request = requestInfo.getRequest(); - final String action = requestInfo.getAction(); if (authentication.getLookedUpBy() == null) { // this user did not really exist // TODO(jaymode) find a better way to indicate lookup failed for a user and we need to fail authz - throw denyRunAs(requestId, authentication, action, request, authzInfo.getAuthenticatedUserAuthorizationInfo()); + listener.onResponse(AuthorizationResult.deny()); } else { final AuthorizationEngine runAsAuthzEngine = getRunAsAuthorizationEngine(authentication); runAsAuthzEngine.authorizeRunAs(requestInfo, authzInfo, listener); @@ -376,7 +397,7 @@ private void authorizeRunAs(final RequestInfo requestInfo, final String requestI * and then checks whether that action is allowed on the targeted index. Items * that fail this checks are {@link BulkItemRequest#abort(String, Exception) * aborted}, with an - * {@link #denial(String, Authentication, String, TransportRequest, AuthorizationInfo) access + * {@link #denialException(Authentication, String, Exception) access * denied} exception. Because a shard level request is for exactly 1 index, and * there are a small number of possible item {@link DocWriteRequest.OpType * types}, the number of distinct authorization checks that need to be performed @@ -447,7 +468,8 @@ private void authorizeBulkItems(RequestInfo requestInfo, AuthorizationInfo authz final IndicesAccessControl.IndexAccessControl indexAccessControl = indicesAccessControl.getIndexPermissions(resolvedIndex); if (indexAccessControl == null || indexAccessControl.isGranted() == false) { - item.abort(resolvedIndex, denial(requestId, authentication, itemAction, request, authzInfo)); + auditTrail.accessDenied(requestId, authentication, itemAction, request, authzInfo); + item.abort(resolvedIndex, denialException(authentication, itemAction, null)); } else if (audit.get()) { auditTrail.accessGranted(requestId, authentication, itemAction, request, authzInfo); } @@ -501,28 +523,6 @@ private void putTransientIfNonExisting(String key, Object value) { } } - private ElasticsearchSecurityException denial(String auditRequestId, Authentication authentication, String action, - TransportRequest request, AuthorizationInfo authzInfo) { - return denial(auditRequestId, authentication, action, request, authzInfo, null); - } - - private ElasticsearchSecurityException denial(String auditRequestId, Authentication authentication, String action, - TransportRequest request, AuthorizationInfo authzInfo, Exception cause) { - auditTrail.accessDenied(auditRequestId, authentication, action, request, authzInfo); - return denialException(authentication, action, cause); - } - - private ElasticsearchSecurityException denyRunAs(String auditRequestId, Authentication authentication, String action, - TransportRequest request, AuthorizationInfo authzInfo, Exception cause) { - auditTrail.runAsDenied(auditRequestId, authentication, action, request, authzInfo); - return denialException(authentication, action, cause); - } - - private ElasticsearchSecurityException denyRunAs(String auditRequestId, Authentication authentication, String action, - TransportRequest request, AuthorizationInfo authzInfo) { - return denyRunAs(auditRequestId, authentication, action, request, authzInfo, null); - } - private ElasticsearchSecurityException denialException(Authentication authentication, String action, Exception cause) { final User authUser = authentication.getUser().authenticatedUser(); // Special case for anonymous user @@ -542,6 +542,54 @@ private ElasticsearchSecurityException denialException(Authentication authentica return authorizationError("action [{}] is unauthorized for user [{}]", cause, action, authUser.principal()); } + private class AuthorizationResultListener implements ActionListener { + + private final Consumer responseConsumer; + private final Consumer failureConsumer; + private final RequestInfo requestInfo; + private final String requestId; + private final AuthorizationInfo authzInfo; + + private AuthorizationResultListener(Consumer responseConsumer, Consumer failureConsumer, RequestInfo requestInfo, + String requestId, AuthorizationInfo authzInfo) { + this.responseConsumer = responseConsumer; + this.failureConsumer = failureConsumer; + this.requestInfo = requestInfo; + this.requestId = requestId; + this.authzInfo = authzInfo; + } + + @Override + public void onResponse(T result) { + if (result.isGranted()) { + if (result.isAuditable()) { + auditTrail.accessGranted(requestId, requestInfo.getAuthentication(), requestInfo.getAction(), requestInfo.getRequest(), + authzInfo); + } + try { + responseConsumer.accept(result); + } catch (Exception e) { + failureConsumer.accept(e); + } + } else { + handleFailure(result.isAuditable(), null); + } + } + + @Override + public void onFailure(Exception e) { + handleFailure(true, e); + } + + private void handleFailure(boolean audit, @Nullable Exception e) { + if (audit) { + auditTrail.accessDenied(requestId, requestInfo.getAuthentication(), requestInfo.getAction(), requestInfo.getRequest(), + authzInfo); + } + failureConsumer.accept(denialException(requestInfo.getAuthentication(), requestInfo.getAction(), e)); + } + } + private static class CachingAsyncSupplier implements AsyncSupplier { private final AsyncSupplier asyncSupplier; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java index c68fc7d0ef6ba..0d2974e4c8abc 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java @@ -6,6 +6,8 @@ package org.elasticsearch.xpack.security.authz; +import org.apache.lucene.util.automaton.Automaton; +import org.apache.lucene.util.automaton.Operations; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.CompositeIndicesRequest; import org.elasticsearch.action.IndicesRequest; @@ -44,8 +46,10 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; @@ -284,6 +288,31 @@ public void loadAuthorizedIndices(RequestInfo requestInfo, AuthorizationInfo aut } } + @Override + public void validateIndexPermissionsAreSubset(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, + Map> indexNameToNewNames, + ActionListener listener) { + if (authorizationInfo instanceof RBACAuthorizationInfo) { + final Role role = ((RBACAuthorizationInfo) authorizationInfo).getRole(); + Map permissionMap = new HashMap<>(); + for (Entry> entry : indexNameToNewNames.entrySet()) { + Automaton existingPermissions = permissionMap.computeIfAbsent(entry.getKey(), role.indices()::allowedActionsMatcher); + for (String alias : entry.getValue()) { + Automaton newNamePermissions = permissionMap.computeIfAbsent(alias, role.indices()::allowedActionsMatcher); + if (Operations.subsetOf(newNamePermissions, existingPermissions) == false) { + listener.onResponse(AuthorizationResult.deny()); + return; + } + } + } + listener.onResponse(AuthorizationResult.granted()); + } else { + listener.onFailure( + new IllegalArgumentException("unsupported authorization info:" + authorizationInfo.getClass().getSimpleName())); + } + + } + static List resolveAuthorizedIndicesFromRole(Role role, String action, Map aliasAndIndexLookup) { Predicate predicate = role.indices().allowedIndicesMatcher(action); @@ -330,8 +359,7 @@ private static boolean checkChangePasswordAction(Authentication authentication) return ReservedRealm.TYPE.equals(realmType) || NativeRealmSettings.TYPE.equals(realmType); } - // FIXME make this pkg private! - public static class RBACAuthorizationInfo implements AuthorizationInfo { + static class RBACAuthorizationInfo implements AuthorizationInfo { private final Role role; private final Map info; @@ -344,8 +372,7 @@ public static class RBACAuthorizationInfo implements AuthorizationInfo { authenticatedUserRole == null ? this : new RBACAuthorizationInfo(authenticatedUserRole, null); } - // FIXME make this pkg private! - public Role getRole() { + Role getRole() { return role; } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/BulkShardRequestInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/BulkShardRequestInterceptor.java similarity index 58% rename from x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/BulkShardRequestInterceptor.java rename to x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/BulkShardRequestInterceptor.java index c9eb571f3ae09..24adb4a2fe0fc 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/BulkShardRequestInterceptor.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/BulkShardRequestInterceptor.java @@ -3,11 +3,12 @@ * 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; +package org.elasticsearch.xpack.security.authz.interceptor; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.bulk.BulkItemRequest; import org.elasticsearch.action.bulk.BulkShardRequest; import org.elasticsearch.action.update.UpdateRequest; @@ -15,16 +16,16 @@ import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.threadpool.ThreadPool; -import org.elasticsearch.transport.TransportRequest; -import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.RequestInfo; import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField; import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; -import org.elasticsearch.xpack.core.security.authz.permission.Role; /** * Similar to {@link UpdateRequestInterceptor}, but checks if there are update requests embedded in a bulk request. */ -public class BulkShardRequestInterceptor implements RequestInterceptor { +public class BulkShardRequestInterceptor implements RequestInterceptor { private static final Logger logger = LogManager.getLogger(BulkShardRequestInterceptor.class); @@ -37,31 +38,36 @@ public BulkShardRequestInterceptor(ThreadPool threadPool, XPackLicenseState lice } @Override - public void intercept(BulkShardRequest request, Authentication authentication, Role userPermissions, String action) { - if (licenseState.isDocumentAndFieldLevelSecurityAllowed()) { + public void intercept(RequestInfo requestInfo, AuthorizationEngine authzEngine, AuthorizationInfo authorizationInfo, + ActionListener listener) { + if (requestInfo.getRequest() instanceof BulkShardRequest && licenseState.isDocumentAndFieldLevelSecurityAllowed()) { IndicesAccessControl indicesAccessControl = threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY); - for (BulkItemRequest bulkItemRequest : request.items()) { + final BulkShardRequest bulkShardRequest = (BulkShardRequest) requestInfo.getRequest(); + for (BulkItemRequest bulkItemRequest : bulkShardRequest.items()) { IndicesAccessControl.IndexAccessControl indexAccessControl = indicesAccessControl.getIndexPermissions(bulkItemRequest.index()); + boolean found = false; if (indexAccessControl != null) { boolean fls = indexAccessControl.getFieldPermissions().hasFieldLevelSecurity(); boolean dls = indexAccessControl.getQueries() != null; if (fls || dls) { if (bulkItemRequest.request() instanceof UpdateRequest) { - throw new ElasticsearchSecurityException("Can't execute a bulk request with update requests embedded if " + - "field or document level security is enabled", RestStatus.BAD_REQUEST); + found = true; + logger.trace("aborting bulk item update request for index [{}]", bulkItemRequest.index()); + bulkItemRequest.abort(bulkItemRequest.index(), new ElasticsearchSecurityException("Can't execute a bulk " + + "item request with update requests embedded if field or document level security is enabled", + RestStatus.BAD_REQUEST)); } } } - logger.trace("intercepted bulk request for index [{}] without any update requests, continuing execution", - bulkItemRequest.index()); + + if (found == false) { + logger.trace("intercepted bulk request for index [{}] without any update requests, continuing execution", + bulkItemRequest.index()); + } } } - } - - @Override - public boolean supports(TransportRequest request) { - return request instanceof BulkShardRequest; + listener.onResponse(null); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FieldAndDocumentLevelSecurityRequestInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FieldAndDocumentLevelSecurityRequestInterceptor.java new file mode 100644 index 0000000000000..eaf54e952b40f --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FieldAndDocumentLevelSecurityRequestInterceptor.java @@ -0,0 +1,68 @@ +/* + * 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.authz.interceptor; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.IndicesRequest; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.RequestInfo; +import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField; +import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; + +/** + * Base class for interceptors that disables features when field level security is configured for indices a request + * is going to execute on. + */ +abstract class FieldAndDocumentLevelSecurityRequestInterceptor implements RequestInterceptor { + + private final ThreadContext threadContext; + private final XPackLicenseState licenseState; + private final Logger logger; + + FieldAndDocumentLevelSecurityRequestInterceptor(ThreadContext threadContext, XPackLicenseState licenseState) { + this.threadContext = threadContext; + this.licenseState = licenseState; + this.logger = LogManager.getLogger(getClass()); + } + + @Override + public void intercept(RequestInfo requestInfo, AuthorizationEngine authorizationEngine, AuthorizationInfo authorizationInfo, + ActionListener listener) { + if (requestInfo.getRequest() instanceof IndicesRequest) { + IndicesRequest indicesRequest = (IndicesRequest) requestInfo.getRequest(); + if (supports(indicesRequest) && licenseState.isDocumentAndFieldLevelSecurityAllowed()) { + final IndicesAccessControl indicesAccessControl = + threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY); + for (String index : indicesRequest.indices()) { + IndicesAccessControl.IndexAccessControl indexAccessControl = indicesAccessControl.getIndexPermissions(index); + if (indexAccessControl != null) { + boolean fieldLevelSecurityEnabled = indexAccessControl.getFieldPermissions().hasFieldLevelSecurity(); + boolean documentLevelSecurityEnabled = indexAccessControl.getQueries() != null; + if (fieldLevelSecurityEnabled || documentLevelSecurityEnabled) { + logger.trace("intercepted request for index [{}] with field level access controls [{}] " + + "document level access controls [{}]. disabling conflicting features", + index, fieldLevelSecurityEnabled, documentLevelSecurityEnabled); + disableFeatures(indicesRequest, fieldLevelSecurityEnabled, documentLevelSecurityEnabled, listener); + return; + } + } + logger.trace("intercepted request for index [{}] without field or document level access controls", index); + } + } + } + listener.onResponse(null); + } + + abstract void disableFeatures(IndicesRequest request, boolean fieldLevelSecurityEnabled, boolean documentLevelSecurityEnabled, + ActionListener listener); + + abstract boolean supports(IndicesRequest request); +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/IndicesAliasesRequestInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/IndicesAliasesRequestInterceptor.java new file mode 100644 index 0000000000000..2893da4938e8c --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/IndicesAliasesRequestInterceptor.java @@ -0,0 +1,105 @@ +/* + * 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.authz.interceptor; + +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.RequestInfo; +import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField; +import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; +import org.elasticsearch.xpack.core.security.support.Exceptions; +import org.elasticsearch.xpack.security.audit.AuditTrailService; +import org.elasticsearch.xpack.security.audit.AuditUtil; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.elasticsearch.action.support.ContextPreservingActionListener.wrapPreservingContext; + +public final class IndicesAliasesRequestInterceptor implements RequestInterceptor { + + 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(RequestInfo requestInfo, AuthorizationEngine authorizationEngine, AuthorizationInfo authorizationInfo, + ActionListener listener) { + if (requestInfo.getRequest() instanceof IndicesAliasesRequest) { + final IndicesAliasesRequest request = (IndicesAliasesRequest) requestInfo.getRequest(); + final XPackLicenseState frozenLicenseState = licenseState.copyCurrentLicenseState(); + if (frozenLicenseState.isAuthAllowed()) { + if (frozenLicenseState.isDocumentAndFieldLevelSecurityAllowed()) { + IndicesAccessControl indicesAccessControl = + threadContext.getTransient(AuthorizationServiceField.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) { + listener.onFailure(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)); + return; + } + } + } + } + } + } + + Map> indexToAliasesMap = request.getAliasActions().stream() + .filter(aliasAction -> aliasAction.actionType() == IndicesAliasesRequest.AliasActions.Type.ADD) + .flatMap(aliasActions -> + Arrays.stream(aliasActions.indices()) + .map(indexName -> new Tuple<>(indexName, Arrays.asList(aliasActions.aliases())))) + .collect(Collectors.toMap(Tuple::v1, Tuple::v2, (existing, toMerge) -> { + List list = new ArrayList<>(existing.size() + toMerge.size()); + list.addAll(existing); + list.addAll(toMerge); + return list; + })); + authorizationEngine.validateIndexPermissionsAreSubset(requestInfo, authorizationInfo, indexToAliasesMap, + wrapPreservingContext(ActionListener.wrap(authzResult -> { + if (authzResult.isGranted()) { + // do not audit success again + listener.onResponse(null); + } else { + auditTrailService.accessDenied(AuditUtil.extractRequestId(threadContext), requestInfo.getAuthentication(), + requestInfo.getAction(), request, authorizationInfo); + listener.onFailure(Exceptions.authorizationError("Adding an alias is not allowed when the alias " + + "has more permissions than any of the indices")); + } + }, listener::onFailure), threadContext)); + } else { + listener.onResponse(null); + } + } else { + listener.onResponse(null); + } + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/RequestInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/RequestInterceptor.java new file mode 100644 index 0000000000000..cfda99653f69d --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/RequestInterceptor.java @@ -0,0 +1,24 @@ +/* + * 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.authz.interceptor; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.RequestInfo; + +/** + * A request interceptor can introspect a request and modify it. + */ +public interface RequestInterceptor { + + /** + * This interceptor will introspect the request and potentially modify it. If the interceptor does not apply + * to the request then the request will not be modified. + */ + void intercept(RequestInfo requestInfo, AuthorizationEngine authorizationEngine, AuthorizationInfo authorizationInfo, + ActionListener listener); +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/ResizeRequestInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/ResizeRequestInterceptor.java new file mode 100644 index 0000000000000..fc18cb12d1bef --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/ResizeRequestInterceptor.java @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security.authz.interceptor; + +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.admin.indices.shrink.ResizeRequest; +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.xpack.core.security.authz.AuthorizationEngine; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.RequestInfo; +import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField; +import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; +import org.elasticsearch.xpack.core.security.support.Exceptions; +import org.elasticsearch.xpack.security.audit.AuditTrailService; + +import java.util.Collections; + +import static org.elasticsearch.action.support.ContextPreservingActionListener.wrapPreservingContext; +import static org.elasticsearch.xpack.security.audit.AuditUtil.extractRequestId; + +public final class ResizeRequestInterceptor implements RequestInterceptor { + + private final ThreadContext threadContext; + private final XPackLicenseState licenseState; + private final AuditTrailService auditTrailService; + + public ResizeRequestInterceptor(ThreadPool threadPool, XPackLicenseState licenseState, + AuditTrailService auditTrailService) { + this.threadContext = threadPool.getThreadContext(); + this.licenseState = licenseState; + this.auditTrailService = auditTrailService; + } + + @Override + public void intercept(RequestInfo requestInfo, AuthorizationEngine authorizationEngine, AuthorizationInfo authorizationInfo, + ActionListener listener) { + if (requestInfo.getRequest() instanceof ResizeRequest) { + final ResizeRequest request = (ResizeRequest) requestInfo.getRequest(); + final XPackLicenseState frozenLicenseState = licenseState.copyCurrentLicenseState(); + if (frozenLicenseState.isAuthAllowed()) { + if (frozenLicenseState.isDocumentAndFieldLevelSecurityAllowed()) { + IndicesAccessControl indicesAccessControl = + threadContext.getTransient(AuthorizationServiceField.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) { + listener.onFailure(new ElasticsearchSecurityException("Resize requests are not allowed for users when " + + "field or document level security is enabled on the source index", RestStatus.BAD_REQUEST)); + return; + } + } + } + + authorizationEngine.validateIndexPermissionsAreSubset(requestInfo, authorizationInfo, + Collections.singletonMap(request.getSourceIndex(), Collections.singletonList(request.getTargetIndexRequest().index())), + wrapPreservingContext(ActionListener.wrap(authzResult -> { + if (authzResult.isGranted()) { + listener.onResponse(null); + } else { + if (authzResult.isAuditable()) { + auditTrailService.accessDenied(extractRequestId(threadContext), requestInfo.getAuthentication(), + requestInfo.getAction(), request, authorizationInfo); + } + listener.onFailure(Exceptions.authorizationError("Resizing an index is not allowed when the target index " + + "has more permissions than the source index")); + } + }, listener::onFailure), threadContext)); + } else { + listener.onResponse(null); + } + } else { + listener.onResponse(null); + } + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/SearchRequestInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/SearchRequestInterceptor.java similarity index 50% rename from x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/SearchRequestInterceptor.java rename to x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/SearchRequestInterceptor.java index 5738d3eef5051..14084b963c3a1 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/SearchRequestInterceptor.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/SearchRequestInterceptor.java @@ -3,42 +3,48 @@ * 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; +package org.elasticsearch.xpack.security.authz.interceptor; import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.IndicesRequest; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.threadpool.ThreadPool; -import org.elasticsearch.transport.TransportRequest; /** * If field level security is enabled this interceptor disables the request cache for search requests. */ -public class SearchRequestInterceptor extends FieldAndDocumentLevelSecurityRequestInterceptor { +public class SearchRequestInterceptor extends FieldAndDocumentLevelSecurityRequestInterceptor { public SearchRequestInterceptor(ThreadPool threadPool, XPackLicenseState licenseState) { super(threadPool.getThreadContext(), licenseState); } @Override - public void disableFeatures(SearchRequest request, boolean fieldLevelSecurityEnabled, boolean documentLevelSecurityEnabled) { + public void disableFeatures(IndicesRequest indicesRequest, boolean fieldLevelSecurityEnabled, boolean documentLevelSecurityEnabled, + ActionListener listener) { + final SearchRequest request = (SearchRequest) indicesRequest; request.requestCache(false); if (documentLevelSecurityEnabled) { if (request.source() != null && request.source().suggest() != null) { - throw new ElasticsearchSecurityException("Suggest isn't supported if document level security is enabled", - RestStatus.BAD_REQUEST); - } - if (request.source() != null && request.source().profile()) { - throw new ElasticsearchSecurityException("A search request cannot be profiled if document level security is enabled", - RestStatus.BAD_REQUEST); + listener.onFailure(new ElasticsearchSecurityException("Suggest isn't supported if document level security is enabled", + RestStatus.BAD_REQUEST)); + } else if (request.source() != null && request.source().profile()) { + listener.onFailure(new ElasticsearchSecurityException("A search request cannot be profiled if document level security " + + "is enabled", RestStatus.BAD_REQUEST)); + } else { + listener.onResponse(null); } + } else { + listener.onResponse(null); } } @Override - public boolean supports(TransportRequest request) { + public boolean supports(IndicesRequest request) { return request instanceof SearchRequest; } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/UpdateRequestInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/UpdateRequestInterceptor.java similarity index 65% rename from x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/UpdateRequestInterceptor.java rename to x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/UpdateRequestInterceptor.java index db265333e6965..ba0c44eb4e5df 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/UpdateRequestInterceptor.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/UpdateRequestInterceptor.java @@ -3,14 +3,15 @@ * 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; +package org.elasticsearch.xpack.security.authz.interceptor; import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.IndicesRequest; import org.elasticsearch.action.update.UpdateRequest; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.threadpool.ThreadPool; -import org.elasticsearch.transport.TransportRequest; /** * A request interceptor that fails update request if field or document level security is enabled. @@ -19,20 +20,21 @@ * because only the fields that a role can see would be used to perform the update and without knowing the user may * remove the other fields, not visible for him, from the document being updated. */ -public class UpdateRequestInterceptor extends FieldAndDocumentLevelSecurityRequestInterceptor { +public class UpdateRequestInterceptor extends FieldAndDocumentLevelSecurityRequestInterceptor { public UpdateRequestInterceptor(ThreadPool threadPool, XPackLicenseState licenseState) { super(threadPool.getThreadContext(), licenseState); } @Override - protected void disableFeatures(UpdateRequest updateRequest, boolean fieldLevelSecurityEnabled, boolean documentLevelSecurityEnabled) { - throw new ElasticsearchSecurityException("Can't execute an update request if field or document level security is enabled", - RestStatus.BAD_REQUEST); + protected void disableFeatures(IndicesRequest updateRequest, boolean fieldLevelSecurityEnabled, boolean documentLevelSecurityEnabled, + ActionListener listener) { + listener.onFailure(new ElasticsearchSecurityException("Can't execute an update request if field or document level security " + + "is enabled", RestStatus.BAD_REQUEST)); } @Override - public boolean supports(TransportRequest request) { + public boolean supports(IndicesRequest request) { return request instanceof UpdateRequest; } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/DocumentLevelSecurityTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/DocumentLevelSecurityTests.java index 8d25f0d836139..77f5b6c57b4c3 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/DocumentLevelSecurityTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/DocumentLevelSecurityTests.java @@ -841,7 +841,7 @@ public void testUpdateApiIsBlocked() throws Exception { ElasticsearchSecurityException securityException = (ElasticsearchSecurityException) bulkItem.getFailure().getCause(); assertThat(securityException.status(), equalTo(RestStatus.BAD_REQUEST)); assertThat(securityException.getMessage(), - equalTo("Can't execute a bulk request with update requests embedded if field or document level security is enabled")); + equalTo("Can't execute a bulk item request with update requests embedded if field or document level security is enabled")); assertThat(client().prepareGet("test", "type", "1").get().getSource().get("field1").toString(), equalTo("value2")); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/FieldLevelSecurityTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/FieldLevelSecurityTests.java index 54832519d8576..3055d1b0f456b 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/FieldLevelSecurityTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/FieldLevelSecurityTests.java @@ -1448,7 +1448,7 @@ public void testUpdateApiIsBlocked() throws Exception { ElasticsearchSecurityException securityException = (ElasticsearchSecurityException) bulkItem.getFailure().getCause(); assertThat(securityException.status(), equalTo(RestStatus.BAD_REQUEST)); assertThat(securityException.getMessage(), - equalTo("Can't execute a bulk request with update requests embedded if field or document level security is enabled")); + equalTo("Can't execute a bulk item request with update requests embedded if field or document level security is enabled")); assertThat(client().prepareGet("test", "type", "1").get().getSource().get("field2").toString(), equalTo("value2")); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilterTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilterTests.java index 97ff7521d1574..78d6e22ac3645 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilterTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilterTests.java @@ -39,7 +39,6 @@ import org.junit.Before; import java.util.Collections; -import java.util.HashSet; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; @@ -83,8 +82,7 @@ public void init() throws Exception { when(state.nodes()).thenReturn(nodes); SecurityContext securityContext = new SecurityContext(settings, threadContext); - filter = new SecurityActionFilter(authcService, authzService, - licenseState, new HashSet<>(), threadPool, securityContext, destructiveOperations); + filter = new SecurityActionFilter(authcService, authzService, licenseState, threadPool, securityContext, destructiveOperations); } public void testApply() throws Exception { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java index 617296e2f501a..55d6f4342b147 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java @@ -227,7 +227,8 @@ public void setup() { }).when(rolesStore).getRoles(any(User.class), any(FieldPermissionsCache.class), any(ActionListener.class)); roleMap.put(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName(), ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR); authorizationService = new AuthorizationService(settings, rolesStore, clusterService, - auditTrail, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(settings), null); + auditTrail, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(settings), null, + Collections.emptySet()); } private void authorize(Authentication authentication, String action, TransportRequest request) { @@ -617,6 +618,8 @@ public void testCreateIndexWithAliasWithoutPermissions() { assertThrowsAuthorizationException( () -> authorize(authentication, CreateIndexAction.NAME, request), IndicesAliasesAction.NAME, "test user"); + verify(auditTrail).accessGranted(eq(requestId), eq(authentication), eq(CreateIndexAction.NAME), eq(request), + authzInfoRoles(new String[]{role.getName()})); verify(auditTrail).accessDenied(eq(requestId), eq(authentication), eq(IndicesAliasesAction.NAME), eq(request), authzInfoRoles(new String[]{role.getName()})); verifyNoMoreInteractions(auditTrail); @@ -651,7 +654,7 @@ public void testDenialForAnonymousUser() { Settings settings = Settings.builder().put(AnonymousUser.ROLES_SETTING.getKey(), "a_all").build(); final AnonymousUser anonymousUser = new AnonymousUser(settings); authorizationService = new AuthorizationService(settings, rolesStore, clusterService, auditTrail, - new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, anonymousUser, null); + new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, anonymousUser, null, Collections.emptySet()); RoleDescriptor role = new RoleDescriptor("a_all", null, new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a").privileges("all").build() }, null); @@ -678,7 +681,8 @@ public void testDenialForAnonymousUserAuthorizationExceptionDisabled() { .build(); final Authentication authentication = createAuthentication(new AnonymousUser(settings)); authorizationService = new AuthorizationService(settings, rolesStore, clusterService, auditTrail, - new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(settings), null); + new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(settings), null, + Collections.emptySet()); RoleDescriptor role = new RoleDescriptor("a_all", null, new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null); @@ -1359,11 +1363,18 @@ public void loadAuthorizedIndices(RequestInfo requestInfo, AuthorizationInfo aut Map aliasAndIndexLookup, ActionListener> listener) { throw new UnsupportedOperationException("not implemented"); } + + @Override + public void validateIndexPermissionsAreSubset(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, + Map> indexNameToNewNames, + ActionListener listener) { + throw new UnsupportedOperationException("not implemented"); + } }; authorizationService = new AuthorizationService(Settings.EMPTY, rolesStore, clusterService, auditTrail, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(Settings.EMPTY), - engine); + engine, Collections.emptySet()); Authentication authentication = createAuthentication(new User("test user", "a_all")); assertEquals(engine, authorizationService.getAuthorizationEngine(authentication)); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/interceptor/IndicesAliasesRequestInterceptorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/interceptor/IndicesAliasesRequestInterceptorTests.java similarity index 65% rename from x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/interceptor/IndicesAliasesRequestInterceptorTests.java rename to x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/interceptor/IndicesAliasesRequestInterceptorTests.java index 08dce12167483..7c1f25aba671d 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/interceptor/IndicesAliasesRequestInterceptorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/interceptor/IndicesAliasesRequestInterceptorTests.java @@ -3,11 +3,13 @@ * 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; +package org.elasticsearch.xpack.security.authz.interceptor; import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.admin.indices.alias.IndicesAliasesAction; import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest; +import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.settings.Settings; @@ -16,18 +18,24 @@ import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationResult; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.EmptyAuthorizationInfo; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.RequestInfo; import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField; import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsDefinition; -import org.elasticsearch.xpack.core.security.authz.permission.Role; -import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.audit.AuditTrailService; import java.util.Collections; +import java.util.Map; import java.util.Set; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -57,7 +65,6 @@ public void testInterceptorThrowsWhenFLSDLSEnabled() { } else { queries = null; } - Role role = Role.builder().add(fieldPermissions, queries, IndexPrivilege.ALL, randomBoolean(), "foo").build(); final String action = IndicesAliasesAction.NAME; IndicesAccessControl accessControl = new IndicesAccessControl(true, Collections.singletonMap("foo", new IndicesAccessControl.IndexAccessControl(true, fieldPermissions, queries))); @@ -74,8 +81,20 @@ public void testInterceptorThrowsWhenFLSDLSEnabled() { if (randomBoolean()) { indicesAliasesRequest.addAliasAction(IndicesAliasesRequest.AliasActions.removeIndex().index("foofoo")); } + PlainActionFuture plainActionFuture = new PlainActionFuture<>(); + RequestInfo requestInfo = new RequestInfo(authentication, indicesAliasesRequest, action); + AuthorizationEngine mockEngine = mock(AuthorizationEngine.class); + doAnswer(invocationOnMock -> { + ActionListener listener = (ActionListener) invocationOnMock.getArguments()[3]; + listener.onResponse(AuthorizationResult.deny()); + return null; + }).when(mockEngine).validateIndexPermissionsAreSubset(eq(requestInfo), eq(EmptyAuthorizationInfo.INSTANCE), any(Map.class), + any(ActionListener.class)); ElasticsearchSecurityException securityException = expectThrows(ElasticsearchSecurityException.class, - () -> interceptor.intercept(indicesAliasesRequest, authentication, role, action)); + () -> { + interceptor.intercept(requestInfo, mockEngine, EmptyAuthorizationInfo.INSTANCE, plainActionFuture); + plainActionFuture.actionGet(); + }); assertEquals("Alias requests are not allowed for users who have field or document level security enabled on one of the indices", securityException.getMessage()); } @@ -85,15 +104,11 @@ public void testInterceptorThrowsWhenTargetHasGreaterPermissions() throws Except when(licenseState.copyCurrentLicenseState()).thenReturn(licenseState); when(licenseState.isAuthAllowed()).thenReturn(true); when(licenseState.isAuditingAllowed()).thenReturn(true); - when(licenseState.isDocumentAndFieldLevelSecurityAllowed()).thenReturn(true); + when(licenseState.isDocumentAndFieldLevelSecurityAllowed()).thenReturn(randomBoolean()); ThreadContext threadContext = new ThreadContext(Settings.EMPTY); AuditTrailService auditTrailService = new AuditTrailService(Collections.emptyList(), licenseState); Authentication authentication = new Authentication(new User("john", "role"), new RealmRef(null, null, null), new RealmRef(null, null, null)); - 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(AuthorizationServiceField.INDICES_PERMISSIONS_KEY, accessControl); @@ -109,10 +124,24 @@ public void testInterceptorThrowsWhenTargetHasGreaterPermissions() throws Except indicesAliasesRequest.addAliasAction(IndicesAliasesRequest.AliasActions.removeIndex().index("foofoo")); } - ElasticsearchSecurityException securityException = expectThrows(ElasticsearchSecurityException.class, - () -> interceptor.intercept(indicesAliasesRequest, authentication, role, action)); - assertEquals("Adding an alias is not allowed when the alias has more permissions than any of the indices", + AuthorizationEngine mockEngine = mock(AuthorizationEngine.class); + { + PlainActionFuture plainActionFuture = new PlainActionFuture<>(); + RequestInfo requestInfo = new RequestInfo(authentication, indicesAliasesRequest, action); + doAnswer(invocationOnMock -> { + ActionListener listener = (ActionListener) invocationOnMock.getArguments()[3]; + listener.onResponse(AuthorizationResult.deny()); + return null; + }).when(mockEngine).validateIndexPermissionsAreSubset(eq(requestInfo), eq(EmptyAuthorizationInfo.INSTANCE), any(Map.class), + any(ActionListener.class)); + ElasticsearchSecurityException securityException = expectThrows(ElasticsearchSecurityException.class, + () -> { + interceptor.intercept(requestInfo, mockEngine, EmptyAuthorizationInfo.INSTANCE, plainActionFuture); + plainActionFuture.actionGet(); + }); + 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(); @@ -123,6 +152,18 @@ public void testInterceptorThrowsWhenTargetHasGreaterPermissions() throws Except if (randomBoolean()) { successRequest.addAliasAction(IndicesAliasesRequest.AliasActions.removeIndex().index("foofoo")); } - interceptor.intercept(successRequest, authentication, role, action); + + { + PlainActionFuture plainActionFuture = new PlainActionFuture<>(); + RequestInfo requestInfo = new RequestInfo(authentication, successRequest, action); + doAnswer(invocationOnMock -> { + ActionListener listener = (ActionListener) invocationOnMock.getArguments()[3]; + listener.onResponse(AuthorizationResult.granted()); + return null; + }).when(mockEngine).validateIndexPermissionsAreSubset(eq(requestInfo), eq(EmptyAuthorizationInfo.INSTANCE), any(Map.class), + any(ActionListener.class)); + interceptor.intercept(requestInfo, mockEngine, EmptyAuthorizationInfo.INSTANCE, plainActionFuture); + plainActionFuture.actionGet(); + } } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/interceptor/ResizeRequestInterceptorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/interceptor/ResizeRequestInterceptorTests.java similarity index 61% rename from x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/interceptor/ResizeRequestInterceptorTests.java rename to x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/interceptor/ResizeRequestInterceptorTests.java index c7835935825ac..287c9670b2609 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/interceptor/ResizeRequestInterceptorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/interceptor/ResizeRequestInterceptorTests.java @@ -3,12 +3,14 @@ * 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; +package org.elasticsearch.xpack.security.authz.interceptor; import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.ActionListener; 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.action.support.PlainActionFuture; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.settings.Settings; @@ -18,6 +20,10 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationResult; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.EmptyAuthorizationInfo; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.RequestInfo; import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField; import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions; @@ -28,8 +34,12 @@ import org.elasticsearch.xpack.security.audit.AuditTrailService; import java.util.Collections; +import java.util.Map; import java.util.Set; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -60,7 +70,6 @@ public void testResizeRequestInterceptorThrowsWhenFLSDLSEnabled() { } else { queries = null; } - Role role = Role.builder().add(fieldPermissions, queries, IndexPrivilege.ALL, randomBoolean(), "foo").build(); final String action = randomFrom(ShrinkAction.NAME, ResizeAction.NAME); IndicesAccessControl accessControl = new IndicesAccessControl(true, Collections.singletonMap("foo", new IndicesAccessControl.IndexAccessControl(true, fieldPermissions, queries))); @@ -69,8 +78,20 @@ public void testResizeRequestInterceptorThrowsWhenFLSDLSEnabled() { ResizeRequestInterceptor resizeRequestInterceptor = new ResizeRequestInterceptor(threadPool, licenseState, auditTrailService); + PlainActionFuture plainActionFuture = new PlainActionFuture<>(); + RequestInfo requestInfo = new RequestInfo(authentication, new ResizeRequest("bar", "foo"), action); + AuthorizationEngine mockEngine = mock(AuthorizationEngine.class); + doAnswer(invocationOnMock -> { + ActionListener listener = (ActionListener) invocationOnMock.getArguments()[3]; + listener.onResponse(AuthorizationResult.deny()); + return null; + }).when(mockEngine).validateIndexPermissionsAreSubset(eq(requestInfo), eq(EmptyAuthorizationInfo.INSTANCE), any(Map.class), + any(ActionListener.class)); ElasticsearchSecurityException securityException = expectThrows(ElasticsearchSecurityException.class, - () -> resizeRequestInterceptor.intercept(new ResizeRequest("bar", "foo"), authentication, role, action)); + () -> { + resizeRequestInterceptor.intercept(requestInfo, mockEngine, EmptyAuthorizationInfo.INSTANCE, plainActionFuture); + plainActionFuture.actionGet(); + }); assertEquals("Resize requests are not allowed for users when field or document level security is enabled on the source index", securityException.getMessage()); } @@ -95,12 +116,38 @@ public void testResizeRequestInterceptorThrowsWhenTargetHasGreaterPermissions() threadContext.putTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY, accessControl); ResizeRequestInterceptor resizeRequestInterceptor = new ResizeRequestInterceptor(threadPool, licenseState, auditTrailService); - ElasticsearchSecurityException securityException = expectThrows(ElasticsearchSecurityException.class, - () -> resizeRequestInterceptor.intercept(new ResizeRequest("target", "source"), authentication, role, action)); - assertEquals("Resizing an index is not allowed when the target index has more permissions than the source index", + + AuthorizationEngine mockEngine = mock(AuthorizationEngine.class); + { + PlainActionFuture plainActionFuture = new PlainActionFuture<>(); + RequestInfo requestInfo = new RequestInfo(authentication, new ResizeRequest("target", "source"), action); + doAnswer(invocationOnMock -> { + ActionListener listener = (ActionListener) invocationOnMock.getArguments()[3]; + listener.onResponse(AuthorizationResult.deny()); + return null; + }).when(mockEngine).validateIndexPermissionsAreSubset(eq(requestInfo), eq(EmptyAuthorizationInfo.INSTANCE), any(Map.class), + any(ActionListener.class)); + ElasticsearchSecurityException securityException = expectThrows(ElasticsearchSecurityException.class, + () -> { + resizeRequestInterceptor.intercept(requestInfo, mockEngine, EmptyAuthorizationInfo.INSTANCE, plainActionFuture); + plainActionFuture.actionGet(); + }); + 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"), authentication, role, action); + { + PlainActionFuture plainActionFuture = new PlainActionFuture<>(); + RequestInfo requestInfo = new RequestInfo(authentication, new ResizeRequest("source", "target"), action); + doAnswer(invocationOnMock -> { + ActionListener listener = (ActionListener) invocationOnMock.getArguments()[3]; + listener.onResponse(AuthorizationResult.granted()); + return null; + }).when(mockEngine).validateIndexPermissionsAreSubset(eq(requestInfo), eq(EmptyAuthorizationInfo.INSTANCE), any(Map.class), + any(ActionListener.class)); + resizeRequestInterceptor.intercept(requestInfo, mockEngine, EmptyAuthorizationInfo.INSTANCE, plainActionFuture); + plainActionFuture.actionGet(); + } } }