From 8ccdc1952ec5c61bf06d87b275daae7c3ecf4735 Mon Sep 17 00:00:00 2001 From: Jay Modi Date: Mon, 7 Jan 2019 13:43:22 -0700 Subject: [PATCH 01/15] Introduce asynchronous RBACEngine (#36245) In order to support the concept of different authorization engines, this change begins the refactoring of the AuthorizationService to support this. Previously, the asynchronous work for authorization was performed by the AsyncAuthorizer class, but this tied the authorization service to a role based implementation. In this change, the authorize method become asynchronous and delegates much of the actual permission checking to an AuthorizationEngine. The pre-existing RBAC permission checking has been abstracted into the RBACEngine. The majority of calls to AuthorizationEngine instances are asynchronous as the underlying implementation may need to make network calls that should not block the current thread, which are often network threads. This change is meant to be built upon. The basic concepts are introduced without proper documentation, plumbing to enable other AuthorizationEngine types, and some items we may want to refactor. For example, the AuthorizedIndices class is lazily loaded but this might actually be something we want to make asynchronous. We pass a lot of the same arguments to the various methods and it would be prudent to wrap these in a class; this class would provide a way for us to pass additional items needed by future enhancements without breaking the interface and requiring updates to all implementations. See #32435 --- .../accesscontrol/IndicesAccessControl.java | 1 + .../authz/permission/IndicesPermission.java | 9 +- .../core/security/authz/permission/Role.java | 8 +- .../core/security/support/Exceptions.java | 6 +- .../authz/store/ReservedRolesStoreTests.java | 12 +- .../action/filter/SecurityActionFilter.java | 36 +- .../IndicesAliasesRequestInterceptor.java | 4 +- .../interceptor/ResizeRequestInterceptor.java | 4 +- .../TransportGetUserPrivilegesAction.java | 13 +- .../user/TransportHasPrivilegesAction.java | 13 +- .../xpack/security/audit/AuditTrail.java | 16 +- .../security/audit/AuditTrailService.java | 26 +- .../security/audit/index/IndexAuditTrail.java | 60 +- .../audit/logfile/LoggingAuditTrail.java | 63 +- .../security/authc/AuthenticationService.java | 6 +- .../security/authz/AuthorizationEngine.java | 111 +++ .../security/authz/AuthorizationService.java | 740 ++++++++---------- .../security/authz/AuthorizationUtils.java | 61 -- .../security/authz/AuthorizedIndices.java | 47 +- .../xpack/security/authz/RBACEngine.java | 406 ++++++++++ .../SecuritySearchOperationListener.java | 10 +- .../authz/store/CompositeRolesStore.java | 45 ++ .../transport/ServerTransportFilter.java | 15 +- .../filter/SecurityActionFilterTests.java | 51 +- ...TransportGetUserPrivilegesActionTests.java | 4 +- .../TransportHasPrivilegesActionTests.java | 11 +- .../audit/AuditTrailServiceTests.java | 17 +- .../index/IndexAuditTrailMutedTests.java | 12 +- .../audit/index/IndexAuditTrailTests.java | 38 +- .../logfile/LoggingAuditTrailFilterTests.java | 216 ++--- .../audit/logfile/LoggingAuditTrailTests.java | 70 +- .../authc/AuthenticationServiceTests.java | 7 +- .../authz/AuthorizationServiceTests.java | 505 +++++------- .../authz/AuthorizedIndicesTests.java | 24 +- .../authz/IndicesAndAliasesResolverTests.java | 18 +- .../xpack/security/authz/RBACEngineTests.java | 194 +++++ .../SecuritySearchOperationListenerTests.java | 48 +- .../accesscontrol/IndicesPermissionTests.java | 25 +- .../authz/store/CompositeRolesStoreTests.java | 121 ++- .../transport/ServerTransportFilterTests.java | 30 +- 40 files changed, 1898 insertions(+), 1205 deletions(-) create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationEngine.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/IndicesAccessControl.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/IndicesAccessControl.java index 6df9ad834c1e5..cf3967b9b563d 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/IndicesAccessControl.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/IndicesAccessControl.java @@ -23,6 +23,7 @@ public class IndicesAccessControl { public static final IndicesAccessControl ALLOW_NO_INDICES = new IndicesAccessControl(true, Collections.singletonMap(IndicesAndAliasesResolverField.NO_INDEX_PLACEHOLDER, new IndicesAccessControl.IndexAccessControl(true, new FieldPermissions(), null))); + public static final IndicesAccessControl DENIED = new IndicesAccessControl(false, Collections.emptyMap()); private final boolean granted; private final Map indexPermissions; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 4936071ee8445..37ea674e2bf9f 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -11,7 +11,6 @@ import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.cluster.metadata.AliasOrIndex; import org.elasticsearch.cluster.metadata.IndexMetaData; -import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesReference; @@ -28,7 +27,6 @@ import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.SortedMap; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.function.Predicate; @@ -152,11 +150,10 @@ public Automaton allowedActionsMatcher(String index) { * Authorizes the provided action against the provided indices, given the current cluster metadata */ public Map authorize(String action, Set requestedIndicesOrAliases, - MetaData metaData, FieldPermissionsCache fieldPermissionsCache) { + Function allAliasesAndIndices, + FieldPermissionsCache fieldPermissionsCache) { // now... every index that is associated with the request, must be granted // by at least one indices permission group - - SortedMap allAliasesAndIndices = metaData.getAliasAndIndexLookup(); Map> fieldPermissionsByIndex = new HashMap<>(); Map roleQueriesByIndex = new HashMap<>(); Map grantedBuilder = new HashMap<>(); @@ -164,7 +161,7 @@ public Map authorize(String act for (String indexOrAlias : requestedIndicesOrAliases) { boolean granted = false; Set concreteIndices = new HashSet<>(); - AliasOrIndex aliasOrIndex = allAliasesAndIndices.get(indexOrAlias); + AliasOrIndex aliasOrIndex = allAliasesAndIndices.apply(indexOrAlias); if (aliasOrIndex != null) { for (IndexMetaData indexMetaData : aliasOrIndex.getIndices()) { concreteIndices.add(indexMetaData.getIndex().getName()); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java index 8a68e71d0b93a..9b48207a8307c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java @@ -5,7 +5,7 @@ */ package org.elasticsearch.xpack.core.security.authz.permission; -import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.cluster.metadata.AliasOrIndex; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.collect.Tuple; @@ -25,6 +25,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.function.Function; public final class Role { @@ -77,10 +78,11 @@ public static Builder builder(RoleDescriptor rd, FieldPermissionsCache fieldPerm * specified action with the requested indices/aliases. At the same time if field and/or document level security * is configured for any group also the allowed fields and role queries are resolved. */ - public IndicesAccessControl authorize(String action, Set requestedIndicesOrAliases, MetaData metaData, + public IndicesAccessControl authorize(String action, Set requestedIndicesOrAliases, + Function aliasAndIndexLookup, FieldPermissionsCache fieldPermissionsCache) { Map indexPermissions = indices.authorize( - action, requestedIndicesOrAliases, metaData, fieldPermissionsCache + action, requestedIndicesOrAliases, aliasAndIndexLookup, fieldPermissionsCache ); // At least one role / indices permission set need to match with all the requested indices/aliases: diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/Exceptions.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/Exceptions.java index 9cf09482a5268..18638b15335bc 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/Exceptions.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/Exceptions.java @@ -27,6 +27,10 @@ public static ElasticsearchSecurityException authenticationError(String msg, Obj } public static ElasticsearchSecurityException authorizationError(String msg, Object... args) { - return new ElasticsearchSecurityException(msg, RestStatus.FORBIDDEN, args); + return new ElasticsearchSecurityException(msg, RestStatus.FORBIDDEN, null, args); + } + + public static ElasticsearchSecurityException authorizationError(String msg, Exception cause, Object... args) { + return new ElasticsearchSecurityException(msg, RestStatus.FORBIDDEN, cause, args); } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java index fc9869a12803f..ce0aea0d6827a 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java @@ -31,6 +31,7 @@ import org.elasticsearch.action.search.SearchAction; import org.elasticsearch.action.update.UpdateAction; import org.elasticsearch.cluster.metadata.AliasMetaData; +import org.elasticsearch.cluster.metadata.AliasOrIndex; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.common.settings.Settings; @@ -118,6 +119,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.Map; +import java.util.SortedMap; import static org.hamcrest.Matchers.hasEntry; import static org.hamcrest.Matchers.is; @@ -587,18 +589,20 @@ public void testSuperuserRole() { .build(); FieldPermissionsCache fieldPermissionsCache = new FieldPermissionsCache(Settings.EMPTY); + SortedMap lookup = metaData.getAliasAndIndexLookup(); Map authzMap = - superuserRole.indices().authorize(SearchAction.NAME, Sets.newHashSet("a1", "ba"), metaData, fieldPermissionsCache); + superuserRole.indices().authorize(SearchAction.NAME, Sets.newHashSet("a1", "ba"), lookup::get, fieldPermissionsCache); assertThat(authzMap.get("a1").isGranted(), is(true)); assertThat(authzMap.get("b").isGranted(), is(true)); - authzMap = superuserRole.indices().authorize(DeleteIndexAction.NAME, Sets.newHashSet("a1", "ba"), metaData, fieldPermissionsCache); + authzMap = + superuserRole.indices().authorize(DeleteIndexAction.NAME, Sets.newHashSet("a1", "ba"), lookup::get, fieldPermissionsCache); assertThat(authzMap.get("a1").isGranted(), is(true)); assertThat(authzMap.get("b").isGranted(), is(true)); - authzMap = superuserRole.indices().authorize(IndexAction.NAME, Sets.newHashSet("a2", "ba"), metaData, fieldPermissionsCache); + authzMap = superuserRole.indices().authorize(IndexAction.NAME, Sets.newHashSet("a2", "ba"), lookup::get, fieldPermissionsCache); assertThat(authzMap.get("a2").isGranted(), is(true)); assertThat(authzMap.get("b").isGranted(), is(true)); authzMap = superuserRole.indices() - .authorize(UpdateSettingsAction.NAME, Sets.newHashSet("aaaaaa", "ba"), metaData, fieldPermissionsCache); + .authorize(UpdateSettingsAction.NAME, Sets.newHashSet("aaaaaa", "ba"), lookup::get, fieldPermissionsCache); assertThat(authzMap.get("aaaaaa").isGranted(), is(true)); assertThat(authzMap.get("b").isGranted(), is(true)); assertTrue(superuserRole.indices().check(SearchAction.NAME)); 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 a0ab370e6dba2..ec1c1cc10fce1 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,14 +28,17 @@ 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.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; @@ -164,21 +167,24 @@ private void authorizeRequest(Authentication aut if (authentication == null) { listener.onFailure(new IllegalArgumentException("authentication must be non null for authorization")); } else { - final AuthorizationUtils.AsyncAuthorizer asyncAuthorizer = new AuthorizationUtils.AsyncAuthorizer(authentication, listener, - (userRoles, runAsRoles) -> { - authzService.authorize(authentication, securityAction, request, userRoles, runAsRoles); - /* - * We use a separate concept for code that needs to be run after authentication and authorization that could - * affect the running of the action. This is done to make it more clear of the state of the request. - */ - for (RequestInterceptor interceptor : requestInterceptors) { - if (interceptor.supports(request)) { - interceptor.intercept(request, authentication, runAsRoles != null ? runAsRoles : userRoles, securityAction); - } - } - listener.onResponse(null); - }); - asyncAuthorizer.authorize(authzService); + 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)); } } } 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 index 3a1234d4525a7..acc51ba8dff09 100644 --- 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 @@ -24,6 +24,8 @@ 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; @@ -72,7 +74,7 @@ public void intercept(IndicesAliasesRequest request, Authentication authenticati 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, - userPermissions.names()); + 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"); } 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 index d5b733c49c968..c0bf38bf8bf98 100644 --- 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 @@ -22,6 +22,7 @@ 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 { @@ -61,7 +62,8 @@ public void intercept(ResizeRequest request, Authentication authentication, Role 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, userPermissions.names()); + 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"); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportGetUserPrivilegesAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportGetUserPrivilegesAction.java index 518c9cb25a01a..273a6b7c7315e 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportGetUserPrivilegesAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportGetUserPrivilegesAction.java @@ -13,6 +13,7 @@ import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.tasks.Task; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; @@ -21,6 +22,7 @@ import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsCache; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsDefinition; import org.elasticsearch.xpack.core.security.authz.permission.IndicesPermission; import org.elasticsearch.xpack.core.security.authz.permission.Role; @@ -29,7 +31,7 @@ import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.Privilege; import org.elasticsearch.xpack.core.security.user.User; -import org.elasticsearch.xpack.security.authz.AuthorizationService; +import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; import java.util.Arrays; import java.util.Collections; @@ -46,14 +48,14 @@ public class TransportGetUserPrivilegesAction extends HandledTransportAction { private final ThreadPool threadPool; - private final AuthorizationService authorizationService; + private final CompositeRolesStore rolesStore; @Inject public TransportGetUserPrivilegesAction(ThreadPool threadPool, TransportService transportService, - ActionFilters actionFilters, AuthorizationService authorizationService) { + ActionFilters actionFilters, CompositeRolesStore rolesStore) { super(GetUserPrivilegesAction.NAME, transportService, actionFilters, GetUserPrivilegesRequest::new); this.threadPool = threadPool; - this.authorizationService = authorizationService; + this.rolesStore = rolesStore; } @Override @@ -66,7 +68,8 @@ protected void doExecute(Task task, GetUserPrivilegesRequest request, ActionList return; } - authorizationService.roles(user, ActionListener.wrap( + // FIXME reuse field permissions cache! + rolesStore.getRoles(user, new FieldPermissionsCache(Settings.EMPTY), ActionListener.wrap( role -> listener.onResponse(buildResponseObject(role)), listener::onFailure)); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesAction.java index 20fa9f522e710..9f490fd77012e 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesAction.java @@ -13,6 +13,7 @@ import org.elasticsearch.action.support.HandledTransportAction; import org.elasticsearch.common.Strings; import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.tasks.Task; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; @@ -21,6 +22,7 @@ import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesResponse; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsCache; import org.elasticsearch.xpack.core.security.authz.permission.IndicesPermission; import org.elasticsearch.xpack.core.security.authz.permission.Role; import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; @@ -30,7 +32,7 @@ import org.elasticsearch.xpack.core.security.authz.privilege.Privilege; import org.elasticsearch.xpack.core.security.support.Automatons; import org.elasticsearch.xpack.core.security.user.User; -import org.elasticsearch.xpack.security.authz.AuthorizationService; +import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; import org.elasticsearch.xpack.security.authz.store.NativePrivilegeStore; import java.util.ArrayList; @@ -51,16 +53,16 @@ public class TransportHasPrivilegesAction extends HandledTransportAction { private final ThreadPool threadPool; - private final AuthorizationService authorizationService; + private final CompositeRolesStore rolesStore; private final NativePrivilegeStore privilegeStore; @Inject public TransportHasPrivilegesAction(ThreadPool threadPool, TransportService transportService, - ActionFilters actionFilters, AuthorizationService authorizationService, + ActionFilters actionFilters, CompositeRolesStore rolesStore, NativePrivilegeStore privilegeStore) { super(HasPrivilegesAction.NAME, transportService, actionFilters, HasPrivilegesRequest::new); this.threadPool = threadPool; - this.authorizationService = authorizationService; + this.rolesStore = rolesStore; this.privilegeStore = privilegeStore; } @@ -74,7 +76,8 @@ protected void doExecute(Task task, HasPrivilegesRequest request, ActionListener return; } - authorizationService.roles(user, ActionListener.wrap( + // FIXME + rolesStore.getRoles(user, new FieldPermissionsCache(Settings.EMPTY), ActionListener.wrap( role -> resolveApplicationPrivileges(request, ActionListener.wrap( applicationPrivilegeLookup -> checkPrivileges(request, role, applicationPrivilegeLookup, listener), listener::onFailure)), diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditTrail.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditTrail.java index 4f5413c30d1f9..40723bba21041 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditTrail.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditTrail.java @@ -10,6 +10,7 @@ import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.security.authz.AuthorizationEngine.AuthorizationInfo; import org.elasticsearch.xpack.security.transport.filter.SecurityIpFilterRule; import java.net.InetAddress; @@ -40,9 +41,11 @@ public interface AuditTrail { void authenticationFailed(String requestId, String realm, AuthenticationToken token, RestRequest request); - void accessGranted(String requestId, Authentication authentication, String action, TransportMessage message, String[] roleNames); + void accessGranted(String requestId, Authentication authentication, String action, TransportMessage message, + AuthorizationInfo authorizationInfo); - void accessDenied(String requestId, Authentication authentication, String action, TransportMessage message, String[] roleNames); + void accessDenied(String requestId, Authentication authentication, String action, TransportMessage message, + AuthorizationInfo authorizationInfo); void tamperedRequest(String requestId, RestRequest request); @@ -60,10 +63,13 @@ public interface AuditTrail { void connectionDenied(InetAddress inetAddress, String profile, SecurityIpFilterRule rule); - void runAsGranted(String requestId, Authentication authentication, String action, TransportMessage message, String[] roleNames); + void runAsGranted(String requestId, Authentication authentication, String action, TransportMessage message, + AuthorizationInfo authorizationInfo); - void runAsDenied(String requestId, Authentication authentication, String action, TransportMessage message, String[] roleNames); + void runAsDenied(String requestId, Authentication authentication, String action, TransportMessage message, + AuthorizationInfo authorizationInfo); - void runAsDenied(String requestId, Authentication authentication, RestRequest request, String[] roleNames); + void runAsDenied(String requestId, Authentication authentication, RestRequest request, + AuthorizationInfo authorizationInfo); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditTrailService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditTrailService.java index d6645227f8eb6..41c1f0787b3dd 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditTrailService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditTrailService.java @@ -11,6 +11,7 @@ import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.security.authz.AuthorizationEngine.AuthorizationInfo; import org.elasticsearch.xpack.security.transport.filter.SecurityIpFilterRule; import java.net.InetAddress; @@ -128,19 +129,21 @@ public void authenticationFailed(String requestId, String realm, AuthenticationT } @Override - public void accessGranted(String requestId, Authentication authentication, String action, TransportMessage msg, String[] roleNames) { + public void accessGranted(String requestId, Authentication authentication, String action, TransportMessage msg, + AuthorizationInfo authorizationInfo) { if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { - auditTrail.accessGranted(requestId, authentication, action, msg, roleNames); + auditTrail.accessGranted(requestId, authentication, action, msg, authorizationInfo); } } } @Override - public void accessDenied(String requestId, Authentication authentication, String action, TransportMessage message, String[] roleNames) { + public void accessDenied(String requestId, Authentication authentication, String action, TransportMessage message, + AuthorizationInfo authorizationInfo) { if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { - auditTrail.accessDenied(requestId, authentication, action, message, roleNames); + auditTrail.accessDenied(requestId, authentication, action, message, authorizationInfo); } } } @@ -191,28 +194,31 @@ public void connectionDenied(InetAddress inetAddress, String profile, SecurityIp } @Override - public void runAsGranted(String requestId, Authentication authentication, String action, TransportMessage message, String[] roleNames) { + public void runAsGranted(String requestId, Authentication authentication, String action, TransportMessage message, + AuthorizationInfo authorizationInfo) { if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { - auditTrail.runAsGranted(requestId, authentication, action, message, roleNames); + auditTrail.runAsGranted(requestId, authentication, action, message, authorizationInfo); } } } @Override - public void runAsDenied(String requestId, Authentication authentication, String action, TransportMessage message, String[] roleNames) { + public void runAsDenied(String requestId, Authentication authentication, String action, TransportMessage message, + AuthorizationInfo authorizationInfo) { if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { - auditTrail.runAsDenied(requestId, authentication, action, message, roleNames); + auditTrail.runAsDenied(requestId, authentication, action, message, authorizationInfo); } } } @Override - public void runAsDenied(String requestId, Authentication authentication, RestRequest request, String[] roleNames) { + public void runAsDenied(String requestId, Authentication authentication, RestRequest request, + AuthorizationInfo authorizationInfo) { if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { - auditTrail.runAsDenied(requestId, authentication, request, roleNames); + auditTrail.runAsDenied(requestId, authentication, request, authorizationInfo); } } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrail.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrail.java index 914a029c0c434..97043bfc3f945 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrail.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrail.java @@ -56,10 +56,12 @@ import org.elasticsearch.xpack.core.security.index.IndexAuditTrailField; import org.elasticsearch.xpack.core.security.user.SystemUser; import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.core.security.user.XPackSecurityUser; import org.elasticsearch.xpack.core.security.user.XPackUser; import org.elasticsearch.xpack.core.template.TemplateUtils; import org.elasticsearch.xpack.security.audit.AuditLevel; import org.elasticsearch.xpack.security.audit.AuditTrail; +import org.elasticsearch.xpack.security.authz.AuthorizationEngine.AuthorizationInfo; import org.elasticsearch.xpack.security.rest.RemoteHostHeader; import org.elasticsearch.xpack.security.support.SecurityIndexManager; import org.elasticsearch.xpack.security.transport.filter.SecurityIpFilterRule; @@ -108,6 +110,7 @@ import static org.elasticsearch.xpack.security.audit.AuditUtil.indices; import static org.elasticsearch.xpack.security.audit.AuditUtil.restRequestContent; import static org.elasticsearch.xpack.security.audit.index.IndexNameResolver.resolve; +import static org.elasticsearch.xpack.security.audit.logfile.LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME; import static org.elasticsearch.xpack.security.support.SecurityIndexManager.SECURITY_VERSION_STRING; /** @@ -601,9 +604,10 @@ public void authenticationFailed(String requestId, String realm, AuthenticationT } @Override - public void accessGranted(String requestId, Authentication authentication, String action, TransportMessage msg, String[] roleNames) { + public void accessGranted(String requestId, Authentication authentication, String action, TransportMessage msg, + AuthorizationInfo authorizationInfo) { final User user = authentication.getUser(); - final boolean isSystem = SystemUser.is(user) || XPackUser.is(user); + final boolean isSystem = SystemUser.is(user) || XPackUser.is(user) || XPackSecurityUser.is(user); final boolean logSystemAccessGranted = isSystem && events.contains(SYSTEM_ACCESS_GRANTED); final boolean shouldLog = logSystemAccessGranted || (isSystem == false && events.contains(ACCESS_GRANTED)); if (shouldLog) { @@ -611,7 +615,7 @@ public void accessGranted(String requestId, Authentication authentication, Strin assert authentication.getAuthenticatedBy() != null; final String authRealmName = authentication.getAuthenticatedBy().getName(); final String lookRealmName = authentication.getLookedUpBy() == null ? null : authentication.getLookedUpBy().getName(); - enqueue(message("access_granted", action, user, roleNames, new Tuple(authRealmName, lookRealmName), indices(msg), + enqueue(message("access_granted", action, user, authorizationInfo, new Tuple<>(authRealmName, lookRealmName), indices(msg), msg), "access_granted"); } catch (final Exception e) { logger.warn("failed to index audit event: [access_granted]", e); @@ -620,14 +624,16 @@ public void accessGranted(String requestId, Authentication authentication, Strin } @Override - public void accessDenied(String requestId, Authentication authentication, String action, TransportMessage message, String[] roleNames) { - if (events.contains(ACCESS_DENIED) && (XPackUser.is(authentication.getUser()) == false)) { + public void accessDenied(String requestId, Authentication authentication, String action, TransportMessage message, + AuthorizationInfo authorizationInfo) { + if (events.contains(ACCESS_DENIED) && (XPackUser.is(authentication.getUser()) == false && + XPackSecurityUser.is(authentication.getUser()) == false)) { try { assert authentication.getAuthenticatedBy() != null; final String authRealmName = authentication.getAuthenticatedBy().getName(); final String lookRealmName = authentication.getLookedUpBy() == null ? null : authentication.getLookedUpBy().getName(); - enqueue(message("access_denied", action, authentication.getUser(), roleNames, new Tuple(authRealmName, lookRealmName), - indices(message), message), "access_denied"); + enqueue(message("access_denied", action, authentication.getUser(), authorizationInfo, + new Tuple<>(authRealmName, lookRealmName), indices(message), message), "access_denied"); } catch (final Exception e) { logger.warn("failed to index audit event: [access_denied]", e); } @@ -690,14 +696,15 @@ public void connectionDenied(InetAddress inetAddress, String profile, SecurityIp } @Override - public void runAsGranted(String requestId, Authentication authentication, String action, TransportMessage message, String[] roleNames) { + public void runAsGranted(String requestId, Authentication authentication, String action, TransportMessage message, + AuthorizationInfo authorizationInfo) { if (events.contains(RUN_AS_GRANTED)) { try { assert authentication.getAuthenticatedBy() != null; final String authRealmName = authentication.getAuthenticatedBy().getName(); final String lookRealmName = authentication.getLookedUpBy() == null ? null : authentication.getLookedUpBy().getName(); - enqueue(message("run_as_granted", action, authentication.getUser(), roleNames, new Tuple<>(authRealmName, lookRealmName), - null, message), "run_as_granted"); + enqueue(message("run_as_granted", action, authentication.getUser(), authorizationInfo, + new Tuple<>(authRealmName, lookRealmName), null, message), "run_as_granted"); } catch (final Exception e) { logger.warn("failed to index audit event: [run_as_granted]", e); } @@ -705,14 +712,15 @@ public void runAsGranted(String requestId, Authentication authentication, String } @Override - public void runAsDenied(String requestId, Authentication authentication, String action, TransportMessage message, String[] roleNames) { + public void runAsDenied(String requestId, Authentication authentication, String action, TransportMessage message, + AuthorizationInfo authorizationInfo) { if (events.contains(RUN_AS_DENIED)) { try { assert authentication.getAuthenticatedBy() != null; final String authRealmName = authentication.getAuthenticatedBy().getName(); final String lookRealmName = authentication.getLookedUpBy() == null ? null : authentication.getLookedUpBy().getName(); - enqueue(message("run_as_denied", action, authentication.getUser(), roleNames, new Tuple<>(authRealmName, lookRealmName), - null, message), "run_as_denied"); + enqueue(message("run_as_denied", action, authentication.getUser(), authorizationInfo, + new Tuple<>(authRealmName, lookRealmName), null, message), "run_as_denied"); } catch (final Exception e) { logger.warn("failed to index audit event: [run_as_denied]", e); } @@ -720,21 +728,21 @@ public void runAsDenied(String requestId, Authentication authentication, String } @Override - public void runAsDenied(String requestId, Authentication authentication, RestRequest request, String[] roleNames) { + public void runAsDenied(String requestId, Authentication authentication, RestRequest request, AuthorizationInfo authorizationInfo) { if (events.contains(RUN_AS_DENIED)) { try { assert authentication.getAuthenticatedBy() != null; final String authRealmName = authentication.getAuthenticatedBy().getName(); final String lookRealmName = authentication.getLookedUpBy() == null ? null : authentication.getLookedUpBy().getName(); - enqueue(message("run_as_denied", new Tuple<>(authRealmName, lookRealmName), authentication.getUser(), roleNames, request), - "run_as_denied"); + enqueue(message("run_as_denied", new Tuple<>(authRealmName, lookRealmName), authentication.getUser(), authorizationInfo, + request), "run_as_denied"); } catch (final Exception e) { logger.warn("failed to index audit event: [run_as_denied]", e); } } } - private Message message(String type, @Nullable String action, @Nullable User user, @Nullable String[] roleNames, + private Message message(String type, @Nullable String action, @Nullable User user, @Nullable AuthorizationInfo authorizationInfo, @Nullable Tuple realms, @Nullable Set indices, TransportMessage message) throws Exception { @@ -746,8 +754,11 @@ private Message message(String type, @Nullable String action, @Nullable User use msg.builder.field(Field.ACTION, action); } addUserAndRealmFields(msg.builder, type, user, realms); - if (roleNames != null) { - msg.builder.array(Field.ROLE_NAMES, roleNames); + if (authorizationInfo != null) { + final String[] roleNames = (String[]) authorizationInfo.asMap().get(PRINCIPAL_ROLES_FIELD_NAME); + if (roleNames != null) { + msg.builder.array(Field.ROLE_NAMES, roleNames); + } } if (indices != null) { msg.builder.array(Field.INDICES, indices.toArray(Strings.EMPTY_ARRAY)); @@ -850,15 +861,18 @@ private Message message(String type, @Nullable String action, @Nullable Authenti return msg.end(); } - private Message message(String type, @Nullable Tuple realms, @Nullable User user, @Nullable String[] roleNames, - RestRequest request) throws Exception { + private Message message(String type, @Nullable Tuple realms, @Nullable User user, + @Nullable AuthorizationInfo authorizationInfo, RestRequest request) throws Exception { Message msg = new Message().start(); common("rest", type, msg.builder); addUserAndRealmFields(msg.builder, type, user, realms); - if (roleNames != null) { - msg.builder.array(Field.ROLE_NAMES, roleNames); + if (authorizationInfo != null) { + final String[] roleNames = (String[]) authorizationInfo.asMap().get(PRINCIPAL_ROLES_FIELD_NAME); + if (roleNames != null) { + msg.builder.array(Field.ROLE_NAMES, roleNames); + } } if (includeRequestBody) { msg.builder.field(Field.REQUEST_BODY, restRequestContent(request)); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java index cdeee882c1bda..a3c64b170887e 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java @@ -34,6 +34,7 @@ import org.elasticsearch.xpack.core.security.user.XPackUser; import org.elasticsearch.xpack.security.audit.AuditLevel; import org.elasticsearch.xpack.security.audit.AuditTrail; +import org.elasticsearch.xpack.security.authz.AuthorizationEngine.AuthorizationInfo; import org.elasticsearch.xpack.security.rest.RemoteHostHeader; import org.elasticsearch.xpack.security.transport.filter.IPFilter; import org.elasticsearch.xpack.security.transport.filter.SecurityIpFilterRule; @@ -50,6 +51,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; import java.util.TreeMap; import java.util.function.Function; @@ -413,13 +415,14 @@ public void authenticationFailed(String requestId, String realm, AuthenticationT } @Override - public void accessGranted(String requestId, Authentication authentication, String action, TransportMessage msg, String[] roleNames) { + public void accessGranted(String requestId, Authentication authentication, String action, TransportMessage msg, + AuthorizationInfo authorizationInfo) { final User user = authentication.getUser(); final boolean isSystem = SystemUser.is(user) || XPackUser.is(user); if ((isSystem && events.contains(SYSTEM_ACCESS_GRANTED)) || ((isSystem == false) && events.contains(ACCESS_GRANTED))) { final Optional indices = indices(msg); if (eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(user), - Optional.of(effectiveRealmName(authentication)), Optional.of(roleNames), indices)) == false) { + Optional.of(effectiveRealmName(authentication)), Optional.of(authorizationInfo), indices)) == false) { final StringMapMessage logEntry = new LogEntryBuilder() .with(EVENT_TYPE_FIELD_NAME, TRANSPORT_ORIGIN_FIELD_VALUE) .with(EVENT_ACTION_FIELD_NAME, "access_granted") @@ -429,9 +432,9 @@ public void accessGranted(String requestId, Authentication authentication, Strin .withSubject(authentication) .withRestOrTransportOrigin(msg, threadContext) .with(INDICES_FIELD_NAME, indices.orElse(null)) - .with(PRINCIPAL_ROLES_FIELD_NAME, roleNames) .withOpaqueId(threadContext) .withXForwardedFor(threadContext) + .with(authorizationInfo.asMap()) .build(); logger.info(logEntry); } @@ -439,11 +442,12 @@ public void accessGranted(String requestId, Authentication authentication, Strin } @Override - public void accessDenied(String requestId, Authentication authentication, String action, TransportMessage message, String[] roleNames) { + public void accessDenied(String requestId, Authentication authentication, String action, TransportMessage message, + AuthorizationInfo authorizationInfo) { if (events.contains(ACCESS_DENIED)) { final Optional indices = indices(message); if (eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(authentication.getUser()), - Optional.of(effectiveRealmName(authentication)), Optional.of(roleNames), indices)) == false) { + Optional.of(effectiveRealmName(authentication)), Optional.of(authorizationInfo), indices)) == false) { final StringMapMessage logEntry = new LogEntryBuilder() .with(EVENT_TYPE_FIELD_NAME, TRANSPORT_ORIGIN_FIELD_VALUE) .with(EVENT_ACTION_FIELD_NAME, "access_denied") @@ -453,7 +457,7 @@ public void accessDenied(String requestId, Authentication authentication, String .withSubject(authentication) .withRestOrTransportOrigin(message, threadContext) .with(INDICES_FIELD_NAME, indices.orElse(null)) - .with(PRINCIPAL_ROLES_FIELD_NAME, roleNames) + .with(authorizationInfo.asMap()) .withOpaqueId(threadContext) .withXForwardedFor(threadContext) .build(); @@ -561,11 +565,12 @@ public void connectionDenied(InetAddress inetAddress, String profile, SecurityIp } @Override - public void runAsGranted(String requestId, Authentication authentication, String action, TransportMessage message, String[] roleNames) { + public void runAsGranted(String requestId, Authentication authentication, String action, TransportMessage message, + AuthorizationInfo authorizationInfo) { if (events.contains(RUN_AS_GRANTED)) { final Optional indices = indices(message); if (eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(authentication.getUser()), - Optional.of(effectiveRealmName(authentication)), Optional.of(roleNames), indices)) == false) { + Optional.of(effectiveRealmName(authentication)), Optional.of(authorizationInfo), indices)) == false) { final StringMapMessage logEntry = new LogEntryBuilder() .with(EVENT_TYPE_FIELD_NAME, TRANSPORT_ORIGIN_FIELD_VALUE) .with(EVENT_ACTION_FIELD_NAME, "run_as_granted") @@ -575,7 +580,7 @@ public void runAsGranted(String requestId, Authentication authentication, String .withRunAsSubject(authentication) .withRestOrTransportOrigin(message, threadContext) .with(INDICES_FIELD_NAME, indices.orElse(null)) - .with(PRINCIPAL_ROLES_FIELD_NAME, roleNames) + .with(authorizationInfo.asMap()) .withOpaqueId(threadContext) .withXForwardedFor(threadContext) .build(); @@ -585,11 +590,12 @@ public void runAsGranted(String requestId, Authentication authentication, String } @Override - public void runAsDenied(String requestId, Authentication authentication, String action, TransportMessage message, String[] roleNames) { + public void runAsDenied(String requestId, Authentication authentication, String action, TransportMessage message, + AuthorizationInfo authorizationInfo) { if (events.contains(RUN_AS_DENIED)) { final Optional indices = indices(message); if (eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(authentication.getUser()), - Optional.of(effectiveRealmName(authentication)), Optional.of(roleNames), indices)) == false) { + Optional.of(effectiveRealmName(authentication)), Optional.of(authorizationInfo), indices)) == false) { final StringMapMessage logEntry = new LogEntryBuilder() .with(EVENT_TYPE_FIELD_NAME, TRANSPORT_ORIGIN_FIELD_VALUE) .with(EVENT_ACTION_FIELD_NAME, "run_as_denied") @@ -599,7 +605,7 @@ public void runAsDenied(String requestId, Authentication authentication, String .withRunAsSubject(authentication) .withRestOrTransportOrigin(message, threadContext) .with(INDICES_FIELD_NAME, indices.orElse(null)) - .with(PRINCIPAL_ROLES_FIELD_NAME, roleNames) + .with(authorizationInfo.asMap()) .withOpaqueId(threadContext) .withXForwardedFor(threadContext) .build(); @@ -609,14 +615,14 @@ public void runAsDenied(String requestId, Authentication authentication, String } @Override - public void runAsDenied(String requestId, Authentication authentication, RestRequest request, String[] roleNames) { + public void runAsDenied(String requestId, Authentication authentication, RestRequest request, AuthorizationInfo authorizationInfo) { if (events.contains(RUN_AS_DENIED) && eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(authentication.getUser()), - Optional.of(effectiveRealmName(authentication)), Optional.of(roleNames), Optional.empty())) == false) { + Optional.of(effectiveRealmName(authentication)), Optional.of(authorizationInfo), Optional.empty())) == false) { final StringMapMessage logEntry = new LogEntryBuilder() .with(EVENT_TYPE_FIELD_NAME, REST_ORIGIN_FIELD_VALUE) .with(EVENT_ACTION_FIELD_NAME, "run_as_denied") - .with(PRINCIPAL_ROLES_FIELD_NAME, roleNames) + .with(authorizationInfo.asMap()) .withRestUri(request) .withRunAsSubject(authentication) .withRestOrigin(request) @@ -759,29 +765,40 @@ LogEntryBuilder with(String key, String[] values) { return this; } + LogEntryBuilder with(Map map) { + for (Entry entry : map.entrySet()) { + Object value = entry.getValue(); + if (value.getClass().isArray()) { + logEntry.with(entry.getKey(), toQuotedJsonArray((Object[]) value)); + } else { + logEntry.with(entry.getKey(), value); + } + } + return this; + } + StringMapMessage build() { return logEntry; } - String toQuotedJsonArray(String[] values) { + String toQuotedJsonArray(Object[] values) { assert values != null; final StringBuilder stringBuilder = new StringBuilder(); final JsonStringEncoder jsonStringEncoder = JsonStringEncoder.getInstance(); stringBuilder.append("["); - for (final String value : values) { + for (final Object value : values) { if (value != null) { if (stringBuilder.length() > 1) { stringBuilder.append(","); } stringBuilder.append("\""); - jsonStringEncoder.quoteAsString(value, stringBuilder); + jsonStringEncoder.quoteAsString(value.toString(), stringBuilder); stringBuilder.append("\""); } } stringBuilder.append("]"); return stringBuilder.toString(); } - } @@ -973,7 +990,8 @@ static final class AuditEventMetaInfo { * user field (such as `anonymous_access_denied`) as well as events from the * "elastic" username. */ - AuditEventMetaInfo(Optional user, Optional realm, Optional roles, Optional indices) { + AuditEventMetaInfo(Optional user, Optional realm, Optional authorizationInfo, + Optional indices) { this.principal = user.map(u -> u.principal()).orElse(""); this.realm = realm.orElse(""); // Supplier indirection and lazy generation of Streams serves 2 purposes: @@ -981,7 +999,10 @@ static final class AuditEventMetaInfo { // conditions on the `principal` and `realm` fields // 2. reusability of the AuditEventMetaInfo instance: in this case Streams have // to be regenerated as they cannot be operated upon twice - this.roles = () -> roles.filter(r -> r.length != 0).map(Arrays::stream).orElse(Stream.of("")); + this.roles = () -> authorizationInfo.filter(info -> { + final Object value = info.asMap().get("user.roles"); + return value != null && value instanceof String[] && ((String[]) value).length != 0; + }).map(info -> Arrays.stream((String[]) info.asMap().get("user.roles"))).orElse(Stream.of("")); this.indices = () -> indices.filter(i -> i.length != 0).map(Arrays::stream).orElse(Stream.of("")); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java index ef04f3d22f854..5f35cdbb2ad61 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java @@ -29,7 +29,6 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationServiceField; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; import org.elasticsearch.xpack.core.security.authc.Realm; -import org.elasticsearch.xpack.core.security.authz.permission.Role; import org.elasticsearch.xpack.core.security.support.Exceptions; import org.elasticsearch.xpack.core.security.user.AnonymousUser; import org.elasticsearch.xpack.core.security.user.SystemUser; @@ -38,6 +37,7 @@ import org.elasticsearch.xpack.security.audit.AuditTrailService; import org.elasticsearch.xpack.security.audit.AuditUtil; import org.elasticsearch.xpack.security.authc.support.RealmUserLookup; +import org.elasticsearch.xpack.security.authz.AuthorizationEngine.EmptyAuthorizationInfo; import java.util.LinkedHashMap; import java.util.List; @@ -526,7 +526,7 @@ ElasticsearchSecurityException anonymousAccessDenied() { @Override ElasticsearchSecurityException runAsDenied(Authentication authentication, AuthenticationToken token) { - auditTrail.runAsDenied(requestId, authentication, action, message, Role.EMPTY.names()); + auditTrail.runAsDenied(requestId, authentication, action, message, EmptyAuthorizationInfo.INSTANCE); return failureHandler.failedAuthentication(message, token, action, threadContext); } @@ -590,7 +590,7 @@ ElasticsearchSecurityException anonymousAccessDenied() { @Override ElasticsearchSecurityException runAsDenied(Authentication authentication, AuthenticationToken token) { - auditTrail.runAsDenied(requestId, authentication, request, Role.EMPTY.names()); + auditTrail.runAsDenied(requestId, authentication, request, EmptyAuthorizationInfo.INSTANCE); return failureHandler.failedAuthentication(request, token, threadContext); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationEngine.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationEngine.java new file mode 100644 index 0000000000000..53146286c26f8 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationEngine.java @@ -0,0 +1,111 @@ +/* + * 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; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.cluster.metadata.AliasOrIndex; +import org.elasticsearch.transport.TransportRequest; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; +import org.elasticsearch.xpack.security.authz.IndicesAndAliasesResolver.ResolvedIndices; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +public interface AuthorizationEngine { + + void resolveAuthorizationInfo(Authentication authentication, TransportRequest request, String action, + ActionListener listener); + + void authorizeRunAs(Authentication authentication, TransportRequest request, String action, AuthorizationInfo authorizationInfo, + ActionListener listener); + + void authorizeClusterAction(Authentication authentication, TransportRequest request, String action, AuthorizationInfo authorizationInfo, + ActionListener listener); + + void authorizeIndexAction(Authentication authentication, TransportRequest request, String action, + AuthorizationInfo authorizationInfo, AsyncSupplier indicesAsyncSupplier, + Function aliasOrIndexFunction, + ActionListener listener); + + List loadAuthorizedIndices(Authentication authentication, String action, AuthorizationInfo info, + Map aliasAndIndexLookup); + + interface AuthorizationInfo { + + Map asMap(); + + default AuthorizationInfo getAuthenticatedUserAuthorizationInfo() { + return this; + } + } + + final class EmptyAuthorizationInfo implements AuthorizationInfo { + + public static final EmptyAuthorizationInfo INSTANCE = new EmptyAuthorizationInfo(); + + private EmptyAuthorizationInfo() {} + + @Override + public Map asMap() { + return Collections.emptyMap(); + } + } + + class AuthorizationResult { + + private final boolean granted; + private final boolean auditable; + + public AuthorizationResult(boolean granted) { + this(granted, true); + } + + public AuthorizationResult(boolean granted, boolean auditable) { + this.granted = granted; + this.auditable = auditable; + } + + public boolean isGranted() { + return granted; + } + + public boolean isAuditable() { + return auditable; + } + + public static AuthorizationResult granted() { + return new AuthorizationResult(true); + } + + public static AuthorizationResult deny() { + return new AuthorizationResult(false); + } + } + + class IndexAuthorizationResult extends AuthorizationResult { + + private final IndicesAccessControl indicesAccessControl; + + IndexAuthorizationResult(boolean auditable, IndicesAccessControl indicesAccessControl) { + super(indicesAccessControl == null || indicesAccessControl.isGranted(), auditable); + this.indicesAccessControl = indicesAccessControl; + } + + public IndicesAccessControl getIndicesAccessControl() { + return indicesAccessControl; + } + } + + @FunctionalInterface + interface AsyncSupplier { + + void get(ActionListener listener); + } +} 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 35a2f5340492d..6a6999f995e7b 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 @@ -3,31 +3,24 @@ * 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; 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.CompositeIndicesRequest; import org.elasticsearch.action.DocWriteRequest; -import org.elasticsearch.action.IndicesRequest; import org.elasticsearch.action.admin.indices.alias.Alias; -import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest; +import org.elasticsearch.action.admin.indices.alias.IndicesAliasesAction; import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; -import org.elasticsearch.action.bulk.BulkAction; import org.elasticsearch.action.bulk.BulkItemRequest; import org.elasticsearch.action.bulk.BulkShardRequest; import org.elasticsearch.action.bulk.TransportShardBulkAction; import org.elasticsearch.action.delete.DeleteAction; -import org.elasticsearch.action.get.MultiGetAction; import org.elasticsearch.action.index.IndexAction; -import org.elasticsearch.action.search.ClearScrollAction; -import org.elasticsearch.action.search.MultiSearchAction; -import org.elasticsearch.action.search.SearchScrollAction; -import org.elasticsearch.action.search.SearchTransportService; +import org.elasticsearch.action.support.GroupedActionListener; import org.elasticsearch.action.support.replication.TransportReplicationAction.ConcreteShardRequest; -import org.elasticsearch.action.termvectors.MultiTermVectorsAction; import org.elasticsearch.action.update.UpdateAction; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.cluster.service.ClusterService; @@ -36,27 +29,17 @@ import org.elasticsearch.common.settings.Setting.Property; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; -import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.index.IndexNotFoundException; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportActionProxy; import org.elasticsearch.transport.TransportRequest; -import org.elasticsearch.xpack.core.security.action.user.AuthenticateAction; -import org.elasticsearch.xpack.core.security.action.user.ChangePasswordAction; -import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesAction; -import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesAction; -import org.elasticsearch.xpack.core.security.action.user.UserRequest; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.AuthenticationFailureHandler; -import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings; +import org.elasticsearch.xpack.core.security.authc.esnative.ClientReservedRealm; 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.ClusterPermission; -import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsCache; -import org.elasticsearch.xpack.core.security.authz.permission.Role; import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; -import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; -import org.elasticsearch.xpack.core.security.support.Automatons; import org.elasticsearch.xpack.core.security.user.AnonymousUser; import org.elasticsearch.xpack.core.security.user.SystemUser; import org.elasticsearch.xpack.core.security.user.User; @@ -64,54 +47,53 @@ 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.security.authc.esnative.ReservedRealm; +import org.elasticsearch.xpack.security.authz.AuthorizationEngine.AsyncSupplier; +import org.elasticsearch.xpack.security.authz.AuthorizationEngine.AuthorizationInfo; +import org.elasticsearch.xpack.security.authz.AuthorizationEngine.AuthorizationResult; +import org.elasticsearch.xpack.security.authz.AuthorizationEngine.EmptyAuthorizationInfo; +import org.elasticsearch.xpack.security.authz.AuthorizationEngine.IndexAuthorizationResult; import org.elasticsearch.xpack.security.authz.IndicesAndAliasesResolver.ResolvedIndices; import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; -import org.elasticsearch.xpack.security.support.SecurityIndexManager; -import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.function.Predicate; +import java.util.concurrent.atomic.AtomicBoolean; +import static org.elasticsearch.action.support.ContextPreservingActionListener.wrapPreservingContext; import static org.elasticsearch.xpack.core.security.SecurityField.setting; import static org.elasticsearch.xpack.core.security.support.Exceptions.authorizationError; +import static org.elasticsearch.xpack.security.audit.logfile.LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME; public class AuthorizationService { public static final Setting ANONYMOUS_AUTHORIZATION_EXCEPTION_SETTING = Setting.boolSetting(setting("authc.anonymous.authz_exception"), true, Property.NodeScope); public static final String ORIGINATING_ACTION_KEY = "_originating_action_name"; - public static final String ROLE_NAMES_KEY = "_effective_role_names"; - - private static final Predicate MONITOR_INDEX_PREDICATE = IndexPrivilege.MONITOR.predicate(); - private static final Predicate SAME_USER_PRIVILEGE = Automatons.predicate( - ChangePasswordAction.NAME, AuthenticateAction.NAME, HasPrivilegesAction.NAME, GetUserPrivilegesAction.NAME); + public static final String AUTHORIZATION_INFO_KEY = "_authz_info"; + static final AuthorizationInfo SYSTEM_AUTHZ_INFO = + () -> Collections.singletonMap(PRINCIPAL_ROLES_FIELD_NAME, new String[] { SystemUser.ROLE_NAME }); - private static final String INDEX_SUB_REQUEST_PRIMARY = IndexAction.NAME + "[p]"; - private static final String INDEX_SUB_REQUEST_REPLICA = IndexAction.NAME + "[r]"; - private static final String DELETE_SUB_REQUEST_PRIMARY = DeleteAction.NAME + "[p]"; - private static final String DELETE_SUB_REQUEST_REPLICA = DeleteAction.NAME + "[r]"; private static final Logger logger = LogManager.getLogger(AuthorizationService.class); + private final Settings settings; private final ClusterService clusterService; - private final CompositeRolesStore rolesStore; private final AuditTrailService auditTrail; private final IndicesAndAliasesResolver indicesAndAliasesResolver; private final AuthenticationFailureHandler authcFailureHandler; private final ThreadContext threadContext; private final AnonymousUser anonymousUser; - private final FieldPermissionsCache fieldPermissionsCache; + private final AuthorizationEngine rbacEngine; private final boolean isAnonymousEnabled; private final boolean anonymousAuthzExceptionEnabled; public AuthorizationService(Settings settings, CompositeRolesStore rolesStore, ClusterService clusterService, AuditTrailService auditTrail, AuthenticationFailureHandler authcFailureHandler, ThreadPool threadPool, AnonymousUser anonymousUser) { - this.rolesStore = rolesStore; this.clusterService = clusterService; this.auditTrail = auditTrail; this.indicesAndAliasesResolver = new IndicesAndAliasesResolver(settings, clusterService); @@ -120,7 +102,8 @@ public AuthorizationService(Settings settings, CompositeRolesStore rolesStore, C this.anonymousUser = anonymousUser; this.isAnonymousEnabled = AnonymousUser.isAnonymousEnabled(settings); this.anonymousAuthzExceptionEnabled = ANONYMOUS_AUTHORIZATION_EXCEPTION_SETTING.get(settings); - this.fieldPermissionsCache = new FieldPermissionsCache(settings); + this.rbacEngine = new RBACEngine(settings, rolesStore); + this.settings = settings; } /** @@ -128,14 +111,16 @@ public AuthorizationService(Settings settings, CompositeRolesStore rolesStore, C * have the appropriate privileges for this action/request, an {@link ElasticsearchSecurityException} * will be thrown. * - * @param authentication The authentication information - * @param action The action - * @param request The request + * @param authentication The authentication information + * @param action The action + * @param originalRequest The request + * @param listener The listener that gets called. A call to {@link ActionListener#onResponse(Object)} indicates success * @throws ElasticsearchSecurityException If the given user is no allowed to execute the given request */ - public void authorize(Authentication authentication, String action, TransportRequest request, Role userRole, - Role runAsRole) throws ElasticsearchSecurityException { - final TransportRequest originalRequest = request; + public void authorize(final Authentication authentication, final String action, final TransportRequest originalRequest, + final ActionListener listener) throws ElasticsearchSecurityException { + // prior to doing any authorization lets set the originating action in the context only + putTransientIfNonExisting(ORIGINATING_ACTION_KEY, action); String auditId = AuditUtil.extractRequestId(threadContext); if (auditId == null) { @@ -144,229 +129,225 @@ public void authorize(Authentication authentication, String action, TransportReq if (isInternalUser(authentication.getUser()) != false) { auditId = AuditUtil.getOrGenerateRequestId(threadContext); } else { - auditTrail.tamperedRequest(null, authentication.getUser(), action, request); + auditTrail.tamperedRequest(null, authentication.getUser(), action, originalRequest); final String message = "Attempt to authorize action [" + action + "] for [" + authentication.getUser().principal() + "] without an existing request-id"; assert false : message; - throw new ElasticsearchSecurityException(message); - } - } - - if (request instanceof ConcreteShardRequest) { - request = ((ConcreteShardRequest) request).getRequest(); - assert TransportActionProxy.isProxyRequest(request) == false : "expected non-proxy request for action: " + action; - } else { - request = TransportActionProxy.unwrapRequest(request); - if (TransportActionProxy.isProxyRequest(originalRequest) && TransportActionProxy.isProxyAction(action) == false) { - throw new IllegalStateException("originalRequest is a proxy request for: [" + request + "] but action: [" - + action + "] isn't"); + listener.onFailure(new ElasticsearchSecurityException(message)); } } - // prior to doing any authorization lets set the originating action in the context only - putTransientIfNonExisting(ORIGINATING_ACTION_KEY, action); - // first we need to check if the user is the system. If it is, we'll just authorize the system access + // sometimes a request might be wrapped within another, which is the case for proxied + // requests and concrete shard requests + final TransportRequest unwrappedRequest = maybeUnwrapRequest(authentication, originalRequest, action, auditId); if (SystemUser.is(authentication.getUser())) { - if (SystemUser.isAuthorized(action)) { - putTransientIfNonExisting(AuthorizationServiceField.INDICES_PERMISSIONS_KEY, IndicesAccessControl.ALLOW_ALL); - putTransientIfNonExisting(ROLE_NAMES_KEY, new String[] { SystemUser.ROLE_NAME }); - auditTrail.accessGranted(auditId, authentication, action, request, new String[] { SystemUser.ROLE_NAME }); - return; - } - throw denial(auditId, authentication, action, request, new String[] { SystemUser.ROLE_NAME }); + // this never goes async so no need to wrap the listener + authorizeSystemUser(authentication, action, auditId, unwrappedRequest, listener); + } else { + final String finalAuditId = auditId; + final ActionListener authzInfoListener = wrapPreservingContext(ActionListener.wrap( + authorizationInfo -> { + putTransientIfNonExisting(AUTHORIZATION_INFO_KEY, authorizationInfo); + maybeAuthorizeRunAs(authentication, action, unwrappedRequest, finalAuditId, authorizationInfo, listener); + }, listener::onFailure), threadContext); + getAuthorizationEngine(authentication).resolveAuthorizationInfo(authentication, unwrappedRequest, action, authzInfoListener); } + } - // get the roles of the authenticated user, which may be different than the effective - Role permission = userRole; - - // check if the request is a run as request + private void maybeAuthorizeRunAs(final Authentication authentication, final String action, final TransportRequest unwrappedRequest, + final String requestId, final AuthorizationInfo authzInfo, final ActionListener listener) { final boolean isRunAs = authentication.getUser().isRunAs(); if (isRunAs) { - // if we are running as a user we looked up then the authentication must contain a lookedUpBy. If it doesn't then this user - // doesn't really exist but the authc service allowed it through to avoid leaking users that exist in the system - if (authentication.getLookedUpBy() == null) { - throw denyRunAs(auditId, authentication, action, request, permission.names()); - } else if (permission.runAs().check(authentication.getUser().principal())) { - auditTrail.runAsGranted(auditId, authentication, action, request, permission.names()); - permission = runAsRole; - } else { - throw denyRunAs(auditId, authentication, action, request, permission.names()); - } + ActionListener runAsListener = wrapPreservingContext(ActionListener.wrap(result -> { + if (result.isGranted()) { + if (result.isAuditable()) { + auditTrail.runAsGranted(requestId, authentication, action, unwrappedRequest, + authzInfo.getAuthenticatedUserAuthorizationInfo()); + } + authorizeAction(authentication, action, requestId, unwrappedRequest, authzInfo, listener); + } else { + listener.onFailure(denyRunAs(requestId, authentication, action, unwrappedRequest, + authzInfo.getAuthenticatedUserAuthorizationInfo())); + } + }, e -> { + // TODO need a failure handler better than this! + listener.onFailure(denyRunAs(requestId, authentication, action, unwrappedRequest, authzInfo, e)); + }), threadContext); + authorizeRunAs(authentication, action, requestId, unwrappedRequest, authzInfo, runAsListener); + } else { + authorizeAction(authentication, action, requestId, unwrappedRequest, authzInfo, listener); } - putTransientIfNonExisting(ROLE_NAMES_KEY, permission.names()); + } - // first, we'll check if the action is a cluster action. If it is, we'll only check it against the cluster permissions + private void authorizeAction(final Authentication authentication, final String action, final String requestId, + final TransportRequest unwrappedRequest, final AuthorizationInfo authzInfo, + final ActionListener listener) { + final AuthorizationEngine authzEngine = getAuthorizationEngine(authentication); if (ClusterPrivilege.ACTION_MATCHER.test(action)) { - final ClusterPermission cluster = permission.cluster(); - if (cluster.check(action, request) || checkSameUserPermissions(action, request, authentication)) { - putTransientIfNonExisting(AuthorizationServiceField.INDICES_PERMISSIONS_KEY, IndicesAccessControl.ALLOW_ALL); - auditTrail.accessGranted(auditId, authentication, action, request, permission.names()); - return; - } - throw denial(auditId, authentication, action, request, permission.names()); - } - - // ok... this is not a cluster action, let's verify it's an indices action - if (!IndexPrivilege.ACTION_MATCHER.test(action)) { - throw denial(auditId, authentication, action, request, permission.names()); - } - - //composite actions are explicitly listed and will be authorized at the sub-request / shard level - if (isCompositeAction(action)) { - if (request instanceof CompositeIndicesRequest == false) { - throw new IllegalStateException("Composite actions must implement " + CompositeIndicesRequest.class.getSimpleName() - + ", " + request.getClass().getSimpleName() + " doesn't"); - } - // we check if the user can execute the action, without looking at indices, which will be authorized at the shard level - if (permission.indices().check(action)) { - auditTrail.accessGranted(auditId, authentication, action, request, permission.names()); - return; - } - throw denial(auditId, authentication, action, request, permission.names()); - } else if (isTranslatedToBulkAction(action)) { - if (request instanceof CompositeIndicesRequest == false) { - throw new IllegalStateException("Bulk translated actions must implement " + CompositeIndicesRequest.class.getSimpleName() - + ", " + request.getClass().getSimpleName() + " doesn't"); - } - // we check if the user can execute the action, without looking at indices, which will be authorized at the shard level - if (permission.indices().check(action)) { - auditTrail.accessGranted(auditId, authentication, action, request, permission.names()); - return; - } - throw denial(auditId, authentication, action, request, permission.names()); - } else if (TransportActionProxy.isProxyAction(action)) { - // we authorize proxied actions once they are "unwrapped" on the next node - if (TransportActionProxy.isProxyRequest(originalRequest) == false) { - throw new IllegalStateException("originalRequest is not a proxy request: [" + originalRequest + "] but action: [" - + action + "] is a proxy action"); - } - if (permission.indices().check(action)) { - auditTrail.accessGranted(auditId, authentication, action, request, permission.names()); - return; - } else { - // we do this here in addition to the denial below since we might run into an assertion on scroll request below if we - // don't have permission to read cross cluster but wrap a scroll request. - throw denial(auditId, authentication, action, request, permission.names()); - } - } - - // some APIs are indices requests that are not actually associated with indices. For example, - // search scroll request, is categorized under the indices context, but doesn't hold indices names - // (in this case, the security check on the indices was done on the search request that initialized - // the scroll. Given that scroll is implemented using a context on the node holding the shard, we - // piggyback on it and enhance the context with the original authentication. This serves as our method - // to validate the scroll id only stays with the same user! - if (request instanceof IndicesRequest == false && request instanceof IndicesAliasesRequest == false) { - //note that clear scroll shard level actions can originate from a clear scroll all, which doesn't require any - //indices permission as it's categorized under cluster. This is why the scroll check is performed - //even before checking if the user has any indices permission. - if (isScrollRelatedAction(action)) { - // if the action is a search scroll action, we first authorize that the user can execute the action for some - // index and if they cannot, we can fail the request early before we allow the execution of the action and in - // turn the shard actions - if (SearchScrollAction.NAME.equals(action) && permission.indices().check(action) == false) { - throw denial(auditId, authentication, action, request, permission.names()); + final ActionListener clusterAuthzListener = wrapPreservingContext(ActionListener.wrap(result -> { + if (result.isGranted()) { + if (result.isAuditable()) { + auditTrail.accessGranted(requestId, authentication, action, unwrappedRequest, authzInfo); + } + putTransientIfNonExisting(AuthorizationServiceField.INDICES_PERMISSIONS_KEY, IndicesAccessControl.ALLOW_ALL); + listener.onResponse(null); } else { - // we store the request as a transient in the ThreadContext in case of a authorization failure at the shard - // level. If authorization fails we will audit a access_denied message and will use the request to retrieve - // information such as the index and the incoming address of the request - auditTrail.accessGranted(auditId, authentication, action, request, permission.names()); - return; + listener.onFailure(denial(requestId, authentication, action, unwrappedRequest, authzInfo)); } - } else { - assert false : - "only scroll related requests are known indices api that don't support retrieving the indices they relate to"; - throw denial(auditId, authentication, action, request, permission.names()); - } + }, e -> { + // TODO need a failure handler better than this! + listener.onFailure(denial(requestId, authentication, action, unwrappedRequest, authzInfo, e)); + }), threadContext); + authzEngine.authorizeClusterAction(authentication, unwrappedRequest, action, authzInfo, clusterAuthzListener); + } else if (IndexPrivilege.ACTION_MATCHER.test(action)) { + final MetaData metaData = clusterService.state().metaData(); + final AsyncSupplier authorizedIndicesSupplier = new CachingAsyncSupplier<>( + authzIndicesListener -> authzIndicesListener.onResponse(new AuthorizedIndices( + () -> authzEngine.loadAuthorizedIndices(authentication, action, authzInfo, metaData.getAliasAndIndexLookup()))) + ); + final AsyncSupplier resolvedIndicesAsyncSupplier = new CachingAsyncSupplier<>((resolvedIndicesListener) -> { + authorizedIndicesSupplier.get(ActionListener.wrap(authorizedIndices -> { + resolveIndexNames(unwrappedRequest, metaData, authorizedIndices, resolvedIndicesListener); + }, e -> { + if (e instanceof IndexNotFoundException) { + auditTrail.accessDenied(requestId, authentication, action, unwrappedRequest, authzInfo); + listener.onFailure(e); + } else { + listener.onFailure(denial(requestId, authentication, action, unwrappedRequest, authzInfo, e)); + } + })); + }); + authzEngine.authorizeIndexAction(authentication, unwrappedRequest, action, 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 unwrappedRequest instanceof CreateIndexRequest; + Set aliases = ((CreateIndexRequest) unwrappedRequest).aliases(); + if (aliases.isEmpty() == false) { + authzEngine.authorizeIndexAction(authentication, unwrappedRequest, IndicesAliasesAction.NAME, authzInfo, + ril -> { + resolvedIndicesAsyncSupplier.get(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, + unwrappedRequest, authzInfo); + } + if (indexAuthorizationResult.isAuditable()) { + auditTrail.accessGranted(requestId, authentication, action, unwrappedRequest, authzInfo); + } + listener.onResponse(null); + } else { + listener.onFailure(denial(requestId, authentication, IndicesAliasesAction.NAME, + unwrappedRequest, 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 unwrappedRequest instanceof BulkShardRequest + : "Action " + action + " requires " + BulkShardRequest.class + " but was " + unwrappedRequest.getClass(); + + authorizeBulkItems(authentication, (BulkShardRequest) unwrappedRequest, authzInfo, authzEngine, + resolvedIndicesAsyncSupplier, authorizedIndicesSupplier, metaData, requestId, + ActionListener.wrap(ignore -> { + if (indexAuthorizationResult.isAuditable()) { + auditTrail.accessGranted(requestId, authentication, action, unwrappedRequest, authzInfo); + } + listener.onResponse(null); + }, listener::onFailure)); + } else { + if (indexAuthorizationResult.isAuditable()) { + auditTrail.accessGranted(requestId, authentication, action, unwrappedRequest, authzInfo); + } + listener.onResponse(null); + } + } else { + listener.onFailure(denial(requestId, authentication, action, unwrappedRequest, authzInfo)); + } + }, listener::onFailure)); + } else { + listener.onFailure(denial(requestId, authentication, action, unwrappedRequest, authzInfo)); } + } - final boolean allowsRemoteIndices = request instanceof IndicesRequest - && IndicesAndAliasesResolver.allowsRemoteIndices((IndicesRequest) request); + private AuthorizationEngine getRunAsAuthorizationEngine(final Authentication authentication) { + return ClientReservedRealm.isReserved(authentication.getUser().authenticatedUser().principal(), settings) ? + rbacEngine : rbacEngine; + } - // If this request does not allow remote indices - // then the user must have permission to perform this action on at least 1 local index - if (allowsRemoteIndices == false && permission.indices().check(action) == false) { - throw denial(auditId, authentication, action, request, permission.names()); - } + private AuthorizationEngine getAuthorizationEngine(final Authentication authentication) { + return ClientReservedRealm.isReserved(authentication.getUser().principal(), settings) ? + rbacEngine : rbacEngine; + } - final MetaData metaData = clusterService.state().metaData(); - final AuthorizedIndices authorizedIndices = new AuthorizedIndices(authentication.getUser(), permission, action, metaData); - final ResolvedIndices resolvedIndices = resolveIndexNames(auditId, authentication, action, request, metaData, - authorizedIndices, permission); - assert !resolvedIndices.isEmpty() - : "every indices request needs to have its indices set thus the resolved indices must not be empty"; - - // If this request does reference any remote indices - // then the user must have permission to perform this action on at least 1 local index - if (resolvedIndices.getRemote().isEmpty() && permission.indices().check(action) == false) { - throw denial(auditId, authentication, action, request, permission.names()); + private void authorizeSystemUser(final Authentication authentication, final String action, final String requestId, + final TransportRequest request, final ActionListener listener) { + if (SystemUser.isAuthorized(action)) { + putTransientIfNonExisting(AuthorizationServiceField.INDICES_PERMISSIONS_KEY, IndicesAccessControl.ALLOW_ALL); + putTransientIfNonExisting(AUTHORIZATION_INFO_KEY, SYSTEM_AUTHZ_INFO); + auditTrail.accessGranted(requestId, authentication, action, request, SYSTEM_AUTHZ_INFO); + listener.onResponse(null); + } else { + listener.onFailure(denial(requestId, authentication, action, request, SYSTEM_AUTHZ_INFO)); } + } - //all wildcard expressions have been resolved and only the security plugin could have set '-*' here. - //'-*' matches no indices so we allow the request to go through, which will yield an empty response - if (resolvedIndices.isNoIndicesPlaceholder()) { - putTransientIfNonExisting(AuthorizationServiceField.INDICES_PERMISSIONS_KEY, IndicesAccessControl.ALLOW_NO_INDICES); - auditTrail.accessGranted(auditId, authentication, action, request, permission.names()); - return; - } + private TransportRequest maybeUnwrapRequest(Authentication authentication, TransportRequest originalRequest, String action, + String requestId) { + final TransportRequest request; - final Set localIndices = new HashSet<>(resolvedIndices.getLocal()); - IndicesAccessControl indicesAccessControl = permission.authorize(action, localIndices, metaData, fieldPermissionsCache); - if (!indicesAccessControl.isGranted()) { - throw denial(auditId, authentication, action, request, permission.names()); - } else if (hasSecurityIndexAccess(indicesAccessControl) - && MONITOR_INDEX_PREDICATE.test(action) == false - && isSuperuser(authentication.getUser()) == false) { - // only the XPackUser is allowed to work with this index, but we should allow indices monitoring actions through for debugging - // purposes. These monitor requests also sometimes resolve indices concretely and then requests them - logger.debug("user [{}] attempted to directly perform [{}] against the security index [{}]", - authentication.getUser().principal(), action, SecurityIndexManager.SECURITY_INDEX_NAME); - throw denial(auditId, authentication, action, request, permission.names()); + if (originalRequest instanceof ConcreteShardRequest) { + request = ((ConcreteShardRequest) originalRequest).getRequest(); + assert TransportActionProxy.isProxyRequest(request) == false : "expected non-proxy request for action: " + action; } else { - putTransientIfNonExisting(AuthorizationServiceField.INDICES_PERMISSIONS_KEY, indicesAccessControl); - } - - //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()) { - Set aliasesAndIndices = Sets.newHashSet(localIndices); - for (Alias alias : aliases) { - aliasesAndIndices.add(alias.name()); - } - indicesAccessControl = permission.authorize("indices:admin/aliases", aliasesAndIndices, metaData, fieldPermissionsCache); - if (!indicesAccessControl.isGranted()) { - throw denial(auditId, authentication, "indices:admin/aliases", request, permission.names()); - } - // no need to re-add the indicesAccessControl in the context, - // because the create index call doesn't do anything FLS or DLS + request = TransportActionProxy.unwrapRequest(originalRequest); + final boolean isOriginalRequestProxyRequest = TransportActionProxy.isProxyRequest(originalRequest); + final boolean isProxyAction = TransportActionProxy.isProxyAction(action); + 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); + } + 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); } } - - if (action.equals(TransportShardBulkAction.ACTION_NAME)) { - // is 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(auditId, authentication, (BulkShardRequest) request, permission, metaData, localIndices, authorizedIndices); - } - - auditTrail.accessGranted(auditId, authentication, action, request, permission.names()); + return request; } private boolean isInternalUser(User user) { return SystemUser.is(user) || XPackUser.is(user) || XPackSecurityUser.is(user); } - private boolean hasSecurityIndexAccess(IndicesAccessControl indicesAccessControl) { - for (String index : SecurityIndexManager.indexNames()) { - final IndicesAccessControl.IndexAccessControl indexPermissions = indicesAccessControl.getIndexPermissions(index); - if (indexPermissions != null && indexPermissions.isGranted()) { - return true; - } + private void authorizeRunAs(final Authentication authentication, final String action, final String requestId, + final TransportRequest request, final AuthorizationInfo authzInfo, + final ActionListener listener) { + 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()); + } else { + final AuthorizationEngine runAsAuthzEngine = getRunAsAuthorizationEngine(authentication); + runAsAuthzEngine.authorizeRunAs(authentication, request, action, authzInfo, listener); } - return false; } /** @@ -376,48 +357,94 @@ private boolean hasSecurityIndexAccess(IndicesAccessControl indicesAccessControl * 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, String[]) access + * {@link #denial(String, Authentication, String, TransportRequest, AuthorizationInfo) 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 * is very small, but the results must be cached, to avoid adding a high * overhead to each bulk request. */ - private void authorizeBulkItems(String auditRequestId, Authentication authentication, BulkShardRequest request, Role permission, - MetaData metaData, Set indices, AuthorizedIndices authorizedIndices) { + private void authorizeBulkItems(Authentication authentication, BulkShardRequest request, AuthorizationInfo authzInfo, + AuthorizationEngine authzEngine, AsyncSupplier resolvedIndicesAsyncSupplier, + AsyncSupplier authorizedIndicesSupplier, + MetaData metaData, String requestId, ActionListener listener) { // Maps original-index -> expanded-index-name (expands date-math, but not aliases) final Map resolvedIndexNames = new HashMap<>(); - // Maps (resolved-index , action) -> is-granted - final Map, Boolean> indexActionAuthority = new HashMap<>(); - for (BulkItemRequest item : request.items()) { - String resolvedIndex = resolvedIndexNames.computeIfAbsent(item.index(), key -> { - final ResolvedIndices resolvedIndices = indicesAndAliasesResolver.resolveIndicesAndAliases(item.request(), metaData, - authorizedIndices); - if (resolvedIndices.getRemote().size() != 0) { - throw illegalArgument("Bulk item should not write to remote indices, but request writes to " - + String.join(",", resolvedIndices.getRemote())); + // Maps action -> resolved indices set + final Map> actionToIndicesMap = new HashMap<>(); + + authorizedIndicesSupplier.get(ActionListener.wrap(authorizedIndices -> { + resolvedIndicesAsyncSupplier.get(ActionListener.wrap(overallResolvedIndices -> { + final Set localIndices = new HashSet<>(overallResolvedIndices.getLocal()); + for (BulkItemRequest item : request.items()) { + String resolvedIndex = resolvedIndexNames.computeIfAbsent(item.index(), key -> { + final ResolvedIndices resolvedIndices = + indicesAndAliasesResolver.resolveIndicesAndAliases(item.request(), metaData, authorizedIndices); + if (resolvedIndices.getRemote().size() != 0) { + throw illegalArgument("Bulk item should not write to remote indices, but request writes to " + + String.join(",", resolvedIndices.getRemote())); + } + if (resolvedIndices.getLocal().size() != 1) { + throw illegalArgument("Bulk item should write to exactly 1 index, but request writes to " + + String.join(",", resolvedIndices.getLocal())); + } + final String resolved = resolvedIndices.getLocal().get(0); + if (localIndices.contains(resolved) == false) { + throw illegalArgument("Found bulk item that writes to index " + resolved + " but the request writes to " + + localIndices); + } + return resolved; + }); + + final String itemAction = getAction(item); + actionToIndicesMap.compute(itemAction, (key, resolvedIndicesSet) -> { + final Set localSet = resolvedIndicesSet != null ? resolvedIndicesSet : new HashSet<>(); + localSet.add(resolvedIndex); + return localSet; + }); } - if (resolvedIndices.getLocal().size() != 1) { - throw illegalArgument("Bulk item should write to exactly 1 index, but request writes to " - + String.join(",", resolvedIndices.getLocal())); - } - final String resolved = resolvedIndices.getLocal().get(0); - if (indices.contains(resolved) == false) { - throw illegalArgument("Found bulk item that writes to index " + resolved + " but the request writes to " + indices); - } - return resolved; - }); - final String itemAction = getAction(item); - final Tuple indexAndAction = new Tuple<>(resolvedIndex, itemAction); - final boolean granted = indexActionAuthority.computeIfAbsent(indexAndAction, key -> { - final IndicesAccessControl itemAccessControl = permission.authorize(itemAction, Collections.singleton(resolvedIndex), - metaData, fieldPermissionsCache); - return itemAccessControl.isGranted(); - }); - if (granted == false) { - item.abort(resolvedIndex, denial(auditRequestId, authentication, itemAction, request, permission.names())); - } - } + + final ActionListener>> bulkAuthzListener = + ActionListener.wrap(collection -> { + final Map actionToIndicesAccessControl = new HashMap<>(); + final AtomicBoolean audit = new AtomicBoolean(false); + collection.forEach(tuple -> { + final IndicesAccessControl existing = + actionToIndicesAccessControl.putIfAbsent(tuple.v1(), tuple.v2().getIndicesAccessControl()); + if (existing != null) { + throw new IllegalStateException("a value already exists for action " + tuple.v1()); + } + if (tuple.v2().isAuditable()) { + audit.set(true); + } + }); + + for (BulkItemRequest item : request.items()) { + final String resolvedIndex = resolvedIndexNames.get(item.index()); + final String itemAction = getAction(item); + final IndicesAccessControl indicesAccessControl = actionToIndicesAccessControl.get(getAction(item)); + final IndicesAccessControl.IndexAccessControl indexAccessControl + = indicesAccessControl.getIndexPermissions(resolvedIndex); + if (indexAccessControl == null || indexAccessControl.isGranted() == false) { + item.abort(resolvedIndex, denial(requestId, authentication, itemAction, request, authzInfo)); + } else if (audit.get()) { + auditTrail.accessGranted(requestId, authentication, itemAction, request, authzInfo); + } + } + listener.onResponse(null); + }, listener::onFailure); + final ActionListener> groupedActionListener = wrapPreservingContext( + new GroupedActionListener<>(bulkAuthzListener, actionToIndicesMap.size(), Collections.emptyList()), threadContext); + + actionToIndicesMap.forEach((bulkItemAction, indices) -> { + authzEngine.authorizeIndexAction(authentication, request, bulkItemAction, authzInfo, + ril -> ril.onResponse(new ResolvedIndices(new ArrayList<>(indices), Collections.emptyList())), + metaData.getAliasAndIndexLookup()::get, ActionListener.wrap(indexAuthorizationResult -> + groupedActionListener.onResponse(new Tuple<>(bulkItemAction, indexAuthorizationResult)), + groupedActionListener::onFailure)); + }); + }, listener::onFailure)); + }, listener::onFailure)); } private IllegalArgumentException illegalArgument(String message) { @@ -439,14 +466,9 @@ private static String getAction(BulkItemRequest item) { throw new IllegalArgumentException("No equivalent action for opType [" + docWriteRequest.opType() + "]"); } - private ResolvedIndices resolveIndexNames(String auditRequestId, Authentication authentication, String action, TransportRequest request, - MetaData metaData, AuthorizedIndices authorizedIndices, Role permission) { - try { - return indicesAndAliasesResolver.resolve(request, metaData, authorizedIndices); - } catch (Exception e) { - auditTrail.accessDenied(auditRequestId, authentication, action, request, permission.names()); - throw e; - } + private void resolveIndexNames(TransportRequest request, MetaData metaData, AuthorizedIndices authorizedIndices, + ActionListener listener) { + listener.onResponse(indicesAndAliasesResolver.resolve(request, metaData, authorizedIndices)); } private void putTransientIfNonExisting(String key, Object value) { @@ -456,155 +478,67 @@ private void putTransientIfNonExisting(String key, Object value) { } } - public void roles(User user, ActionListener roleActionListener) { - // we need to special case the internal users in this method, if we apply the anonymous roles to every user including these system - // user accounts then we run into the chance of a deadlock because then we need to get a role that we may be trying to get as the - // internal user. The SystemUser is special cased as it has special privileges to execute internal actions and should never be - // passed into this method. The XPackUser has the Superuser role and we can simply return that - if (SystemUser.is(user)) { - throw new IllegalArgumentException("the user [" + user.principal() + "] is the system user and we should never try to get its" + - " roles"); - } - if (XPackUser.is(user)) { - assert XPackUser.INSTANCE.roles().length == 1; - roleActionListener.onResponse(XPackUser.ROLE); - return; - } - if (XPackSecurityUser.is(user)) { - roleActionListener.onResponse(ReservedRolesStore.SUPERUSER_ROLE); - return; - } - - Set roleNames = new HashSet<>(); - Collections.addAll(roleNames, user.roles()); - if (isAnonymousEnabled && anonymousUser.equals(user) == false) { - if (anonymousUser.roles().length == 0) { - throw new IllegalStateException("anonymous is only enabled when the anonymous user has roles"); - } - Collections.addAll(roleNames, anonymousUser.roles()); - } - - if (roleNames.isEmpty()) { - roleActionListener.onResponse(Role.EMPTY); - } else if (roleNames.contains(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName())) { - roleActionListener.onResponse(ReservedRolesStore.SUPERUSER_ROLE); - } else { - rolesStore.roles(roleNames, fieldPermissionsCache, roleActionListener); - } - } - - private static boolean isCompositeAction(String action) { - return action.equals(BulkAction.NAME) || - action.equals(MultiGetAction.NAME) || - action.equals(MultiTermVectorsAction.NAME) || - action.equals(MultiSearchAction.NAME) || - action.equals("indices:data/read/mpercolate") || - action.equals("indices:data/read/msearch/template") || - action.equals("indices:data/read/search/template") || - action.equals("indices:data/write/reindex") || - action.equals("indices:data/read/sql") || - action.equals("indices:data/read/sql/translate"); - } - - private static boolean isTranslatedToBulkAction(String action) { - return action.equals(IndexAction.NAME) || - action.equals(DeleteAction.NAME) || - action.equals(INDEX_SUB_REQUEST_PRIMARY) || - action.equals(INDEX_SUB_REQUEST_REPLICA) || - action.equals(DELETE_SUB_REQUEST_PRIMARY) || - action.equals(DELETE_SUB_REQUEST_REPLICA); - } - - private static boolean isScrollRelatedAction(String action) { - return action.equals(SearchScrollAction.NAME) || - action.equals(SearchTransportService.FETCH_ID_SCROLL_ACTION_NAME) || - action.equals(SearchTransportService.QUERY_FETCH_SCROLL_ACTION_NAME) || - action.equals(SearchTransportService.QUERY_SCROLL_ACTION_NAME) || - action.equals(SearchTransportService.FREE_CONTEXT_SCROLL_ACTION_NAME) || - action.equals(ClearScrollAction.NAME) || - action.equals("indices:data/read/sql/close_cursor") || - action.equals(SearchTransportService.CLEAR_SCROLL_CONTEXTS_ACTION_NAME); - } - - static boolean checkSameUserPermissions(String action, TransportRequest request, Authentication authentication) { - final boolean actionAllowed = SAME_USER_PRIVILEGE.test(action); - if (actionAllowed) { - if (request instanceof UserRequest == false) { - assert false : "right now only a user request should be allowed"; - return false; - } - UserRequest userRequest = (UserRequest) request; - String[] usernames = userRequest.usernames(); - if (usernames == null || usernames.length != 1 || usernames[0] == null) { - assert false : "this role should only be used for actions to apply to a single user"; - return false; - } - final String username = usernames[0]; - final boolean sameUsername = authentication.getUser().principal().equals(username); - if (sameUsername && ChangePasswordAction.NAME.equals(action)) { - return checkChangePasswordAction(authentication); - } - - assert AuthenticateAction.NAME.equals(action) || HasPrivilegesAction.NAME.equals(action) - || GetUserPrivilegesAction.NAME.equals(action) || sameUsername == false - : "Action '" + action + "' should not be possible when sameUsername=" + sameUsername; - return sameUsername; - } - return false; + ElasticsearchSecurityException denial(String auditRequestId, Authentication authentication, String action, TransportRequest request, + AuthorizationInfo authzInfo) { + return denial(auditRequestId, authentication, action, request, authzInfo, null); } - private static boolean checkChangePasswordAction(Authentication authentication) { - // we need to verify that this user was authenticated by or looked up by a realm type that support password changes - // otherwise we open ourselves up to issues where a user in a different realm could be created with the same username - // and do malicious things - final boolean isRunAs = authentication.getUser().isRunAs(); - final String realmType; - if (isRunAs) { - realmType = authentication.getLookedUpBy().getType(); - } else { - realmType = authentication.getAuthenticatedBy().getType(); - } - - assert realmType != null; - // ensure the user was authenticated by a realm that we can change a password for. The native realm is an internal realm and - // right now only one can exist in the realm configuration - if this changes we should update this check - return ReservedRealm.TYPE.equals(realmType) || NativeRealmSettings.TYPE.equals(realmType); + 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); } - ElasticsearchSecurityException denial(String auditRequestId, Authentication authentication, String action, TransportRequest request, - String[] roleNames) { - auditTrail.accessDenied(auditRequestId, authentication, action, request, roleNames); - return denialException(authentication, action); + 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, String[] roleNames) { - auditTrail.runAsDenied(auditRequestId, authentication, action, request, roleNames); - return denialException(authentication, action); + TransportRequest request, AuthorizationInfo authzInfo) { + return denyRunAs(auditRequestId, authentication, action, request, authzInfo, null); } - private ElasticsearchSecurityException denialException(Authentication authentication, String action) { + private ElasticsearchSecurityException denialException(Authentication authentication, String action, Exception cause) { final User authUser = authentication.getUser().authenticatedUser(); // Special case for anonymous user if (isAnonymousEnabled && anonymousUser.equals(authUser)) { if (anonymousAuthzExceptionEnabled == false) { - throw authcFailureHandler.authenticationRequired(action, threadContext); + return authcFailureHandler.authenticationRequired(action, threadContext); } } // check for run as if (authentication.getUser().isRunAs()) { logger.debug("action [{}] is unauthorized for user [{}] run as [{}]", action, authUser.principal(), - authentication.getUser().principal()); - return authorizationError("action [{}] is unauthorized for user [{}] run as [{}]", action, authUser.principal(), - authentication.getUser().principal()); + authentication.getUser().principal()); + return authorizationError("action [{}] is unauthorized for user [{}] run as [{}]", cause, action, authUser.principal(), + authentication.getUser().principal()); } logger.debug("action [{}] is unauthorized for user [{}]", action, authUser.principal()); - return authorizationError("action [{}] is unauthorized for user [{}]", action, authUser.principal()); + return authorizationError("action [{}] is unauthorized for user [{}]", cause, action, authUser.principal()); } - static boolean isSuperuser(User user) { - return Arrays.stream(user.roles()) - .anyMatch(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName()::equals); + private static class CachingAsyncSupplier implements AsyncSupplier { + + private final AsyncSupplier asyncSupplier; + private V value = null; + + private CachingAsyncSupplier(AsyncSupplier supplier) { + this.asyncSupplier = supplier; + } + + @Override + public synchronized void get(ActionListener listener) { + if (value == null) { + asyncSupplier.get(ActionListener.wrap(loaded -> { + value = loaded; + listener.onResponse(value); + }, listener::onFailure)); + } else { + listener.onResponse(value); + } + } } public static void addSettings(List> settings) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationUtils.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationUtils.java index 193e3bdb3bd5c..0397fac1027ea 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationUtils.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationUtils.java @@ -6,20 +6,15 @@ package org.elasticsearch.xpack.security.authz; import org.elasticsearch.Version; -import org.elasticsearch.action.ActionListener; -import org.elasticsearch.common.util.concurrent.CountDown; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.xpack.core.ClientHelper; import org.elasticsearch.xpack.core.security.SecurityContext; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.AuthenticationField; -import org.elasticsearch.xpack.core.security.authz.permission.Role; import org.elasticsearch.xpack.core.security.support.Automatons; -import org.elasticsearch.xpack.core.security.user.SystemUser; import org.elasticsearch.xpack.core.security.user.XPackSecurityUser; import org.elasticsearch.xpack.core.security.user.XPackUser; -import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Predicate; @@ -126,60 +121,4 @@ public static void switchUserBasedOnActionOriginAndExecute(ThreadContext threadC private static boolean isInternalAction(String action) { return INTERNAL_PREDICATE.test(action); } - - /** - * A base class to authorize authorize a given {@link Authentication} against it's users or run-as users roles. - * This class fetches the roles for the users asynchronously and then authenticates the in the callback. - */ - public static class AsyncAuthorizer { - - private final ActionListener listener; - private final BiConsumer consumer; - private final Authentication authentication; - private volatile Role userRoles; - private volatile Role runAsRoles; - private CountDown countDown = new CountDown(2); // we expect only two responses!! - - public AsyncAuthorizer(Authentication authentication, ActionListener listener, BiConsumer consumer) { - this.consumer = consumer; - this.listener = listener; - this.authentication = authentication; - } - - public void authorize(AuthorizationService service) { - if (SystemUser.is(authentication.getUser().authenticatedUser())) { - assert authentication.getUser().isRunAs() == false; - setUserRoles(null); // we can inform the listener immediately - nothing to fetch for us on system user - setRunAsRoles(null); - } else { - service.roles(authentication.getUser().authenticatedUser(), ActionListener.wrap(this::setUserRoles, listener::onFailure)); - if (authentication.getUser().isRunAs()) { - service.roles(authentication.getUser(), ActionListener.wrap(this::setRunAsRoles, listener::onFailure)); - } else { - setRunAsRoles(null); - } - } - } - - private void setUserRoles(Role roles) { - this.userRoles = roles; - maybeRun(); - } - - private void setRunAsRoles(Role roles) { - this.runAsRoles = roles; - maybeRun(); - } - - private void maybeRun() { - if (countDown.countDown()) { - try { - consumer.accept(userRoles, runAsRoles); - } catch (Exception e) { - listener.onFailure(e); - } - } - } - - } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizedIndices.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizedIndices.java index 3068a3993d309..83d95e3e45998 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizedIndices.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizedIndices.java @@ -5,61 +5,26 @@ */ package org.elasticsearch.xpack.security.authz; -import org.elasticsearch.cluster.metadata.AliasOrIndex; -import org.elasticsearch.cluster.metadata.MetaData; -import org.elasticsearch.xpack.core.security.authz.permission.Role; -import org.elasticsearch.xpack.core.security.user.User; -import org.elasticsearch.xpack.security.support.SecurityIndexManager; - -import java.util.ArrayList; -import java.util.Collections; import java.util.List; -import java.util.Map; -import java.util.function.Predicate; - -import static org.elasticsearch.xpack.security.authz.AuthorizationService.isSuperuser; +import java.util.function.Supplier; /** * Abstraction used to make sure that we lazily load authorized indices only when requested and only maximum once per request. Also * makes sure that authorized indices don't get updated throughout the same request for the same user. */ class AuthorizedIndices { - private final User user; - private final String action; - private final MetaData metaData; - private final Role userRoles; + + private final Supplier> supplier; private List authorizedIndices; - AuthorizedIndices(User user, Role userRoles, String action, MetaData metaData) { - this.user = user; - this.userRoles = userRoles; - this.action = action; - this.metaData = metaData; + AuthorizedIndices(Supplier> authorizedIndicesSupplier) { + this.supplier = authorizedIndicesSupplier; } List get() { if (authorizedIndices == null) { - authorizedIndices = load(); + authorizedIndices = supplier.get(); } return authorizedIndices; } - - private List load() { - Predicate predicate = userRoles.indices().allowedIndicesMatcher(action); - - List indicesAndAliases = new ArrayList<>(); - // TODO: can this be done smarter? I think there are usually more indices/aliases in the cluster then indices defined a roles? - for (Map.Entry entry : metaData.getAliasAndIndexLookup().entrySet()) { - String aliasOrIndex = entry.getKey(); - if (predicate.test(aliasOrIndex)) { - indicesAndAliases.add(aliasOrIndex); - } - } - - if (isSuperuser(user) == false) { - // we should filter out all of the security indices from wildcards - indicesAndAliases.removeAll(SecurityIndexManager.indexNames()); - } - return Collections.unmodifiableList(indicesAndAliases); - } } 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 new file mode 100644 index 0000000000000..07dd030f890d7 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java @@ -0,0 +1,406 @@ +/* + * 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; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.CompositeIndicesRequest; +import org.elasticsearch.action.IndicesRequest; +import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest; +import org.elasticsearch.action.bulk.BulkAction; +import org.elasticsearch.action.bulk.BulkShardRequest; +import org.elasticsearch.action.delete.DeleteAction; +import org.elasticsearch.action.get.MultiGetAction; +import org.elasticsearch.action.index.IndexAction; +import org.elasticsearch.action.search.ClearScrollAction; +import org.elasticsearch.action.search.MultiSearchAction; +import org.elasticsearch.action.search.SearchScrollAction; +import org.elasticsearch.action.search.SearchTransportService; +import org.elasticsearch.action.termvectors.MultiTermVectorsAction; +import org.elasticsearch.cluster.metadata.AliasOrIndex; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.transport.TransportActionProxy; +import org.elasticsearch.transport.TransportRequest; +import org.elasticsearch.xpack.core.security.action.user.AuthenticateAction; +import org.elasticsearch.xpack.core.security.action.user.ChangePasswordAction; +import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesAction; +import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesAction; +import org.elasticsearch.xpack.core.security.action.user.UserRequest; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings; +import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; +import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsCache; +import org.elasticsearch.xpack.core.security.authz.permission.Role; +import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; +import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; +import org.elasticsearch.xpack.core.security.support.Automatons; +import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; +import org.elasticsearch.xpack.security.authz.IndicesAndAliasesResolver.ResolvedIndices; +import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; +import org.elasticsearch.xpack.security.support.SecurityIndexManager; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; + +import static org.elasticsearch.xpack.security.audit.logfile.LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME; + +public class RBACEngine implements AuthorizationEngine { + + private static final Predicate SAME_USER_PRIVILEGE = Automatons.predicate( + ChangePasswordAction.NAME, AuthenticateAction.NAME, HasPrivilegesAction.NAME, GetUserPrivilegesAction.NAME); + private static final Predicate MONITOR_INDEX_PREDICATE = IndexPrivilege.MONITOR.predicate(); + private static final String INDEX_SUB_REQUEST_PRIMARY = IndexAction.NAME + "[p]"; + private static final String INDEX_SUB_REQUEST_REPLICA = IndexAction.NAME + "[r]"; + private static final String DELETE_SUB_REQUEST_PRIMARY = DeleteAction.NAME + "[p]"; + private static final String DELETE_SUB_REQUEST_REPLICA = DeleteAction.NAME + "[r]"; + + private static final Logger logger = LogManager.getLogger(RBACEngine.class); + + private final CompositeRolesStore rolesStore; + private final FieldPermissionsCache fieldPermissionsCache; + + RBACEngine(Settings settings, CompositeRolesStore rolesStore) { + this.rolesStore = rolesStore; + this.fieldPermissionsCache = new FieldPermissionsCache(settings); + } + + @Override + public void resolveAuthorizationInfo(Authentication authentication, TransportRequest request, String action, + ActionListener listener) { + getRoles(authentication.getUser(), ActionListener.wrap(role -> { + if (authentication.getUser().isRunAs()) { + getRoles(authentication.getUser().authenticatedUser(), ActionListener.wrap( + authenticatedUserRole -> listener.onResponse(new RBACAuthorizationInfo(role, authenticatedUserRole)), + listener::onFailure)); + } else { + listener.onResponse(new RBACAuthorizationInfo(role, role)); + } + }, listener::onFailure)); + } + + private void getRoles(User user, ActionListener listener) { + rolesStore.getRoles(user, fieldPermissionsCache, listener); + } + + @Override + public void authorizeRunAs(Authentication authentication, TransportRequest request, String action, AuthorizationInfo authorizationInfo, + ActionListener listener) { + if (authorizationInfo instanceof RBACAuthorizationInfo) { + final Role role = ((RBACAuthorizationInfo) authorizationInfo).getAuthenticatedUserAuthorizationInfo().getRole(); + listener.onResponse(new AuthorizationResult(role.runAs().check(authentication.getUser().principal()))); + } else { + listener.onFailure(new IllegalArgumentException("unsupported authorization info:" + + authorizationInfo.getClass().getSimpleName())); + } + } + + @Override + public void authorizeClusterAction(Authentication authentication, TransportRequest request, String action, + AuthorizationInfo authorizationInfo, ActionListener listener) { + if (authorizationInfo instanceof RBACAuthorizationInfo) { + final Role role = ((RBACAuthorizationInfo) authorizationInfo).getRole(); + if (role.cluster().check(action, request)) { + listener.onResponse(AuthorizationResult.granted()); + } else if (checkSameUserPermissions(action, request, authentication)) { + listener.onResponse(AuthorizationResult.granted()); + } else { + listener.onResponse(AuthorizationResult.deny()); + } + } else { + listener.onFailure(new IllegalArgumentException("unsupported authorization info:" + + authorizationInfo.getClass().getSimpleName())); + } + } + + // pkg private for testing + boolean checkSameUserPermissions(String action, TransportRequest request, Authentication authentication) { + final boolean actionAllowed = SAME_USER_PRIVILEGE.test(action); + if (actionAllowed) { + if (request instanceof UserRequest == false) { + assert false : "right now only a user request should be allowed"; + return false; + } + UserRequest userRequest = (UserRequest) request; + String[] usernames = userRequest.usernames(); + if (usernames == null || usernames.length != 1 || usernames[0] == null) { + assert false : "this role should only be used for actions to apply to a single user"; + return false; + } + final String username = usernames[0]; + final boolean sameUsername = authentication.getUser().principal().equals(username); + if (sameUsername && ChangePasswordAction.NAME.equals(action)) { + return checkChangePasswordAction(authentication); + } + + assert AuthenticateAction.NAME.equals(action) || HasPrivilegesAction.NAME.equals(action) + || GetUserPrivilegesAction.NAME.equals(action) || sameUsername == false + : "Action '" + action + "' should not be possible when sameUsername=" + sameUsername; + return sameUsername; + } + return false; + } + + private static boolean shouldAuthorizeIndexActionNameOnly(String action, TransportRequest request) { + switch (action) { + case BulkAction.NAME: + case IndexAction.NAME: + case DeleteAction.NAME: + case INDEX_SUB_REQUEST_PRIMARY: + case INDEX_SUB_REQUEST_REPLICA: + case DELETE_SUB_REQUEST_PRIMARY: + case DELETE_SUB_REQUEST_REPLICA: + case MultiGetAction.NAME: + case MultiTermVectorsAction.NAME: + case MultiSearchAction.NAME: + case "indices:data/read/mpercolate": + case "indices:data/read/msearch/template": + case "indices:data/read/search/template": + case "indices:data/write/reindex": + case "indices:data/read/sql": + case "indices:data/read/sql/translate": + if (request instanceof BulkShardRequest) { + return false; + } + if (request instanceof CompositeIndicesRequest == false) { + throw new IllegalStateException("Composite and bulk actions must implement " + + CompositeIndicesRequest.class.getSimpleName() + ", " + request.getClass().getSimpleName() + " doesn't. Action " + + action); + } + return true; + default: + return false; + } + } + + @Override + public void authorizeIndexAction(Authentication authentication, TransportRequest request, String action, + AuthorizationInfo authorizationInfo, AsyncSupplier indicesAsyncSupplier, + Function aliasOrIndexFunction, + ActionListener listener) { + if (TransportActionProxy.isProxyAction(action) || shouldAuthorizeIndexActionNameOnly(action, request)) { + // we've already validated that the request is a proxy request so we can skip that but we still + // need to validate that the action is allowed and then move on + authorizeIndexActionName(action, authorizationInfo, null, listener); + } else if (request instanceof IndicesRequest == false && request instanceof IndicesAliasesRequest == false) { + // scroll is special + // some APIs are indices requests that are not actually associated with indices. For example, + // search scroll request, is categorized under the indices context, but doesn't hold indices names + // (in this case, the security check on the indices was done on the search request that initialized + // the scroll. Given that scroll is implemented using a context on the node holding the shard, we + // piggyback on it and enhance the context with the original authentication. This serves as our method + // to validate the scroll id only stays with the same user! + // note that clear scroll shard level actions can originate from a clear scroll all, which doesn't require any + // indices permission as it's categorized under cluster. This is why the scroll check is performed + // even before checking if the user has any indices permission. + if (isScrollRelatedAction(action)) { + // if the action is a search scroll action, we first authorize that the user can execute the action for some + // index and if they cannot, we can fail the request early before we allow the execution of the action and in + // turn the shard actions + if (SearchScrollAction.NAME.equals(action)) { + authorizeIndexActionName(action, authorizationInfo, null, listener); + } else { + // we store the request as a transient in the ThreadContext in case of a authorization failure at the shard + // level. If authorization fails we will audit a access_denied message and will use the request to retrieve + // information such as the index and the incoming address of the request + listener.onResponse(new IndexAuthorizationResult(true, IndicesAccessControl.ALLOW_NO_INDICES)); + } + } else { + assert false : + "only scroll related requests are known indices api that don't support retrieving the indices they relate to"; + listener.onFailure(new IllegalStateException("only scroll related requests are known indices api that don't support " + + "retrieving the indices they relate to")); + } + } else if (request instanceof IndicesRequest && + IndicesAndAliasesResolver.allowsRemoteIndices((IndicesRequest) request)) { + // remote indices are allowed + indicesAsyncSupplier.get(ActionListener.wrap(resolvedIndices -> { + assert !resolvedIndices.isEmpty() + : "every indices request needs to have its indices set thus the resolved indices must not be empty"; + //all wildcard expressions have been resolved and only the security plugin could have set '-*' here. + //'-*' matches no indices so we allow the request to go through, which will yield an empty response + if (resolvedIndices.isNoIndicesPlaceholder()) { + // check action name + authorizeIndexActionName(action, authorizationInfo, IndicesAccessControl.ALLOW_NO_INDICES, listener); + } else { + buildIndicesAccessControl(authentication, action, authorizationInfo, + Sets.newHashSet(resolvedIndices.getLocal()), aliasOrIndexFunction, listener); + } + }, listener::onFailure)); + } else { + authorizeIndexActionName(action, authorizationInfo, IndicesAccessControl.ALLOW_NO_INDICES, + ActionListener.wrap(indexAuthorizationResult -> { + if (indexAuthorizationResult.isGranted()) { + indicesAsyncSupplier.get(ActionListener.wrap(resolvedIndices -> { + assert !resolvedIndices.isEmpty() + : "every indices request needs to have its indices set thus the resolved indices must not be empty"; + //all wildcard expressions have been resolved and only the security plugin could have set '-*' here. + //'-*' matches no indices so we allow the request to go through, which will yield an empty response + if (resolvedIndices.isNoIndicesPlaceholder()) { + listener.onResponse(new IndexAuthorizationResult(true, IndicesAccessControl.ALLOW_NO_INDICES)); + } else { + buildIndicesAccessControl(authentication, action, authorizationInfo, + Sets.newHashSet(resolvedIndices.getLocal()), aliasOrIndexFunction, listener); + } + }, listener::onFailure)); + } else { + listener.onResponse(indexAuthorizationResult); + } + }, listener::onFailure)); + } + } + + private void authorizeIndexActionName(String action, AuthorizationInfo authorizationInfo, IndicesAccessControl grantedValue, + ActionListener listener) { + if (authorizationInfo instanceof RBACAuthorizationInfo) { + final Role role = ((RBACAuthorizationInfo) authorizationInfo).getRole(); + if (role.indices().check(action)) { + listener.onResponse(new IndexAuthorizationResult(true, grantedValue)); + } else { + listener.onResponse(new IndexAuthorizationResult(true, IndicesAccessControl.DENIED)); + } + } else { + listener.onFailure(new IllegalArgumentException("unsupported authorization info:" + + authorizationInfo.getClass().getSimpleName())); + } + } + + @Override + public List loadAuthorizedIndices(Authentication authentication, String action, AuthorizationInfo authorizationInfo, + Map aliasAndIndexLookup) { + if (authorizationInfo instanceof RBACAuthorizationInfo) { + final Role role = ((RBACAuthorizationInfo) authorizationInfo).getRole(); + return resolveAuthorizedIndicesFromRole(role, action, aliasAndIndexLookup); + } else { + throw new IllegalArgumentException("unsupported authorization info:" + authorizationInfo.getClass().getSimpleName()); + } + } + + static List resolveAuthorizedIndicesFromRole(Role role, String action, Map aliasAndIndexLookup) { + Predicate predicate = role.indices().allowedIndicesMatcher(action); + + List indicesAndAliases = new ArrayList<>(); + // TODO: can this be done smarter? I think there are usually more indices/aliases in the cluster then indices defined a roles? + for (Map.Entry entry : aliasAndIndexLookup.entrySet()) { + String aliasOrIndex = entry.getKey(); + if (predicate.test(aliasOrIndex)) { + indicesAndAliases.add(aliasOrIndex); + } + } + + if (Arrays.asList(role.names()).contains(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName()) == false) { + // we should filter out all of the security indices from wildcards + indicesAndAliases.removeAll(SecurityIndexManager.indexNames()); + } + return Collections.unmodifiableList(indicesAndAliases); + } + + private void buildIndicesAccessControl(Authentication authentication, String action, + AuthorizationInfo authorizationInfo, Set indices, + Function aliasAndIndexLookup, + ActionListener listener) { + if (authorizationInfo instanceof RBACAuthorizationInfo) { + final Role role = ((RBACAuthorizationInfo) authorizationInfo).getRole(); + final IndicesAccessControl accessControl = role.authorize(action, indices, aliasAndIndexLookup, fieldPermissionsCache); + if (accessControl.isGranted() && hasSecurityIndexAccess(accessControl) && MONITOR_INDEX_PREDICATE.test(action) == false + && isSuperuser(authentication.getUser()) == false) { + // only superusers are allowed to work with this index, but we should allow indices monitoring actions through + // for debugging + // purposes. These monitor requests also sometimes resolve indices concretely and then requests them + logger.debug("user [{}] attempted to directly perform [{}] against the security index [{}]", + authentication.getUser().principal(), action, SecurityIndexManager.SECURITY_INDEX_NAME); + listener.onResponse(new IndexAuthorizationResult(true, new IndicesAccessControl(false, Collections.emptyMap()))); + } else { + listener.onResponse(new IndexAuthorizationResult(true, accessControl)); + } + } else { + listener.onFailure(new IllegalArgumentException("unsupported authorization info:" + + authorizationInfo.getClass().getSimpleName())); + } + } + + private static boolean hasSecurityIndexAccess(IndicesAccessControl indicesAccessControl) { + for (String index : SecurityIndexManager.indexNames()) { + final IndicesAccessControl.IndexAccessControl indexPermissions = indicesAccessControl.getIndexPermissions(index); + if (indexPermissions != null && indexPermissions.isGranted()) { + return true; + } + } + return false; + } + + private static boolean isSuperuser(User user) { + return Arrays.asList(user.roles()).contains(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName()); + } + + private static boolean checkChangePasswordAction(Authentication authentication) { + // we need to verify that this user was authenticated by or looked up by a realm type that support password changes + // otherwise we open ourselves up to issues where a user in a different realm could be created with the same username + // and do malicious things + final boolean isRunAs = authentication.getUser().isRunAs(); + final String realmType; + if (isRunAs) { + realmType = authentication.getLookedUpBy().getType(); + } else { + realmType = authentication.getAuthenticatedBy().getType(); + } + + assert realmType != null; + // ensure the user was authenticated by a realm that we can change a password for. The native realm is an internal realm and + // right now only one can exist in the realm configuration - if this changes we should update this check + return ReservedRealm.TYPE.equals(realmType) || NativeRealmSettings.TYPE.equals(realmType); + } + + // FIXME make this pkg private! + public static class RBACAuthorizationInfo implements AuthorizationInfo { + + private final Role role; + private final Map info; + private final RBACAuthorizationInfo authenticatedUserAuthorizationInfo; + + RBACAuthorizationInfo(Role role, Role authenticatedUserRole) { + this.role = role; + this.info = Collections.singletonMap(PRINCIPAL_ROLES_FIELD_NAME, role.names()); + this.authenticatedUserAuthorizationInfo = + authenticatedUserRole == null ? this : new RBACAuthorizationInfo(authenticatedUserRole, null); + } + + // FIXME make this pkg private! + public Role getRole() { + return role; + } + + @Override + public Map asMap() { + return info; + } + + @Override + public RBACAuthorizationInfo getAuthenticatedUserAuthorizationInfo() { + return authenticatedUserAuthorizationInfo; + } + } + + private static boolean isScrollRelatedAction(String action) { + return action.equals(SearchScrollAction.NAME) || + action.equals(SearchTransportService.FETCH_ID_SCROLL_ACTION_NAME) || + action.equals(SearchTransportService.QUERY_FETCH_SCROLL_ACTION_NAME) || + action.equals(SearchTransportService.QUERY_SCROLL_ACTION_NAME) || + action.equals(SearchTransportService.FREE_CONTEXT_SCROLL_ACTION_NAME) || + action.equals(ClearScrollAction.NAME) || + action.equals("indices:data/read/sql/close_cursor") || + action.equals(SearchTransportService.CLEAR_SCROLL_CONTEXTS_ACTION_NAME); + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/SecuritySearchOperationListener.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/SecuritySearchOperationListener.java index 044552d9d7710..c38053fcf30aa 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/SecuritySearchOperationListener.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/SecuritySearchOperationListener.java @@ -16,9 +16,10 @@ import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.AuthenticationField; import org.elasticsearch.xpack.security.audit.AuditUtil; +import org.elasticsearch.xpack.security.authz.AuthorizationEngine.AuthorizationInfo; +import static org.elasticsearch.xpack.security.authz.AuthorizationService.AUTHORIZATION_INFO_KEY; import static org.elasticsearch.xpack.security.authz.AuthorizationService.ORIGINATING_ACTION_KEY; -import static org.elasticsearch.xpack.security.authz.AuthorizationService.ROLE_NAMES_KEY; /** * A {@link SearchOperationListener} that is used to provide authorization for scroll requests. @@ -64,7 +65,7 @@ public void validateSearchContext(SearchContext searchContext, TransportRequest final Authentication current = Authentication.getAuthentication(threadContext); final String action = threadContext.getTransient(ORIGINATING_ACTION_KEY); ensureAuthenticatedUserIsSame(originalAuth, current, auditTrailService, searchContext.id(), action, request, - AuditUtil.extractRequestId(threadContext), threadContext.getTransient(ROLE_NAMES_KEY)); + AuditUtil.extractRequestId(threadContext), threadContext.getTransient(AUTHORIZATION_INFO_KEY)); } } } @@ -76,7 +77,8 @@ public void validateSearchContext(SearchContext searchContext, TransportRequest * (or lookup) realm. To work around this we compare the username and the originating realm type. */ static void ensureAuthenticatedUserIsSame(Authentication original, Authentication current, AuditTrailService auditTrailService, - long id, String action, TransportRequest request, String requestId, String[] roleNames) { + long id, String action, TransportRequest request, String requestId, + AuthorizationInfo authorizationInfo) { // this is really a best effort attempt since we cannot guarantee principal uniqueness // and realm names can change between nodes. final boolean samePrincipal = original.getUser().principal().equals(current.getUser().principal()); @@ -95,7 +97,7 @@ static void ensureAuthenticatedUserIsSame(Authentication original, Authenticatio final boolean sameUser = samePrincipal && sameRealmType; if (sameUser == false) { - auditTrailService.accessDenied(requestId, current, action, request, roleNames); + auditTrailService.accessDenied(requestId, current, action, request, authorizationInfo); throw new SearchContextMissingException(id); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java index 6168192d4077f..f062670da5653 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java @@ -36,6 +36,11 @@ import org.elasticsearch.xpack.core.security.authz.privilege.Privilege; import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; import org.elasticsearch.xpack.core.security.authz.store.RoleRetrievalResult; +import org.elasticsearch.xpack.core.security.user.AnonymousUser; +import org.elasticsearch.xpack.core.security.user.SystemUser; +import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.core.security.user.XPackSecurityUser; +import org.elasticsearch.xpack.core.security.user.XPackUser; import org.elasticsearch.xpack.security.support.SecurityIndexManager; import java.util.ArrayList; @@ -95,6 +100,8 @@ public class CompositeRolesStore { private final Cache negativeLookupCache; private final ThreadContext threadContext; private final AtomicLong numInvalidation = new AtomicLong(); + private final AnonymousUser anonymousUser; + private final boolean isAnonymousEnabled; private final List, ActionListener>> builtInRoleProviders; private final List, ActionListener>> allRoleProviders; @@ -130,6 +137,8 @@ public CompositeRolesStore(Settings settings, FileRolesStore fileRolesStore, Nat allList.addAll(rolesProviders); this.allRoleProviders = Collections.unmodifiableList(allList); } + this.anonymousUser = new AnonymousUser(settings); + this.isAnonymousEnabled = AnonymousUser.isAnonymousEnabled(settings); } public void roles(Set roleNames, FieldPermissionsCache fieldPermissionsCache, ActionListener roleActionListener) { @@ -180,6 +189,42 @@ public void roles(Set roleNames, FieldPermissionsCache fieldPermissionsC } } + public void getRoles(User user, FieldPermissionsCache fieldPermissionsCache, ActionListener roleActionListener) { + // we need to special case the internal users in this method, if we apply the anonymous roles to every user including these system + // user accounts then we run into the chance of a deadlock because then we need to get a role that we may be trying to get as the + // internal user. The SystemUser is special cased as it has special privileges to execute internal actions and should never be + // passed into this method. The XPackUser has the Superuser role and we can simply return that + if (SystemUser.is(user)) { + throw new IllegalArgumentException("the user [" + user.principal() + "] is the system user and we should never try to get its" + + " roles"); + } + if (XPackUser.is(user)) { + assert XPackUser.INSTANCE.roles().length == 1; + roleActionListener.onResponse(XPackUser.ROLE); + return; + } + if (XPackSecurityUser.is(user)) { + roleActionListener.onResponse(ReservedRolesStore.SUPERUSER_ROLE); + return; + } + + Set roleNames = new HashSet<>(Arrays.asList(user.roles())); + if (isAnonymousEnabled && anonymousUser.equals(user) == false) { + if (anonymousUser.roles().length == 0) { + throw new IllegalStateException("anonymous is only enabled when the anonymous user has roles"); + } + Collections.addAll(roleNames, anonymousUser.roles()); + } + + if (roleNames.isEmpty()) { + roleActionListener.onResponse(Role.EMPTY); + } else if (roleNames.contains(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName())) { + roleActionListener.onResponse(ReservedRolesStore.SUPERUSER_ROLE); + } else { + roles(roleNames, fieldPermissionsCache, roleActionListener); + } + } + private void roleDescriptors(Set roleNames, ActionListener rolesResultListener) { final Set filteredRoleNames = roleNames.stream().filter((s) -> { if (negativeLookupCache.get(s) != null) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/ServerTransportFilter.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/ServerTransportFilter.java index 40ad10b8acb88..29ea8838f58e6 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/ServerTransportFilter.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/ServerTransportFilter.java @@ -30,7 +30,6 @@ import org.elasticsearch.xpack.security.action.SecurityActionMapper; import org.elasticsearch.xpack.security.authc.AuthenticationService; import org.elasticsearch.xpack.security.authz.AuthorizationService; -import org.elasticsearch.xpack.security.authz.AuthorizationUtils; import java.io.IOException; @@ -121,20 +120,10 @@ requests from all the nodes are attached with a user (either a serialize SystemUser.is(authentication.getUser()) == false) { securityContext.executeAsUser(SystemUser.INSTANCE, (ctx) -> { final Authentication replaced = Authentication.getAuthentication(threadContext); - final AuthorizationUtils.AsyncAuthorizer asyncAuthorizer = - new AuthorizationUtils.AsyncAuthorizer(replaced, listener, (userRoles, runAsRoles) -> { - authzService.authorize(replaced, securityAction, request, userRoles, runAsRoles); - listener.onResponse(null); - }); - asyncAuthorizer.authorize(authzService); + authzService.authorize(replaced, securityAction, request, listener); }, version); } else { - final AuthorizationUtils.AsyncAuthorizer asyncAuthorizer = - new AuthorizationUtils.AsyncAuthorizer(authentication, listener, (userRoles, runAsRoles) -> { - authzService.authorize(authentication, securityAction, request, userRoles, runAsRoles); - listener.onResponse(null); - }); - asyncAuthorizer.authorize(authzService); + authzService.authorize(authentication, securityAction, request, listener); } }, listener::onFailure)); } 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 b14ed2e4848b8..97ff7521d1574 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 @@ -27,11 +27,11 @@ import org.elasticsearch.tasks.Task; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.xpack.core.security.SecurityContext; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; import org.elasticsearch.xpack.core.security.authc.AuthenticationField; -import org.elasticsearch.xpack.core.security.authz.permission.Role; import org.elasticsearch.xpack.core.security.user.SystemUser; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.AuthenticationService; @@ -100,15 +100,14 @@ public void testApply() throws Exception { callback.onResponse(authentication); return Void.TYPE; }).when(authcService).authenticate(eq("_action"), eq(request), eq(SystemUser.INSTANCE), any(ActionListener.class)); - final Role empty = Role.EMPTY; doAnswer((i) -> { - ActionListener callback = - (ActionListener) i.getArguments()[1]; - callback.onResponse(empty); + ActionListener callback = (ActionListener) i.getArguments()[3]; + callback.onResponse(null); return Void.TYPE; - }).when(authzService).roles(any(User.class), any(ActionListener.class)); + }).when(authzService) + .authorize(any(Authentication.class), any(String.class), any(TransportRequest.class), any(ActionListener.class)); filter.apply(task, "_action", request, listener, chain); - verify(authzService).authorize(authentication, "_action", request, empty, null); + verify(authzService).authorize(eq(authentication), eq("_action"), eq(request), any(ActionListener.class)); verify(chain).proceed(eq(task), eq("_action"), eq(request), isA(ContextPreservingActionListener.class)); } @@ -127,20 +126,18 @@ public void testApplyRestoresThreadContext() throws Exception { callback.onResponse(authentication); return Void.TYPE; }).when(authcService).authenticate(eq("_action"), eq(request), eq(SystemUser.INSTANCE), any(ActionListener.class)); - final Role empty = Role.EMPTY; doAnswer((i) -> { - ActionListener callback = - (ActionListener) i.getArguments()[1]; - assertEquals(authentication, threadContext.getTransient(AuthenticationField.AUTHENTICATION_KEY)); - callback.onResponse(empty); + ActionListener callback = (ActionListener) i.getArguments()[3]; + callback.onResponse(null); return Void.TYPE; - }).when(authzService).roles(any(User.class), any(ActionListener.class)); + }).when(authzService) + .authorize(any(Authentication.class), any(String.class), any(TransportRequest.class), any(ActionListener.class)); assertNull(threadContext.getTransient(AuthenticationField.AUTHENTICATION_KEY)); filter.apply(task, "_action", request, listener, chain); assertNull(threadContext.getTransient(AuthenticationField.AUTHENTICATION_KEY)); - verify(authzService).authorize(authentication, "_action", request, empty, null); + verify(authzService).authorize(eq(authentication), eq("_action"), eq(request), any(ActionListener.class)); verify(chain).proceed(eq(task), eq("_action"), eq(request), isA(ContextPreservingActionListener.class)); } @@ -169,6 +166,12 @@ public void testApplyAsSystemUser() throws Exception { callback.onResponse(threadContext.getTransient(AuthenticationField.AUTHENTICATION_KEY)); return Void.TYPE; }).when(authcService).authenticate(eq(action), eq(request), eq(SystemUser.INSTANCE), any(ActionListener.class)); + doAnswer((i) -> { + ActionListener callback = (ActionListener) i.getArguments()[3]; + callback.onResponse(null); + return Void.TYPE; + }).when(authzService) + .authorize(any(Authentication.class), any(String.class), any(TransportRequest.class), any(ActionListener.class)); filter.apply(task, action, request, listener, chain); @@ -198,19 +201,18 @@ public void testApplyDestructiveOperations() throws Exception { callback.onResponse(authentication); return Void.TYPE; }).when(authcService).authenticate(eq(action), eq(request), eq(SystemUser.INSTANCE), any(ActionListener.class)); - final Role empty = Role.EMPTY; doAnswer((i) -> { - ActionListener callback = - (ActionListener) i.getArguments()[1]; - callback.onResponse(empty); + ActionListener callback = (ActionListener) i.getArguments()[3]; + callback.onResponse(null); return Void.TYPE; - }).when(authzService).roles(any(User.class), any(ActionListener.class)); + }).when(authzService) + .authorize(any(Authentication.class), any(String.class), any(TransportRequest.class), any(ActionListener.class)); filter.apply(task, action, request, listener, chain); if (failDestructiveOperations) { verify(listener).onFailure(isA(IllegalArgumentException.class)); verifyNoMoreInteractions(authzService, chain); } else { - verify(authzService).authorize(authentication, action, request, empty, null); + verify(authzService).authorize(eq(authentication), eq(action), eq(request), any(ActionListener.class)); verify(chain).proceed(eq(task), eq(action), eq(request), isA(ContextPreservingActionListener.class)); } } @@ -229,14 +231,7 @@ public void testActionProcessException() throws Exception { callback.onResponse(authentication); return Void.TYPE; }).when(authcService).authenticate(eq("_action"), eq(request), eq(SystemUser.INSTANCE), any(ActionListener.class)); - doAnswer((i) -> { - ActionListener callback = - (ActionListener) i.getArguments()[1]; - callback.onResponse(Role.EMPTY); - return Void.TYPE; - }).when(authzService).roles(any(User.class), any(ActionListener.class)); - doThrow(exception).when(authzService).authorize(eq(authentication), eq("_action"), eq(request), any(Role.class), - any(Role.class)); + doThrow(exception).when(authzService).authorize(eq(authentication), eq("_action"), eq(request), any(ActionListener.class)); filter.apply(task, "_action", request, listener, chain); verify(listener).onFailure(exception); verifyNoMoreInteractions(chain); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportGetUserPrivilegesActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportGetUserPrivilegesActionTests.java index 8ff5378cbfc22..7eef030b70739 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportGetUserPrivilegesActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportGetUserPrivilegesActionTests.java @@ -21,7 +21,7 @@ import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivileges.ManageApplicationPrivileges; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.Privilege; -import org.elasticsearch.xpack.security.authz.AuthorizationService; +import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; import java.util.Collections; import java.util.Set; @@ -49,7 +49,7 @@ public void testBuildResponseObject() { .build(); final TransportGetUserPrivilegesAction action = new TransportGetUserPrivilegesAction(mock(ThreadPool.class), - mock(TransportService.class), mock(ActionFilters.class), mock(AuthorizationService.class)); + mock(TransportService.class), mock(ActionFilters.class), mock(CompositeRolesStore.class)); final GetUserPrivilegesResponse response = action.buildResponseObject(role); assertThat(response.getClusterPrivileges(), containsInAnyOrder("monitor", "manage_watcher")); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesActionTests.java index 00ee02aaf00dd..8d734d5d79925 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesActionTests.java @@ -29,13 +29,14 @@ import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.AuthenticationField; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsCache; import org.elasticsearch.xpack.core.security.authz.permission.Role; import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.user.User; -import org.elasticsearch.xpack.security.authz.AuthorizationService; +import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; import org.elasticsearch.xpack.security.authz.store.NativePrivilegeStore; import org.hamcrest.Matchers; import org.junit.Before; @@ -86,12 +87,12 @@ public void setup() { when(authentication.getUser()).thenReturn(user); - AuthorizationService authorizationService = mock(AuthorizationService.class); + CompositeRolesStore rolesStore = mock(CompositeRolesStore.class); Mockito.doAnswer(invocationOnMock -> { - ActionListener listener = (ActionListener) invocationOnMock.getArguments()[1]; + ActionListener listener = (ActionListener) invocationOnMock.getArguments()[2]; listener.onResponse(role); return null; - }).when(authorizationService).roles(eq(user), any(ActionListener.class)); + }).when(rolesStore).getRoles(eq(user), any(FieldPermissionsCache.class), any(ActionListener.class)); applicationPrivileges = new ArrayList<>(); NativePrivilegeStore privilegeStore = mock(NativePrivilegeStore.class); @@ -104,7 +105,7 @@ public void setup() { return null; }).when(privilegeStore).getPrivileges(any(Collection.class), any(Collection.class), any(ActionListener.class)); - action = new TransportHasPrivilegesAction(threadPool, transportService, mock(ActionFilters.class), authorizationService, + action = new TransportHasPrivilegesAction(threadPool, transportService, mock(ActionFilters.class), rolesStore, privilegeStore); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/AuditTrailServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/AuditTrailServiceTests.java index d4289080a9b30..fe79ec099f363 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/AuditTrailServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/AuditTrailServiceTests.java @@ -13,15 +13,18 @@ import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.security.authz.AuthorizationEngine.AuthorizationInfo; import org.elasticsearch.xpack.security.transport.filter.IPFilter; import org.elasticsearch.xpack.security.transport.filter.SecurityIpFilterRule; import org.junit.Before; import java.net.InetAddress; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import static java.util.Collections.unmodifiableList; +import static org.elasticsearch.xpack.security.audit.logfile.LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; @@ -147,13 +150,14 @@ public void testAnonymousAccess() throws Exception { public void testAccessGranted() throws Exception { Authentication authentication =new Authentication(new User("_username", "r1"), new RealmRef(null, null, null), new RealmRef(null, null, null)); - String[] roles = new String[] { randomAlphaOfLengthBetween(1, 6) }; + AuthorizationInfo authzInfo = + () -> Collections.singletonMap(PRINCIPAL_ROLES_FIELD_NAME, new String[] { randomAlphaOfLengthBetween(1, 6) }); final String requestId = randomAlphaOfLengthBetween(6, 12); - service.accessGranted(requestId, authentication, "_action", message, roles); + service.accessGranted(requestId, authentication, "_action", message, authzInfo); verify(licenseState).isAuditingAllowed(); if (isAuditingAllowed) { for (AuditTrail auditTrail : auditTrails) { - verify(auditTrail).accessGranted(requestId, authentication, "_action", message, roles); + verify(auditTrail).accessGranted(requestId, authentication, "_action", message, authzInfo); } } else { verifyZeroInteractions(auditTrails.toArray((Object[]) new AuditTrail[auditTrails.size()])); @@ -163,13 +167,14 @@ public void testAccessGranted() throws Exception { public void testAccessDenied() throws Exception { Authentication authentication = new Authentication(new User("_username", "r1"), new RealmRef(null, null, null), new RealmRef(null, null, null)); - String[] roles = new String[] { randomAlphaOfLengthBetween(1, 6) }; + AuthorizationInfo authzInfo = + () -> Collections.singletonMap(PRINCIPAL_ROLES_FIELD_NAME, new String[] { randomAlphaOfLengthBetween(1, 6) }); final String requestId = randomAlphaOfLengthBetween(6, 12); - service.accessDenied(requestId, authentication, "_action", message, roles); + service.accessDenied(requestId, authentication, "_action", message, authzInfo); verify(licenseState).isAuditingAllowed(); if (isAuditingAllowed) { for (AuditTrail auditTrail : auditTrails) { - verify(auditTrail).accessDenied(requestId, authentication, "_action", message, roles); + verify(auditTrail).accessDenied(requestId, authentication, "_action", message, authzInfo); } } else { verifyZeroInteractions(auditTrails.toArray((Object[]) new AuditTrail[auditTrails.size()])); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailMutedTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailMutedTests.java index 45e54cb2d5464..de541f84061a2 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailMutedTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailMutedTests.java @@ -28,6 +28,7 @@ import org.elasticsearch.xpack.core.security.user.SystemUser; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.audit.index.IndexAuditTrail.State; +import org.elasticsearch.xpack.security.authz.AuthorizationEngine.EmptyAuthorizationInfo; import org.elasticsearch.xpack.security.transport.filter.SecurityIpFilterRule; import org.junit.After; import org.junit.Before; @@ -175,7 +176,7 @@ public void testAccessGrantedMuted() { final TransportMessage message = mock(TransportMessage.class); final Authentication authentication = mock(Authentication.class); auditTrail.accessGranted(randomAlphaOfLengthBetween(6, 12), authentication, randomAlphaOfLengthBetween(6, 40), message, - new String[] { "role" }); + EmptyAuthorizationInfo.INSTANCE); assertThat(messageEnqueued.get(), is(false)); assertThat(clientCalled.get(), is(false)); verifyZeroInteractions(message); @@ -185,7 +186,8 @@ public void testSystemAccessGrantedMuted() { createAuditTrail(randomFrom(new String[] { "access_granted" }, null)); final TransportMessage message = mock(TransportMessage.class); final Authentication authentication = new Authentication(SystemUser.INSTANCE, new RealmRef(null, null, null), null); - auditTrail.accessGranted(randomAlphaOfLengthBetween(6, 12), authentication, "internal:foo", message, new String[] { "role" }); + auditTrail.accessGranted(randomAlphaOfLengthBetween(6, 12), authentication, "internal:foo", message, + EmptyAuthorizationInfo.INSTANCE); assertThat(messageEnqueued.get(), is(false)); assertThat(clientCalled.get(), is(false)); @@ -197,7 +199,7 @@ public void testAccessDeniedMuted() { final TransportMessage message = mock(TransportMessage.class); final Authentication authentication = mock(Authentication.class); auditTrail.accessDenied(randomAlphaOfLengthBetween(6, 12), authentication, randomAlphaOfLengthBetween(6, 40), message, - new String[] { "role" }); + EmptyAuthorizationInfo.INSTANCE); assertThat(messageEnqueued.get(), is(false)); assertThat(clientCalled.get(), is(false)); @@ -252,7 +254,7 @@ public void testRunAsGrantedMuted() { Authentication authentication = mock(Authentication.class); auditTrail.runAsGranted(randomAlphaOfLengthBetween(6, 12), authentication, randomAlphaOfLengthBetween(6, 40), message, - new String[] { "role" }); + EmptyAuthorizationInfo.INSTANCE); assertThat(messageEnqueued.get(), is(false)); assertThat(clientCalled.get(), is(false)); @@ -265,7 +267,7 @@ public void testRunAsDeniedMuted() { Authentication authentication = mock(Authentication.class); auditTrail.runAsDenied(randomAlphaOfLengthBetween(6, 12), authentication, randomAlphaOfLengthBetween(6, 40), message, - new String[] { "role" }); + EmptyAuthorizationInfo.INSTANCE); assertThat(messageEnqueued.get(), is(false)); assertThat(clientCalled.get(), is(false)); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailTests.java index 9fe510435c5ab..0aef5ba4f57e8 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailTests.java @@ -60,7 +60,9 @@ import org.elasticsearch.xpack.core.security.user.SystemUser; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.LocalStateSecurity; +import org.elasticsearch.xpack.security.audit.index.IndexAuditTrail.Field; import org.elasticsearch.xpack.security.audit.index.IndexAuditTrail.Message; +import org.elasticsearch.xpack.security.authz.AuthorizationEngine.AuthorizationInfo; import org.elasticsearch.xpack.security.support.SecurityIndexManager; import org.elasticsearch.xpack.security.transport.filter.IPFilter; import org.elasticsearch.xpack.security.transport.filter.SecurityIpFilterRule; @@ -75,6 +77,7 @@ import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Locale; @@ -89,6 +92,7 @@ import static org.elasticsearch.xpack.security.audit.index.IndexNameResolver.Rollover.HOURLY; import static org.elasticsearch.xpack.security.audit.index.IndexNameResolver.Rollover.MONTHLY; import static org.elasticsearch.xpack.security.audit.index.IndexNameResolver.Rollover.WEEKLY; +import static org.elasticsearch.xpack.security.audit.logfile.LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; @@ -596,8 +600,10 @@ public void testAccessGranted() throws Exception { } else { user = new User("_username", new String[]{"r1"}); } - String role = randomAlphaOfLengthBetween(1, 6); - auditor.accessGranted(randomAlphaOfLengthBetween(6, 12), createAuthentication(user), "_action", message, new String[] { role }); + final String role = randomAlphaOfLengthBetween(1, 6); + AuthorizationInfo authzInfo = + () -> Collections.singletonMap(PRINCIPAL_ROLES_FIELD_NAME, new String[] { role }); + auditor.accessGranted(randomAlphaOfLengthBetween(6, 12), createAuthentication(user), "_action", message, authzInfo); SearchHit hit = getIndexedAuditMessage(enqueuedMessage.get()); assertAuditMessage(hit, "transport", "access_granted"); @@ -613,7 +619,7 @@ public void testAccessGranted() throws Exception { assertThat(sourceMap.get("realm"), is("authRealm")); } assertEquals("_action", sourceMap.get("action")); - assertThat((Iterable) sourceMap.get(IndexAuditTrail.Field.ROLE_NAMES), containsInAnyOrder(role)); + assertEquals(Arrays.asList((String[])authzInfo.asMap().get(PRINCIPAL_ROLES_FIELD_NAME)), sourceMap.get(Field.ROLE_NAMES)); if (message instanceof IndicesRequest) { List indices = (List) sourceMap.get("indices"); assertThat(indices, containsInAnyOrder((Object[]) ((IndicesRequest) message).indices())); @@ -624,9 +630,10 @@ public void testAccessGranted() throws Exception { public void testSystemAccessGranted() throws Exception { initialize(new String[] { "system_access_granted" }, null); TransportMessage message = randomBoolean() ? new RemoteHostMockMessage() : new LocalHostMockMessage(); - String role = randomAlphaOfLengthBetween(1, 6); + AuthorizationInfo authzInfo = + () -> Collections.singletonMap(PRINCIPAL_ROLES_FIELD_NAME, new String[] { SystemUser.ROLE_NAME }); auditor.accessGranted(randomAlphaOfLength(8), createAuthentication(SystemUser.INSTANCE), "internal:_action", message, - new String[] { role }); + authzInfo); SearchHit hit = getIndexedAuditMessage(enqueuedMessage.get()); assertAuditMessage(hit, "transport", "access_granted"); @@ -635,7 +642,7 @@ public void testSystemAccessGranted() throws Exception { assertEquals(SystemUser.INSTANCE.principal(), sourceMap.get("principal")); assertThat(sourceMap.get("realm"), is("authRealm")); assertEquals("internal:_action", sourceMap.get("action")); - assertThat((Iterable) sourceMap.get(IndexAuditTrail.Field.ROLE_NAMES), containsInAnyOrder(role)); + assertEquals(Arrays.asList((String[])authzInfo.asMap().get(PRINCIPAL_ROLES_FIELD_NAME)), sourceMap.get(Field.ROLE_NAMES)); assertEquals(sourceMap.get("request"), message.getClass().getSimpleName()); } @@ -649,8 +656,9 @@ public void testAccessDenied() throws Exception { } else { user = new User("_username", new String[]{"r1"}); } - String role = randomAlphaOfLengthBetween(1, 6); - auditor.accessDenied(randomAlphaOfLengthBetween(6, 12), createAuthentication(user), "_action", message, new String[] { role }); + AuthorizationInfo authzInfo = + () -> Collections.singletonMap(PRINCIPAL_ROLES_FIELD_NAME, new String[] { "r1" }); + auditor.accessDenied(randomAlphaOfLengthBetween(6, 12), createAuthentication(user), "_action", message, authzInfo); SearchHit hit = getIndexedAuditMessage(enqueuedMessage.get()); Map sourceMap = hit.getSourceAsMap(); @@ -671,7 +679,7 @@ public void testAccessDenied() throws Exception { assertThat(indices, containsInAnyOrder((Object[]) ((IndicesRequest) message).indices())); } assertEquals(sourceMap.get("request"), message.getClass().getSimpleName()); - assertThat((Iterable) sourceMap.get(IndexAuditTrail.Field.ROLE_NAMES), containsInAnyOrder(role)); + assertEquals(Arrays.asList((String[])authzInfo.asMap().get(PRINCIPAL_ROLES_FIELD_NAME)), sourceMap.get(Field.ROLE_NAMES)); } public void testTamperedRequestRest() throws Exception { @@ -762,8 +770,10 @@ public void testRunAsGranted() throws Exception { initialize(); TransportMessage message = randomFrom(new RemoteHostMockMessage(), new LocalHostMockMessage(), new MockIndicesTransportMessage()); User user = new User("running as", new String[]{"r2"}, new User("_username", new String[] {"r1"})); - String role = randomAlphaOfLengthBetween(1, 6); - auditor.runAsGranted(randomAlphaOfLengthBetween(6, 12), createAuthentication(user), "_action", message, new String[] { role }); + final String role = randomAlphaOfLengthBetween(1, 6); + AuthorizationInfo authzInfo = + () -> Collections.singletonMap(PRINCIPAL_ROLES_FIELD_NAME, new String[] { role }); + auditor.runAsGranted(randomAlphaOfLengthBetween(6, 12), createAuthentication(user), "_action", message, authzInfo); SearchHit hit = getIndexedAuditMessage(enqueuedMessage.get()); assertAuditMessage(hit, "transport", "run_as_granted"); @@ -773,7 +783,7 @@ public void testRunAsGranted() throws Exception { assertThat(sourceMap.get("realm"), is("authRealm")); assertThat(sourceMap.get("run_as_principal"), is("running as")); assertThat(sourceMap.get("run_as_realm"), is("lookRealm")); - assertThat((Iterable) sourceMap.get(IndexAuditTrail.Field.ROLE_NAMES), containsInAnyOrder(role)); + assertEquals(Arrays.asList((String[]) authzInfo.asMap().get(PRINCIPAL_ROLES_FIELD_NAME)), sourceMap.get(Field.ROLE_NAMES)); assertEquals("_action", sourceMap.get("action")); assertEquals(sourceMap.get("request"), message.getClass().getSimpleName()); } @@ -782,7 +792,9 @@ public void testRunAsDenied() throws Exception { initialize(); TransportMessage message = randomFrom(new RemoteHostMockMessage(), new LocalHostMockMessage(), new MockIndicesTransportMessage()); User user = new User("running as", new String[]{"r2"}, new User("_username", new String[] {"r1"})); - auditor.runAsDenied(randomAlphaOfLengthBetween(6, 12), createAuthentication(user), "_action", message, new String[] { "r1" }); + AuthorizationInfo authzInfo = + () -> Collections.singletonMap(PRINCIPAL_ROLES_FIELD_NAME, new String[] { "r1" }); + auditor.runAsDenied(randomAlphaOfLengthBetween(6, 12), createAuthentication(user), "_action", message, authzInfo); SearchHit hit = getIndexedAuditMessage(enqueuedMessage.get()); assertAuditMessage(hit, "transport", "run_as_denied"); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailFilterTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailFilterTests.java index 8cb3dbf01b247..ae2b6cad3204b 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailFilterTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailFilterTests.java @@ -32,6 +32,7 @@ import org.elasticsearch.xpack.security.audit.logfile.LoggingAuditTrail.AuditEventMetaInfo; import org.elasticsearch.xpack.security.audit.logfile.LoggingAuditTrailTests.MockMessage; import org.elasticsearch.xpack.security.audit.logfile.LoggingAuditTrailTests.RestContent; +import org.elasticsearch.xpack.security.authz.AuthorizationEngine.AuthorizationInfo; import org.elasticsearch.xpack.security.rest.RemoteHostHeader; import org.elasticsearch.xpack.security.transport.filter.SecurityIpFilterRule; import org.junit.Before; @@ -47,6 +48,7 @@ import java.util.Optional; import java.util.stream.Collectors; +import static org.elasticsearch.xpack.security.audit.logfile.LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME; import static org.hamcrest.Matchers.is; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -113,7 +115,7 @@ public void testSingleCompletePolicyPredicate() throws Exception { assertTrue("Matches the filter predicate.", auditTrail.eventFilterPolicyRegistry.ignorePredicate() .test(new AuditEventMetaInfo( Optional.of(randomFrom(filteredUsers)), Optional.of(randomFrom(filteredRealms)), - Optional.of(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles).toArray(new String[0])), + Optional.of(authzInfo(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles).toArray(new String[0]))), Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0]))))); final User unfilteredUser; if (randomBoolean()) { @@ -126,22 +128,26 @@ public void testSingleCompletePolicyPredicate() throws Exception { assertFalse("Does not match the filter predicate because of the user.", auditTrail.eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(unfilteredUser), Optional.of(randomFrom(filteredRealms)), - Optional.of(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles).toArray(new String[0])), + Optional.of(authzInfo(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles) + .toArray(new String[0]))), Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0]))))); assertFalse("Does not match the filter predicate because of the empty user.", auditTrail.eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.empty(), Optional.of(randomFrom(filteredRealms)), - Optional.of(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles).toArray(new String[0])), + Optional.of(authzInfo(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles) + .toArray(new String[0]))), Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0]))))); assertFalse("Does not match the filter predicate because of the realm.", auditTrail.eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(randomFrom(filteredUsers)), Optional.of(UNFILTER_MARKER + randomAlphaOfLengthBetween(1, 8)), - Optional.of(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles).toArray(new String[0])), + Optional.of(authzInfo(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles) + .toArray(new String[0]))), Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0]))))); assertFalse("Does not match the filter predicate because of the empty realm.", auditTrail.eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(randomFrom(filteredUsers)), Optional.empty(), - Optional.of(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles).toArray(new String[0])), + Optional.of(authzInfo(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles) + .toArray(new String[0]))), Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0]))))); final List someRolesDoNotMatch = new ArrayList<>(randomSubsetOf(randomIntBetween(0, filteredRoles.size()), filteredRoles)); for (int i = 0; i < randomIntBetween(1, 8); i++) { @@ -149,9 +155,9 @@ public void testSingleCompletePolicyPredicate() throws Exception { } assertFalse("Does not match the filter predicate because of some of the roles.", auditTrail.eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(randomFrom(filteredUsers)), - Optional.of(randomFrom(filteredRealms)), Optional.of(someRolesDoNotMatch.toArray(new String[0])), + Optional.of(randomFrom(filteredRealms)), Optional.of(authzInfo(someRolesDoNotMatch.toArray(new String[0]))), Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0]))))); - final Optional emptyRoles = randomBoolean() ? Optional.empty() : Optional.of(new String[0]); + final Optional emptyRoles = randomBoolean() ? Optional.empty() : Optional.of(authzInfo(new String[0])); assertFalse("Does not match the filter predicate because of the empty roles.", auditTrail.eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(randomFrom(filteredUsers)), Optional.of(randomFrom(filteredRealms)), emptyRoles, @@ -164,13 +170,15 @@ public void testSingleCompletePolicyPredicate() throws Exception { assertFalse("Does not match the filter predicate because of some of the indices.", auditTrail.eventFilterPolicyRegistry.ignorePredicate() .test(new AuditEventMetaInfo(Optional.of(randomFrom(filteredUsers)), Optional.of(randomFrom(filteredRealms)), - Optional.of(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles).toArray(new String[0])), + Optional.of(authzInfo(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles) + .toArray(new String[0]))), Optional.of(someIndicesDoNotMatch.toArray(new String[0]))))); final Optional emptyIndices = randomBoolean() ? Optional.empty() : Optional.of(new String[0]); assertFalse("Does not match the filter predicate because of the empty indices.", auditTrail.eventFilterPolicyRegistry.ignorePredicate() .test(new AuditEventMetaInfo(Optional.of(randomFrom(filteredUsers)), Optional.of(randomFrom(filteredRealms)), - Optional.of(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles).toArray(new String[0])), + Optional.of(authzInfo(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles) + .toArray(new String[0]))), emptyIndices))); } @@ -212,7 +220,8 @@ public void testSingleCompleteWithEmptyFieldPolicyPredicate() throws Exception { assertTrue("Matches the filter predicate.", auditTrail.eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(randomFrom(filteredUsers)), Optional.of(randomFrom(filteredRealms)), - Optional.of(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles).toArray(new String[0])), + Optional.of(authzInfo(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles) + .toArray(new String[0]))), Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0]))))); final User unfilteredUser; if (randomBoolean()) { @@ -225,22 +234,26 @@ public void testSingleCompleteWithEmptyFieldPolicyPredicate() throws Exception { assertFalse("Does not match the filter predicate because of the user.", auditTrail.eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(unfilteredUser), Optional.of(randomFrom(filteredRealms)), - Optional.of(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles).toArray(new String[0])), + Optional.of(authzInfo(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles) + .toArray(new String[0]))), Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0]))))); assertTrue("Matches the filter predicate because of the empty user.", auditTrail.eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.empty(), Optional.of(randomFrom(filteredRealms)), - Optional.of(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles).toArray(new String[0])), + Optional.of(authzInfo(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles) + .toArray(new String[0]))), Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0]))))); assertFalse("Does not match the filter predicate because of the realm.", auditTrail.eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(randomFrom(filteredUsers)), Optional.of(UNFILTER_MARKER + randomAlphaOfLengthBetween(1, 8)), - Optional.of(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles).toArray(new String[0])), + Optional.of(authzInfo(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles) + .toArray(new String[0]))), Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0]))))); assertTrue("Matches the filter predicate because of the empty realm.", auditTrail.eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(randomFrom(filteredUsers)), Optional.empty(), - Optional.of(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles).toArray(new String[0])), + Optional.of(authzInfo(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles) + .toArray(new String[0]))), Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0]))))); final List someRolesDoNotMatch = new ArrayList<>(randomSubsetOf(randomIntBetween(0, filteredRoles.size()), filteredRoles)); for (int i = 0; i < randomIntBetween(1, 8); i++) { @@ -248,9 +261,9 @@ public void testSingleCompleteWithEmptyFieldPolicyPredicate() throws Exception { } assertFalse("Does not match the filter predicate because of some of the roles.", auditTrail.eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(randomFrom(filteredUsers)), - Optional.of(randomFrom(filteredRealms)), Optional.of(someRolesDoNotMatch.toArray(new String[0])), + Optional.of(randomFrom(filteredRealms)), Optional.of(authzInfo(someRolesDoNotMatch.toArray(new String[0]))), Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0]))))); - final Optional emptyRoles = randomBoolean() ? Optional.empty() : Optional.of(new String[0]); + final Optional emptyRoles = randomBoolean() ? Optional.empty() : Optional.of(authzInfo(new String[0])); assertTrue("Matches the filter predicate because of the empty roles.", auditTrail.eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(randomFrom(filteredUsers)), Optional.of(randomFrom(filteredRealms)), emptyRoles, @@ -263,12 +276,14 @@ public void testSingleCompleteWithEmptyFieldPolicyPredicate() throws Exception { assertFalse("Does not match the filter predicate because of some of the indices.", auditTrail.eventFilterPolicyRegistry.ignorePredicate() .test(new AuditEventMetaInfo(Optional.of(randomFrom(filteredUsers)), Optional.of(randomFrom(filteredRealms)), - Optional.of(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles).toArray(new String[0])), + Optional.of(authzInfo(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles) + .toArray(new String[0]))), Optional.of(someIndicesDoNotMatch.toArray(new String[0]))))); final Optional emptyIndices = randomBoolean() ? Optional.empty() : Optional.of(new String[0]); assertTrue("Matches the filter predicate because of the empty indices.", auditTrail.eventFilterPolicyRegistry.ignorePredicate() .test(new AuditEventMetaInfo(Optional.of(randomFrom(filteredUsers)), Optional.of(randomFrom(filteredRealms)), - Optional.of(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles).toArray(new String[0])), + Optional.of(authzInfo(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles) + .toArray(new String[0]))), emptyIndices))); } @@ -317,26 +332,28 @@ public void testTwoPolicyPredicatesWithMissingFields() throws Exception { assertTrue("Matches both the first and the second filter predicates.", auditTrail.eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(randomFrom(filteredUsers)), Optional.of(randomFrom(filteredRealms)), - Optional.of(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles).toArray(new String[0])), + Optional.of(authzInfo(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles) + .toArray(new String[0]))), Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0]))))); // matches first policy but not the second assertTrue("Matches the first filter predicate but not the second.", auditTrail.eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(unfilteredUser), Optional.of(randomFrom(filteredRealms)), - Optional.of(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles).toArray(new String[0])), + Optional.of(authzInfo(randomSubsetOf(randomIntBetween(1, filteredRoles.size()), filteredRoles) + .toArray(new String[0]))), Optional.of(someIndicesDoNotMatch.toArray(new String[0]))))); // matches the second policy but not the first assertTrue("Matches the second filter predicate but not the first.", auditTrail.eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(randomFrom(filteredUsers)), Optional.of(UNFILTER_MARKER + randomAlphaOfLengthBetween(1, 8)), - Optional.of(someRolesDoNotMatch.toArray(new String[0])), + Optional.of(authzInfo(someRolesDoNotMatch.toArray(new String[0]))), Optional.of(randomSubsetOf(randomIntBetween(1, filteredIndices.size()), filteredIndices).toArray(new String[0]))))); // matches neither the first nor the second policies assertFalse("Matches neither the first nor the second filter predicates.", auditTrail.eventFilterPolicyRegistry.ignorePredicate() .test(new AuditEventMetaInfo(Optional.of(unfilteredUser), Optional.of(UNFILTER_MARKER + randomAlphaOfLengthBetween(1, 8)), - Optional.of(someRolesDoNotMatch.toArray(new String[0])), + Optional.of(authzInfo(someRolesDoNotMatch.toArray(new String[0]))), Optional.of(someIndicesDoNotMatch.toArray(new String[0]))))); } @@ -469,55 +486,61 @@ public void testUsersFilter() throws Exception { threadContext.stashContext(); // accessGranted - auditTrail.accessGranted(randomAlphaOfLength(8), unfilteredAuthentication, "_action", message, new String[] { "role1" }); + auditTrail.accessGranted(randomAlphaOfLength(8), unfilteredAuthentication, "_action", message, authzInfo(new String[] { "role1" })); assertThat("AccessGranted message: unfiltered user is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessGranted(randomAlphaOfLength(8), filteredAuthentication, "_action", message, new String[] { "role1" }); + auditTrail.accessGranted(randomAlphaOfLength(8), filteredAuthentication, "_action", message, authzInfo(new String[] { "role1" })); assertThat("AccessGranted message: filtered user is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); auditTrail.accessGranted(randomAlphaOfLength(8), createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), - "internal:_action", message, new String[] { "role1" }); + "internal:_action", message, authzInfo(new String[] { "role1" })); assertThat("AccessGranted internal message: system user is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessGranted(randomAlphaOfLength(8), unfilteredAuthentication, "internal:_action", message, new String[] { "role1" }); + auditTrail.accessGranted(randomAlphaOfLength(8), unfilteredAuthentication, "internal:_action", message, + authzInfo(new String[] { "role1" })); assertThat("AccessGranted internal message: unfiltered user is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessGranted(randomAlphaOfLength(8), filteredAuthentication, "internal:_action", message, new String[] { "role1" }); + auditTrail.accessGranted(randomAlphaOfLength(8), filteredAuthentication, "internal:_action", message, + authzInfo(new String[] { "role1" })); assertThat("AccessGranted internal message: filtered user is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); // accessDenied - auditTrail.accessDenied(randomAlphaOfLength(8), unfilteredAuthentication, "_action", message, new String[] { "role1" }); + auditTrail.accessDenied(randomAlphaOfLength(8), unfilteredAuthentication, "_action", message, + authzInfo(new String[] { "role1" })); assertThat("AccessDenied message: unfiltered user is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessDenied(randomAlphaOfLength(8), filteredAuthentication, "_action", message, new String[] { "role1" }); + auditTrail.accessDenied(randomAlphaOfLength(8), filteredAuthentication, "_action", message, + authzInfo(new String[] { "role1" })); assertThat("AccessDenied message: filtered user is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); auditTrail.accessDenied(randomAlphaOfLength(8), createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), "internal:_action", - message, new String[] { "role1" }); + message, authzInfo(new String[] { "role1" })); assertThat("AccessDenied internal message: system user is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessDenied(randomAlphaOfLength(8), unfilteredAuthentication, "internal:_action", message, new String[] { "role1" }); + auditTrail.accessDenied(randomAlphaOfLength(8), unfilteredAuthentication, "internal:_action", message, + authzInfo(new String[] { "role1" })); assertThat("AccessDenied internal message: unfiltered user is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessDenied(randomAlphaOfLength(8), filteredAuthentication, "internal:_action", message, new String[] { "role1" }); + auditTrail.accessDenied(randomAlphaOfLength(8), filteredAuthentication, "internal:_action", message, + authzInfo(new String[] { "role1" })); assertThat("AccessDenied internal message: filtered user is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); @@ -573,36 +596,36 @@ public void testUsersFilter() throws Exception { // runAsGranted auditTrail.runAsGranted(randomAlphaOfLength(8), unfilteredAuthentication, "_action", new MockMessage(threadContext), - new String[] { "role1" }); + authzInfo(new String[] { "role1" })); assertThat("RunAsGranted message: unfiltered user is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); auditTrail.runAsGranted(randomAlphaOfLength(8), filteredAuthentication, "_action", new MockMessage(threadContext), - new String[] { "role1" }); + authzInfo(new String[] { "role1" })); assertThat("RunAsGranted message: filtered user is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); // runAsDenied auditTrail.runAsDenied(randomAlphaOfLength(8), unfilteredAuthentication, "_action", new MockMessage(threadContext), - new String[] { "role1" }); + authzInfo(new String[] { "role1" })); assertThat("RunAsDenied message: unfiltered user is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); auditTrail.runAsDenied(randomAlphaOfLength(8), filteredAuthentication, "_action", new MockMessage(threadContext), - new String[] { "role1" }); + authzInfo(new String[] { "role1" })); assertThat("RunAsDenied message: filtered user is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); - auditTrail.runAsDenied(randomAlphaOfLength(8), unfilteredAuthentication, getRestRequest(), new String[] { "role1" }); + auditTrail.runAsDenied(randomAlphaOfLength(8), unfilteredAuthentication, getRestRequest(), authzInfo(new String[] { "role1" })); assertThat("RunAsDenied rest request: unfiltered user is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.runAsDenied(randomAlphaOfLength(8), filteredAuthentication, getRestRequest(), new String[] { "role1" }); + auditTrail.runAsDenied(randomAlphaOfLength(8), filteredAuthentication, getRestRequest(), authzInfo(new String[] { "role1" })); assertThat("RunAsDenied rest request: filtered user is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); @@ -747,74 +770,74 @@ public void testRealmsFilter() throws Exception { // accessGranted auditTrail.accessGranted(randomAlphaOfLength(8), createAuthentication(user, filteredRealm), "_action", message, - new String[] { "role1" }); + authzInfo(new String[] { "role1" })); assertThat("AccessGranted message: filtered realm is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); auditTrail.accessGranted(randomAlphaOfLength(8), createAuthentication(user, unfilteredRealm), "_action", message, - new String[] { "role1" }); + authzInfo(new String[] { "role1" })); assertThat("AccessGranted message: unfiltered realm is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); auditTrail.accessGranted(randomAlphaOfLength(8), createAuthentication(SystemUser.INSTANCE, filteredRealm), "internal:_action", - message, new String[] { "role1" }); + message, authzInfo(new String[] { "role1" })); assertThat("AccessGranted internal message system user: filtered realm is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); auditTrail.accessGranted(randomAlphaOfLength(8), createAuthentication(SystemUser.INSTANCE, unfilteredRealm), "internal:_action", - message, new String[] { "role1" }); + message, authzInfo(new String[] { "role1" })); assertThat("AccessGranted internal message system user: unfiltered realm is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); auditTrail.accessGranted(randomAlphaOfLength(8), createAuthentication(user, filteredRealm), "internal:_action", message, - new String[] { "role1" }); + authzInfo(new String[] { "role1" })); assertThat("AccessGranted internal message: filtered realm is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); auditTrail.accessGranted(randomAlphaOfLength(8), createAuthentication(user, unfilteredRealm), "internal:_action", message, - new String[] { "role1" }); + authzInfo(new String[] { "role1" })); assertThat("AccessGranted internal message: unfiltered realm is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); // accessDenied auditTrail.accessDenied(randomAlphaOfLength(8), createAuthentication(user, filteredRealm), "_action", message, - new String[] { "role1" }); + authzInfo(new String[] { "role1" })); assertThat("AccessDenied message: filtered realm is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); auditTrail.accessDenied(randomAlphaOfLength(8), createAuthentication(user, unfilteredRealm), "_action", message, - new String[] { "role1" }); + authzInfo(new String[] { "role1" })); assertThat("AccessDenied message: unfiltered realm is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); auditTrail.accessDenied(randomAlphaOfLength(8), createAuthentication(SystemUser.INSTANCE, filteredRealm), "internal:_action", - message, new String[] { "role1" }); + message, authzInfo(new String[] { "role1" })); assertThat("AccessDenied internal message system user: filtered realm is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); auditTrail.accessDenied(randomAlphaOfLength(8), createAuthentication(SystemUser.INSTANCE, unfilteredRealm), "internal:_action", - message, new String[] { "role1" }); + message, authzInfo(new String[] { "role1" })); assertThat("AccessDenied internal message system user: unfiltered realm is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); auditTrail.accessDenied(randomAlphaOfLength(8), createAuthentication(user, filteredRealm), "internal:_action", message, - new String[] { "role1" }); + authzInfo(new String[] { "role1" })); assertThat("AccessGranted internal message: filtered realm is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); auditTrail.accessDenied(randomAlphaOfLength(8), createAuthentication(user, unfilteredRealm), "internal:_action", message, - new String[] { "role1" }); + authzInfo(new String[] { "role1" })); assertThat("AccessGranted internal message: unfiltered realm is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); @@ -869,38 +892,38 @@ public void testRealmsFilter() throws Exception { // runAsGranted auditTrail.runAsGranted(randomAlphaOfLength(8), createAuthentication(user, filteredRealm), "_action", - new MockMessage(threadContext), new String[] { "role1" }); + new MockMessage(threadContext), authzInfo(new String[] { "role1" })); assertThat("RunAsGranted message: filtered realm is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); auditTrail.runAsGranted(randomAlphaOfLength(8), createAuthentication(user, unfilteredRealm), "_action", - new MockMessage(threadContext), new String[] { "role1" }); + new MockMessage(threadContext), authzInfo(new String[] { "role1" })); assertThat("RunAsGranted message: unfiltered realm is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); // runAsDenied auditTrail.runAsDenied(randomAlphaOfLength(8), createAuthentication(user, filteredRealm), "_action", new MockMessage(threadContext), - new String[] { "role1" }); + authzInfo(new String[] { "role1" })); assertThat("RunAsDenied message: filtered realm is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); auditTrail.runAsDenied(randomAlphaOfLength(8), createAuthentication(user, unfilteredRealm), "_action", - new MockMessage(threadContext), new String[] { "role1" }); + new MockMessage(threadContext), authzInfo(new String[] { "role1" })); assertThat("RunAsDenied message: unfiltered realm is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); auditTrail.runAsDenied(randomAlphaOfLength(8), createAuthentication(user, filteredRealm), getRestRequest(), - new String[] { "role1" }); + authzInfo(new String[] { "role1" })); assertThat("RunAsDenied rest request: filtered realm is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); auditTrail.runAsDenied(randomAlphaOfLength(8), createAuthentication(user, unfilteredRealm), getRestRequest(), - new String[] { "role1" }); + authzInfo(new String[] { "role1" })); assertThat("RunAsDenied rest request: unfiltered realm is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); @@ -1064,67 +1087,67 @@ public void testRolesFilter() throws Exception { threadContext.stashContext(); // accessGranted - auditTrail.accessGranted(randomAlphaOfLength(8), authentication, "_action", message, unfilteredRoles); + auditTrail.accessGranted(randomAlphaOfLength(8), authentication, "_action", message, authzInfo(unfilteredRoles)); assertThat("AccessGranted message: unfiltered roles filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessGranted(randomAlphaOfLength(8), authentication, "_action", message, filteredRoles); + auditTrail.accessGranted(randomAlphaOfLength(8), authentication, "_action", message, authzInfo(filteredRoles)); assertThat("AccessGranted message: filtered roles not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); auditTrail.accessGranted(randomAlphaOfLength(8), createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), - "internal:_action", message, unfilteredRoles); + "internal:_action", message, authzInfo(unfilteredRoles)); assertThat("AccessGranted internal message system user: unfiltered roles filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); auditTrail.accessGranted(randomAlphaOfLength(8), createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), - "internal:_action", message, filteredRoles); + "internal:_action", message, authzInfo(filteredRoles)); assertThat("AccessGranted internal message system user: filtered roles not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessGranted(randomAlphaOfLength(8), authentication, "internal:_action", message, unfilteredRoles); + auditTrail.accessGranted(randomAlphaOfLength(8), authentication, "internal:_action", message, authzInfo(unfilteredRoles)); assertThat("AccessGranted internal message: unfiltered roles filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessGranted(randomAlphaOfLength(8), authentication, "internal:_action", message, filteredRoles); + auditTrail.accessGranted(randomAlphaOfLength(8), authentication, "internal:_action", message, authzInfo(filteredRoles)); assertThat("AccessGranted internal message: filtered roles not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); // accessDenied - auditTrail.accessDenied(randomAlphaOfLength(8), authentication, "_action", message, unfilteredRoles); + auditTrail.accessDenied(randomAlphaOfLength(8), authentication, "_action", message, authzInfo(unfilteredRoles)); assertThat("AccessDenied message: unfiltered roles filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessDenied(randomAlphaOfLength(8), authentication, "_action", message, filteredRoles); + auditTrail.accessDenied(randomAlphaOfLength(8), authentication, "_action", message, authzInfo(filteredRoles)); assertThat("AccessDenied message: filtered roles not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); auditTrail.accessDenied(randomAlphaOfLength(8), createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), "internal:_action", - message, unfilteredRoles); + message, authzInfo(unfilteredRoles)); assertThat("AccessDenied internal message system user: unfiltered roles filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); auditTrail.accessDenied(randomAlphaOfLength(8), createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), "internal:_action", - message, filteredRoles); + message, authzInfo(filteredRoles)); assertThat("AccessDenied internal message system user: filtered roles not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessDenied(randomAlphaOfLength(8), authentication, "internal:_action", message, unfilteredRoles); + auditTrail.accessDenied(randomAlphaOfLength(8), authentication, "internal:_action", message, authzInfo(unfilteredRoles)); assertThat("AccessDenied internal message: unfiltered roles filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessDenied(randomAlphaOfLength(8), authentication, "internal:_action", message, filteredRoles); + auditTrail.accessDenied(randomAlphaOfLength(8), authentication, "internal:_action", message, authzInfo(filteredRoles)); assertThat("AccessDenied internal message: filtered roles not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); @@ -1150,33 +1173,36 @@ public void testRolesFilter() throws Exception { threadContext.stashContext(); // runAsGranted - auditTrail.runAsGranted(randomAlphaOfLength(8), authentication, "_action", new MockMessage(threadContext), unfilteredRoles); + auditTrail.runAsGranted(randomAlphaOfLength(8), authentication, "_action", new MockMessage(threadContext), + authzInfo(unfilteredRoles)); assertThat("RunAsGranted message: unfiltered roles filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.runAsGranted(randomAlphaOfLength(8), authentication, "_action", new MockMessage(threadContext), filteredRoles); + auditTrail.runAsGranted(randomAlphaOfLength(8), authentication, "_action", new MockMessage(threadContext), + authzInfo(filteredRoles)); assertThat("RunAsGranted message: filtered roles not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); // runAsDenied - auditTrail.runAsDenied(randomAlphaOfLength(8), authentication, "_action", new MockMessage(threadContext), unfilteredRoles); + auditTrail.runAsDenied(randomAlphaOfLength(8), authentication, "_action", new MockMessage(threadContext), + authzInfo(unfilteredRoles)); assertThat("RunAsDenied message: unfiltered roles filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.runAsDenied(randomAlphaOfLength(8), authentication, "_action", new MockMessage(threadContext), filteredRoles); + auditTrail.runAsDenied(randomAlphaOfLength(8), authentication, "_action", new MockMessage(threadContext), authzInfo(filteredRoles)); assertThat("RunAsDenied message: filtered roles not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); - auditTrail.runAsDenied(randomAlphaOfLength(8), authentication, getRestRequest(), unfilteredRoles); + auditTrail.runAsDenied(randomAlphaOfLength(8), authentication, getRestRequest(), authzInfo(unfilteredRoles)); assertThat("RunAsDenied rest request: unfiltered roles filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.runAsDenied(randomAlphaOfLength(8), authentication, getRestRequest(), filteredRoles); + auditTrail.runAsDenied(randomAlphaOfLength(8), authentication, getRestRequest(), authzInfo(filteredRoles)); assertThat("RunAsDenied rest request: filtered roles not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); @@ -1385,7 +1411,7 @@ public void testIndicesFilter() throws Exception { threadContext.stashContext(); // accessGranted - auditTrail.accessGranted(randomAlphaOfLength(8), authentication, "_action", noIndexMessage, new String[] { "role1" }); + auditTrail.accessGranted(randomAlphaOfLength(8), authentication, "_action", noIndexMessage, authzInfo(new String[] { "role1" })); if (filterMissingIndices) { assertThat("AccessGranted message no index: not filtered out by the missing indices filter", logOutput.size(), is(0)); } else { @@ -1396,19 +1422,19 @@ public void testIndicesFilter() throws Exception { auditTrail.accessGranted(randomAlphaOfLength(8), authentication, "_action", new MockIndicesRequest(threadContext, unfilteredIndices), - new String[] { "role1" }); + authzInfo(new String[] { "role1" })); assertThat("AccessGranted message unfiltered indices: filtered out by indices filter", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); auditTrail.accessGranted(randomAlphaOfLength(8), authentication, "_action", new MockIndicesRequest(threadContext, filteredIndices), - new String[] { "role1" }); + authzInfo(new String[] { "role1" })); assertThat("AccessGranted message filtered indices: not filtered out by indices filter", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); auditTrail.accessGranted(randomAlphaOfLength(8), createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), - "internal:_action", noIndexMessage, new String[] { "role1" }); + "internal:_action", noIndexMessage, authzInfo(new String[] { "role1" })); if (filterMissingIndices) { assertThat("AccessGranted message system user no index: not filtered out by the missing indices filter", logOutput.size(), is(0)); @@ -1419,19 +1445,19 @@ public void testIndicesFilter() throws Exception { threadContext.stashContext(); auditTrail.accessGranted(randomAlphaOfLength(8), createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), - "internal:_action", new MockIndicesRequest(threadContext, unfilteredIndices), new String[] { "role1" }); + "internal:_action", new MockIndicesRequest(threadContext, unfilteredIndices), authzInfo(new String[] { "role1" })); assertThat("AccessGranted message system user unfiltered indices: filtered out by indices filter", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); auditTrail.accessGranted(randomAlphaOfLength(8), createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), - "internal:_action", new MockIndicesRequest(threadContext, filteredIndices), new String[] { "role1" }); + "internal:_action", new MockIndicesRequest(threadContext, filteredIndices), authzInfo(new String[] { "role1" })); assertThat("AccessGranted message system user filtered indices: not filtered out by indices filter", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); // accessDenied - auditTrail.accessDenied(randomAlphaOfLength(8), authentication, "_action", noIndexMessage, new String[] { "role1" }); + auditTrail.accessDenied(randomAlphaOfLength(8), authentication, "_action", noIndexMessage, authzInfo(new String[] { "role1" })); if (filterMissingIndices) { assertThat("AccessDenied message no index: not filtered out by the missing indices filter", logOutput.size(), is(0)); } else { @@ -1441,19 +1467,19 @@ public void testIndicesFilter() throws Exception { threadContext.stashContext(); auditTrail.accessDenied(randomAlphaOfLength(8), authentication, "_action", new MockIndicesRequest(threadContext, unfilteredIndices), - new String[] { "role1" }); + authzInfo(new String[] { "role1" })); assertThat("AccessDenied message unfiltered indices: filtered out by indices filter", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); auditTrail.accessDenied(randomAlphaOfLength(8), authentication, "_action", new MockIndicesRequest(threadContext, filteredIndices), - new String[] { "role1" }); + authzInfo(new String[] { "role1" })); assertThat("AccessDenied message filtered indices: not filtered out by indices filter", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); auditTrail.accessDenied(randomAlphaOfLength(8), createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), "internal:_action", - noIndexMessage, new String[] { "role1" }); + noIndexMessage, authzInfo(new String[] { "role1" })); if (filterMissingIndices) { assertThat("AccessDenied message system user no index: not filtered out by the missing indices filter", logOutput.size(), is(0)); @@ -1465,14 +1491,14 @@ public void testIndicesFilter() throws Exception { auditTrail.accessDenied(randomAlphaOfLength(8), createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), "internal:_action", new MockIndicesRequest(threadContext, unfilteredIndices), - new String[] { "role1" }); + authzInfo(new String[] { "role1" })); assertThat("AccessDenied message system user unfiltered indices: filtered out by indices filter", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); auditTrail.accessDenied(randomAlphaOfLength(8), createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), "internal:_action", new MockIndicesRequest(threadContext, filteredIndices), - new String[] { "role1" }); + authzInfo(new String[] { "role1" })); assertThat("AccessGranted message system user filtered indices: not filtered out by indices filter", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); @@ -1498,7 +1524,7 @@ public void testIndicesFilter() throws Exception { threadContext.stashContext(); // runAsGranted - auditTrail.runAsGranted(randomAlphaOfLength(8), authentication, "_action", noIndexMessage, new String[] { "role1" }); + auditTrail.runAsGranted(randomAlphaOfLength(8), authentication, "_action", noIndexMessage, authzInfo(new String[] { "role1" })); if (filterMissingIndices) { assertThat("RunAsGranted message no index: not filtered out by missing indices filter", logOutput.size(), is(0)); } else { @@ -1508,19 +1534,19 @@ public void testIndicesFilter() throws Exception { threadContext.stashContext(); auditTrail.runAsGranted(randomAlphaOfLength(8), authentication, "_action", new MockIndicesRequest(threadContext, unfilteredIndices), - new String[] { "role1" }); + authzInfo(new String[] { "role1" })); assertThat("RunAsGranted message unfiltered indices: filtered out by indices filter", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); auditTrail.runAsGranted(randomAlphaOfLength(8), authentication, "_action", new MockIndicesRequest(threadContext, filteredIndices), - new String[] { "role1" }); + authzInfo(new String[] { "role1" })); assertThat("RunAsGranted message filtered indices: not filtered out by indices filter", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); // runAsDenied - auditTrail.runAsDenied(randomAlphaOfLength(8), authentication, "_action", noIndexMessage, new String[] { "role1" }); + auditTrail.runAsDenied(randomAlphaOfLength(8), authentication, "_action", noIndexMessage, authzInfo(new String[] { "role1" })); if (filterMissingIndices) { assertThat("RunAsDenied message no index: not filtered out by missing indices filter", logOutput.size(), is(0)); } else { @@ -1530,18 +1556,18 @@ public void testIndicesFilter() throws Exception { threadContext.stashContext(); auditTrail.runAsDenied(randomAlphaOfLength(8), authentication, "_action", new MockIndicesRequest(threadContext, unfilteredIndices), - new String[] { "role1" }); + authzInfo(new String[] { "role1" })); assertThat("RunAsDenied message unfiltered indices: filtered out by indices filter", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); auditTrail.runAsDenied(randomAlphaOfLength(8), authentication, "_action", new MockIndicesRequest(threadContext, filteredIndices), - new String[] { "role1" }); + authzInfo(new String[] { "role1" })); assertThat("RunAsDenied message filtered indices: not filtered out by indices filter", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); - auditTrail.runAsDenied(randomAlphaOfLength(8), authentication, getRestRequest(), new String[] { "role1" }); + auditTrail.runAsDenied(randomAlphaOfLength(8), authentication, getRestRequest(), authzInfo(new String[] { "role1" })); if (filterMissingIndices) { assertThat("RunAsDenied rest request: not filtered out by missing indices filter", logOutput.size(), is(0)); } else { @@ -1677,5 +1703,7 @@ public String toString() { } } - + private static AuthorizationInfo authzInfo(String[] roles) { + return () -> Collections.singletonMap(PRINCIPAL_ROLES_FIELD_NAME, roles); + } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java index da4823a3f3a69..3df903991138e 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java @@ -40,6 +40,7 @@ import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.audit.AuditTrail; import org.elasticsearch.xpack.security.audit.AuditUtil; +import org.elasticsearch.xpack.security.authz.AuthorizationEngine.AuthorizationInfo; import org.elasticsearch.xpack.security.rest.RemoteHostHeader; import org.elasticsearch.xpack.security.transport.filter.IPFilter; import org.elasticsearch.xpack.security.transport.filter.SecurityIpFilterRule; @@ -63,6 +64,8 @@ import java.util.Map; import java.util.Properties; import java.util.regex.Pattern; + +import static org.elasticsearch.xpack.security.audit.logfile.LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; @@ -480,11 +483,12 @@ public void testAuthenticationFailedRealmRest() throws Exception { public void testAccessGranted() throws Exception { final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); - final String[] roles = randomArray(0, 4, String[]::new, () -> randomBoolean() ? null : randomAlphaOfLengthBetween(1, 4)); + final String[] expectedRoles = randomArray(0, 4, String[]::new, () -> randomBoolean() ? null : randomAlphaOfLengthBetween(1, 4)); + final AuthorizationInfo authorizationInfo = () -> Collections.singletonMap(PRINCIPAL_ROLES_FIELD_NAME, expectedRoles); final Authentication authentication = createAuthentication(); final String requestId = randomRequestId(); - auditTrail.accessGranted(requestId, authentication, "_action", message, roles); + auditTrail.accessGranted(requestId, authentication, "_action", message, authorizationInfo); final MapBuilder checkedFields = new MapBuilder<>(commonFields); final MapBuilder checkedArrayFields = new MapBuilder<>(); checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.TRANSPORT_ORIGIN_FIELD_VALUE) @@ -492,7 +496,7 @@ public void testAccessGranted() throws Exception { .put(LoggingAuditTrail.ACTION_FIELD_NAME, "_action") .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) .put(LoggingAuditTrail.REQUEST_ID_FIELD_NAME, requestId); - checkedArrayFields.put(LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME, roles); + checkedArrayFields.put(PRINCIPAL_ROLES_FIELD_NAME, (String[]) authorizationInfo.asMap().get(PRINCIPAL_ROLES_FIELD_NAME)); subject(authentication, checkedFields); restOrTransportOrigin(message, threadContext, checkedFields); indicesRequest(message, checkedFields, checkedArrayFields); @@ -507,16 +511,17 @@ public void testAccessGranted() throws Exception { .put("xpack.security.audit.logfile.events.exclude", "access_granted") .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.accessGranted(requestId, authentication, "_action", message, roles); + auditTrail.accessGranted(requestId, authentication, "_action", message, authorizationInfo); assertEmptyLog(logger); } public void testAccessGrantedInternalSystemAction() throws Exception { final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); - final String[] roles = randomArray(0, 4, String[]::new, () -> randomBoolean() ? null : randomAlphaOfLengthBetween(1, 4)); + final String[] expectedRoles = randomArray(0, 4, String[]::new, () -> randomBoolean() ? null : randomAlphaOfLengthBetween(1, 4)); + final AuthorizationInfo authorizationInfo = () -> Collections.singletonMap(PRINCIPAL_ROLES_FIELD_NAME, expectedRoles); final Authentication authentication = new Authentication(SystemUser.INSTANCE, new RealmRef("_reserved", "test", "foo"), null); final String requestId = randomRequestId(); - auditTrail.accessGranted(requestId, authentication, "internal:_action", message, roles); + auditTrail.accessGranted(requestId, authentication, "internal:_action", message, authorizationInfo); assertEmptyLog(logger); // test enabled @@ -525,7 +530,7 @@ public void testAccessGrantedInternalSystemAction() throws Exception { .put("xpack.security.audit.logfile.events.include", "system_access_granted") .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.accessGranted(requestId, authentication, "internal:_action", message, roles); + auditTrail.accessGranted(requestId, authentication, "internal:_action", message, authorizationInfo); final MapBuilder checkedFields = new MapBuilder<>(commonFields); final MapBuilder checkedArrayFields = new MapBuilder<>(); checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.TRANSPORT_ORIGIN_FIELD_VALUE) @@ -535,7 +540,7 @@ public void testAccessGrantedInternalSystemAction() throws Exception { .put(LoggingAuditTrail.ACTION_FIELD_NAME, "internal:_action") .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) .put(LoggingAuditTrail.REQUEST_ID_FIELD_NAME, requestId); - checkedArrayFields.put(LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME, roles); + checkedArrayFields.put(PRINCIPAL_ROLES_FIELD_NAME, (String[]) authorizationInfo.asMap().get(PRINCIPAL_ROLES_FIELD_NAME)); restOrTransportOrigin(message, threadContext, checkedFields); indicesRequest(message, checkedFields, checkedArrayFields); opaqueId(threadContext, checkedFields); @@ -545,11 +550,12 @@ public void testAccessGrantedInternalSystemAction() throws Exception { public void testAccessGrantedInternalSystemActionNonSystemUser() throws Exception { final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); - final String[] roles = randomArray(0, 4, String[]::new, () -> randomBoolean() ? null : randomAlphaOfLengthBetween(1, 4)); + final String[] expectedRoles = randomArray(0, 4, String[]::new, () -> randomBoolean() ? null : randomAlphaOfLengthBetween(1, 4)); + final AuthorizationInfo authorizationInfo = () -> Collections.singletonMap(PRINCIPAL_ROLES_FIELD_NAME, expectedRoles); final Authentication authentication = createAuthentication(); final String requestId = randomRequestId(); - auditTrail.accessGranted(requestId, authentication, "internal:_action", message, roles); + auditTrail.accessGranted(requestId, authentication, "internal:_action", message, authorizationInfo); final MapBuilder checkedFields = new MapBuilder<>(commonFields); final MapBuilder checkedArrayFields = new MapBuilder<>(); checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.TRANSPORT_ORIGIN_FIELD_VALUE) @@ -557,7 +563,7 @@ public void testAccessGrantedInternalSystemActionNonSystemUser() throws Exceptio .put(LoggingAuditTrail.ACTION_FIELD_NAME, "internal:_action") .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) .put(LoggingAuditTrail.REQUEST_ID_FIELD_NAME, requestId); - checkedArrayFields.put(LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME, roles); + checkedArrayFields.put(PRINCIPAL_ROLES_FIELD_NAME, (String[]) authorizationInfo.asMap().get(PRINCIPAL_ROLES_FIELD_NAME)); subject(authentication, checkedFields); restOrTransportOrigin(message, threadContext, checkedFields); indicesRequest(message, checkedFields, checkedArrayFields); @@ -572,17 +578,18 @@ public void testAccessGrantedInternalSystemActionNonSystemUser() throws Exceptio .put("xpack.security.audit.logfile.events.exclude", "access_granted") .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.accessGranted(requestId, authentication, "internal:_action", message, roles); + auditTrail.accessGranted(requestId, authentication, "internal:_action", message, authorizationInfo); assertEmptyLog(logger); } public void testAccessDenied() throws Exception { final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); - final String[] roles = randomArray(0, 4, String[]::new, () -> randomBoolean() ? null : randomAlphaOfLengthBetween(1, 4)); + final String[] expectedRoles = randomArray(0, 4, String[]::new, () -> randomBoolean() ? null : randomAlphaOfLengthBetween(1, 4)); + final AuthorizationInfo authorizationInfo = () -> Collections.singletonMap(PRINCIPAL_ROLES_FIELD_NAME, expectedRoles); final Authentication authentication = createAuthentication(); final String requestId = randomRequestId(); - auditTrail.accessDenied(requestId, authentication, "_action/bar", message, roles); + auditTrail.accessDenied(requestId, authentication, "_action/bar", message, authorizationInfo); final MapBuilder checkedFields = new MapBuilder<>(commonFields); final MapBuilder checkedArrayFields = new MapBuilder<>(); checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.TRANSPORT_ORIGIN_FIELD_VALUE) @@ -590,7 +597,7 @@ public void testAccessDenied() throws Exception { .put(LoggingAuditTrail.ACTION_FIELD_NAME, "_action/bar") .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) .put(LoggingAuditTrail.REQUEST_ID_FIELD_NAME, requestId); - checkedArrayFields.put(LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME, roles); + checkedArrayFields.put(PRINCIPAL_ROLES_FIELD_NAME, (String[]) authorizationInfo.asMap().get(PRINCIPAL_ROLES_FIELD_NAME)); subject(authentication, checkedFields); restOrTransportOrigin(message, threadContext, checkedFields); indicesRequest(message, checkedFields, checkedArrayFields); @@ -606,7 +613,7 @@ public void testAccessDenied() throws Exception { .put("xpack.security.audit.logfile.events.exclude", "access_denied") .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.accessDenied(requestId, authentication, "_action", message, roles); + auditTrail.accessDenied(requestId, authentication, "_action", message, authorizationInfo); assertEmptyLog(logger); } @@ -779,14 +786,15 @@ public void testConnectionGranted() throws Exception { public void testRunAsGranted() throws Exception { final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); - final String[] roles = randomArray(0, 4, String[]::new, () -> randomBoolean() ? null : randomAlphaOfLengthBetween(1, 4)); + final String[] expectedRoles = randomArray(0, 4, String[]::new, () -> randomBoolean() ? null : randomAlphaOfLengthBetween(1, 4)); + final AuthorizationInfo authorizationInfo = () -> Collections.singletonMap(PRINCIPAL_ROLES_FIELD_NAME, expectedRoles); final Authentication authentication = new Authentication( new User("running as", new String[] { "r2" }, new User("_username", new String[] { "r1" })), new RealmRef("authRealm", "test", "foo"), new RealmRef("lookRealm", "up", "by")); final String requestId = randomRequestId(); - auditTrail.runAsGranted(requestId, authentication, "_action", message, roles); + auditTrail.runAsGranted(requestId, authentication, "_action", message, authorizationInfo); final MapBuilder checkedFields = new MapBuilder<>(commonFields); final MapBuilder checkedArrayFields = new MapBuilder<>(); checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.TRANSPORT_ORIGIN_FIELD_VALUE) @@ -798,7 +806,7 @@ public void testRunAsGranted() throws Exception { .put(LoggingAuditTrail.ACTION_FIELD_NAME, "_action") .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) .put(LoggingAuditTrail.REQUEST_ID_FIELD_NAME, requestId); - checkedArrayFields.put(LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME, roles); + checkedArrayFields.put(PRINCIPAL_ROLES_FIELD_NAME, (String[]) authorizationInfo.asMap().get(PRINCIPAL_ROLES_FIELD_NAME)); restOrTransportOrigin(message, threadContext, checkedFields); indicesRequest(message, checkedFields, checkedArrayFields); opaqueId(threadContext, checkedFields); @@ -812,20 +820,21 @@ public void testRunAsGranted() throws Exception { .put("xpack.security.audit.logfile.events.exclude", "run_as_granted") .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.runAsGranted(requestId, authentication, "_action", message, roles); + auditTrail.runAsGranted(requestId, authentication, "_action", message, authorizationInfo); assertEmptyLog(logger); } public void testRunAsDenied() throws Exception { final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); - final String[] roles = randomArray(0, 4, String[]::new, () -> randomAlphaOfLengthBetween(1, 4)); + final String[] expectedRoles = randomArray(0, 4, String[]::new, () -> randomBoolean() ? null : randomAlphaOfLengthBetween(1, 4)); + final AuthorizationInfo authorizationInfo = () -> Collections.singletonMap(PRINCIPAL_ROLES_FIELD_NAME, expectedRoles); final Authentication authentication = new Authentication( new User("running as", new String[] { "r2" }, new User("_username", new String[] { "r1" })), new RealmRef("authRealm", "test", "foo"), new RealmRef("lookRealm", "up", "by")); final String requestId = randomRequestId(); - auditTrail.runAsDenied(requestId, authentication, "_action", message, roles); + auditTrail.runAsDenied(requestId, authentication, "_action", message, authorizationInfo); final MapBuilder checkedFields = new MapBuilder<>(commonFields); final MapBuilder checkedArrayFields = new MapBuilder<>(); checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.TRANSPORT_ORIGIN_FIELD_VALUE) @@ -837,7 +846,7 @@ public void testRunAsDenied() throws Exception { .put(LoggingAuditTrail.ACTION_FIELD_NAME, "_action") .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) .put(LoggingAuditTrail.REQUEST_ID_FIELD_NAME, requestId); - checkedArrayFields.put(LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME, roles); + checkedArrayFields.put(PRINCIPAL_ROLES_FIELD_NAME, (String[]) authorizationInfo.asMap().get(PRINCIPAL_ROLES_FIELD_NAME)); restOrTransportOrigin(message, threadContext, checkedFields); indicesRequest(message, checkedFields, checkedArrayFields); opaqueId(threadContext, checkedFields); @@ -851,7 +860,7 @@ public void testRunAsDenied() throws Exception { .put("xpack.security.audit.logfile.events.exclude", "run_as_denied") .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.runAsDenied(requestId, authentication, "_action", message, roles); + auditTrail.runAsDenied(requestId, authentication, "_action", message, authorizationInfo); assertEmptyLog(logger); } @@ -956,7 +965,8 @@ public void testRequestsWithoutIndices() throws Exception { .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); final User user = new User("_username", new String[] { "r1" }); - final String role = randomAlphaOfLengthBetween(1, 6); + final AuthorizationInfo authorizationInfo = + () -> Collections.singletonMap(PRINCIPAL_ROLES_FIELD_NAME, new String[] { randomAlphaOfLengthBetween(1, 6) }); final String realm = randomAlphaOfLengthBetween(1, 6); // transport messages without indices final TransportMessage[] messages = new TransportMessage[] { new MockMessage(threadContext), @@ -977,10 +987,10 @@ public void testRequestsWithoutIndices() throws Exception { auditTrail.authenticationFailed("_req_id", realm, new MockToken(), "_action", message); assertThat(output.size(), is(logEntriesCount++)); assertThat(output.get(logEntriesCount - 2), not(containsString("indices="))); - auditTrail.accessGranted("_req_id", createAuthentication(), "_action", message, new String[]{role}); + auditTrail.accessGranted("_req_id", createAuthentication(), "_action", message, authorizationInfo); assertThat(output.size(), is(logEntriesCount++)); assertThat(output.get(logEntriesCount - 2), not(containsString("indices="))); - auditTrail.accessDenied("_req_id", createAuthentication(), "_action", message, new String[]{role}); + auditTrail.accessDenied("_req_id", createAuthentication(), "_action", message, authorizationInfo); assertThat(output.size(), is(logEntriesCount++)); assertThat(output.get(logEntriesCount - 2), not(containsString("indices="))); auditTrail.tamperedRequest("_req_id", "_action", message); @@ -989,10 +999,10 @@ public void testRequestsWithoutIndices() throws Exception { auditTrail.tamperedRequest("_req_id", user, "_action", message); assertThat(output.size(), is(logEntriesCount++)); assertThat(output.get(logEntriesCount - 2), not(containsString("indices="))); - auditTrail.runAsGranted("_req_id", createAuthentication(), "_action", message, new String[]{role}); + auditTrail.runAsGranted("_req_id", createAuthentication(), "_action", message, authorizationInfo); assertThat(output.size(), is(logEntriesCount++)); assertThat(output.get(logEntriesCount - 2), not(containsString("indices="))); - auditTrail.runAsDenied("_req_id", createAuthentication(), "_action", message, new String[]{role}); + auditTrail.runAsDenied("_req_id", createAuthentication(), "_action", message, authorizationInfo); assertThat(output.size(), is(logEntriesCount++)); assertThat(output.get(logEntriesCount - 2), not(containsString("indices="))); auditTrail.authenticationSuccess("_req_id", realm, user, "_action", message); @@ -1041,7 +1051,7 @@ private void assertMsg(Logger logger, Map checkFields, Map x + "," + y) .orElse("") + "]"; final Pattern logEntryFieldPattern = Pattern.compile(Pattern.quote("\"" + checkArrayField.getKey() + "\":" + quotedValue)); - assertThat("Field " + checkArrayField.getKey() + " value mismatch. Expected " + quotedValue, + assertThat("Field " + checkArrayField.getKey() + " value mismatch. Expected " + quotedValue + ".\nLog line: " + logLine, logEntryFieldPattern.matcher(logLine).find(), is(true)); // remove checked field logLine = logEntryFieldPattern.matcher(logLine).replaceFirst(""); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java index e7354b9b32564..b45fe7255393e 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java @@ -56,7 +56,6 @@ import org.elasticsearch.xpack.core.security.authc.Realm; import org.elasticsearch.xpack.core.security.authc.Realm.Factory; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; -import org.elasticsearch.xpack.core.security.authz.permission.Role; import org.elasticsearch.xpack.core.security.user.AnonymousUser; import org.elasticsearch.xpack.core.security.user.SystemUser; import org.elasticsearch.xpack.core.security.user.User; @@ -64,6 +63,7 @@ import org.elasticsearch.xpack.security.audit.AuditUtil; import org.elasticsearch.xpack.security.authc.AuthenticationService.Authenticator; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; +import org.elasticsearch.xpack.security.authz.AuthorizationEngine.EmptyAuthorizationInfo; import org.elasticsearch.xpack.security.support.SecurityIndexManager; import org.junit.After; import org.junit.Before; @@ -850,7 +850,7 @@ public void testRunAsWithEmptyRunAsUsernameRest() throws Exception { fail("exception should be thrown"); } catch (ElasticsearchException e) { String reqId = expectAuditRequestId(); - verify(auditTrail).runAsDenied(eq(reqId), any(Authentication.class), eq(restRequest), eq(Role.EMPTY.names())); + verify(auditTrail).runAsDenied(eq(reqId), any(Authentication.class), eq(restRequest), eq(EmptyAuthorizationInfo.INSTANCE)); verifyNoMoreInteractions(auditTrail); } } @@ -868,7 +868,8 @@ public void testRunAsWithEmptyRunAsUsername() throws Exception { authenticateBlocking("_action", message, null); fail("exception should be thrown"); } catch (ElasticsearchException e) { - verify(auditTrail).runAsDenied(eq(reqId), any(Authentication.class), eq("_action"), eq(message), eq(Role.EMPTY.names())); + verify(auditTrail).runAsDenied(eq(reqId), any(Authentication.class), eq("_action"), eq(message), + eq(EmptyAuthorizationInfo.INSTANCE)); verifyNoMoreInteractions(auditTrail); } } 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 2a9d832f0f012..02da62c6ebec9 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 @@ -10,11 +10,10 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.CompositeIndicesRequest; import org.elasticsearch.action.DocWriteRequest; +import org.elasticsearch.action.LatchedActionListener; import org.elasticsearch.action.MockIndicesRequest; import org.elasticsearch.action.admin.cluster.health.ClusterHealthAction; import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest; -import org.elasticsearch.action.admin.cluster.state.ClusterStateAction; -import org.elasticsearch.action.admin.cluster.stats.ClusterStatsAction; import org.elasticsearch.action.admin.indices.alias.Alias; import org.elasticsearch.action.admin.indices.alias.IndicesAliasesAction; import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest; @@ -71,7 +70,6 @@ import org.elasticsearch.action.termvectors.TermVectorsRequest; import org.elasticsearch.action.update.UpdateAction; import org.elasticsearch.action.update.UpdateRequest; -import org.elasticsearch.client.Client; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.MetaData; @@ -85,7 +83,6 @@ import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.index.IndexNotFoundException; import org.elasticsearch.index.shard.ShardId; -import org.elasticsearch.license.GetLicenseAction; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportActionProxy; @@ -94,20 +91,9 @@ import org.elasticsearch.xpack.core.security.action.privilege.DeletePrivilegesRequest; import org.elasticsearch.xpack.core.security.action.user.AuthenticateAction; import org.elasticsearch.xpack.core.security.action.user.AuthenticateRequest; -import org.elasticsearch.xpack.core.security.action.user.AuthenticateRequestBuilder; -import org.elasticsearch.xpack.core.security.action.user.ChangePasswordAction; -import org.elasticsearch.xpack.core.security.action.user.ChangePasswordRequest; -import org.elasticsearch.xpack.core.security.action.user.ChangePasswordRequestBuilder; -import org.elasticsearch.xpack.core.security.action.user.DeleteUserAction; -import org.elasticsearch.xpack.core.security.action.user.PutUserAction; -import org.elasticsearch.xpack.core.security.action.user.UserRequest; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; import org.elasticsearch.xpack.core.security.authc.DefaultAuthenticationFailureHandler; -import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings; -import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings; -import org.elasticsearch.xpack.core.security.authc.ldap.LdapRealmSettings; -import org.elasticsearch.xpack.core.security.authc.pki.PkiRealmSettings; import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField; import org.elasticsearch.xpack.core.security.authz.IndicesAndAliasesResolverField; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; @@ -123,15 +109,16 @@ import org.elasticsearch.xpack.core.security.user.ElasticUser; import org.elasticsearch.xpack.core.security.user.SystemUser; import org.elasticsearch.xpack.core.security.user.User; -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.security.authc.esnative.ReservedRealm; +import org.elasticsearch.xpack.security.authz.AuthorizationEngine.AuthorizationInfo; import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; import org.elasticsearch.xpack.security.authz.store.NativePrivilegeStore; import org.elasticsearch.xpack.sql.action.SqlQueryAction; import org.elasticsearch.xpack.sql.action.SqlQueryRequest; import org.junit.Before; +import org.mockito.ArgumentMatcher; +import org.mockito.Matchers; import org.mockito.Mockito; import java.util.ArrayList; @@ -144,29 +131,30 @@ import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.concurrent.CountDownLatch; import java.util.function.Predicate; import static java.util.Arrays.asList; import static org.elasticsearch.test.SecurityTestsUtils.assertAuthenticationException; import static org.elasticsearch.test.SecurityTestsUtils.assertThrowsAuthorizationException; import static org.elasticsearch.test.SecurityTestsUtils.assertThrowsAuthorizationExceptionRunAs; +import static org.elasticsearch.xpack.security.audit.logfile.LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME; import static org.elasticsearch.xpack.security.support.SecurityIndexManager.SECURITY_INDEX_NAME; import static org.hamcrest.Matchers.arrayContaining; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.endsWith; import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.startsWith; 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.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; public class AuthorizationServiceTests extends ESTestCase { @@ -188,6 +176,7 @@ public void setup() { .build(); final ClusterSettings clusterSettings = new ClusterSettings(settings, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS); when(clusterService.getClusterSettings()).thenReturn(clusterSettings); + when(clusterService.state()).thenReturn(ClusterState.EMPTY_STATE); auditTrail = mock(AuditTrailService.class); threadContext = new ThreadContext(settings); threadPool = mock(ThreadPool.class); @@ -207,7 +196,8 @@ public void setup() { doAnswer((i) -> { ActionListener callback = (ActionListener) i.getArguments()[2]; - Set names = (Set) i.getArguments()[0]; + User user = (User) i.getArguments()[0]; + Set names = new HashSet<>(Arrays.asList(user.roles())); assertNotNull(names); Set roleDescriptors = new HashSet<>(); for (String name : names) { @@ -225,19 +215,15 @@ public void setup() { ); } return Void.TYPE; - }).when(rolesStore).roles(any(Set.class), any(FieldPermissionsCache.class), any(ActionListener.class)); + }).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)); } private void authorize(Authentication authentication, String action, TransportRequest request) { PlainActionFuture future = new PlainActionFuture<>(); - AuthorizationUtils.AsyncAuthorizer authorizer = new AuthorizationUtils.AsyncAuthorizer(authentication, future, - (userRoles, runAsRoles) -> { - authorizationService.authorize(authentication, action, request, userRoles, runAsRoles); - future.onResponse(null); - }); - authorizer.authorize(authorizationService); + authorizationService.authorize(authentication, action, request, future); future.actionGet(); } @@ -252,7 +238,8 @@ public void testActionsForSystemUserIsAuthorized() { "indices:admin/settings/update" }; for (String action : actions) { authorize(authentication, action, request); - verify(auditTrail).accessGranted(requestId, authentication, action, request, new String[] { SystemUser.ROLE_NAME }); + verify(auditTrail).accessGranted(eq(requestId), eq(authentication), eq(action), eq(request), + authzInfoRoles(new String[] { SystemUser.ROLE_NAME })); } verifyNoMoreInteractions(auditTrail); @@ -265,7 +252,8 @@ public void testIndicesActionsForSystemUserWhichAreNotAuthorized() { assertThrowsAuthorizationException( () -> authorize(authentication, "indices:", request), "indices:", SystemUser.INSTANCE.principal()); - verify(auditTrail).accessDenied(requestId, authentication, "indices:", request, new String[]{SystemUser.ROLE_NAME}); + verify(auditTrail).accessDenied(eq(requestId), eq(authentication), eq("indices:"), eq(request), + authzInfoRoles(new String[]{SystemUser.ROLE_NAME})); verifyNoMoreInteractions(auditTrail); } @@ -276,8 +264,8 @@ public void testClusterAdminActionsForSystemUserWhichAreNotAuthorized() { assertThrowsAuthorizationException( () -> authorize(authentication, "cluster:admin/whatever", request), "cluster:admin/whatever", SystemUser.INSTANCE.principal()); - verify(auditTrail).accessDenied(requestId, authentication, "cluster:admin/whatever", request, - new String[] { SystemUser.ROLE_NAME }); + verify(auditTrail).accessDenied(eq(requestId), eq(authentication), eq("cluster:admin/whatever"), eq(request), + authzInfoRoles(new String[] { SystemUser.ROLE_NAME })); verifyNoMoreInteractions(auditTrail); } @@ -288,8 +276,8 @@ public void testClusterAdminSnapshotStatusActionForSystemUserWhichIsNotAuthorize assertThrowsAuthorizationException( () -> authorize(authentication, "cluster:admin/snapshot/status", request), "cluster:admin/snapshot/status", SystemUser.INSTANCE.principal()); - verify(auditTrail).accessDenied(requestId, authentication, "cluster:admin/snapshot/status", request, - new String[] { SystemUser.ROLE_NAME }); + verify(auditTrail).accessDenied(eq(requestId), eq(authentication), eq("cluster:admin/snapshot/status"), eq(request), + authzInfoRoles(new String[] { SystemUser.ROLE_NAME })); verifyNoMoreInteractions(auditTrail); } @@ -309,7 +297,8 @@ public void testAuthorizeUsingConditionalPrivileges() { roleMap.put("role1", role); authorize(authentication, DeletePrivilegesAction.NAME, request); - verify(auditTrail).accessGranted(requestId, authentication, DeletePrivilegesAction.NAME, request, new String[]{role.getName()}); + verify(auditTrail).accessGranted(eq(requestId), eq(authentication), eq(DeletePrivilegesAction.NAME), eq(request), + authzInfoRoles(new String[]{role.getName()})); verifyNoMoreInteractions(auditTrail); } @@ -331,7 +320,8 @@ public void testAuthorizationDeniedWhenConditionalPrivilegesDoNotMatch() { assertThrowsAuthorizationException( () -> authorize(authentication, DeletePrivilegesAction.NAME, request), DeletePrivilegesAction.NAME, "user1"); - verify(auditTrail).accessDenied(requestId, authentication, DeletePrivilegesAction.NAME, request, new String[]{role.getName()}); + verify(auditTrail).accessDenied(eq(requestId), eq(authentication), eq(DeletePrivilegesAction.NAME), eq(request), + authzInfoRoles(new String[]{role.getName()})); verifyNoMoreInteractions(auditTrail); } @@ -343,7 +333,8 @@ public void testNoRolesCausesDenial() { assertThrowsAuthorizationException( () -> authorize(authentication, "indices:a", request), "indices:a", "test user"); - verify(auditTrail).accessDenied(requestId, authentication, "indices:a", request, Role.EMPTY.names()); + verify(auditTrail).accessDenied(eq(requestId), eq(authentication), eq("indices:a"), eq(request), + authzInfoRoles(Role.EMPTY.names())); verifyNoMoreInteractions(auditTrail); } @@ -354,7 +345,8 @@ public void testUserWithNoRolesCanPerformRemoteSearch() { mockEmptyMetaData(); final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); authorize(authentication, SearchAction.NAME, request); - verify(auditTrail).accessGranted(requestId, authentication, SearchAction.NAME, request, Role.EMPTY.names()); + verify(auditTrail).accessGranted(eq(requestId), eq(authentication), eq(SearchAction.NAME), eq(request), + authzInfoRoles(Role.EMPTY.names())); verifyNoMoreInteractions(auditTrail); } @@ -372,7 +364,8 @@ public void testUserWithNoRolesCannotPerformLocalSearch() { assertThrowsAuthorizationException( () -> authorize(authentication, SearchAction.NAME, request), SearchAction.NAME, "test user"); - verify(auditTrail).accessDenied(requestId, authentication, SearchAction.NAME, request, Role.EMPTY.names()); + verify(auditTrail).accessDenied(eq(requestId), eq(authentication), eq(SearchAction.NAME), eq(request), + authzInfoRoles(Role.EMPTY.names())); verifyNoMoreInteractions(auditTrail); } @@ -389,7 +382,8 @@ public void testUserWithNoRolesCanPerformMultiClusterSearch() { assertThrowsAuthorizationException( () -> authorize(authentication, SearchAction.NAME, request), SearchAction.NAME, "test user"); - verify(auditTrail).accessDenied(requestId, authentication, SearchAction.NAME, request, Role.EMPTY.names()); + verify(auditTrail).accessDenied(eq(requestId), eq(authentication), eq(SearchAction.NAME), eq(request), + authzInfoRoles(Role.EMPTY.names())); verifyNoMoreInteractions(auditTrail); } @@ -401,7 +395,8 @@ public void testUserWithNoRolesCannotSql() { assertThrowsAuthorizationException( () -> authorize(authentication, SqlQueryAction.NAME, request), SqlQueryAction.NAME, "test user"); - verify(auditTrail).accessDenied(requestId, authentication, SqlQueryAction.NAME, request, Role.EMPTY.names()); + verify(auditTrail).accessDenied(eq(requestId), eq(authentication), eq(SqlQueryAction.NAME), eq(request), + authzInfoRoles(Role.EMPTY.names())); verifyNoMoreInteractions(auditTrail); } /** @@ -417,7 +412,8 @@ public void testRemoteIndicesOnlyWorkWithApplicableRequestTypes() { assertThrowsAuthorizationException( () -> authorize(authentication, DeleteIndexAction.NAME, request), DeleteIndexAction.NAME, "test user"); - verify(auditTrail).accessDenied(requestId, authentication, DeleteIndexAction.NAME, request, Role.EMPTY.names()); + verify(auditTrail).accessDenied(eq(requestId), eq(authentication), eq(DeleteIndexAction.NAME), eq(request), + authzInfoRoles(Role.EMPTY.names())); verifyNoMoreInteractions(auditTrail); } @@ -434,7 +430,7 @@ public void testUnknownRoleCausesDenial() { assertThrowsAuthorizationException( () -> authorize(authentication, action, request), action, "test user"); - verify(auditTrail).accessDenied(requestId, authentication, action, request, Role.EMPTY.names()); + verify(auditTrail).accessDenied(eq(requestId), eq(authentication), eq(action), eq(request), authzInfoRoles(Role.EMPTY.names())); verifyNoMoreInteractions(auditTrail); } @@ -442,14 +438,15 @@ public void testThatNonIndicesAndNonClusterActionIsDenied() { final TransportRequest request = mock(TransportRequest.class); final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); final Authentication authentication = createAuthentication(new User("test user", "a_all")); - final RoleDescriptor role = new RoleDescriptor("a_role", null, + final RoleDescriptor role = new RoleDescriptor("a_all", null, new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null); roleMap.put("a_all", role); assertThrowsAuthorizationException( () -> authorize(authentication, "whatever", request), "whatever", "test user"); - verify(auditTrail).accessDenied(requestId, authentication, "whatever", request, new String[]{role.getName()}); + verify(auditTrail).accessDenied(eq(requestId), eq(authentication), eq("whatever"), eq(request), + authzInfoRoles(new String[]{role.getName()})); verifyNoMoreInteractions(auditTrail); } @@ -463,14 +460,15 @@ public void testThatRoleWithNoIndicesIsDenied() { TransportRequest request = tuple.v2(); final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); final Authentication authentication = createAuthentication(new User("test user", "no_indices")); - RoleDescriptor role = new RoleDescriptor("a_role", null, null, null); + RoleDescriptor role = new RoleDescriptor("no_indices", null, null, null); roleMap.put("no_indices", role); mockEmptyMetaData(); assertThrowsAuthorizationException( () -> authorize(authentication, action, request), action, "test user"); - verify(auditTrail).accessDenied(requestId, authentication, action, request, new String[]{role.getName()}); + verify(auditTrail).accessDenied(eq(requestId), eq(authentication), eq(action), eq(request), + authzInfoRoles(new String[]{role.getName()})); verifyNoMoreInteractions(auditTrail); } @@ -480,11 +478,12 @@ public void testElasticUserAuthorizedForNonChangePasswordRequestsWhenNotInSetupM final Tuple request = randomCompositeRequest(); authorize(authentication, request.v1(), request.v2()); - verify(auditTrail).accessGranted(requestId, authentication, request.v1(), request.v2(), new String[]{ElasticUser.ROLE_NAME}); + verify(auditTrail).accessGranted(eq(requestId), eq(authentication), eq(request.v1()), eq(request.v2()), + authzInfoRoles(new String[]{ElasticUser.ROLE_NAME})); } - public void testSearchAgainstEmptyCluster() { - RoleDescriptor role = new RoleDescriptor("a_role", null, + public void testSearchAgainstEmptyCluster() throws Exception { + RoleDescriptor role = new RoleDescriptor("a_all", null, new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null); final Authentication authentication = createAuthentication(new User("test user", "a_all")); final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); @@ -500,7 +499,8 @@ public void testSearchAgainstEmptyCluster() { assertThrowsAuthorizationException( () -> authorize(authentication, SearchAction.NAME, searchRequest), SearchAction.NAME, "test user"); - verify(auditTrail).accessDenied(requestId, authentication, SearchAction.NAME, searchRequest, new String[]{role.getName()}); + verify(auditTrail).accessDenied(eq(requestId), eq(authentication), eq(SearchAction.NAME), eq(searchRequest), + authzInfoRoles(new String[]{role.getName()})); verifyNoMoreInteractions(auditTrail); } @@ -508,18 +508,27 @@ public void testSearchAgainstEmptyCluster() { //ignore_unavailable and allow_no_indices both set to true, user is not authorized for this index nor does it exist SearchRequest searchRequest = new SearchRequest("does_not_exist") .indicesOptions(IndicesOptions.fromOptions(true, true, true, false)); - authorize(authentication, SearchAction.NAME, searchRequest); - verify(auditTrail).accessGranted(requestId, authentication, SearchAction.NAME, searchRequest, new String[]{role.getName()}); - final IndicesAccessControl indicesAccessControl = threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY); - final IndicesAccessControl.IndexAccessControl indexAccessControl = - indicesAccessControl.getIndexPermissions(IndicesAndAliasesResolverField.NO_INDEX_PLACEHOLDER); - assertFalse(indexAccessControl.getFieldPermissions().hasFieldLevelSecurity()); - assertNull(indexAccessControl.getQueries()); + final ActionListener listener = ActionListener.wrap(ignore -> { + final IndicesAccessControl indicesAccessControl = + threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY); + assertNotNull(indicesAccessControl); + final IndicesAccessControl.IndexAccessControl indexAccessControl = + indicesAccessControl.getIndexPermissions(IndicesAndAliasesResolverField.NO_INDEX_PLACEHOLDER); + assertFalse(indexAccessControl.getFieldPermissions().hasFieldLevelSecurity()); + assertNull(indexAccessControl.getQueries()); + }, e -> { + fail(e.getMessage()); + }); + final CountDownLatch latch = new CountDownLatch(1); + authorizationService.authorize(authentication, SearchAction.NAME, searchRequest, new LatchedActionListener<>(listener, latch)); + latch.await(); + verify(auditTrail).accessGranted(eq(requestId), eq(authentication), eq(SearchAction.NAME), eq(searchRequest), + authzInfoRoles(new String[]{role.getName()})); } } public void testScrollRelatedRequestsAllowed() { - RoleDescriptor role = new RoleDescriptor("a_role", null, + RoleDescriptor role = new RoleDescriptor("a_all", null, new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null); final Authentication authentication = createAuthentication(new User("test user", "a_all")); roleMap.put("a_all", role); @@ -528,42 +537,42 @@ public void testScrollRelatedRequestsAllowed() { final ClearScrollRequest clearScrollRequest = new ClearScrollRequest(); authorize(authentication, ClearScrollAction.NAME, clearScrollRequest); - verify(auditTrail).accessGranted(requestId, authentication, ClearScrollAction.NAME, clearScrollRequest, - new String[]{role.getName()}); + verify(auditTrail).accessGranted(eq(requestId), eq(authentication), eq(ClearScrollAction.NAME), eq(clearScrollRequest), + authzInfoRoles(new String[]{role.getName()})); final SearchScrollRequest searchScrollRequest = new SearchScrollRequest(); authorize(authentication, SearchScrollAction.NAME, searchScrollRequest); - verify(auditTrail).accessGranted(requestId, authentication, SearchScrollAction.NAME, searchScrollRequest, - new String[]{role.getName()}); + verify(auditTrail).accessGranted(eq(requestId), eq(authentication), eq(SearchScrollAction.NAME), eq(searchScrollRequest), + authzInfoRoles(new String[]{role.getName()})); // We have to use a mock request for other Scroll actions as the actual requests are package private to SearchTransportService final TransportRequest request = mock(TransportRequest.class); authorize(authentication, SearchTransportService.CLEAR_SCROLL_CONTEXTS_ACTION_NAME, request); - verify(auditTrail).accessGranted(requestId, authentication, SearchTransportService.CLEAR_SCROLL_CONTEXTS_ACTION_NAME, request, - new String[]{role.getName()}); + verify(auditTrail).accessGranted(eq(requestId), eq(authentication), eq(SearchTransportService.CLEAR_SCROLL_CONTEXTS_ACTION_NAME), + eq(request), authzInfoRoles(new String[]{role.getName()})); authorize(authentication, SearchTransportService.FETCH_ID_SCROLL_ACTION_NAME, request); - verify(auditTrail).accessGranted(requestId, authentication, SearchTransportService.FETCH_ID_SCROLL_ACTION_NAME, request, - new String[]{role.getName()}); + verify(auditTrail).accessGranted(eq(requestId), eq(authentication), eq(SearchTransportService.FETCH_ID_SCROLL_ACTION_NAME), + eq(request), authzInfoRoles(new String[]{role.getName()})); authorize(authentication, SearchTransportService.QUERY_FETCH_SCROLL_ACTION_NAME, request); - verify(auditTrail).accessGranted(requestId, authentication, SearchTransportService.QUERY_FETCH_SCROLL_ACTION_NAME, request, - new String[]{role.getName()}); + verify(auditTrail).accessGranted(eq(requestId), eq(authentication), eq(SearchTransportService.QUERY_FETCH_SCROLL_ACTION_NAME), + eq(request), authzInfoRoles(new String[]{role.getName()})); authorize(authentication, SearchTransportService.QUERY_SCROLL_ACTION_NAME, request); - verify(auditTrail).accessGranted(requestId, authentication, SearchTransportService.QUERY_SCROLL_ACTION_NAME, request, - new String[]{role.getName()}); + verify(auditTrail).accessGranted(eq(requestId), eq(authentication), eq(SearchTransportService.QUERY_SCROLL_ACTION_NAME), + eq(request), authzInfoRoles(new String[]{role.getName()})); authorize(authentication, SearchTransportService.FREE_CONTEXT_SCROLL_ACTION_NAME, request); - verify(auditTrail).accessGranted(requestId, authentication, SearchTransportService.FREE_CONTEXT_SCROLL_ACTION_NAME, request, - new String[]{role.getName()}); + verify(auditTrail).accessGranted(eq(requestId), eq(authentication), eq(SearchTransportService.FREE_CONTEXT_SCROLL_ACTION_NAME), + eq(request), authzInfoRoles(new String[]{role.getName()})); verifyNoMoreInteractions(auditTrail); } public void testAuthorizeIndicesFailures() { TransportRequest request = new GetIndexRequest().indices("b"); ClusterState state = mockEmptyMetaData(); - RoleDescriptor role = new RoleDescriptor("a_role", null, + RoleDescriptor role = new RoleDescriptor("a_all", null, new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null); final Authentication authentication = createAuthentication(new User("test user", "a_all")); roleMap.put("a_all", role); @@ -572,7 +581,8 @@ public void testAuthorizeIndicesFailures() { assertThrowsAuthorizationException( () -> authorize(authentication, "indices:a", request), "indices:a", "test user"); - verify(auditTrail).accessDenied(requestId, authentication, "indices:a", request, new String[]{role.getName()}); + verify(auditTrail).accessDenied(eq(requestId), eq(authentication), eq("indices:a"), eq(request), + authzInfoRoles(new String[]{role.getName()})); verifyNoMoreInteractions(auditTrail); verify(clusterService, times(1)).state(); verify(state, times(1)).metaData(); @@ -582,7 +592,7 @@ public void testCreateIndexWithAliasWithoutPermissions() { CreateIndexRequest request = new CreateIndexRequest("a"); request.alias(new Alias("a2")); ClusterState state = mockEmptyMetaData(); - RoleDescriptor role = new RoleDescriptor("a_role", null, + RoleDescriptor role = new RoleDescriptor("a_all", null, new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null); final Authentication authentication = createAuthentication(new User("test user", "a_all")); roleMap.put("a_all", role); @@ -591,7 +601,8 @@ public void testCreateIndexWithAliasWithoutPermissions() { assertThrowsAuthorizationException( () -> authorize(authentication, CreateIndexAction.NAME, request), IndicesAliasesAction.NAME, "test user"); - verify(auditTrail).accessDenied(requestId, authentication, IndicesAliasesAction.NAME, request, new String[]{role.getName()}); + verify(auditTrail).accessDenied(eq(requestId), eq(authentication), eq(IndicesAliasesAction.NAME), eq(request), + authzInfoRoles(new String[]{role.getName()})); verifyNoMoreInteractions(auditTrail); verify(clusterService).state(); verify(state, times(1)).metaData(); @@ -609,7 +620,10 @@ public void testCreateIndexWithAlias() { authorize(authentication, CreateIndexAction.NAME, request); - verify(auditTrail).accessGranted(requestId, authentication, CreateIndexAction.NAME, request, new String[]{role.getName()}); + verify(auditTrail).accessGranted(eq(requestId), eq(authentication), eq(CreateIndexAction.NAME), eq(request), + authzInfoRoles(new String[]{role.getName()})); + verify(auditTrail).accessGranted(eq(requestId), eq(authentication), eq("indices:admin/aliases"), eq(request), + authzInfoRoles(new String[]{role.getName()})); verifyNoMoreInteractions(auditTrail); verify(clusterService).state(); verify(state, times(1)).metaData(); @@ -632,7 +646,8 @@ public void testDenialForAnonymousUser() { assertThrowsAuthorizationException( () -> authorize(authentication, "indices:a", request), "indices:a", anonymousUser.principal()); - verify(auditTrail).accessDenied(requestId, authentication, "indices:a", request, new String[]{role.getName()}); + verify(auditTrail).accessDenied(eq(requestId), eq(authentication), eq("indices:a"), eq(request), + authzInfoRoles(new String[]{role.getName()})); verifyNoMoreInteractions(auditTrail); verify(clusterService, times(1)).state(); verify(state, times(1)).metaData(); @@ -657,7 +672,8 @@ public void testDenialForAnonymousUserAuthorizationExceptionDisabled() { final ElasticsearchSecurityException securityException = expectThrows(ElasticsearchSecurityException.class, () -> authorize(authentication, "indices:a", request)); assertAuthenticationException(securityException, containsString("action [indices:a] requires authentication")); - verify(auditTrail).accessDenied(requestId, authentication, "indices:a", request, new String[]{role.getName()}); + verify(auditTrail).accessDenied(eq(requestId), eq(authentication), eq("indices:a"), eq(request), + authzInfoRoles(new String[]{role.getName()})); verifyNoMoreInteractions(auditTrail); verify(clusterService, times(1)).state(); verify(state, times(1)).metaData(); @@ -678,7 +694,8 @@ public void testAuditTrailIsRecordedWhenIndexWildcardThrowsError() { () -> authorize(authentication, GetIndexAction.NAME, request)); assertThat(nfe.getIndex(), is(notNullValue())); assertThat(nfe.getIndex().getName(), is("not-an-index-*")); - verify(auditTrail).accessDenied(requestId, authentication, GetIndexAction.NAME, request, new String[]{role.getName()}); + verify(auditTrail).accessDenied(eq(requestId), eq(authentication), eq(GetIndexAction.NAME), eq(request), + authzInfoRoles(new String[]{role.getName()})); verifyNoMoreInteractions(auditTrail); verify(clusterService).state(); verify(state, times(1)).metaData(); @@ -692,22 +709,23 @@ public void testRunAsRequestWithNoRolesUser() { assertThrowsAuthorizationExceptionRunAs( () -> authorize(authentication, "indices:a", request), "indices:a", "test user", "run as me"); // run as [run as me] - verify(auditTrail).runAsDenied(requestId, authentication, "indices:a", request, Role.EMPTY.names()); + verify(auditTrail).runAsDenied(eq(requestId), eq(authentication), eq("indices:a"), eq(request), + authzInfoRoles(Role.EMPTY.names())); verifyNoMoreInteractions(auditTrail); } public void testRunAsRequestWithoutLookedUpBy() { final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); AuthenticateRequest request = new AuthenticateRequest("run as me"); - roleMap.put("can run as", ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR); - User user = new User("run as me", Strings.EMPTY_ARRAY, new User("test user", new String[]{"can run as"})); + roleMap.put("superuser", ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR); + User user = new User("run as me", Strings.EMPTY_ARRAY, new User("test user", new String[]{"superuser"})); Authentication authentication = new Authentication(user, new RealmRef("foo", "bar", "baz"), null); assertNotEquals(user.authenticatedUser(), user); assertThrowsAuthorizationExceptionRunAs( () -> authorize(authentication, AuthenticateAction.NAME, request), AuthenticateAction.NAME, "test user", "run as me"); // run as [run as me] - verify(auditTrail).runAsDenied(requestId, authentication, AuthenticateAction.NAME, request, - new String[] { ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName() }); + verify(auditTrail).runAsDenied(eq(requestId), eq(authentication), eq(AuthenticateAction.NAME), eq(request), + authzInfoRoles(new String[] { ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName() })); verifyNoMoreInteractions(auditTrail); } @@ -725,7 +743,8 @@ public void testRunAsRequestRunningAsUnAllowedUser() { assertThrowsAuthorizationExceptionRunAs( () -> authorize(authentication, "indices:a", request), "indices:a", "test user", "run as me"); - verify(auditTrail).runAsDenied(requestId, authentication, "indices:a", request, new String[]{role.getName()}); + verify(auditTrail).runAsDenied(eq(requestId), eq(authentication), eq("indices:a"), eq(request), + authzInfoRoles(new String[]{role.getName()})); verifyNoMoreInteractions(auditTrail); } @@ -760,11 +779,14 @@ public void testRunAsRequestWithRunAsUserWithoutPermission() { assertThrowsAuthorizationExceptionRunAs( () -> authorize(authentication, "indices:a", request), "indices:a", "test user", "run as me"); - verify(auditTrail).runAsGranted(requestId, authentication, "indices:a", request, new String[]{runAsRole.getName()}); + verify(auditTrail).runAsGranted(eq(requestId), eq(authentication), eq("indices:a"), eq(request), + authzInfoRoles(new String[]{runAsRole.getName()})); if (indexExists) { - verify(auditTrail).accessDenied(requestId, authentication, "indices:a", request, new String[]{bRole.getName()}); + verify(auditTrail).accessDenied(eq(requestId), eq(authentication), eq("indices:a"), eq(request), + authzInfoRoles(new String[]{bRole.getName()})); } else { - verify(auditTrail).accessDenied(requestId, authentication, "indices:a", request, Role.EMPTY.names()); + verify(auditTrail).accessDenied(eq(requestId), eq(authentication), eq("indices:a"), eq(request), + authzInfoRoles(Role.EMPTY.names())); } verifyNoMoreInteractions(auditTrail); } @@ -792,13 +814,15 @@ public void testRunAsRequestWithValidPermissions() { final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); authorize(authentication, "indices:a", request); - verify(auditTrail).runAsGranted(requestId, authentication, "indices:a", request, new String[]{runAsRole.getName()}); - verify(auditTrail).accessGranted(requestId, authentication, "indices:a", request, new String[]{bRole.getName()}); + verify(auditTrail).runAsGranted(eq(requestId), eq(authentication), eq("indices:a"), eq(request), + authzInfoRoles(new String[]{runAsRole.getName()})); + verify(auditTrail).accessGranted(eq(requestId), eq(authentication), eq("indices:a"), eq(request), + authzInfoRoles(new String[]{bRole.getName()})); verifyNoMoreInteractions(auditTrail); } public void testNonXPackUserCannotExecuteOperationAgainstSecurityIndex() { - RoleDescriptor role = new RoleDescriptor("all access", new String[]{"all"}, + RoleDescriptor role = new RoleDescriptor("all_access", new String[]{"all"}, new IndicesPrivileges[]{IndicesPrivileges.builder().indices("*").privileges("all").build()}, null); final Authentication authentication = createAuthentication(new User("all_access_user", "all_access")); roleMap.put("all_access", role); @@ -835,19 +859,22 @@ public void testNonXPackUserCannotExecuteOperationAgainstSecurityIndex() { assertThrowsAuthorizationException( () -> authorize(authentication, action, request), action, "all_access_user"); - verify(auditTrail).accessDenied(requestId, authentication, action, request, new String[]{role.getName()}); + verify(auditTrail).accessDenied(eq(requestId), eq(authentication), eq(action), eq(request), + authzInfoRoles(new String[]{role.getName()})); verifyNoMoreInteractions(auditTrail); } // we should allow waiting for the health of the index or any index if the user has this permission ClusterHealthRequest request = new ClusterHealthRequest(SECURITY_INDEX_NAME); authorize(authentication, ClusterHealthAction.NAME, request); - verify(auditTrail).accessGranted(requestId, authentication, ClusterHealthAction.NAME, request, new String[]{role.getName()}); + verify(auditTrail).accessGranted(eq(requestId), eq(authentication), eq(ClusterHealthAction.NAME), eq(request), + authzInfoRoles(new String[]{role.getName()})); // multiple indices request = new ClusterHealthRequest(SECURITY_INDEX_NAME, "foo", "bar"); authorize(authentication, ClusterHealthAction.NAME, request); - verify(auditTrail).accessGranted(requestId, authentication, ClusterHealthAction.NAME, request, new String[]{role.getName()}); + verify(auditTrail).accessGranted(eq(requestId), eq(authentication), eq(ClusterHealthAction.NAME), eq(request), + authzInfoRoles(new String[]{role.getName()})); verifyNoMoreInteractions(auditTrail); final SearchRequest searchRequest = new SearchRequest("_all"); @@ -857,7 +884,7 @@ public void testNonXPackUserCannotExecuteOperationAgainstSecurityIndex() { } public void testGrantedNonXPackUserCanExecuteMonitoringOperationsAgainstSecurityIndex() { - RoleDescriptor role = new RoleDescriptor("all access", new String[]{"all"}, + RoleDescriptor role = new RoleDescriptor("all_access", new String[]{"all"}, new IndicesPrivileges[]{IndicesPrivileges.builder().indices("*").privileges("all").build()}, null); final Authentication authentication = createAuthentication(new User("all_access_user", "all_access")); roleMap.put("all_access", role); @@ -884,7 +911,8 @@ public void testGrantedNonXPackUserCanExecuteMonitoringOperationsAgainstSecurity final String action = requestTuple.v1(); final TransportRequest request = requestTuple.v2(); authorize(authentication, action, request); - verify(auditTrail).accessGranted(requestId, authentication, action, request, new String[]{role.getName()}); + verify(auditTrail).accessGranted(eq(requestId), eq(authentication), eq(action), eq(request), + authzInfoRoles(new String[]{role.getName()})); } } @@ -928,7 +956,7 @@ public void testSuperusersCanExecuteOperationAgainstSecurityIndex() { final TransportRequest request = requestTuple.v2(); final Authentication authentication = createAuthentication(superuser); authorize(authentication, action, request); - verify(auditTrail).accessGranted(requestId, authentication, action, request, superuser.roles()); + verify(auditTrail).accessGranted(eq(requestId), eq(authentication), eq(action), eq(request), authzInfoRoles(superuser.roles())); } } @@ -948,52 +976,10 @@ public void testSuperusersCanExecuteOperationAgainstSecurityIndexWithWildcard() String action = SearchAction.NAME; SearchRequest request = new SearchRequest("_all"); authorize(createAuthentication(superuser), action, request); - verify(auditTrail).accessGranted(requestId, authentication, action, request, superuser.roles()); + verify(auditTrail).accessGranted(eq(requestId), eq(authentication), eq(action), eq(request), authzInfoRoles(superuser.roles())); assertThat(request.indices(), arrayContaining(".security")); } - public void testAnonymousRolesAreAppliedToOtherUsers() { - TransportRequest request = new ClusterHealthRequest(); - Settings settings = Settings.builder().put(AnonymousUser.ROLES_SETTING.getKey(), "anonymous_user_role").build(); - final AnonymousUser anonymousUser = new AnonymousUser(settings); - authorizationService = new AuthorizationService(settings, rolesStore, clusterService, auditTrail, - new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, anonymousUser); - roleMap.put("anonymous_user_role", new RoleDescriptor("anonymous_user_role", new String[]{"all"}, - new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null)); - mockEmptyMetaData(); - AuditUtil.getOrGenerateRequestId(threadContext); - - // sanity check the anonymous user - authorize(createAuthentication(anonymousUser), ClusterHealthAction.NAME, request); - authorize(createAuthentication(anonymousUser), IndicesExistsAction.NAME, new IndicesExistsRequest("a")); - - // test the no role user - final User userWithNoRoles = new User("no role user"); - authorize(createAuthentication(userWithNoRoles), ClusterHealthAction.NAME, request); - authorize(createAuthentication(userWithNoRoles), IndicesExistsAction.NAME, new IndicesExistsRequest("a")); - } - - public void testDefaultRoleUserWithoutRoles() { - PlainActionFuture rolesFuture = new PlainActionFuture<>(); - authorizationService.roles(new User("no role user"), rolesFuture); - final Role roles = rolesFuture.actionGet(); - assertEquals(Role.EMPTY, roles); - } - - public void testAnonymousUserEnabledRoleAdded() { - Settings settings = Settings.builder().put(AnonymousUser.ROLES_SETTING.getKey(), "anonymous_user_role").build(); - final AnonymousUser anonymousUser = new AnonymousUser(settings); - authorizationService = new AuthorizationService(settings, rolesStore, clusterService, auditTrail, - new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, anonymousUser); - roleMap.put("anonymous_user_role", new RoleDescriptor("anonymous_user_role", new String[]{"all"}, - new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null)); - mockEmptyMetaData(); - PlainActionFuture rolesFuture = new PlainActionFuture<>(); - authorizationService.roles(new User("no role user"), rolesFuture); - final Role roles = rolesFuture.actionGet(); - assertThat(Arrays.asList(roles.names()), hasItem("anonymous_user_role")); - } - public void testCompositeActionsAreImmediatelyRejected() { //if the user has no permission for composite actions against any index, the request fails straight-away in the main action final Tuple compositeRequest = randomCompositeRequest(); @@ -1006,7 +992,8 @@ public void testCompositeActionsAreImmediatelyRejected() { assertThrowsAuthorizationException( () -> authorize(authentication, action, request), action, "test user"); - verify(auditTrail).accessDenied(requestId, authentication, action, request, new String[]{role.getName()}); + verify(auditTrail).accessDenied(eq(requestId), eq(authentication), eq(action), eq(request), + authzInfoRoles(new String[] { role.getName() })); verifyNoMoreInteractions(auditTrail); } @@ -1023,7 +1010,8 @@ public void testCompositeActionsIndicesAreNotChecked() { final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); authorize(authentication, action, request); - verify(auditTrail).accessGranted(requestId, authentication, action, request, new String[]{role.getName()}); + verify(auditTrail).accessGranted(eq(requestId), eq(authentication), eq(action), eq(request), + authzInfoRoles(new String[] { role.getName() })); verifyNoMoreInteractions(auditTrail); } @@ -1037,7 +1025,7 @@ public void testCompositeActionsMustImplementCompositeIndicesRequest() { null)); IllegalStateException illegalStateException = expectThrows(IllegalStateException.class, () -> authorize(createAuthentication(user), action, request)); - assertThat(illegalStateException.getMessage(), containsString("Composite actions must implement CompositeIndicesRequest")); + assertThat(illegalStateException.getMessage(), containsString("Composite and bulk actions must implement CompositeIndicesRequest")); } public void testCompositeActionsIndicesAreCheckedAtTheShardLevel() { @@ -1109,12 +1097,16 @@ public void testAuthorizationOfIndividualBulkItems() { final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); authorize(authentication, action, request); - verify(auditTrail).accessDenied(requestId, authentication, DeleteAction.NAME, request, - new String[] { role.getName() }); // alias-1 delete - verify(auditTrail).accessDenied(requestId, authentication, IndexAction.NAME, request, - new String[] { role.getName() }); // alias-2 index - verify(auditTrail).accessGranted(requestId, authentication, action, request, - new String[] { role.getName() }); // bulk request is allowed + verify(auditTrail, times(2)).accessGranted(eq(requestId), eq(authentication), eq(DeleteAction.NAME), eq(request), + authzInfoRoles(new String[] { role.getName() })); // concrete-index and alias-2 delete + verify(auditTrail, times(2)).accessGranted(eq(requestId), eq(authentication), eq(IndexAction.NAME), eq(request), + authzInfoRoles(new String[] { role.getName() })); // concrete-index and alias-1 index + verify(auditTrail).accessDenied(eq(requestId), eq(authentication), eq(DeleteAction.NAME), eq(request), + authzInfoRoles(new String[] { role.getName() })); // alias-1 delete + verify(auditTrail).accessDenied(eq(requestId), eq(authentication), eq(IndexAction.NAME), eq(request), + authzInfoRoles(new String[] { role.getName() })); // alias-2 index + verify(auditTrail).accessGranted(eq(requestId), eq(authentication), eq(action), eq(request), + authzInfoRoles(new String[] { role.getName() })); // bulk request is allowed verifyNoMoreInteractions(auditTrail); } @@ -1141,10 +1133,13 @@ public void testAuthorizationOfIndividualBulkItemsWithDateMath() { authorize(authentication, action, request); // both deletes should fail - verify(auditTrail, Mockito.times(2)).accessDenied(requestId, authentication, DeleteAction.NAME, request, - new String[]{role.getName()}); + verify(auditTrail, times(2)).accessDenied(eq(requestId), eq(authentication), eq(DeleteAction.NAME), eq(request), + authzInfoRoles(new String[] { role.getName() })); + verify(auditTrail, times(2)).accessGranted(eq(requestId), eq(authentication), eq(IndexAction.NAME), eq(request), + authzInfoRoles(new String[] { role.getName() })); // bulk request is allowed - verify(auditTrail).accessGranted(requestId, authentication, action, request, new String[]{role.getName()}); + verify(auditTrail).accessGranted(eq(requestId), eq(authentication), eq(action), eq(request), + authzInfoRoles(new String[]{role.getName()})); verifyNoMoreInteractions(auditTrail); } @@ -1154,148 +1149,6 @@ private BulkShardRequest createBulkShardRequest(String indexName, TriFunction randomCompositeRequest() { switch (randomIntBetween(0, 7)) { case 0: @@ -1322,20 +1175,6 @@ private static Tuple randomCompositeRequest() { private static class MockCompositeIndicesRequest extends TransportRequest implements CompositeIndicesRequest { } - public void testDoesNotUseRolesStoreForXPackUser() { - PlainActionFuture rolesFuture = new PlainActionFuture<>(); - authorizationService.roles(XPackUser.INSTANCE, rolesFuture); - final Role roles = rolesFuture.actionGet(); - assertThat(roles, equalTo(XPackUser.ROLE)); - verifyZeroInteractions(rolesStore); - } - - public void testGetRolesForSystemUserThrowsException() { - IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> authorizationService.roles(SystemUser.INSTANCE, - null)); - assertEquals("the user [_system] is the system user and we should never try to get its roles", iae.getMessage()); - } - private static Authentication createAuthentication(User user) { RealmRef lookedUpBy = user.authenticatedUser() == user ? null : new RealmRef("looked", "up", "by"); return new Authentication(user, new RealmRef("test", "test", "foo"), lookedUpBy); @@ -1354,8 +1193,10 @@ public void testProxyRequestFailsOnNonProxyAction() { TransportRequest transportRequest = TransportActionProxy.wrapRequest(node, request); final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); User user = new User("test user", "role"); - IllegalStateException illegalStateException = expectThrows(IllegalStateException.class, + ElasticsearchSecurityException ese = expectThrows(ElasticsearchSecurityException.class, () -> authorize(createAuthentication(user), "indices:some/action", transportRequest)); + assertThat(ese.getCause(), instanceOf(IllegalStateException.class)); + IllegalStateException illegalStateException = (IllegalStateException) ese.getCause(); assertThat(illegalStateException.getMessage(), startsWith("originalRequest is a proxy request for: [org.elasticsearch.transport.TransportRequest$")); assertThat(illegalStateException.getMessage(), endsWith("] but action: [indices:some/action] isn't")); @@ -1365,8 +1206,10 @@ public void testProxyRequestFailsOnNonProxyRequest() { TransportRequest request = TransportRequest.Empty.INSTANCE; User user = new User("test user", "role"); AuditUtil.getOrGenerateRequestId(threadContext); - IllegalStateException illegalStateException = expectThrows(IllegalStateException.class, + ElasticsearchSecurityException ese = expectThrows(ElasticsearchSecurityException.class, () -> authorize(createAuthentication(user), TransportActionProxy.getProxyAction("indices:some/action"), request)); + assertThat(ese.getCause(), instanceOf(IllegalStateException.class)); + IllegalStateException illegalStateException = (IllegalStateException) ese.getCause(); assertThat(illegalStateException.getMessage(), startsWith("originalRequest is not a proxy request: [org.elasticsearch.transport.TransportRequest$")); assertThat(illegalStateException.getMessage(), @@ -1385,12 +1228,13 @@ public void testProxyRequestAuthenticationDenied() { assertThrowsAuthorizationException( () -> authorize(authentication, action, transportRequest), action, "test user"); - verify(auditTrail).accessDenied(requestId, authentication, action, proxiedRequest, new String[]{role.getName()}); + verify(auditTrail).accessDenied(eq(requestId), eq(authentication), eq(action), eq(proxiedRequest), + authzInfoRoles(new String[]{role.getName()})); verifyNoMoreInteractions(auditTrail); } public void testProxyRequestAuthenticationGrantedWithAllPrivileges() { - RoleDescriptor role = new RoleDescriptor("a_role", null, + RoleDescriptor role = new RoleDescriptor("a_all", null, new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null); final Authentication authentication = createAuthentication(new User("test user", "a_all")); roleMap.put("a_all", role); @@ -1403,11 +1247,12 @@ public void testProxyRequestAuthenticationGrantedWithAllPrivileges() { final TransportRequest transportRequest = TransportActionProxy.wrapRequest(node, clearScrollRequest); final String action = TransportActionProxy.getProxyAction(SearchTransportService.CLEAR_SCROLL_CONTEXTS_ACTION_NAME); authorize(authentication, action, transportRequest); - verify(auditTrail).accessGranted(requestId, authentication, action, clearScrollRequest, new String[]{role.getName()}); + verify(auditTrail).accessGranted(eq(requestId), eq(authentication), eq(action), eq(clearScrollRequest), + authzInfoRoles(new String[]{role.getName()})); } public void testProxyRequestAuthenticationGranted() { - RoleDescriptor role = new RoleDescriptor("a_role", null, + RoleDescriptor role = new RoleDescriptor("a_all", null, new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("read_cross_cluster").build()}, null); final Authentication authentication = createAuthentication(new User("test user", "a_all")); roleMap.put("a_all", role); @@ -1419,12 +1264,13 @@ public void testProxyRequestAuthenticationGranted() { final TransportRequest transportRequest = TransportActionProxy.wrapRequest(node, clearScrollRequest); final String action = TransportActionProxy.getProxyAction(SearchTransportService.CLEAR_SCROLL_CONTEXTS_ACTION_NAME); authorize(authentication, action, transportRequest); - verify(auditTrail).accessGranted(requestId, authentication, action, clearScrollRequest, new String[]{role.getName()}); + verify(auditTrail).accessGranted(eq(requestId), eq(authentication), eq(action), eq(clearScrollRequest), + authzInfoRoles(new String[]{role.getName()})); } public void testProxyRequestAuthenticationDeniedWithReadPrivileges() { final Authentication authentication = createAuthentication(new User("test user", "a_all")); - final RoleDescriptor role = new RoleDescriptor("a_role", null, + final RoleDescriptor role = new RoleDescriptor("a_all", null, new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("read").build()}, null); roleMap.put("a_all", role); final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); @@ -1435,6 +1281,29 @@ public void testProxyRequestAuthenticationDeniedWithReadPrivileges() { String action = TransportActionProxy.getProxyAction(SearchTransportService.CLEAR_SCROLL_CONTEXTS_ACTION_NAME); assertThrowsAuthorizationException( () -> authorize(authentication, action, transportRequest), action, "test user"); - verify(auditTrail).accessDenied(requestId, authentication, action, clearScrollRequest, new String[]{role.getName()}); + verify(auditTrail).accessDenied(eq(requestId), eq(authentication), eq(action), eq(clearScrollRequest), + authzInfoRoles(new String[]{role.getName()})); + } + + static AuthorizationInfo authzInfoRoles(String[] expectedRoles) { + return Matchers.argThat(new RBACAuthorizationInfoRoleMatcher(expectedRoles)); + } + + private static class RBACAuthorizationInfoRoleMatcher extends ArgumentMatcher { + + private final String[] wanted; + + RBACAuthorizationInfoRoleMatcher(String[] expectedRoles) { + this.wanted = expectedRoles; + } + + @Override + public boolean matches(Object item) { + if (item instanceof AuthorizationInfo) { + final String[] found = (String[]) ((AuthorizationInfo) item).asMap().get(PRINCIPAL_ROLES_FIELD_NAME); + return Arrays.equals(wanted, found); + } + return false; + } } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java index c48ac4568989b..5e043a572e9c6 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java @@ -20,7 +20,6 @@ import org.elasticsearch.xpack.core.security.authz.permission.Role; import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; -import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; import org.elasticsearch.xpack.security.support.SecurityIndexManager; @@ -32,15 +31,13 @@ public class AuthorizedIndicesTests extends ESTestCase { public void testAuthorizedIndicesUserWithoutRoles() { - User user = new User("test user"); - AuthorizedIndices authorizedIndices = new AuthorizedIndices(user, Role.EMPTY, "", - MetaData.EMPTY_META_DATA); + AuthorizedIndices authorizedIndices = new AuthorizedIndices( + () -> RBACEngine.resolveAuthorizedIndicesFromRole(Role.EMPTY, "", MetaData.EMPTY_META_DATA.getAliasAndIndexLookup())); List list = authorizedIndices.get(); assertTrue(list.isEmpty()); } public void testAuthorizedIndicesUserWithSomeRoles() { - User user = new User("test user", "a_star", "b"); RoleDescriptor aStarRole = new RoleDescriptor("a_star", null, new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a*").privileges("all").build() }, null); RoleDescriptor bRole = new RoleDescriptor("b", null, @@ -63,7 +60,8 @@ public void testAuthorizedIndicesUserWithSomeRoles() { final Set descriptors = Sets.newHashSet(aStarRole, bRole); CompositeRolesStore.buildRoleFromDescriptors(descriptors, new FieldPermissionsCache(Settings.EMPTY), null, future); Role roles = future.actionGet(); - AuthorizedIndices authorizedIndices = new AuthorizedIndices(user, roles, SearchAction.NAME, metaData); + AuthorizedIndices authorizedIndices = new AuthorizedIndices( + () -> RBACEngine.resolveAuthorizedIndicesFromRole(roles, SearchAction.NAME, metaData.getAliasAndIndexLookup())); List list = authorizedIndices.get(); assertThat(list, containsInAnyOrder("a1", "a2", "aaaaaa", "b", "ab")); assertFalse(list.contains("bbbbb")); @@ -71,15 +69,14 @@ public void testAuthorizedIndicesUserWithSomeRoles() { } public void testAuthorizedIndicesUserWithSomeRolesEmptyMetaData() { - User user = new User("test user", "role"); Role role = Role.builder("role").add(IndexPrivilege.ALL, "*").build(); - AuthorizedIndices authorizedIndices = new AuthorizedIndices(user, role, SearchAction.NAME, MetaData.EMPTY_META_DATA); + AuthorizedIndices authorizedIndices = new AuthorizedIndices( + () -> RBACEngine.resolveAuthorizedIndicesFromRole(role, SearchAction.NAME, MetaData.EMPTY_META_DATA.getAliasAndIndexLookup())); List list = authorizedIndices.get(); assertTrue(list.isEmpty()); } public void testSecurityIndicesAreRemovedFromRegularUser() { - User user = new User("test user", "user_role"); Role role = Role.builder("user_role").add(IndexPrivilege.ALL, "*").cluster(ClusterPrivilege.ALL).build(); Settings indexSettings = Settings.builder().put("index.version.created", Version.CURRENT).build(); MetaData metaData = MetaData.builder() @@ -89,14 +86,14 @@ public void testSecurityIndicesAreRemovedFromRegularUser() { .numberOfShards(1).numberOfReplicas(0).build(), true) .build(); - AuthorizedIndices authorizedIndices = new AuthorizedIndices(user, role, SearchAction.NAME, metaData); + AuthorizedIndices authorizedIndices = new AuthorizedIndices( + () -> RBACEngine.resolveAuthorizedIndicesFromRole(role, SearchAction.NAME, metaData.getAliasAndIndexLookup())); List list = authorizedIndices.get(); assertThat(list, containsInAnyOrder("an-index", "another-index")); } public void testSecurityIndicesAreNotRemovedFromSuperUsers() { - User user = new User("admin", "kibana_user", "superuser"); - Role role = Role.builder("kibana_user+superuser").add(IndexPrivilege.ALL, "*").cluster(ClusterPrivilege.ALL).build(); + Role role = Role.builder("admin", "kibana_user", "superuser").add(IndexPrivilege.ALL, "*").cluster(ClusterPrivilege.ALL).build(); Settings indexSettings = Settings.builder().put("index.version.created", Version.CURRENT).build(); MetaData metaData = MetaData.builder() .put(new IndexMetaData.Builder("an-index").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) @@ -105,7 +102,8 @@ public void testSecurityIndicesAreNotRemovedFromSuperUsers() { .numberOfShards(1).numberOfReplicas(0).build(), true) .build(); - AuthorizedIndices authorizedIndices = new AuthorizedIndices(user, role, SearchAction.NAME, metaData); + AuthorizedIndices authorizedIndices = new AuthorizedIndices( + () -> RBACEngine.resolveAuthorizedIndicesFromRole(role, SearchAction.NAME, metaData.getAliasAndIndexLookup())); List list = authorizedIndices.get(); assertThat(list, containsInAnyOrder("an-index", "another-index", SecurityIndexManager.SECURITY_INDEX_NAME)); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java index 83edb189e2935..c0bfe261f579c 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java @@ -49,21 +49,17 @@ import org.elasticsearch.protocol.xpack.graph.GraphExploreRequest; import org.elasticsearch.search.internal.ShardSearchTransportRequest; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.xpack.core.graph.action.GraphExploreAction; -import org.elasticsearch.xpack.core.security.authc.DefaultAuthenticationFailureHandler; import org.elasticsearch.xpack.core.security.authz.IndicesAndAliasesResolverField; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.IndicesPrivileges; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsCache; import org.elasticsearch.xpack.core.security.authz.permission.Role; import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; -import org.elasticsearch.xpack.core.security.user.AnonymousUser; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.user.XPackSecurityUser; import org.elasticsearch.xpack.core.security.user.XPackUser; -import org.elasticsearch.xpack.security.audit.AuditTrailService; import org.elasticsearch.xpack.security.authz.IndicesAndAliasesResolver.ResolvedIndices; import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; import org.elasticsearch.xpack.security.support.SecurityIndexManager; @@ -74,7 +70,6 @@ import org.junit.Before; import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -93,6 +88,7 @@ import static org.hamcrest.Matchers.not; import static org.mockito.Matchers.any; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -103,10 +99,10 @@ public class IndicesAndAliasesResolverTests extends ESTestCase { private User userNoIndices; private CompositeRolesStore rolesStore; private MetaData metaData; - private AuthorizationService authzService; private IndicesAndAliasesResolver defaultIndicesResolver; private IndexNameExpressionResolver indexNameExpressionResolver; private Map roleMap; + private FieldPermissionsCache fieldPermissionsCache; @Before public void setup() { @@ -146,6 +142,7 @@ public void setup() { metaData = SecurityTestUtils.addAliasToMetaData(metaData, securityIndexName); } this.metaData = metaData; + this.fieldPermissionsCache = new FieldPermissionsCache(settings); user = new User("user", "role"); userDashIndices = new User("dash", "dash"); @@ -186,12 +183,10 @@ public void setup() { } return Void.TYPE; }).when(rolesStore).roles(any(Set.class), any(FieldPermissionsCache.class), any(ActionListener.class)); + doCallRealMethod().when(rolesStore).getRoles(any(User.class), any(FieldPermissionsCache.class), any(ActionListener.class)); ClusterService clusterService = mock(ClusterService.class); when(clusterService.getClusterSettings()).thenReturn(new ClusterSettings(settings, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS)); - authzService = new AuthorizationService(settings, rolesStore, clusterService, - mock(AuditTrailService.class), new DefaultAuthenticationFailureHandler(Collections.emptyMap()), mock(ThreadPool.class), - new AnonymousUser(settings)); defaultIndicesResolver = new IndicesAndAliasesResolver(settings, clusterService); } @@ -1365,8 +1360,9 @@ public void testDynamicPutMappingRequestFromAlias() { private AuthorizedIndices buildAuthorizedIndices(User user, String action) { PlainActionFuture rolesListener = new PlainActionFuture<>(); - authzService.roles(user, rolesListener); - return new AuthorizedIndices(user, rolesListener.actionGet(), action, metaData); + rolesStore.getRoles(user, fieldPermissionsCache, rolesListener); + return new AuthorizedIndices( + () -> RBACEngine.resolveAuthorizedIndicesFromRole(rolesListener.actionGet(), action, metaData.getAliasAndIndexLookup())); } public static IndexMetaData.Builder indexBuilder(String index) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java new file mode 100644 index 0000000000000..cf0a5a909a1d4 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java @@ -0,0 +1,194 @@ +/* + * 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; + +import org.elasticsearch.action.admin.cluster.health.ClusterHealthAction; +import org.elasticsearch.action.admin.cluster.state.ClusterStateAction; +import org.elasticsearch.action.admin.cluster.stats.ClusterStatsAction; +import org.elasticsearch.client.Client; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.license.GetLicenseAction; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.transport.TransportRequest; +import org.elasticsearch.xpack.core.security.action.user.AuthenticateAction; +import org.elasticsearch.xpack.core.security.action.user.AuthenticateRequest; +import org.elasticsearch.xpack.core.security.action.user.AuthenticateRequestBuilder; +import org.elasticsearch.xpack.core.security.action.user.ChangePasswordAction; +import org.elasticsearch.xpack.core.security.action.user.ChangePasswordRequest; +import org.elasticsearch.xpack.core.security.action.user.ChangePasswordRequestBuilder; +import org.elasticsearch.xpack.core.security.action.user.DeleteUserAction; +import org.elasticsearch.xpack.core.security.action.user.PutUserAction; +import org.elasticsearch.xpack.core.security.action.user.UserRequest; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings; +import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings; +import org.elasticsearch.xpack.core.security.authc.ldap.LdapRealmSettings; +import org.elasticsearch.xpack.core.security.authc.pki.PkiRealmSettings; +import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; +import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; +import org.junit.Before; + +import static org.hamcrest.Matchers.instanceOf; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +public class RBACEngineTests extends ESTestCase { + + private RBACEngine engine; + + @Before + public void createEngine() { + engine = new RBACEngine(Settings.EMPTY, mock(CompositeRolesStore.class)); + } + + public void testSameUserPermission() { + final User user = new User("joe"); + final boolean changePasswordRequest = randomBoolean(); + final TransportRequest request = changePasswordRequest ? + new ChangePasswordRequestBuilder(mock(Client.class)).username(user.principal()).request() : + new AuthenticateRequestBuilder(mock(Client.class)).username(user.principal()).request(); + final String action = changePasswordRequest ? ChangePasswordAction.NAME : AuthenticateAction.NAME; + final Authentication authentication = mock(Authentication.class); + final Authentication.RealmRef authenticatedBy = mock(Authentication.RealmRef.class); + when(authentication.getUser()).thenReturn(user); + when(authentication.getAuthenticatedBy()).thenReturn(authenticatedBy); + when(authenticatedBy.getType()) + .thenReturn(changePasswordRequest ? randomFrom(ReservedRealm.TYPE, NativeRealmSettings.TYPE) : + randomAlphaOfLengthBetween(4, 12)); + + assertThat(request, instanceOf(UserRequest.class)); + assertTrue(engine.checkSameUserPermissions(action, request, authentication)); + } + + public void testSameUserPermissionDoesNotAllowNonMatchingUsername() { + final User authUser = new User("admin", new String[]{"bar"}); + final User user = new User("joe", null, authUser); + final boolean changePasswordRequest = randomBoolean(); + final String username = randomFrom("", "joe" + randomAlphaOfLengthBetween(1, 5), randomAlphaOfLengthBetween(3, 10)); + final TransportRequest request = changePasswordRequest ? + new ChangePasswordRequestBuilder(mock(Client.class)).username(username).request() : + new AuthenticateRequestBuilder(mock(Client.class)).username(username).request(); + final String action = changePasswordRequest ? ChangePasswordAction.NAME : AuthenticateAction.NAME; + final Authentication authentication = mock(Authentication.class); + final Authentication.RealmRef authenticatedBy = mock(Authentication.RealmRef.class); + when(authentication.getUser()).thenReturn(user); + when(authentication.getAuthenticatedBy()).thenReturn(authenticatedBy); + when(authenticatedBy.getType()) + .thenReturn(changePasswordRequest ? randomFrom(ReservedRealm.TYPE, NativeRealmSettings.TYPE) : + randomAlphaOfLengthBetween(4, 12)); + + assertThat(request, instanceOf(UserRequest.class)); + assertFalse(engine.checkSameUserPermissions(action, request, authentication)); + + when(authentication.getUser()).thenReturn(user); + final Authentication.RealmRef lookedUpBy = mock(Authentication.RealmRef.class); + when(authentication.getLookedUpBy()).thenReturn(lookedUpBy); + when(lookedUpBy.getType()) + .thenReturn(changePasswordRequest ? randomFrom(ReservedRealm.TYPE, NativeRealmSettings.TYPE) : + randomAlphaOfLengthBetween(4, 12)); + // this should still fail since the username is still different + assertFalse(engine.checkSameUserPermissions(action, request, authentication)); + + if (request instanceof ChangePasswordRequest) { + ((ChangePasswordRequest) request).username("joe"); + } else { + ((AuthenticateRequest) request).username("joe"); + } + assertTrue(engine.checkSameUserPermissions(action, request, authentication)); + } + + public void testSameUserPermissionDoesNotAllowOtherActions() { + final User user = mock(User.class); + final TransportRequest request = mock(TransportRequest.class); + final String action = randomFrom(PutUserAction.NAME, DeleteUserAction.NAME, ClusterHealthAction.NAME, ClusterStateAction.NAME, + ClusterStatsAction.NAME, GetLicenseAction.NAME); + final Authentication authentication = mock(Authentication.class); + final Authentication.RealmRef authenticatedBy = mock(Authentication.RealmRef.class); + final boolean runAs = randomBoolean(); + when(authentication.getUser()).thenReturn(user); + when(user.authenticatedUser()).thenReturn(runAs ? new User("authUser") : user); + when(user.isRunAs()).thenReturn(runAs); + when(authentication.getAuthenticatedBy()).thenReturn(authenticatedBy); + when(authenticatedBy.getType()) + .thenReturn(randomAlphaOfLengthBetween(4, 12)); + + assertFalse(engine.checkSameUserPermissions(action, request, authentication)); + verifyZeroInteractions(user, request, authentication); + } + + public void testSameUserPermissionRunAsChecksAuthenticatedBy() { + final User authUser = new User("admin", new String[]{"bar"}); + final String username = "joe"; + final User user = new User(username, null, authUser); + final boolean changePasswordRequest = randomBoolean(); + final TransportRequest request = changePasswordRequest ? + new ChangePasswordRequestBuilder(mock(Client.class)).username(username).request() : + new AuthenticateRequestBuilder(mock(Client.class)).username(username).request(); + final String action = changePasswordRequest ? ChangePasswordAction.NAME : AuthenticateAction.NAME; + final Authentication authentication = mock(Authentication.class); + final Authentication.RealmRef authenticatedBy = mock(Authentication.RealmRef.class); + final Authentication.RealmRef lookedUpBy = mock(Authentication.RealmRef.class); + when(authentication.getUser()).thenReturn(user); + when(authentication.getAuthenticatedBy()).thenReturn(authenticatedBy); + when(authentication.getLookedUpBy()).thenReturn(lookedUpBy); + when(lookedUpBy.getType()) + .thenReturn(changePasswordRequest ? randomFrom(ReservedRealm.TYPE, NativeRealmSettings.TYPE) : + randomAlphaOfLengthBetween(4, 12)); + assertTrue(engine.checkSameUserPermissions(action, request, authentication)); + + when(authentication.getUser()).thenReturn(authUser); + assertFalse(engine.checkSameUserPermissions(action, request, authentication)); + } + + public void testSameUserPermissionDoesNotAllowChangePasswordForOtherRealms() { + final User user = new User("joe"); + final ChangePasswordRequest request = new ChangePasswordRequestBuilder(mock(Client.class)).username(user.principal()).request(); + final String action = ChangePasswordAction.NAME; + final Authentication authentication = mock(Authentication.class); + final Authentication.RealmRef authenticatedBy = mock(Authentication.RealmRef.class); + when(authentication.getUser()).thenReturn(user); + when(authentication.getAuthenticatedBy()).thenReturn(authenticatedBy); + when(authenticatedBy.getType()).thenReturn(randomFrom(LdapRealmSettings.LDAP_TYPE, FileRealmSettings.TYPE, + LdapRealmSettings.AD_TYPE, PkiRealmSettings.TYPE, + randomAlphaOfLengthBetween(4, 12))); + + assertThat(request, instanceOf(UserRequest.class)); + assertFalse(engine.checkSameUserPermissions(action, request, authentication)); + verify(authenticatedBy).getType(); + verify(authentication).getAuthenticatedBy(); + verify(authentication, times(2)).getUser(); + verifyNoMoreInteractions(authenticatedBy, authentication); + } + + public void testSameUserPermissionDoesNotAllowChangePasswordForLookedUpByOtherRealms() { + final User authUser = new User("admin", new String[]{"bar"}); + final User user = new User("joe", null, authUser); + final ChangePasswordRequest request = new ChangePasswordRequestBuilder(mock(Client.class)).username(user.principal()).request(); + final String action = ChangePasswordAction.NAME; + final Authentication authentication = mock(Authentication.class); + final Authentication.RealmRef authenticatedBy = mock(Authentication.RealmRef.class); + final Authentication.RealmRef lookedUpBy = mock(Authentication.RealmRef.class); + when(authentication.getUser()).thenReturn(user); + when(authentication.getAuthenticatedBy()).thenReturn(authenticatedBy); + when(authentication.getLookedUpBy()).thenReturn(lookedUpBy); + when(lookedUpBy.getType()).thenReturn(randomFrom(LdapRealmSettings.LDAP_TYPE, FileRealmSettings.TYPE, + LdapRealmSettings.AD_TYPE, PkiRealmSettings.TYPE, + randomAlphaOfLengthBetween(4, 12))); + + assertThat(request, instanceOf(UserRequest.class)); + assertFalse(engine.checkSameUserPermissions(action, request, authentication)); + verify(authentication).getLookedUpBy(); + verify(authentication, times(2)).getUser(); + verify(lookedUpBy).getType(); + verifyNoMoreInteractions(authentication, lookedUpBy, authenticatedBy); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/SecuritySearchOperationListenerTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/SecuritySearchOperationListenerTests.java index b6fe4346e62a1..b4cf84f414ada 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/SecuritySearchOperationListenerTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/SecuritySearchOperationListenerTests.java @@ -24,11 +24,17 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationField; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.audit.AuditTrailService; +import org.elasticsearch.xpack.security.authz.AuthorizationEngine.AuthorizationInfo; + +import java.util.Collections; import static org.elasticsearch.mock.orig.Mockito.verifyNoMoreInteractions; +import static org.elasticsearch.xpack.security.audit.logfile.LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME; +import static org.elasticsearch.xpack.security.authz.AuthorizationService.AUTHORIZATION_INFO_KEY; import static org.elasticsearch.xpack.security.authz.AuthorizationService.ORIGINATING_ACTION_KEY; -import static org.elasticsearch.xpack.security.authz.AuthorizationService.ROLE_NAMES_KEY; +import static org.elasticsearch.xpack.security.authz.AuthorizationServiceTests.authzInfoRoles; import static org.elasticsearch.xpack.security.authz.SecuritySearchOperationListener.ensureAuthenticatedUserIsSame; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -112,13 +118,15 @@ public void testValidateSearchContext() throws Exception { Authentication authentication = new Authentication(new User("test", "role"), new RealmRef(realmName, type, nodeName), null); authentication.writeToContext(threadContext); threadContext.putTransient(ORIGINATING_ACTION_KEY, "action"); - threadContext.putTransient(ROLE_NAMES_KEY, authentication.getUser().roles()); + threadContext.putTransient(AUTHORIZATION_INFO_KEY, + (AuthorizationInfo) () -> Collections.singletonMap(PRINCIPAL_ROLES_FIELD_NAME, authentication.getUser().roles())); final InternalScrollSearchRequest request = new InternalScrollSearchRequest(); SearchContextMissingException expected = expectThrows(SearchContextMissingException.class, () -> listener.validateSearchContext(testSearchContext, request)); assertEquals(testSearchContext.id(), expected.id()); verify(licenseState, times(3)).isAuthAllowed(); - verify(auditTrailService).accessDenied(null, authentication, "action", request, authentication.getUser().roles()); + verify(auditTrailService).accessDenied(eq(null), eq(authentication), eq("action"), eq(request), + authzInfoRoles(authentication.getUser().roles())); } // another user running as the original user @@ -146,13 +154,15 @@ public void testValidateSearchContext() throws Exception { new Authentication(new User("authenticated", "runas"), new RealmRef(realmName, type, nodeName), null); authentication.writeToContext(threadContext); threadContext.putTransient(ORIGINATING_ACTION_KEY, "action"); - threadContext.putTransient(ROLE_NAMES_KEY, authentication.getUser().roles()); + threadContext.putTransient(AUTHORIZATION_INFO_KEY, + (AuthorizationInfo) () -> Collections.singletonMap(PRINCIPAL_ROLES_FIELD_NAME, authentication.getUser().roles())); final InternalScrollSearchRequest request = new InternalScrollSearchRequest(); SearchContextMissingException expected = expectThrows(SearchContextMissingException.class, () -> listener.validateSearchContext(testSearchContext, request)); assertEquals(testSearchContext.id(), expected.id()); verify(licenseState, times(5)).isAuthAllowed(); - verify(auditTrailService).accessDenied(null, authentication, "action", request, authentication.getUser().roles()); + verify(auditTrailService).accessDenied(eq(null), eq(authentication), eq("action"), eq(request), + authzInfoRoles(authentication.getUser().roles())); } } @@ -166,21 +176,24 @@ public void testEnsuredAuthenticatedUserIsSame() { AuditTrailService auditTrail = mock(AuditTrailService.class); final String auditId = randomAlphaOfLengthBetween(8, 20); - ensureAuthenticatedUserIsSame(original, current, auditTrail, id, action, request, auditId, original.getUser().roles()); + ensureAuthenticatedUserIsSame(original, current, auditTrail, id, action, request, auditId, + () -> Collections.singletonMap(PRINCIPAL_ROLES_FIELD_NAME, original.getUser().roles())); verifyZeroInteractions(auditTrail); // original user being run as User user = new User(new User("test", "role"), new User("authenticated", "runas")); current = new Authentication(user, new RealmRef("realm", "file", "node"), new RealmRef(randomAlphaOfLengthBetween(1, 16), "file", "node")); - ensureAuthenticatedUserIsSame(original, current, auditTrail, id, action, request, auditId, original.getUser().roles()); + ensureAuthenticatedUserIsSame(original, current, auditTrail, id, action, request, auditId, + () -> Collections.singletonMap(PRINCIPAL_ROLES_FIELD_NAME, original.getUser().roles())); verifyZeroInteractions(auditTrail); // both user are run as current = new Authentication(user, new RealmRef("realm", "file", "node"), new RealmRef(randomAlphaOfLengthBetween(1, 16), "file", "node")); Authentication runAs = current; - ensureAuthenticatedUserIsSame(runAs, current, auditTrail, id, action, request, auditId, original.getUser().roles()); + ensureAuthenticatedUserIsSame(runAs, current, auditTrail, id, action, request, auditId, + () -> Collections.singletonMap(PRINCIPAL_ROLES_FIELD_NAME, original.getUser().roles())); verifyZeroInteractions(auditTrail); // different authenticated by type @@ -188,36 +201,39 @@ public void testEnsuredAuthenticatedUserIsSame() { new Authentication(new User("test", "role"), new RealmRef("realm", randomAlphaOfLength(5), "node"), null); SearchContextMissingException e = expectThrows(SearchContextMissingException.class, () -> ensureAuthenticatedUserIsSame(original, differentRealmType, auditTrail, id, action, request, auditId, - original.getUser().roles())); + () -> Collections.singletonMap(PRINCIPAL_ROLES_FIELD_NAME, original.getUser().roles()))); assertEquals(id, e.id()); - verify(auditTrail).accessDenied(auditId, differentRealmType, action, request, original.getUser().roles()); + verify(auditTrail).accessDenied(eq(auditId), eq(differentRealmType), eq(action), eq(request), + authzInfoRoles(original.getUser().roles())); // wrong user Authentication differentUser = new Authentication(new User("test2", "role"), new RealmRef("realm", "realm", "node"), null); e = expectThrows(SearchContextMissingException.class, () -> ensureAuthenticatedUserIsSame(original, differentUser, auditTrail, id, action, request, auditId, - original.getUser().roles())); + () -> Collections.singletonMap(PRINCIPAL_ROLES_FIELD_NAME, original.getUser().roles()))); assertEquals(id, e.id()); - verify(auditTrail).accessDenied(auditId, differentUser, action, request, original.getUser().roles()); + verify(auditTrail).accessDenied(eq(auditId), eq(differentUser), eq(action), eq(request), + authzInfoRoles(original.getUser().roles())); // run as different user Authentication diffRunAs = new Authentication(new User(new User("test2", "role"), new User("authenticated", "runas")), new RealmRef("realm", "file", "node1"), new RealmRef("realm", "file", "node1")); e = expectThrows(SearchContextMissingException.class, () -> ensureAuthenticatedUserIsSame(original, diffRunAs, auditTrail, id, action, request, auditId, - original.getUser().roles())); + () -> Collections.singletonMap(PRINCIPAL_ROLES_FIELD_NAME, original.getUser().roles()))); assertEquals(id, e.id()); - verify(auditTrail).accessDenied(auditId, diffRunAs, action, request, original.getUser().roles()); + verify(auditTrail).accessDenied(eq(auditId), eq(diffRunAs), eq(action), eq(request), authzInfoRoles(original.getUser().roles())); // run as different looked up by type Authentication runAsDiffType = new Authentication(user, new RealmRef("realm", "file", "node"), new RealmRef(randomAlphaOfLengthBetween(1, 16), randomAlphaOfLengthBetween(5, 12), "node")); e = expectThrows(SearchContextMissingException.class, () -> ensureAuthenticatedUserIsSame(runAs, runAsDiffType, auditTrail, id, action, request, auditId, - original.getUser().roles())); + () -> Collections.singletonMap(PRINCIPAL_ROLES_FIELD_NAME, original.getUser().roles()))); assertEquals(id, e.id()); - verify(auditTrail).accessDenied(auditId, runAsDiffType, action, request, original.getUser().roles()); + verify(auditTrail).accessDenied(eq(auditId), eq(runAsDiffType), eq(action), eq(request), + authzInfoRoles(original.getUser().roles())); } static class TestScrollSearchContext extends TestSearchContext { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java index 61724e44f6154..7f17fc1c8b7f7 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java @@ -9,6 +9,7 @@ import org.elasticsearch.Version; import org.elasticsearch.action.search.SearchAction; import org.elasticsearch.cluster.metadata.AliasMetaData; +import org.elasticsearch.cluster.metadata.AliasOrIndex; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.common.Strings; @@ -34,6 +35,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.SortedMap; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; @@ -52,13 +54,14 @@ public void testAuthorize() { .putAlias(AliasMetaData.builder("_alias")); MetaData md = MetaData.builder().put(imbBuilder).build(); FieldPermissionsCache fieldPermissionsCache = new FieldPermissionsCache(Settings.EMPTY); + SortedMap lookup = md.getAliasAndIndexLookup(); // basics: Set query = Collections.singleton(new BytesArray("{}")); String[] fields = new String[]{"_field"}; Role role = Role.builder("_role") .add(new FieldPermissions(fieldPermissionDef(fields, null)), query, IndexPrivilege.ALL, "_index").build(); - IndicesAccessControl permissions = role.authorize(SearchAction.NAME, Sets.newHashSet("_index"), md, fieldPermissionsCache); + IndicesAccessControl permissions = role.authorize(SearchAction.NAME, Sets.newHashSet("_index"), lookup::get, fieldPermissionsCache); assertThat(permissions.getIndexPermissions("_index"), notNullValue()); assertTrue(permissions.getIndexPermissions("_index").getFieldPermissions().grantsAccessTo("_field")); assertTrue(permissions.getIndexPermissions("_index").getFieldPermissions().hasFieldLevelSecurity()); @@ -68,7 +71,7 @@ public void testAuthorize() { // no document level security: role = Role.builder("_role") .add(new FieldPermissions(fieldPermissionDef(fields, null)), null, IndexPrivilege.ALL, "_index").build(); - permissions = role.authorize(SearchAction.NAME, Sets.newHashSet("_index"), md, fieldPermissionsCache); + permissions = role.authorize(SearchAction.NAME, Sets.newHashSet("_index"), lookup::get, fieldPermissionsCache); assertThat(permissions.getIndexPermissions("_index"), notNullValue()); assertTrue(permissions.getIndexPermissions("_index").getFieldPermissions().grantsAccessTo("_field")); assertTrue(permissions.getIndexPermissions("_index").getFieldPermissions().hasFieldLevelSecurity()); @@ -76,7 +79,7 @@ public void testAuthorize() { // no field level security: role = Role.builder("_role").add(new FieldPermissions(), query, IndexPrivilege.ALL, "_index").build(); - permissions = role.authorize(SearchAction.NAME, Sets.newHashSet("_index"), md, fieldPermissionsCache); + permissions = role.authorize(SearchAction.NAME, Sets.newHashSet("_index"), lookup::get, fieldPermissionsCache); assertThat(permissions.getIndexPermissions("_index"), notNullValue()); assertFalse(permissions.getIndexPermissions("_index").getFieldPermissions().hasFieldLevelSecurity()); assertThat(permissions.getIndexPermissions("_index").getQueries().size(), equalTo(1)); @@ -86,7 +89,7 @@ public void testAuthorize() { role = Role.builder("_role") .add(new FieldPermissions(fieldPermissionDef(fields, null)), query, IndexPrivilege.ALL, "_alias") .build(); - permissions = role.authorize(SearchAction.NAME, Sets.newHashSet("_alias"), md, fieldPermissionsCache); + permissions = role.authorize(SearchAction.NAME, Sets.newHashSet("_alias"), lookup::get, fieldPermissionsCache); assertThat(permissions.getIndexPermissions("_index"), notNullValue()); assertTrue(permissions.getIndexPermissions("_index").getFieldPermissions().grantsAccessTo("_field")); assertTrue(permissions.getIndexPermissions("_index").getFieldPermissions().hasFieldLevelSecurity()); @@ -104,7 +107,7 @@ public void testAuthorize() { new String[]{randomAlphaOfLengthBetween(1, 10), "*"}); role = Role.builder("_role") .add(new FieldPermissions(fieldPermissionDef(allFields, null)), query, IndexPrivilege.ALL, "_alias").build(); - permissions = role.authorize(SearchAction.NAME, Sets.newHashSet("_alias"), md, fieldPermissionsCache); + permissions = role.authorize(SearchAction.NAME, Sets.newHashSet("_alias"), lookup::get, fieldPermissionsCache); assertThat(permissions.getIndexPermissions("_index"), notNullValue()); assertFalse(permissions.getIndexPermissions("_index").getFieldPermissions().hasFieldLevelSecurity()); assertThat(permissions.getIndexPermissions("_index").getQueries().size(), equalTo(1)); @@ -123,7 +126,7 @@ public void testAuthorize() { ) .putAlias(AliasMetaData.builder("_alias")); md = MetaData.builder(md).put(imbBuilder1).build(); - + lookup = md.getAliasAndIndexLookup(); // match all fields with more than one permission Set fooQuery = Collections.singleton(new BytesArray("{foo}")); @@ -132,7 +135,7 @@ public void testAuthorize() { role = Role.builder("_role") .add(new FieldPermissions(fieldPermissionDef(allFields, null)), fooQuery, IndexPrivilege.ALL, "_alias") .add(new FieldPermissions(fieldPermissionDef(allFields, null)), query, IndexPrivilege.ALL, "_alias").build(); - permissions = role.authorize(SearchAction.NAME, Sets.newHashSet("_alias"), md, fieldPermissionsCache); + permissions = role.authorize(SearchAction.NAME, Sets.newHashSet("_alias"), lookup::get, fieldPermissionsCache); Set bothQueries = Sets.union(fooQuery, query); assertThat(permissions.getIndexPermissions("_index"), notNullValue()); assertFalse(permissions.getIndexPermissions("_index").getFieldPermissions().hasFieldLevelSecurity()); @@ -161,6 +164,7 @@ public void testAuthorizeMultipleGroupsMixedDls() { .putAlias(AliasMetaData.builder("_alias")); MetaData md = MetaData.builder().put(imbBuilder).build(); FieldPermissionsCache fieldPermissionsCache = new FieldPermissionsCache(Settings.EMPTY); + SortedMap lookup = md.getAliasAndIndexLookup(); Set query = Collections.singleton(new BytesArray("{}")); String[] fields = new String[]{"_field"}; @@ -168,7 +172,7 @@ public void testAuthorizeMultipleGroupsMixedDls() { .add(new FieldPermissions(fieldPermissionDef(fields, null)), query, IndexPrivilege.ALL, "_index") .add(new FieldPermissions(fieldPermissionDef(null, null)), null, IndexPrivilege.ALL, "*") .build(); - IndicesAccessControl permissions = role.authorize(SearchAction.NAME, Sets.newHashSet("_index"), md, fieldPermissionsCache); + IndicesAccessControl permissions = role.authorize(SearchAction.NAME, Sets.newHashSet("_index"), lookup::get, fieldPermissionsCache); assertThat(permissions.getIndexPermissions("_index"), notNullValue()); assertTrue(permissions.getIndexPermissions("_index").getFieldPermissions().grantsAccessTo("_field")); assertFalse(permissions.getIndexPermissions("_index").getFieldPermissions().hasFieldLevelSecurity()); @@ -217,6 +221,7 @@ public void testCorePermissionAuthorize() { .put(new IndexMetaData.Builder("a1").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) .put(new IndexMetaData.Builder("a2").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) .build(); + SortedMap lookup = metaData.getAliasAndIndexLookup(); FieldPermissionsCache fieldPermissionsCache = new FieldPermissionsCache(Settings.EMPTY); IndicesPermission.Group group1 = new IndicesPermission.Group(IndexPrivilege.ALL, new FieldPermissions(), null, "a1"); @@ -224,7 +229,7 @@ public void testCorePermissionAuthorize() { new FieldPermissions(fieldPermissionDef(null, new String[]{"denied_field"})), null, "a1"); IndicesPermission core = new IndicesPermission(group1, group2); Map authzMap = - core.authorize(SearchAction.NAME, Sets.newHashSet("a1", "ba"), metaData, fieldPermissionsCache); + core.authorize(SearchAction.NAME, Sets.newHashSet("a1", "ba"), lookup::get, fieldPermissionsCache); assertTrue(authzMap.get("a1").getFieldPermissions().grantsAccessTo("denied_field")); assertTrue(authzMap.get("a1").getFieldPermissions().grantsAccessTo(randomAlphaOfLength(5))); // did not define anything for ba so we allow all @@ -242,7 +247,7 @@ public void testCorePermissionAuthorize() { IndicesPermission.Group group4 = new IndicesPermission.Group(IndexPrivilege.ALL, new FieldPermissions(fieldPermissionDef(new String[]{"*_field2"}, new String[]{"denied_field2"})), null, "a2"); core = new IndicesPermission(group1, group2, group3, group4); - authzMap = core.authorize(SearchAction.NAME, Sets.newHashSet("a1", "a2"), metaData, fieldPermissionsCache); + authzMap = core.authorize(SearchAction.NAME, Sets.newHashSet("a1", "a2"), lookup::get, fieldPermissionsCache); assertFalse(authzMap.get("a1").getFieldPermissions().hasFieldLevelSecurity()); assertFalse(authzMap.get("a2").getFieldPermissions().grantsAccessTo("denied_field2")); assertFalse(authzMap.get("a2").getFieldPermissions().grantsAccessTo("denied_field")); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java index 6801ffd6bdf84..b3fc18cb30de0 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java @@ -42,6 +42,10 @@ import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; import org.elasticsearch.xpack.core.security.authz.store.RoleRetrievalResult; +import org.elasticsearch.xpack.core.security.user.AnonymousUser; +import org.elasticsearch.xpack.core.security.user.SystemUser; +import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.core.security.user.XPackUser; import org.elasticsearch.xpack.security.support.SecurityIndexManager; import java.io.IOException; @@ -62,6 +66,7 @@ import static org.elasticsearch.mock.orig.Mockito.verifyNoMoreInteractions; import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasItem; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anySetOf; import static org.mockito.Matchers.eq; @@ -454,8 +459,8 @@ public void testMergingRolesWithFls() { .settings(Settings.builder().put("index.version.created", Version.CURRENT).build()) .numberOfShards(1).numberOfReplicas(0).build(), true) .build(); - Map acls = - role.indices().authorize("indices:data/read/search", Collections.singleton("test"), metaData, cache); + Map acls = role.indices().authorize("indices:data/read/search", + Collections.singleton("test"), metaData.getAliasAndIndexLookup()::get, cache); assertFalse(acls.isEmpty()); assertTrue(acls.get("test").getFieldPermissions().grantsAccessTo("L1.foo")); assertFalse(acls.get("test").getFieldPermissions().grantsAccessTo("L2.foo")); @@ -764,6 +769,118 @@ public void invalidateAll() { assertEquals(2, numInvalidation.get()); } + public void testDefaultRoleUserWithoutRoles() { + final FileRolesStore fileRolesStore = mock(FileRolesStore.class); + doCallRealMethod().when(fileRolesStore).accept(any(Set.class), any(ActionListener.class)); + final NativeRolesStore nativeRolesStore = mock(NativeRolesStore.class); + doCallRealMethod().when(nativeRolesStore).accept(any(Set.class), any(ActionListener.class)); + when(fileRolesStore.roleDescriptors(anySetOf(String.class))).thenReturn(Collections.emptySet()); + doAnswer((invocationOnMock) -> { + ActionListener callback = (ActionListener) invocationOnMock.getArguments()[1]; + callback.onResponse(RoleRetrievalResult.failure(new RuntimeException("intentionally failed!"))); + return null; + }).when(nativeRolesStore).getRoleDescriptors(isA(Set.class), any(ActionListener.class)); + final ReservedRolesStore reservedRolesStore = spy(new ReservedRolesStore()); + + final CompositeRolesStore compositeRolesStore = + new CompositeRolesStore(SECURITY_ENABLED_SETTINGS, fileRolesStore, nativeRolesStore, reservedRolesStore, + mock(NativePrivilegeStore.class), Collections.emptyList(), new ThreadContext(SECURITY_ENABLED_SETTINGS), + new XPackLicenseState(SECURITY_ENABLED_SETTINGS)); + verify(fileRolesStore).addListener(any(Consumer.class)); // adds a listener in ctor + + + PlainActionFuture rolesFuture = new PlainActionFuture<>(); + compositeRolesStore.getRoles(new User("no role user"), new FieldPermissionsCache(SECURITY_ENABLED_SETTINGS), rolesFuture); + final Role roles = rolesFuture.actionGet(); + assertEquals(Role.EMPTY, roles); + } + + public void testAnonymousUserEnabledRoleAdded() { + Settings settings = Settings.builder() + .put(SECURITY_ENABLED_SETTINGS) + .put(AnonymousUser.ROLES_SETTING.getKey(), "anonymous_user_role") + .build(); + final FileRolesStore fileRolesStore = mock(FileRolesStore.class); + doCallRealMethod().when(fileRolesStore).accept(any(Set.class), any(ActionListener.class)); + final NativeRolesStore nativeRolesStore = mock(NativeRolesStore.class); + doCallRealMethod().when(nativeRolesStore).accept(any(Set.class), any(ActionListener.class)); + doAnswer(invocationOnMock -> { + Set names = (Set) invocationOnMock.getArguments()[0]; + if (names.size() == 1 && names.contains("anonymous_user_role")) { + RoleDescriptor rd = new RoleDescriptor("anonymous_user_role", null, null, null); + return Collections.singleton(rd); + } + return Collections.emptySet(); + }). + when(fileRolesStore).roleDescriptors(anySetOf(String.class)); + doAnswer((invocationOnMock) -> { + ActionListener callback = (ActionListener) invocationOnMock.getArguments()[1]; + callback.onResponse(RoleRetrievalResult.failure(new RuntimeException("intentionally failed!"))); + return null; + }).when(nativeRolesStore).getRoleDescriptors(isA(Set.class), any(ActionListener.class)); + final ReservedRolesStore reservedRolesStore = spy(new ReservedRolesStore()); + + final CompositeRolesStore compositeRolesStore = + new CompositeRolesStore(settings, fileRolesStore, nativeRolesStore, reservedRolesStore, + mock(NativePrivilegeStore.class), Collections.emptyList(), new ThreadContext(settings), + new XPackLicenseState(settings)); + verify(fileRolesStore).addListener(any(Consumer.class)); // adds a listener in ctor + + PlainActionFuture rolesFuture = new PlainActionFuture<>(); + compositeRolesStore.getRoles(new User("no role user"), new FieldPermissionsCache(Settings.EMPTY), rolesFuture); + final Role roles = rolesFuture.actionGet(); + assertThat(Arrays.asList(roles.names()), hasItem("anonymous_user_role")); + } + + public void testDoesNotUseRolesStoreForXPackUser() { + final FileRolesStore fileRolesStore = mock(FileRolesStore.class); + doCallRealMethod().when(fileRolesStore).accept(any(Set.class), any(ActionListener.class)); + final NativeRolesStore nativeRolesStore = mock(NativeRolesStore.class); + doCallRealMethod().when(nativeRolesStore).accept(any(Set.class), any(ActionListener.class)); + when(fileRolesStore.roleDescriptors(anySetOf(String.class))).thenReturn(Collections.emptySet()); + doAnswer((invocationOnMock) -> { + ActionListener callback = (ActionListener) invocationOnMock.getArguments()[1]; + callback.onResponse(RoleRetrievalResult.failure(new RuntimeException("intentionally failed!"))); + return null; + }).when(nativeRolesStore).getRoleDescriptors(isA(Set.class), any(ActionListener.class)); + final ReservedRolesStore reservedRolesStore = spy(new ReservedRolesStore()); + + final CompositeRolesStore compositeRolesStore = + new CompositeRolesStore(SECURITY_ENABLED_SETTINGS, fileRolesStore, nativeRolesStore, reservedRolesStore, + mock(NativePrivilegeStore.class), Collections.emptyList(), new ThreadContext(SECURITY_ENABLED_SETTINGS), + new XPackLicenseState(SECURITY_ENABLED_SETTINGS)); + verify(fileRolesStore).addListener(any(Consumer.class)); // adds a listener in ctor + + PlainActionFuture rolesFuture = new PlainActionFuture<>(); + compositeRolesStore.getRoles(XPackUser.INSTANCE, new FieldPermissionsCache(Settings.EMPTY), rolesFuture); + final Role roles = rolesFuture.actionGet(); + assertThat(roles, equalTo(XPackUser.ROLE)); + verifyNoMoreInteractions(fileRolesStore, nativeRolesStore, reservedRolesStore); + } + + public void testGetRolesForSystemUserThrowsException() { + final FileRolesStore fileRolesStore = mock(FileRolesStore.class); + doCallRealMethod().when(fileRolesStore).accept(any(Set.class), any(ActionListener.class)); + final NativeRolesStore nativeRolesStore = mock(NativeRolesStore.class); + doCallRealMethod().when(nativeRolesStore).accept(any(Set.class), any(ActionListener.class)); + when(fileRolesStore.roleDescriptors(anySetOf(String.class))).thenReturn(Collections.emptySet()); + doAnswer((invocationOnMock) -> { + ActionListener callback = (ActionListener) invocationOnMock.getArguments()[1]; + callback.onResponse(RoleRetrievalResult.failure(new RuntimeException("intentionally failed!"))); + return null; + }).when(nativeRolesStore).getRoleDescriptors(isA(Set.class), any(ActionListener.class)); + final ReservedRolesStore reservedRolesStore = spy(new ReservedRolesStore()); + + final CompositeRolesStore compositeRolesStore = + new CompositeRolesStore(SECURITY_ENABLED_SETTINGS, fileRolesStore, nativeRolesStore, reservedRolesStore, + mock(NativePrivilegeStore.class), Collections.emptyList(), new ThreadContext(SECURITY_ENABLED_SETTINGS), + new XPackLicenseState(SECURITY_ENABLED_SETTINGS)); + verify(fileRolesStore).addListener(any(Consumer.class)); // adds a listener in ctor + IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, + () -> compositeRolesStore.getRoles(SystemUser.INSTANCE, null, null)); + assertEquals("the user [_system] is the system user and we should never try to get its roles", iae.getMessage()); + } + private static class InMemoryRolesProvider implements BiConsumer, ActionListener> { private final Function, RoleRetrievalResult> roleDescriptorsFunc; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/ServerTransportFilterTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/ServerTransportFilterTests.java index dd340cb5839a0..350c55a558cb6 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/ServerTransportFilterTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/ServerTransportFilterTests.java @@ -25,8 +25,6 @@ import org.elasticsearch.xpack.core.security.SecurityContext; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; -import org.elasticsearch.xpack.core.security.authz.permission.Role; -import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; import org.elasticsearch.xpack.core.security.user.SystemUser; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.user.XPackUser; @@ -37,7 +35,6 @@ import java.io.IOException; import java.util.Collections; -import static org.elasticsearch.mock.orig.Mockito.times; import static org.elasticsearch.xpack.core.security.support.Exceptions.authenticationError; import static org.elasticsearch.xpack.core.security.support.Exceptions.authorizationError; import static org.hamcrest.Matchers.equalTo; @@ -89,7 +86,7 @@ public void testInbound() throws Exception { PlainActionFuture future = new PlainActionFuture<>(); filter.inbound("_action", request, channel, future); //future.get(); // don't block it's not called really just mocked - verify(authzService).authorize(authentication, "_action", request, null, null); + verify(authzService).authorize(eq(authentication), eq("_action"), eq(request), any(ActionListener.class)); } public void testInboundDestructiveOperations() throws Exception { @@ -113,7 +110,7 @@ public void testInboundDestructiveOperations() throws Exception { verify(listener).onFailure(isA(IllegalArgumentException.class)); verifyNoMoreInteractions(authzService); } else { - verify(authzService).authorize(authentication, action, request, null, null); + verify(authzService).authorize(eq(authentication), eq(action), eq(request), any(ActionListener.class)); } } @@ -148,18 +145,11 @@ public void testInboundAuthorizationException() throws Exception { callback.onResponse(authentication); return Void.TYPE; }).when(authcService).authenticate(eq("_action"), eq(request), eq((User)null), any(ActionListener.class)); - final Role empty = Role.EMPTY; - doAnswer((i) -> { - ActionListener callback = - (ActionListener) i.getArguments()[1]; - callback.onResponse(empty); - return Void.TYPE; - }).when(authzService).roles(any(User.class), any(ActionListener.class)); when(authentication.getVersion()).thenReturn(Version.CURRENT); when(authentication.getUser()).thenReturn(XPackUser.INSTANCE); PlainActionFuture future = new PlainActionFuture<>(); - doThrow(authorizationError("authz failed")).when(authzService).authorize(authentication, "_action", request, - empty, null); + doThrow(authorizationError("authz failed")) + .when(authzService).authorize(eq(authentication), eq("_action"), eq(request), any(ActionListener.class)); ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, () -> { filter.inbound("_action", request, channel, future); future.actionGet(); @@ -186,12 +176,6 @@ public void testNodeProfileAllowsNodeActions() throws Exception { ServerTransportFilter filter = getNodeFilter(true); TransportRequest request = mock(TransportRequest.class); Authentication authentication = new Authentication(new User("test", "superuser"), new RealmRef("test", "test", "node1"), null); - doAnswer((i) -> { - ActionListener callback = - (ActionListener) i.getArguments()[1]; - callback.onResponse(authentication.getUser().equals(i.getArguments()[0]) ? ReservedRolesStore.SUPERUSER_ROLE : null); - return Void.TYPE; - }).when(authzService).roles(any(User.class), any(ActionListener.class)); doAnswer((i) -> { ActionListener callback = (ActionListener) i.getArguments()[3]; @@ -207,13 +191,11 @@ public void testNodeProfileAllowsNodeActions() throws Exception { filter.inbound(internalAction, request, channel, new PlainActionFuture<>()); verify(authcService).authenticate(eq(internalAction), eq(request), eq((User)null), any(ActionListener.class)); - verify(authzService).roles(eq(authentication.getUser()), any(ActionListener.class)); - verify(authzService).authorize(authentication, internalAction, request, ReservedRolesStore.SUPERUSER_ROLE, null); + verify(authzService).authorize(eq(authentication), eq(internalAction), eq(request), any(ActionListener.class)); filter.inbound(nodeOrShardAction, request, channel, new PlainActionFuture<>()); verify(authcService).authenticate(eq(nodeOrShardAction), eq(request), eq((User)null), any(ActionListener.class)); - verify(authzService, times(2)).roles(eq(authentication.getUser()), any(ActionListener.class)); - verify(authzService).authorize(authentication, nodeOrShardAction, request, ReservedRolesStore.SUPERUSER_ROLE, null); + verify(authzService).authorize(eq(authentication), eq(nodeOrShardAction), eq(request), any(ActionListener.class)); verifyNoMoreInteractions(authcService, authzService); } From 1362ab67fe1710690520d3e782c975d46fd3b936 Mon Sep 17 00:00:00 2001 From: Jay Modi Date: Wed, 16 Jan 2019 10:23:45 -0700 Subject: [PATCH 02/15] Replace AuthorizedIndices class with a List (#37328) This change replaces the AuthorizedIndices class with a simple list. The change to a simple list does remove the lazy loading of the authorized indices in favor of simpler code as the loading of this list is now an asynchronous operation that is delegated to the authorization engine. --- .../security/authz/AuthorizationEngine.java | 4 +- .../security/authz/AuthorizationService.java | 11 ++-- .../security/authz/AuthorizedIndices.java | 30 --------- .../authz/IndicesAndAliasesResolver.java | 15 +++-- .../xpack/security/authz/RBACEngine.java | 9 +-- .../authz/AuthorizedIndicesTests.java | 33 +++++----- .../authz/IndicesAndAliasesResolverTests.java | 63 +++++++++---------- 7 files changed, 64 insertions(+), 101 deletions(-) delete mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizedIndices.java diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationEngine.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationEngine.java index 53146286c26f8..820da970a50bb 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationEngine.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationEngine.java @@ -34,8 +34,8 @@ void authorizeIndexAction(Authentication authentication, TransportRequest reques Function aliasOrIndexFunction, ActionListener listener); - List loadAuthorizedIndices(Authentication authentication, String action, AuthorizationInfo info, - Map aliasAndIndexLookup); + void loadAuthorizedIndices(Authentication authentication, String action, AuthorizationInfo info, + Map aliasAndIndexLookup, ActionListener> listener); interface AuthorizationInfo { 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 6a6999f995e7b..08ecaeb7f9af4 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 @@ -201,10 +201,9 @@ private void authorizeAction(final Authentication authentication, final String a authzEngine.authorizeClusterAction(authentication, unwrappedRequest, action, authzInfo, clusterAuthzListener); } else if (IndexPrivilege.ACTION_MATCHER.test(action)) { final MetaData metaData = clusterService.state().metaData(); - final AsyncSupplier authorizedIndicesSupplier = new CachingAsyncSupplier<>( - authzIndicesListener -> authzIndicesListener.onResponse(new AuthorizedIndices( - () -> authzEngine.loadAuthorizedIndices(authentication, action, authzInfo, metaData.getAliasAndIndexLookup()))) - ); + final AsyncSupplier> authorizedIndicesSupplier = new CachingAsyncSupplier<>(authzIndicesListener -> + authzEngine.loadAuthorizedIndices(authentication, action, authzInfo, metaData.getAliasAndIndexLookup(), + authzIndicesListener)); final AsyncSupplier resolvedIndicesAsyncSupplier = new CachingAsyncSupplier<>((resolvedIndicesListener) -> { authorizedIndicesSupplier.get(ActionListener.wrap(authorizedIndices -> { resolveIndexNames(unwrappedRequest, metaData, authorizedIndices, resolvedIndicesListener); @@ -366,7 +365,7 @@ private void authorizeRunAs(final Authentication authentication, final String ac */ private void authorizeBulkItems(Authentication authentication, BulkShardRequest request, AuthorizationInfo authzInfo, AuthorizationEngine authzEngine, AsyncSupplier resolvedIndicesAsyncSupplier, - AsyncSupplier authorizedIndicesSupplier, + AsyncSupplier> authorizedIndicesSupplier, MetaData metaData, String requestId, ActionListener listener) { // Maps original-index -> expanded-index-name (expands date-math, but not aliases) final Map resolvedIndexNames = new HashMap<>(); @@ -466,7 +465,7 @@ private static String getAction(BulkItemRequest item) { throw new IllegalArgumentException("No equivalent action for opType [" + docWriteRequest.opType() + "]"); } - private void resolveIndexNames(TransportRequest request, MetaData metaData, AuthorizedIndices authorizedIndices, + private void resolveIndexNames(TransportRequest request, MetaData metaData, List authorizedIndices, ActionListener listener) { listener.onResponse(indicesAndAliasesResolver.resolve(request, metaData, authorizedIndices)); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizedIndices.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizedIndices.java deleted file mode 100644 index 83d95e3e45998..0000000000000 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizedIndices.java +++ /dev/null @@ -1,30 +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.authz; - -import java.util.List; -import java.util.function.Supplier; - -/** - * Abstraction used to make sure that we lazily load authorized indices only when requested and only maximum once per request. Also - * makes sure that authorized indices don't get updated throughout the same request for the same user. - */ -class AuthorizedIndices { - - private final Supplier> supplier; - private List authorizedIndices; - - AuthorizedIndices(Supplier> authorizedIndicesSupplier) { - this.supplier = authorizedIndicesSupplier; - } - - List get() { - if (authorizedIndices == null) { - authorizedIndices = supplier.get(); - } - return authorizedIndices; - } -} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java index aa1461b189a39..48c11655bb5f8 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java @@ -87,7 +87,7 @@ class IndicesAndAliasesResolver { * Otherwise, N will be added to the local index list. */ - ResolvedIndices resolve(TransportRequest request, MetaData metaData, AuthorizedIndices authorizedIndices) { + ResolvedIndices resolve(TransportRequest request, MetaData metaData, List authorizedIndices) { if (request instanceof IndicesAliasesRequest) { ResolvedIndices.Builder resolvedIndicesBuilder = new ResolvedIndices.Builder(); IndicesAliasesRequest indicesAliasesRequest = (IndicesAliasesRequest) request; @@ -107,7 +107,7 @@ ResolvedIndices resolve(TransportRequest request, MetaData metaData, AuthorizedI } - ResolvedIndices resolveIndicesAndAliases(IndicesRequest indicesRequest, MetaData metaData, AuthorizedIndices authorizedIndices) { + ResolvedIndices resolveIndicesAndAliases(IndicesRequest indicesRequest, MetaData metaData, List authorizedIndices) { final ResolvedIndices.Builder resolvedIndicesBuilder = new ResolvedIndices.Builder(); boolean indicesReplacedWithNoIndices = false; if (indicesRequest instanceof PutMappingRequest && ((PutMappingRequest) indicesRequest).getConcreteIndex() != null) { @@ -134,7 +134,7 @@ ResolvedIndices resolveIndicesAndAliases(IndicesRequest indicesRequest, MetaData // check for all and return list of authorized indices if (IndexNameExpressionResolver.isAllIndices(indicesList(indicesRequest.indices()))) { if (replaceWildcards) { - for (String authorizedIndex : authorizedIndices.get()) { + for (String authorizedIndex : authorizedIndices) { if (isIndexVisible(authorizedIndex, indicesOptions, metaData)) { resolvedIndicesBuilder.addLocal(authorizedIndex); } @@ -150,11 +150,11 @@ ResolvedIndices resolveIndicesAndAliases(IndicesRequest indicesRequest, MetaData split = new ResolvedIndices(Arrays.asList(indicesRequest.indices()), Collections.emptyList()); } List replaced = replaceWildcardsWithAuthorizedIndices(split.getLocal(), indicesOptions, metaData, - authorizedIndices.get(), replaceWildcards); + authorizedIndices, replaceWildcards); if (indicesOptions.ignoreUnavailable()) { //out of all the explicit names (expanded from wildcards and original ones that were left untouched) //remove all the ones that the current user is not authorized for and ignore them - replaced = replaced.stream().filter(authorizedIndices.get()::contains).collect(Collectors.toList()); + replaced = replaced.stream().filter(authorizedIndices::contains).collect(Collectors.toList()); } resolvedIndicesBuilder.addLocal(replaced); resolvedIndicesBuilder.addRemote(split.getRemote()); @@ -195,7 +195,7 @@ ResolvedIndices resolveIndicesAndAliases(IndicesRequest indicesRequest, MetaData AliasesRequest aliasesRequest = (AliasesRequest) indicesRequest; if (aliasesRequest.expandAliasesWildcards()) { List aliases = replaceWildcardsWithAuthorizedAliases(aliasesRequest.aliases(), - loadAuthorizedAliases(authorizedIndices.get(), metaData)); + loadAuthorizedAliases(authorizedIndices, metaData)); aliasesRequest.replaceAliases(aliases.toArray(new String[aliases.size()])); } if (indicesReplacedWithNoIndices) { @@ -226,9 +226,8 @@ ResolvedIndices resolveIndicesAndAliases(IndicesRequest indicesRequest, MetaData * request's concrete index is not in the list of authorized indices, then we need to look to * see if this can be authorized against an alias */ - static String getPutMappingIndexOrAlias(PutMappingRequest request, AuthorizedIndices authorizedIndices, MetaData metaData) { + static String getPutMappingIndexOrAlias(PutMappingRequest request, List authorizedIndicesList, MetaData metaData) { final String concreteIndexName = request.getConcreteIndex().getName(); - final List authorizedIndicesList = authorizedIndices.get(); // validate that the concrete index exists, otherwise there is no remapping that we could do final AliasOrIndex aliasOrIndex = metaData.getAliasAndIndexLookup().get(concreteIndexName); 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 07dd030f890d7..5c36106bab6cb 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 @@ -278,13 +278,14 @@ private void authorizeIndexActionName(String action, AuthorizationInfo authoriza } @Override - public List loadAuthorizedIndices(Authentication authentication, String action, AuthorizationInfo authorizationInfo, - Map aliasAndIndexLookup) { + public void loadAuthorizedIndices(Authentication authentication, String action, AuthorizationInfo authorizationInfo, + Map aliasAndIndexLookup, ActionListener> listener) { if (authorizationInfo instanceof RBACAuthorizationInfo) { final Role role = ((RBACAuthorizationInfo) authorizationInfo).getRole(); - return resolveAuthorizedIndicesFromRole(role, action, aliasAndIndexLookup); + listener.onResponse(resolveAuthorizedIndicesFromRole(role, action, aliasAndIndexLookup)); } else { - throw new IllegalArgumentException("unsupported authorization info:" + authorizationInfo.getClass().getSimpleName()); + listener.onFailure( + new IllegalArgumentException("unsupported authorization info:" + authorizationInfo.getClass().getSimpleName())); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java index 5e043a572e9c6..0a9aaef521222 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java @@ -31,10 +31,9 @@ public class AuthorizedIndicesTests extends ESTestCase { public void testAuthorizedIndicesUserWithoutRoles() { - AuthorizedIndices authorizedIndices = new AuthorizedIndices( - () -> RBACEngine.resolveAuthorizedIndicesFromRole(Role.EMPTY, "", MetaData.EMPTY_META_DATA.getAliasAndIndexLookup())); - List list = authorizedIndices.get(); - assertTrue(list.isEmpty()); + List authorizedIndices = + RBACEngine.resolveAuthorizedIndicesFromRole(Role.EMPTY, "", MetaData.EMPTY_META_DATA.getAliasAndIndexLookup()); + assertTrue(authorizedIndices.isEmpty()); } public void testAuthorizedIndicesUserWithSomeRoles() { @@ -60,9 +59,8 @@ public void testAuthorizedIndicesUserWithSomeRoles() { final Set descriptors = Sets.newHashSet(aStarRole, bRole); CompositeRolesStore.buildRoleFromDescriptors(descriptors, new FieldPermissionsCache(Settings.EMPTY), null, future); Role roles = future.actionGet(); - AuthorizedIndices authorizedIndices = new AuthorizedIndices( - () -> RBACEngine.resolveAuthorizedIndicesFromRole(roles, SearchAction.NAME, metaData.getAliasAndIndexLookup())); - List list = authorizedIndices.get(); + List list = + RBACEngine.resolveAuthorizedIndicesFromRole(roles, SearchAction.NAME, metaData.getAliasAndIndexLookup()); assertThat(list, containsInAnyOrder("a1", "a2", "aaaaaa", "b", "ab")); assertFalse(list.contains("bbbbb")); assertFalse(list.contains("ba")); @@ -70,10 +68,9 @@ public void testAuthorizedIndicesUserWithSomeRoles() { public void testAuthorizedIndicesUserWithSomeRolesEmptyMetaData() { Role role = Role.builder("role").add(IndexPrivilege.ALL, "*").build(); - AuthorizedIndices authorizedIndices = new AuthorizedIndices( - () -> RBACEngine.resolveAuthorizedIndicesFromRole(role, SearchAction.NAME, MetaData.EMPTY_META_DATA.getAliasAndIndexLookup())); - List list = authorizedIndices.get(); - assertTrue(list.isEmpty()); + List authorizedIndices = + RBACEngine.resolveAuthorizedIndicesFromRole(role, SearchAction.NAME, MetaData.EMPTY_META_DATA.getAliasAndIndexLookup()); + assertTrue(authorizedIndices.isEmpty()); } public void testSecurityIndicesAreRemovedFromRegularUser() { @@ -86,10 +83,9 @@ public void testSecurityIndicesAreRemovedFromRegularUser() { .numberOfShards(1).numberOfReplicas(0).build(), true) .build(); - AuthorizedIndices authorizedIndices = new AuthorizedIndices( - () -> RBACEngine.resolveAuthorizedIndicesFromRole(role, SearchAction.NAME, metaData.getAliasAndIndexLookup())); - List list = authorizedIndices.get(); - assertThat(list, containsInAnyOrder("an-index", "another-index")); + List authorizedIndices = + RBACEngine.resolveAuthorizedIndicesFromRole(role, SearchAction.NAME, metaData.getAliasAndIndexLookup()); + assertThat(authorizedIndices, containsInAnyOrder("an-index", "another-index")); } public void testSecurityIndicesAreNotRemovedFromSuperUsers() { @@ -102,9 +98,8 @@ public void testSecurityIndicesAreNotRemovedFromSuperUsers() { .numberOfShards(1).numberOfReplicas(0).build(), true) .build(); - AuthorizedIndices authorizedIndices = new AuthorizedIndices( - () -> RBACEngine.resolveAuthorizedIndicesFromRole(role, SearchAction.NAME, metaData.getAliasAndIndexLookup())); - List list = authorizedIndices.get(); - assertThat(list, containsInAnyOrder("an-index", "another-index", SecurityIndexManager.SECURITY_INDEX_NAME)); + List authorizedIndices = + RBACEngine.resolveAuthorizedIndicesFromRole(role, SearchAction.NAME, metaData.getAliasAndIndexLookup()); + assertThat(authorizedIndices, containsInAnyOrder("an-index", "another-index", SecurityIndexManager.SECURITY_INDEX_NAME)); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java index c0bfe261f579c..2f1bc047554fd 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java @@ -575,7 +575,7 @@ public void testSearchWithRemoteAndLocalIndices() { public void testSearchWithRemoteAndLocalWildcards() { SearchRequest request = new SearchRequest("*:foo", "r*:bar*", "remote:baz*", "bar*", "foofoo"); request.indicesOptions(IndicesOptions.fromOptions(randomBoolean(), randomBoolean(), true, false)); - final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, SearchAction.NAME); + final List authorizedIndices = buildAuthorizedIndices(user, SearchAction.NAME); final ResolvedIndices resolved = resolveIndices(request, authorizedIndices); assertThat(resolved.getRemote(), containsInAnyOrder("remote:foo", "other_remote:foo", "remote:bar*", "remote:baz*")); assertThat(resolved.getLocal(), containsInAnyOrder("bar", "foofoo")); @@ -693,7 +693,7 @@ public void testResolveIndicesAliasesRequestDeleteActions() { IndicesAliasesRequest request = new IndicesAliasesRequest(); request.addAliasAction(AliasActions.remove().index("foo").alias("foofoobar")); request.addAliasAction(AliasActions.remove().index("foofoo").alias("barbaz")); - final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, IndicesAliasesAction.NAME); + final List authorizedIndices = buildAuthorizedIndices(user, IndicesAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); //the union of all indices and aliases gets returned String[] expectedIndices = new String[]{"foo", "foofoobar", "foofoo", "barbaz"}; @@ -709,7 +709,7 @@ public void testResolveIndicesAliasesRequestDeleteActionsMissingIndex() { IndicesAliasesRequest request = new IndicesAliasesRequest(); request.addAliasAction(AliasActions.remove().index("foo").alias("foofoobar")); request.addAliasAction(AliasActions.remove().index("missing_index").alias("missing_alias")); - final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, IndicesAliasesAction.NAME); + final List authorizedIndices = buildAuthorizedIndices(user, IndicesAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); //the union of all indices and aliases gets returned, doesn't matter is some of them don't exist String[] expectedIndices = new String[]{"foo", "foofoobar", "missing_index", "missing_alias"}; @@ -725,7 +725,7 @@ public void testResolveWildcardsIndicesAliasesRequestDeleteActions() { IndicesAliasesRequest request = new IndicesAliasesRequest(); request.addAliasAction(AliasActions.remove().index("foo*").alias("foofoobar")); request.addAliasAction(AliasActions.remove().index("bar*").alias("barbaz")); - final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, IndicesAliasesAction.NAME); + final List authorizedIndices = buildAuthorizedIndices(user, IndicesAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); //union of all resolved indices and aliases gets returned, based on what user is authorized for String[] expectedIndices = new String[]{"foofoobar", "foofoo", "bar", "barbaz"}; @@ -742,7 +742,7 @@ public void testResolveAliasesWildcardsIndicesAliasesRequestDeleteActions() { IndicesAliasesRequest request = new IndicesAliasesRequest(); request.addAliasAction(AliasActions.remove().index("*").alias("foo*")); request.addAliasAction(AliasActions.remove().index("*bar").alias("foo*")); - final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, IndicesAliasesAction.NAME); + final List authorizedIndices = buildAuthorizedIndices(user, IndicesAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); //union of all resolved indices and aliases gets returned, based on what user is authorized for //note that the index side will end up containing matching aliases too, which is fine, as es core would do @@ -760,7 +760,7 @@ public void testResolveAllAliasesWildcardsIndicesAliasesRequestDeleteActions() { IndicesAliasesRequest request = new IndicesAliasesRequest(); request.addAliasAction(AliasActions.remove().index("*").alias("_all")); request.addAliasAction(AliasActions.remove().index("_all").aliases("_all", "explicit")); - final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, IndicesAliasesAction.NAME); + final List authorizedIndices = buildAuthorizedIndices(user, IndicesAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); //union of all resolved indices and aliases gets returned, based on what user is authorized for //note that the index side will end up containing matching aliases too, which is fine, as es core would do @@ -788,7 +788,7 @@ public void testResolveWildcardsIndicesAliasesRequestAddAndDeleteActions() { IndicesAliasesRequest request = new IndicesAliasesRequest(); request.addAliasAction(AliasActions.remove().index("foo*").alias("foofoobar")); request.addAliasAction(AliasActions.add().index("bar*").alias("foofoobar")); - final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, IndicesAliasesAction.NAME); + final List authorizedIndices = buildAuthorizedIndices(user, IndicesAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); //union of all resolved indices and aliases gets returned, based on what user is authorized for String[] expectedIndices = new String[]{"foofoobar", "foofoo", "bar"}; @@ -803,7 +803,7 @@ public void testResolveWildcardsIndicesAliasesRequestAddAndDeleteActions() { public void testResolveGetAliasesRequestStrict() { GetAliasesRequest request = new GetAliasesRequest("alias1").indices("foo", "foofoo"); request.indicesOptions(IndicesOptions.fromOptions(false, randomBoolean(), randomBoolean(), randomBoolean())); - final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); + final List authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); //the union of all indices and aliases gets returned String[] expectedIndices = new String[]{"alias1", "foo", "foofoo"}; @@ -816,7 +816,7 @@ public void testResolveGetAliasesRequestStrict() { public void testResolveGetAliasesRequestIgnoreUnavailable() { GetAliasesRequest request = new GetAliasesRequest("alias1").indices("foo", "foofoo"); request.indicesOptions(IndicesOptions.fromOptions(true, randomBoolean(), randomBoolean(), randomBoolean())); - final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); + final List authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); String[] expectedIndices = new String[]{"alias1", "foofoo"}; assertThat(indices.size(), equalTo(expectedIndices.length)); @@ -830,7 +830,7 @@ public void testResolveGetAliasesRequestMissingIndexStrict() { request.indicesOptions(IndicesOptions.fromOptions(false, randomBoolean(), true, randomBoolean())); request.indices("missing"); request.aliases("alias2"); - final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); + final List authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); //the union of all indices and aliases gets returned, missing is not an existing index/alias but that doesn't make any difference String[] expectedIndices = new String[]{"alias2", "missing"}; @@ -863,7 +863,7 @@ public void testGetAliasesRequestMissingIndexStrict() { request.indicesOptions(IndicesOptions.fromOptions(false, randomBoolean(), randomBoolean(), randomBoolean())); request.indices("missing"); request.aliases("alias2"); - final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); + final List authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); String[] expectedIndices = new String[]{"alias2", "missing"}; assertThat(indices.size(), equalTo(expectedIndices.length)); @@ -877,7 +877,7 @@ public void testResolveWildcardsGetAliasesRequestStrictExpand() { request.indicesOptions(IndicesOptions.fromOptions(false, randomBoolean(), true, true)); request.aliases("alias1"); request.indices("foo*"); - final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); + final List authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); //the union of all resolved indices and aliases gets returned, based on indices and aliases that user is authorized for String[] expectedIndices = new String[]{"alias1", "foofoo", "foofoo-closed", "foofoobar", "foobarfoo"}; @@ -893,7 +893,7 @@ public void testResolveWildcardsGetAliasesRequestStrictExpandOpen() { request.indicesOptions(IndicesOptions.fromOptions(false, randomBoolean(), true, false)); request.aliases("alias1"); request.indices("foo*"); - final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); + final List authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); //the union of all resolved indices and aliases gets returned, based on indices and aliases that user is authorized for String[] expectedIndices = new String[]{"alias1", "foofoo", "foofoobar", "foobarfoo"}; @@ -909,7 +909,7 @@ public void testResolveWildcardsGetAliasesRequestLenientExpandOpen() { request.indicesOptions(IndicesOptions.fromOptions(true, randomBoolean(), true, false)); request.aliases("alias1"); request.indices("foo*", "bar", "missing"); - final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); + final List authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); //the union of all resolved indices and aliases gets returned, based on indices and aliases that user is authorized for String[] expectedIndices = new String[]{"alias1", "foofoo", "foofoobar", "foobarfoo", "bar"}; @@ -945,7 +945,7 @@ public void testResolveAllGetAliasesRequest() { request.indices("_all"); } request.aliases("alias1"); - final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); + final List authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); //the union of all resolved indices and aliases gets returned String[] expectedIndices = new String[]{"bar", "bar-closed", "foofoobar", "foobarfoo", "foofoo", "foofoo-closed", "alias1"}; @@ -966,7 +966,7 @@ public void testResolveAllGetAliasesRequestExpandWildcardsOpenOnly() { request.indices("_all"); } request.aliases("alias1"); - final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); + final List authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); //the union of all resolved indices and aliases gets returned String[] expectedIndices = new String[]{"bar", "foofoobar", "foobarfoo", "foofoo", "alias1"}; @@ -1025,7 +1025,7 @@ public void testResolveAllAliasesGetAliasesRequest() { if (randomBoolean()) { request.indices("_all"); } - final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); + final List authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); //the union of all resolved indices and aliases gets returned String[] expectedIndices = new String[]{"bar", "bar-closed", "foofoobar", "foobarfoo", "foofoo", "foofoo-closed"}; @@ -1040,7 +1040,7 @@ public void testResolveAllAndExplicitAliasesGetAliasesRequest() { if (randomBoolean()) { request.indices("_all"); } - final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); + final List authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); //the union of all resolved indices and aliases gets returned String[] expectedIndices = new String[]{"bar", "bar-closed", "foofoobar", "foobarfoo", "foofoo", "foofoo-closed", "explicit"}; @@ -1055,7 +1055,7 @@ public void testResolveAllAndWildcardsAliasesGetAliasesRequest() { if (randomBoolean()) { request.indices("_all"); } - final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); + final List authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); //the union of all resolved indices and aliases gets returned String[] expectedIndices = new String[]{"bar", "bar-closed", "foofoobar", "foobarfoo", "foofoo", "foofoo-closed"}; @@ -1069,7 +1069,7 @@ public void testResolveAliasesWildcardsGetAliasesRequest() { GetAliasesRequest request = new GetAliasesRequest(); request.indices("*bar"); request.aliases("foo*"); - final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); + final List authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); //union of all resolved indices and aliases gets returned, based on what user is authorized for //note that the index side will end up containing matching aliases too, which is fine, as es core would do @@ -1093,7 +1093,7 @@ public void testResolveAliasesWildcardsGetAliasesRequestNoAuthorizedIndices() { public void testResolveAliasesExclusionWildcardsGetAliasesRequest() { GetAliasesRequest request = new GetAliasesRequest(); request.aliases("foo*","-foobar*"); - final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); + final List authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); //union of all resolved indices and aliases gets returned, based on what user is authorized for //note that the index side will end up containing matching aliases too, which is fine, as es core would do @@ -1172,7 +1172,7 @@ public void testCompositeIndicesRequestIsNotSupported() { } public void testResolveAdminAction() { - final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, DeleteIndexAction.NAME); + final List authorizedIndices = buildAuthorizedIndices(user, DeleteIndexAction.NAME); { RefreshRequest request = new RefreshRequest("*"); List indices = resolveIndices(request, authorizedIndices).getLocal(); @@ -1216,14 +1216,14 @@ public void testIndicesExists() { public void testXPackSecurityUserHasAccessToSecurityIndex() { SearchRequest request = new SearchRequest(); { - final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(XPackSecurityUser.INSTANCE, SearchAction.NAME); + final List authorizedIndices = buildAuthorizedIndices(XPackSecurityUser.INSTANCE, SearchAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); assertThat(indices, hasItem(SecurityIndexManager.SECURITY_INDEX_NAME)); } { IndicesAliasesRequest aliasesRequest = new IndicesAliasesRequest(); aliasesRequest.addAliasAction(AliasActions.add().alias("security_alias").index(SECURITY_INDEX_NAME)); - final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(XPackSecurityUser.INSTANCE, IndicesAliasesAction.NAME); + final List authorizedIndices = buildAuthorizedIndices(XPackSecurityUser.INSTANCE, IndicesAliasesAction.NAME); List indices = resolveIndices(aliasesRequest, authorizedIndices).getLocal(); assertThat(indices, hasItem(SecurityIndexManager.SECURITY_INDEX_NAME)); } @@ -1231,7 +1231,7 @@ public void testXPackSecurityUserHasAccessToSecurityIndex() { public void testXPackUserDoesNotHaveAccessToSecurityIndex() { SearchRequest request = new SearchRequest(); - final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(XPackUser.INSTANCE, SearchAction.NAME); + final List authorizedIndices = buildAuthorizedIndices(XPackUser.INSTANCE, SearchAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); assertThat(indices, not(hasItem(SecurityIndexManager.SECURITY_INDEX_NAME))); } @@ -1243,7 +1243,7 @@ public void testNonXPackUserAccessingSecurityIndex() { { SearchRequest request = new SearchRequest(); - final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(allAccessUser, SearchAction.NAME); + final List authorizedIndices = buildAuthorizedIndices(allAccessUser, SearchAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); assertThat(indices, not(hasItem(SecurityIndexManager.SECURITY_INDEX_NAME))); } @@ -1251,7 +1251,7 @@ public void testNonXPackUserAccessingSecurityIndex() { { IndicesAliasesRequest aliasesRequest = new IndicesAliasesRequest(); aliasesRequest.addAliasAction(AliasActions.add().alias("security_alias1").index("*")); - final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(allAccessUser, IndicesAliasesAction.NAME); + final List authorizedIndices = buildAuthorizedIndices(allAccessUser, IndicesAliasesAction.NAME); List indices = resolveIndices(aliasesRequest, authorizedIndices).getLocal(); assertThat(indices, not(hasItem(SecurityIndexManager.SECURITY_INDEX_NAME))); } @@ -1343,7 +1343,7 @@ public void testAliasDateMathExpressionNotSupported() { public void testDynamicPutMappingRequestFromAlias() { PutMappingRequest request = new PutMappingRequest(Strings.EMPTY_ARRAY).setConcreteIndex(new Index("foofoo", UUIDs.base64UUID())); User user = new User("alias-writer", "alias_read_write"); - AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, PutMappingAction.NAME); + List authorizedIndices = buildAuthorizedIndices(user, PutMappingAction.NAME); String putMappingIndexOrAlias = IndicesAndAliasesResolver.getPutMappingIndexOrAlias(request, authorizedIndices, metaData); assertEquals("barbaz", putMappingIndexOrAlias); @@ -1358,11 +1358,10 @@ public void testDynamicPutMappingRequestFromAlias() { // TODO with the removal of DeleteByQuery is there another way to test resolving a write action? - private AuthorizedIndices buildAuthorizedIndices(User user, String action) { + private List buildAuthorizedIndices(User user, String action) { PlainActionFuture rolesListener = new PlainActionFuture<>(); rolesStore.getRoles(user, fieldPermissionsCache, rolesListener); - return new AuthorizedIndices( - () -> RBACEngine.resolveAuthorizedIndicesFromRole(rolesListener.actionGet(), action, metaData.getAliasAndIndexLookup())); + return RBACEngine.resolveAuthorizedIndicesFromRole(rolesListener.actionGet(), action, metaData.getAliasAndIndexLookup()); } public static IndexMetaData.Builder indexBuilder(String index) { @@ -1371,7 +1370,7 @@ public static IndexMetaData.Builder indexBuilder(String index) { .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0)); } - private ResolvedIndices resolveIndices(TransportRequest request, AuthorizedIndices authorizedIndices) { + private ResolvedIndices resolveIndices(TransportRequest request, List authorizedIndices) { return defaultIndicesResolver.resolve(request, this.metaData, authorizedIndices); } From 9a240c6bbe83c569843d483fa6d01bcdd1822819 Mon Sep 17 00:00:00 2001 From: Jay Modi Date: Thu, 17 Jan 2019 19:33:35 -0700 Subject: [PATCH 03/15] Encapsulate request, auth, and action name (#37495) This change introduces a new class called RequestInfo that encapsulates the common objects that are passed to the authorization engine methods. By doing so, we give ourselves a way of adding additional data without breaking the interface. Additionally, this also reduces the need to ensure we pass these three parameters in the same order everywhere for consistency. --- .../security/authz/AuthorizationEngine.java | 41 ++++++-- .../security/authz/AuthorizationService.java | 95 +++++++++++-------- .../xpack/security/authz/RBACEngine.java | 29 +++--- 3 files changed, 101 insertions(+), 64 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationEngine.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationEngine.java index 820da970a50bb..3faffddc904c2 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationEngine.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationEngine.java @@ -20,21 +20,17 @@ public interface AuthorizationEngine { - void resolveAuthorizationInfo(Authentication authentication, TransportRequest request, String action, - ActionListener listener); + void resolveAuthorizationInfo(RequestInfo requestInfo, ActionListener listener); - void authorizeRunAs(Authentication authentication, TransportRequest request, String action, AuthorizationInfo authorizationInfo, - ActionListener listener); + void authorizeRunAs(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, ActionListener listener); - void authorizeClusterAction(Authentication authentication, TransportRequest request, String action, AuthorizationInfo authorizationInfo, - ActionListener listener); + void authorizeClusterAction(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, ActionListener listener); - void authorizeIndexAction(Authentication authentication, TransportRequest request, String action, - AuthorizationInfo authorizationInfo, AsyncSupplier indicesAsyncSupplier, - Function aliasOrIndexFunction, + void authorizeIndexAction(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, + AsyncSupplier indicesAsyncSupplier, Function aliasOrIndexFunction, ActionListener listener); - void loadAuthorizedIndices(Authentication authentication, String action, AuthorizationInfo info, + void loadAuthorizedIndices(RequestInfo requestInfo, AuthorizationInfo info, Map aliasAndIndexLookup, ActionListener> listener); interface AuthorizationInfo { @@ -58,6 +54,31 @@ public Map asMap() { } } + final class RequestInfo { + + private final Authentication authentication; + private final TransportRequest request; + private final String action; + + public RequestInfo(Authentication authentication, TransportRequest request, String action) { + this.authentication = authentication; + this.request = request; + this.action = action; + } + + public String getAction() { + return action; + } + + public Authentication getAuthentication() { + return authentication; + } + + public TransportRequest getRequest() { + return request; + } + } + class AuthorizationResult { private final boolean granted; 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 08ecaeb7f9af4..914d917c6e472 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 @@ -52,6 +52,7 @@ import org.elasticsearch.xpack.security.authz.AuthorizationEngine.AuthorizationResult; import org.elasticsearch.xpack.security.authz.AuthorizationEngine.EmptyAuthorizationInfo; import org.elasticsearch.xpack.security.authz.AuthorizationEngine.IndexAuthorizationResult; +import org.elasticsearch.xpack.security.authz.AuthorizationEngine.RequestInfo; import org.elasticsearch.xpack.security.authz.IndicesAndAliasesResolver.ResolvedIndices; import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; @@ -145,78 +146,84 @@ public void authorize(final Authentication authentication, final String action, authorizeSystemUser(authentication, action, auditId, unwrappedRequest, listener); } else { final String finalAuditId = auditId; + final RequestInfo requestInfo = new RequestInfo(authentication, unwrappedRequest, action); final ActionListener authzInfoListener = wrapPreservingContext(ActionListener.wrap( authorizationInfo -> { putTransientIfNonExisting(AUTHORIZATION_INFO_KEY, authorizationInfo); - maybeAuthorizeRunAs(authentication, action, unwrappedRequest, finalAuditId, authorizationInfo, listener); + maybeAuthorizeRunAs(requestInfo, finalAuditId, authorizationInfo, listener); }, listener::onFailure), threadContext); - getAuthorizationEngine(authentication).resolveAuthorizationInfo(authentication, unwrappedRequest, action, authzInfoListener); + getAuthorizationEngine(authentication).resolveAuthorizationInfo(requestInfo, authzInfoListener); } } - private void maybeAuthorizeRunAs(final Authentication authentication, final String action, final TransportRequest unwrappedRequest, - final String requestId, final AuthorizationInfo authzInfo, final ActionListener listener) { + private void maybeAuthorizeRunAs(final RequestInfo requestInfo, final String requestId, final AuthorizationInfo authzInfo, + final ActionListener listener) { + final Authentication authentication = requestInfo.getAuthentication(); + final TransportRequest request = requestInfo.getRequest(); + final String action = requestInfo.getAction(); final boolean isRunAs = authentication.getUser().isRunAs(); if (isRunAs) { ActionListener runAsListener = wrapPreservingContext(ActionListener.wrap(result -> { if (result.isGranted()) { if (result.isAuditable()) { - auditTrail.runAsGranted(requestId, authentication, action, unwrappedRequest, + auditTrail.runAsGranted(requestId, authentication, action, request, authzInfo.getAuthenticatedUserAuthorizationInfo()); } - authorizeAction(authentication, action, requestId, unwrappedRequest, authzInfo, listener); + authorizeAction(requestInfo, requestId, authzInfo, listener); } else { - listener.onFailure(denyRunAs(requestId, authentication, action, unwrappedRequest, + listener.onFailure(denyRunAs(requestId, authentication, action, request, authzInfo.getAuthenticatedUserAuthorizationInfo())); } }, e -> { // TODO need a failure handler better than this! - listener.onFailure(denyRunAs(requestId, authentication, action, unwrappedRequest, authzInfo, e)); + listener.onFailure(denyRunAs(requestId, authentication, action, request, authzInfo, e)); }), threadContext); - authorizeRunAs(authentication, action, requestId, unwrappedRequest, authzInfo, runAsListener); + authorizeRunAs(requestInfo, requestId, authzInfo, runAsListener); } else { - authorizeAction(authentication, action, requestId, unwrappedRequest, authzInfo, listener); + authorizeAction(requestInfo, requestId, authzInfo, listener); } } - private void authorizeAction(final Authentication authentication, final String action, final String requestId, - final TransportRequest unwrappedRequest, final AuthorizationInfo authzInfo, + private void authorizeAction(final RequestInfo requestInfo, final String requestId, final AuthorizationInfo authzInfo, final ActionListener listener) { + final Authentication authentication = requestInfo.getAuthentication(); + final TransportRequest request = requestInfo.getRequest(); + 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, unwrappedRequest, authzInfo); + 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, unwrappedRequest, authzInfo)); + listener.onFailure(denial(requestId, authentication, action, request, authzInfo)); } }, e -> { // TODO need a failure handler better than this! - listener.onFailure(denial(requestId, authentication, action, unwrappedRequest, authzInfo, e)); + listener.onFailure(denial(requestId, authentication, action, request, authzInfo, e)); }), threadContext); - authzEngine.authorizeClusterAction(authentication, unwrappedRequest, action, authzInfo, clusterAuthzListener); + authzEngine.authorizeClusterAction(requestInfo, authzInfo, clusterAuthzListener); } else if (IndexPrivilege.ACTION_MATCHER.test(action)) { final MetaData metaData = clusterService.state().metaData(); final AsyncSupplier> authorizedIndicesSupplier = new CachingAsyncSupplier<>(authzIndicesListener -> - authzEngine.loadAuthorizedIndices(authentication, action, authzInfo, metaData.getAliasAndIndexLookup(), + authzEngine.loadAuthorizedIndices(requestInfo, authzInfo, metaData.getAliasAndIndexLookup(), authzIndicesListener)); final AsyncSupplier resolvedIndicesAsyncSupplier = new CachingAsyncSupplier<>((resolvedIndicesListener) -> { authorizedIndicesSupplier.get(ActionListener.wrap(authorizedIndices -> { - resolveIndexNames(unwrappedRequest, metaData, authorizedIndices, resolvedIndicesListener); + resolveIndexNames(request, metaData, authorizedIndices, resolvedIndicesListener); }, e -> { if (e instanceof IndexNotFoundException) { - auditTrail.accessDenied(requestId, authentication, action, unwrappedRequest, authzInfo); + auditTrail.accessDenied(requestId, authentication, action, request, authzInfo); listener.onFailure(e); } else { - listener.onFailure(denial(requestId, authentication, action, unwrappedRequest, authzInfo, e)); + listener.onFailure(denial(requestId, authentication, action, request, authzInfo, e)); } })); }); - authzEngine.authorizeIndexAction(authentication, unwrappedRequest, action, authzInfo, resolvedIndicesAsyncSupplier, + authzEngine.authorizeIndexAction(requestInfo, authzInfo, resolvedIndicesAsyncSupplier, metaData.getAliasAndIndexLookup()::get, ActionListener.wrap(indexAuthorizationResult -> { if (indexAuthorizationResult.isGranted()) { if (indexAuthorizationResult.getIndicesAccessControl() != null) { @@ -225,10 +232,11 @@ private void authorizeAction(final Authentication authentication, final String a } //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 unwrappedRequest instanceof CreateIndexRequest; - Set aliases = ((CreateIndexRequest) unwrappedRequest).aliases(); + assert request instanceof CreateIndexRequest; + Set aliases = ((CreateIndexRequest) request).aliases(); if (aliases.isEmpty() == false) { - authzEngine.authorizeIndexAction(authentication, unwrappedRequest, IndicesAliasesAction.NAME, authzInfo, + final RequestInfo aliasesRequestInfo = new RequestInfo(authentication, request, IndicesAliasesAction.NAME); + authzEngine.authorizeIndexAction(aliasesRequestInfo, authzInfo, ril -> { resolvedIndicesAsyncSupplier.get(ActionListener.wrap(resolvedIndices -> { List aliasesAndIndices = new ArrayList<>(resolvedIndices.getLocal()); @@ -243,15 +251,15 @@ private void authorizeAction(final Authentication authentication, final String a if (authorizationResult.isGranted()) { if (authorizationResult.isAuditable()) { auditTrail.accessGranted(requestId, authentication, IndicesAliasesAction.NAME, - unwrappedRequest, authzInfo); + request, authzInfo); } if (indexAuthorizationResult.isAuditable()) { - auditTrail.accessGranted(requestId, authentication, action, unwrappedRequest, authzInfo); + auditTrail.accessGranted(requestId, authentication, action, request, authzInfo); } listener.onResponse(null); } else { listener.onFailure(denial(requestId, authentication, IndicesAliasesAction.NAME, - unwrappedRequest, authzInfo)); + request, authzInfo)); } }, listener::onFailure)); } else { @@ -259,29 +267,28 @@ private void authorizeAction(final Authentication authentication, final String a } } else if (action.equals(TransportShardBulkAction.ACTION_NAME)) { // if this is performing multiple actions on the index, then check each of those actions. - assert unwrappedRequest instanceof BulkShardRequest - : "Action " + action + " requires " + BulkShardRequest.class + " but was " + unwrappedRequest.getClass(); + assert request instanceof BulkShardRequest + : "Action " + action + " requires " + BulkShardRequest.class + " but was " + request.getClass(); - authorizeBulkItems(authentication, (BulkShardRequest) unwrappedRequest, authzInfo, authzEngine, - resolvedIndicesAsyncSupplier, authorizedIndicesSupplier, metaData, requestId, - ActionListener.wrap(ignore -> { + authorizeBulkItems(requestInfo, authzInfo, authzEngine, resolvedIndicesAsyncSupplier, authorizedIndicesSupplier, + metaData, requestId, ActionListener.wrap(ignore -> { if (indexAuthorizationResult.isAuditable()) { - auditTrail.accessGranted(requestId, authentication, action, unwrappedRequest, authzInfo); + auditTrail.accessGranted(requestId, authentication, action, request, authzInfo); } listener.onResponse(null); }, listener::onFailure)); } else { if (indexAuthorizationResult.isAuditable()) { - auditTrail.accessGranted(requestId, authentication, action, unwrappedRequest, authzInfo); + auditTrail.accessGranted(requestId, authentication, action, request, authzInfo); } listener.onResponse(null); } } else { - listener.onFailure(denial(requestId, authentication, action, unwrappedRequest, authzInfo)); + listener.onFailure(denial(requestId, authentication, action, request, authzInfo)); } }, listener::onFailure)); } else { - listener.onFailure(denial(requestId, authentication, action, unwrappedRequest, authzInfo)); + listener.onFailure(denial(requestId, authentication, action, request, authzInfo)); } } @@ -336,16 +343,18 @@ private boolean isInternalUser(User user) { return SystemUser.is(user) || XPackUser.is(user) || XPackSecurityUser.is(user); } - private void authorizeRunAs(final Authentication authentication, final String action, final String requestId, - final TransportRequest request, final AuthorizationInfo authzInfo, + private void authorizeRunAs(final RequestInfo requestInfo, final String requestId, 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()); } else { final AuthorizationEngine runAsAuthzEngine = getRunAsAuthorizationEngine(authentication); - runAsAuthzEngine.authorizeRunAs(authentication, request, action, authzInfo, listener); + runAsAuthzEngine.authorizeRunAs(requestInfo, authzInfo, listener); } } @@ -363,10 +372,12 @@ private void authorizeRunAs(final Authentication authentication, final String ac * is very small, but the results must be cached, to avoid adding a high * overhead to each bulk request. */ - private void authorizeBulkItems(Authentication authentication, BulkShardRequest request, AuthorizationInfo authzInfo, + private void authorizeBulkItems(RequestInfo requestInfo, AuthorizationInfo authzInfo, AuthorizationEngine authzEngine, AsyncSupplier resolvedIndicesAsyncSupplier, AsyncSupplier> authorizedIndicesSupplier, MetaData metaData, String requestId, ActionListener listener) { + final Authentication authentication = requestInfo.getAuthentication(); + final BulkShardRequest request = (BulkShardRequest) requestInfo.getRequest(); // Maps original-index -> expanded-index-name (expands date-math, but not aliases) final Map resolvedIndexNames = new HashMap<>(); // Maps action -> resolved indices set @@ -436,7 +447,9 @@ private void authorizeBulkItems(Authentication authentication, BulkShardRequest new GroupedActionListener<>(bulkAuthzListener, actionToIndicesMap.size(), Collections.emptyList()), threadContext); actionToIndicesMap.forEach((bulkItemAction, indices) -> { - authzEngine.authorizeIndexAction(authentication, request, bulkItemAction, authzInfo, + final RequestInfo bulkItemInfo = + new RequestInfo(requestInfo.getAuthentication(), requestInfo.getRequest(), bulkItemAction); + authzEngine.authorizeIndexAction(bulkItemInfo, authzInfo, ril -> ril.onResponse(new ResolvedIndices(new ArrayList<>(indices), Collections.emptyList())), metaData.getAliasAndIndexLookup()::get, ActionListener.wrap(indexAuthorizationResult -> groupedActionListener.onResponse(new Tuple<>(bulkItemAction, indexAuthorizationResult)), 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 5c36106bab6cb..861975bcf4d6f 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 @@ -78,8 +78,8 @@ public class RBACEngine implements AuthorizationEngine { } @Override - public void resolveAuthorizationInfo(Authentication authentication, TransportRequest request, String action, - ActionListener listener) { + public void resolveAuthorizationInfo(RequestInfo requestInfo, ActionListener listener) { + final Authentication authentication = requestInfo.getAuthentication(); getRoles(authentication.getUser(), ActionListener.wrap(role -> { if (authentication.getUser().isRunAs()) { getRoles(authentication.getUser().authenticatedUser(), ActionListener.wrap( @@ -96,11 +96,11 @@ private void getRoles(User user, ActionListener listener) { } @Override - public void authorizeRunAs(Authentication authentication, TransportRequest request, String action, AuthorizationInfo authorizationInfo, - ActionListener listener) { + public void authorizeRunAs(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, ActionListener listener) { if (authorizationInfo instanceof RBACAuthorizationInfo) { final Role role = ((RBACAuthorizationInfo) authorizationInfo).getAuthenticatedUserAuthorizationInfo().getRole(); - listener.onResponse(new AuthorizationResult(role.runAs().check(authentication.getUser().principal()))); + listener.onResponse( + new AuthorizationResult(role.runAs().check(requestInfo.getAuthentication().getUser().principal()))); } else { listener.onFailure(new IllegalArgumentException("unsupported authorization info:" + authorizationInfo.getClass().getSimpleName())); @@ -108,13 +108,13 @@ public void authorizeRunAs(Authentication authentication, TransportRequest reque } @Override - public void authorizeClusterAction(Authentication authentication, TransportRequest request, String action, - AuthorizationInfo authorizationInfo, ActionListener listener) { + public void authorizeClusterAction(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, + ActionListener listener) { if (authorizationInfo instanceof RBACAuthorizationInfo) { final Role role = ((RBACAuthorizationInfo) authorizationInfo).getRole(); - if (role.cluster().check(action, request)) { + if (role.cluster().check(requestInfo.getAction(), requestInfo.getRequest())) { listener.onResponse(AuthorizationResult.granted()); - } else if (checkSameUserPermissions(action, request, authentication)) { + } else if (checkSameUserPermissions(requestInfo.getAction(), requestInfo.getRequest(), requestInfo.getAuthentication())) { listener.onResponse(AuthorizationResult.granted()); } else { listener.onResponse(AuthorizationResult.deny()); @@ -186,10 +186,13 @@ private static boolean shouldAuthorizeIndexActionNameOnly(String action, Transpo } @Override - public void authorizeIndexAction(Authentication authentication, TransportRequest request, String action, - AuthorizationInfo authorizationInfo, AsyncSupplier indicesAsyncSupplier, + public void authorizeIndexAction(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, + AsyncSupplier indicesAsyncSupplier, Function aliasOrIndexFunction, ActionListener listener) { + final String action = requestInfo.getAction(); + final TransportRequest request = requestInfo.getRequest(); + final Authentication authentication = requestInfo.getAuthentication(); if (TransportActionProxy.isProxyAction(action) || shouldAuthorizeIndexActionNameOnly(action, request)) { // we've already validated that the request is a proxy request so we can skip that but we still // need to validate that the action is allowed and then move on @@ -278,11 +281,11 @@ private void authorizeIndexActionName(String action, AuthorizationInfo authoriza } @Override - public void loadAuthorizedIndices(Authentication authentication, String action, AuthorizationInfo authorizationInfo, + public void loadAuthorizedIndices(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, Map aliasAndIndexLookup, ActionListener> listener) { if (authorizationInfo instanceof RBACAuthorizationInfo) { final Role role = ((RBACAuthorizationInfo) authorizationInfo).getRole(); - listener.onResponse(resolveAuthorizedIndicesFromRole(role, action, aliasAndIndexLookup)); + listener.onResponse(resolveAuthorizedIndicesFromRole(role, requestInfo.getAction(), aliasAndIndexLookup)); } else { listener.onFailure( new IllegalArgumentException("unsupported authorization info:" + authorizationInfo.getClass().getSimpleName())); From 83cde40f679bc4425d1802b911b596b06547dab3 Mon Sep 17 00:00:00 2001 From: Jay Modi Date: Tue, 22 Jan 2019 08:03:19 -0700 Subject: [PATCH 04/15] Add javadoc to the AuthorizationEngine interface (#37620) This commit adds javadocs to the AuthorizationEngine interface aimed at developers of an authorization engine. Additionally, some classes were also moved to the core project so that they are ready to be exposed once we allow authorization engines to be plugged in. --- .../security/authz/AuthorizationEngine.java | 281 ++++++++++++++++++ .../core/security/authz/ResolvedIndices.java | 111 +++++++ .../action/filter/SecurityActionFilter.java | 2 +- .../xpack/security/audit/AuditTrail.java | 2 +- .../security/audit/AuditTrailService.java | 2 +- .../security/audit/index/IndexAuditTrail.java | 2 +- .../audit/logfile/LoggingAuditTrail.java | 2 +- .../security/authc/AuthenticationService.java | 2 +- .../security/authz/AuthorizationEngine.java | 132 -------- .../security/authz/AuthorizationService.java | 27 +- .../authz/IndicesAndAliasesResolver.java | 98 +----- .../xpack/security/authz/RBACEngine.java | 7 +- .../SecuritySearchOperationListener.java | 2 +- .../audit/AuditTrailServiceTests.java | 2 +- .../index/IndexAuditTrailMutedTests.java | 2 +- .../audit/index/IndexAuditTrailTests.java | 2 +- .../logfile/LoggingAuditTrailFilterTests.java | 2 +- .../audit/logfile/LoggingAuditTrailTests.java | 2 +- .../authc/AuthenticationServiceTests.java | 2 +- .../authz/AuthorizationServiceTests.java | 2 +- .../authz/IndicesAndAliasesResolverTests.java | 2 +- .../SecuritySearchOperationListenerTests.java | 2 +- 22 files changed, 427 insertions(+), 261 deletions(-) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/ResolvedIndices.java delete mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationEngine.java 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 new file mode 100644 index 0000000000000..a0d7a85f6a579 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java @@ -0,0 +1,281 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.security.authz; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.cluster.metadata.AliasOrIndex; +import org.elasticsearch.transport.TransportRequest; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; +import org.elasticsearch.xpack.core.security.user.User; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +/** + *

+ * An AuthorizationEngine is responsible for making the core decisions about whether a request + * should be authorized or not. The engine can and usually will be called multiple times during + * the authorization of a request. Security categorizes requests into a few different buckets + * and uses the action name as the indicator of what a request will perform. Internally, the + * action name is used to map a {@link TransportRequest} to the actual + * {@link org.elasticsearch.action.support.TransportAction} that will handle the request. + *


+ *

+ * Requests can be a cluster request or an indices request. Cluster requests + * are requests that tend to be global in nature; they could affect the whole cluster. + * Indices requests are those that deal with specific indices; the actions could have the affect + * of reading data, modifying data, creating an index, deleting an index, or modifying metadata. + *


+ *

+ * Each call to the engine will contain a {@link RequestInfo} object that contains the request, + * action name, and the authentication associated with the request. This data is provided by the + * engine so that all information about the request can be used to make the authorization decision. + *


+ * The methods of the engine will be called in the following order: + *
    + *
  1. {@link #resolveAuthorizationInfo(RequestInfo, ActionListener)} to retrieve information + * necessary to authorize the given user. It is important to note that the {@link RequestInfo} + * may contain an {@link Authentication} object that actually has two users when the + * run as feature is used and this method should resolve the information for both. + * To check for the presence of run as, use the {@link User#isRunAs()} method on the user + * retrieved using the {@link Authentication#getUser()} method.
  2. + *
  3. {@link #authorizeRunAs(RequestInfo, AuthorizationInfo, ActionListener)} if the request + * is making use of the run as feature. This method is used to ensure the authenticated user + * can actually impersonate the user running the request.
  4. + *
  5. {@link #authorizeClusterAction(RequestInfo, AuthorizationInfo, ActionListener)} if the + * request is a cluster level operation.
  6. + *
  7. {@link #authorizeIndexAction(RequestInfo, AuthorizationInfo, AsyncSupplier, Function, ActionListener)} if + * the request is a an index action. This method may be called multiple times for a single + * request as the request may be made up of sub-requests that also need to be authorized. The async supplier + * for resolved indices will invoke the + * {@link #loadAuthorizedIndices(RequestInfo, AuthorizationInfo, Map, ActionListener)} method + * if it is used as part of the authorization process.
  8. + *
+ *

+ * NOTE: the {@link #loadAuthorizedIndices(RequestInfo, AuthorizationInfo, Map, ActionListener)} + * method may be called prior to {@link #authorizeIndexAction(RequestInfo, AuthorizationInfo, AsyncSupplier, Function, ActionListener)} + * in cases where wildcards need to be expanded. + *


+ * Authorization engines can be called from various threads including network threads that should + * not be blocked waiting for I/O. Network threads in elasticsearch are limited and we rely on + * asynchronous processing to ensure optimal use of network threads; this is unlike many other Java + * based servers that have a thread for each concurrent request and blocking operations could take + * place on those threads. Given this it is imperative that the implementations used here do not + * block when calling out to an external service or waiting on some data. + */ +public interface AuthorizationEngine { + + /** + * Asynchronously resolves any necessary information to authorize the given user(s). This could + * include retrieval of permissions from an index or external system. + * + * @param requestInfo object contain the request and associated information such as the action + * and associated user(s) + * @param listener the listener to be notified of success using {@link ActionListener#onResponse(Object)} + * or failure using {@link ActionListener#onFailure(Exception)} + */ + void resolveAuthorizationInfo(RequestInfo requestInfo, ActionListener listener); + + /** + * Asynchronously authorizes an attempt for a user to run as another user. + * + * @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 listener the listener to be notified of the authorization result + */ + void authorizeRunAs(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, ActionListener listener); + + /** + * Asynchronously authorizes a cluster action. + * + * @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 listener the listener to be notified of the authorization result + */ + void authorizeClusterAction(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, ActionListener listener); + + /** + * Asynchronously authorizes an action that operates on an index. The indices and aliases that + * the request is attempting to operate on can be retrieved using the {@link AsyncSupplier} for + * {@link ResolvedIndices}. The resolved indices will contain the exact list of indices and aliases + * that the request is attempting to take action on; in other words this supplier handles wildcard + * expansion and datemath expressions. + * + * @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 indicesAsyncSupplier the asynchronous supplier for the indices that this request is + * attempting to operate on + * @param aliasOrIndexFunction a function that when given a string name, returns the cluster + * metadata specific to that alias or index + * @param listener the listener to be notified of the authorization result + */ + void authorizeIndexAction(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, + AsyncSupplier indicesAsyncSupplier, Function aliasOrIndexFunction, + ActionListener listener); + + /** + * + * @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 aliasAndIndexLookup a function that when given a string name, returns the cluster + * metadata specific to that alias or index + * @param listener the listener to be notified of the authorization result + */ + void loadAuthorizedIndices(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, + Map aliasAndIndexLookup, ActionListener> listener); + + /** + * Interface for objects that contains the information needed to authorize a request + */ + interface AuthorizationInfo { + + /** + * @return a map representation of the authorization information. This map will be used to + * augment the data that is audited, so in the case of RBAC this map could contain the + * role names. + */ + Map asMap(); + + /** + * This method should be overridden in case of run as. Authorization info is only retrieved + * a single time and should represent the information to authorize both run as and the + * operation being performed. + */ + default AuthorizationInfo getAuthenticatedUserAuthorizationInfo() { + return this; + } + } + + /** + * Implementation of authorization info that is used in cases where we were not able to resolve + * the authorization info + */ + final class EmptyAuthorizationInfo implements AuthorizationInfo { + + public static final EmptyAuthorizationInfo INSTANCE = new EmptyAuthorizationInfo(); + + private EmptyAuthorizationInfo() {} + + @Override + public Map asMap() { + return Collections.emptyMap(); + } + } + + /** + * A class that encapsulates information about the request that is being authorized including + * the actual transport request, the authentication, and the action being invoked. + */ + final class RequestInfo { + + private final Authentication authentication; + private final TransportRequest request; + private final String action; + + public RequestInfo(Authentication authentication, TransportRequest request, String action) { + this.authentication = authentication; + this.request = request; + this.action = action; + } + + public String getAction() { + return action; + } + + public Authentication getAuthentication() { + return authentication; + } + + public TransportRequest getRequest() { + return request; + } + } + + /** + * Represents the result of authorization. This includes whether the actions should be granted + * and if this should be considered an auditable event. + */ + class AuthorizationResult { + + private final boolean granted; + private final boolean auditable; + + /** + * Create an authorization result with the provided granted value that is auditable + */ + public AuthorizationResult(boolean granted) { + this(granted, true); + } + + public AuthorizationResult(boolean granted, boolean auditable) { + this.granted = granted; + this.auditable = auditable; + } + + public boolean isGranted() { + return granted; + } + + public boolean isAuditable() { + return auditable; + } + + /** + * Returns a new authorization result that is granted and auditable + */ + public static AuthorizationResult granted() { + return new AuthorizationResult(true); + } + + /** + * Returns a new authorization result that is denied and auditable + */ + public static AuthorizationResult deny() { + return new AuthorizationResult(false); + } + } + + /** + * An extension of {@link AuthorizationResult} that is specific to index requests. Index requests + * need to return a {@link IndicesAccessControl} object representing the users permissions to indices + * that are being operated on. + */ + class IndexAuthorizationResult extends AuthorizationResult { + + private final IndicesAccessControl indicesAccessControl; + + public IndexAuthorizationResult(boolean auditable, IndicesAccessControl indicesAccessControl) { + super(indicesAccessControl == null || indicesAccessControl.isGranted(), auditable); + this.indicesAccessControl = indicesAccessControl; + } + + public IndicesAccessControl getIndicesAccessControl() { + return indicesAccessControl; + } + } + + @FunctionalInterface + interface AsyncSupplier { + + /** + * Asynchronously retrieves the value that is being supplied and notifies the listener upon + * completion. + */ + void getAsync(ActionListener listener); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/ResolvedIndices.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/ResolvedIndices.java new file mode 100644 index 0000000000000..f74a94cbaa665 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/ResolvedIndices.java @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.security.authz; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.elasticsearch.xpack.core.security.authz.IndicesAndAliasesResolverField.NO_INDEX_PLACEHOLDER; + +/** + * Stores a collection of index names separated into "local" and "remote". + * This allows the resolution and categorization to take place exactly once per-request. + */ +public final class ResolvedIndices { + private final List local; + private final List remote; + + public ResolvedIndices(List local, List remote) { + this.local = Collections.unmodifiableList(local); + this.remote = Collections.unmodifiableList(remote); + } + + /** + * Returns the collection of index names that have been stored as "local" indices. + * This is a List because order may be important. For example [ "a*" , "-a1" ] is interpreted differently + * to [ "-a1", "a*" ]. As a consequence, this list may contain duplicates. + */ + public List getLocal() { + return local; + } + + /** + * Returns the collection of index names that have been stored as "remote" indices. + */ + public List getRemote() { + return remote; + } + + /** + * @return true if both the {@link #getLocal() local} and {@link #getRemote() remote} index lists are empty. + */ + public boolean isEmpty() { + return local.isEmpty() && remote.isEmpty(); + } + + /** + * @return true if the {@link #getRemote() remote} index lists is empty, and the local index list contains the + * {@link IndicesAndAliasesResolverField#NO_INDEX_PLACEHOLDER no-index-placeholder} and nothing else. + */ + public boolean isNoIndicesPlaceholder() { + return remote.isEmpty() && local.size() == 1 && local.contains(NO_INDEX_PLACEHOLDER); + } + + public String[] toArray() { + final String[] array = new String[local.size() + remote.size()]; + int i = 0; + for (String index : local) { + array[i++] = index; + } + for (String index : remote) { + array[i++] = index; + } + return array; + } + + /** + * Builder class for ResolvedIndices that allows for the building of a list of indices + * without the need to construct new objects and merging them together + */ + public static class Builder { + + private final List local = new ArrayList<>(); + private final List remote = new ArrayList<>(); + + /** add a local index name */ + public void addLocal(String index) { + local.add(index); + } + + /** adds the array of local index names */ + public void addLocal(String[] indices) { + local.addAll(Arrays.asList(indices)); + } + + /** adds the list of local index names */ + public void addLocal(List indices) { + local.addAll(indices); + } + + /** adds the list of remote index names */ + public void addRemote(List indices) { + remote.addAll(indices); + } + + /** @return true if both the local and remote index lists are empty. */ + public boolean isEmpty() { + return local.isEmpty() && remote.isEmpty(); + } + + /** @return a immutable ResolvedIndices instance with the local and remote index lists */ + public ResolvedIndices build() { + return new ResolvedIndices(local, remote); + } + } +} 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 ec1c1cc10fce1..3ae21f21c71a8 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 @@ -35,7 +35,7 @@ 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.security.authz.AuthorizationEngine.AuthorizationInfo; +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; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditTrail.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditTrail.java index 40723bba21041..e99b822e1dca1 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditTrail.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditTrail.java @@ -10,7 +10,7 @@ import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; import org.elasticsearch.xpack.core.security.user.User; -import org.elasticsearch.xpack.security.authz.AuthorizationEngine.AuthorizationInfo; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo; import org.elasticsearch.xpack.security.transport.filter.SecurityIpFilterRule; import java.net.InetAddress; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditTrailService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditTrailService.java index 41c1f0787b3dd..38bb93d8bcf50 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditTrailService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditTrailService.java @@ -11,7 +11,7 @@ import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; import org.elasticsearch.xpack.core.security.user.User; -import org.elasticsearch.xpack.security.authz.AuthorizationEngine.AuthorizationInfo; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo; import org.elasticsearch.xpack.security.transport.filter.SecurityIpFilterRule; import java.net.InetAddress; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrail.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrail.java index 97043bfc3f945..7bea96b9e84c7 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrail.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrail.java @@ -61,7 +61,7 @@ import org.elasticsearch.xpack.core.template.TemplateUtils; import org.elasticsearch.xpack.security.audit.AuditLevel; import org.elasticsearch.xpack.security.audit.AuditTrail; -import org.elasticsearch.xpack.security.authz.AuthorizationEngine.AuthorizationInfo; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo; import org.elasticsearch.xpack.security.rest.RemoteHostHeader; import org.elasticsearch.xpack.security.support.SecurityIndexManager; import org.elasticsearch.xpack.security.transport.filter.SecurityIpFilterRule; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java index ea926ef6eb0a4..cc35e917153c5 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java @@ -34,7 +34,7 @@ import org.elasticsearch.xpack.core.security.user.XPackUser; import org.elasticsearch.xpack.security.audit.AuditLevel; import org.elasticsearch.xpack.security.audit.AuditTrail; -import org.elasticsearch.xpack.security.authz.AuthorizationEngine.AuthorizationInfo; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo; import org.elasticsearch.xpack.security.rest.RemoteHostHeader; import org.elasticsearch.xpack.security.transport.filter.IPFilter; import org.elasticsearch.xpack.security.transport.filter.SecurityIpFilterRule; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java index 542cea977be32..f5b7902ff0e5b 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java @@ -41,7 +41,7 @@ import org.elasticsearch.xpack.security.audit.AuditTrailService; import org.elasticsearch.xpack.security.audit.AuditUtil; import org.elasticsearch.xpack.security.authc.support.RealmUserLookup; -import org.elasticsearch.xpack.security.authz.AuthorizationEngine.EmptyAuthorizationInfo; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.EmptyAuthorizationInfo; import org.elasticsearch.xpack.security.support.SecurityIndexManager; import java.util.ArrayList; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationEngine.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationEngine.java deleted file mode 100644 index 3faffddc904c2..0000000000000 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationEngine.java +++ /dev/null @@ -1,132 +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.authz; - -import org.elasticsearch.action.ActionListener; -import org.elasticsearch.cluster.metadata.AliasOrIndex; -import org.elasticsearch.transport.TransportRequest; -import org.elasticsearch.xpack.core.security.authc.Authentication; -import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; -import org.elasticsearch.xpack.security.authz.IndicesAndAliasesResolver.ResolvedIndices; - -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.function.Function; - -public interface AuthorizationEngine { - - void resolveAuthorizationInfo(RequestInfo requestInfo, ActionListener listener); - - void authorizeRunAs(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, ActionListener listener); - - void authorizeClusterAction(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, ActionListener listener); - - void authorizeIndexAction(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, - AsyncSupplier indicesAsyncSupplier, Function aliasOrIndexFunction, - ActionListener listener); - - void loadAuthorizedIndices(RequestInfo requestInfo, AuthorizationInfo info, - Map aliasAndIndexLookup, ActionListener> listener); - - interface AuthorizationInfo { - - Map asMap(); - - default AuthorizationInfo getAuthenticatedUserAuthorizationInfo() { - return this; - } - } - - final class EmptyAuthorizationInfo implements AuthorizationInfo { - - public static final EmptyAuthorizationInfo INSTANCE = new EmptyAuthorizationInfo(); - - private EmptyAuthorizationInfo() {} - - @Override - public Map asMap() { - return Collections.emptyMap(); - } - } - - final class RequestInfo { - - private final Authentication authentication; - private final TransportRequest request; - private final String action; - - public RequestInfo(Authentication authentication, TransportRequest request, String action) { - this.authentication = authentication; - this.request = request; - this.action = action; - } - - public String getAction() { - return action; - } - - public Authentication getAuthentication() { - return authentication; - } - - public TransportRequest getRequest() { - return request; - } - } - - class AuthorizationResult { - - private final boolean granted; - private final boolean auditable; - - public AuthorizationResult(boolean granted) { - this(granted, true); - } - - public AuthorizationResult(boolean granted, boolean auditable) { - this.granted = granted; - this.auditable = auditable; - } - - public boolean isGranted() { - return granted; - } - - public boolean isAuditable() { - return auditable; - } - - public static AuthorizationResult granted() { - return new AuthorizationResult(true); - } - - public static AuthorizationResult deny() { - return new AuthorizationResult(false); - } - } - - class IndexAuthorizationResult extends AuthorizationResult { - - private final IndicesAccessControl indicesAccessControl; - - IndexAuthorizationResult(boolean auditable, IndicesAccessControl indicesAccessControl) { - super(indicesAccessControl == null || indicesAccessControl.isGranted(), auditable); - this.indicesAccessControl = indicesAccessControl; - } - - public IndicesAccessControl getIndicesAccessControl() { - return indicesAccessControl; - } - } - - @FunctionalInterface - interface AsyncSupplier { - - void get(ActionListener listener); - } -} 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 914d917c6e472..2e76b0cd622d5 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 @@ -36,7 +36,9 @@ import org.elasticsearch.xpack.core.security.authc.Authentication; 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.AuthorizationServiceField; +import org.elasticsearch.xpack.core.security.authz.ResolvedIndices; import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; @@ -47,13 +49,12 @@ 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.security.authz.AuthorizationEngine.AsyncSupplier; -import org.elasticsearch.xpack.security.authz.AuthorizationEngine.AuthorizationInfo; -import org.elasticsearch.xpack.security.authz.AuthorizationEngine.AuthorizationResult; -import org.elasticsearch.xpack.security.authz.AuthorizationEngine.EmptyAuthorizationInfo; -import org.elasticsearch.xpack.security.authz.AuthorizationEngine.IndexAuthorizationResult; -import org.elasticsearch.xpack.security.authz.AuthorizationEngine.RequestInfo; -import org.elasticsearch.xpack.security.authz.IndicesAndAliasesResolver.ResolvedIndices; +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.store.CompositeRolesStore; import java.util.ArrayList; @@ -212,7 +213,7 @@ private void authorizeAction(final RequestInfo requestInfo, final String request authzEngine.loadAuthorizedIndices(requestInfo, authzInfo, metaData.getAliasAndIndexLookup(), authzIndicesListener)); final AsyncSupplier resolvedIndicesAsyncSupplier = new CachingAsyncSupplier<>((resolvedIndicesListener) -> { - authorizedIndicesSupplier.get(ActionListener.wrap(authorizedIndices -> { + authorizedIndicesSupplier.getAsync(ActionListener.wrap(authorizedIndices -> { resolveIndexNames(request, metaData, authorizedIndices, resolvedIndicesListener); }, e -> { if (e instanceof IndexNotFoundException) { @@ -238,7 +239,7 @@ private void authorizeAction(final RequestInfo requestInfo, final String request final RequestInfo aliasesRequestInfo = new RequestInfo(authentication, request, IndicesAliasesAction.NAME); authzEngine.authorizeIndexAction(aliasesRequestInfo, authzInfo, ril -> { - resolvedIndicesAsyncSupplier.get(ActionListener.wrap(resolvedIndices -> { + resolvedIndicesAsyncSupplier.getAsync(ActionListener.wrap(resolvedIndices -> { List aliasesAndIndices = new ArrayList<>(resolvedIndices.getLocal()); for (Alias alias : aliases) { aliasesAndIndices.add(alias.name()); @@ -383,8 +384,8 @@ private void authorizeBulkItems(RequestInfo requestInfo, AuthorizationInfo authz // Maps action -> resolved indices set final Map> actionToIndicesMap = new HashMap<>(); - authorizedIndicesSupplier.get(ActionListener.wrap(authorizedIndices -> { - resolvedIndicesAsyncSupplier.get(ActionListener.wrap(overallResolvedIndices -> { + authorizedIndicesSupplier.getAsync(ActionListener.wrap(authorizedIndices -> { + resolvedIndicesAsyncSupplier.getAsync(ActionListener.wrap(overallResolvedIndices -> { final Set localIndices = new HashSet<>(overallResolvedIndices.getLocal()); for (BulkItemRequest item : request.items()) { String resolvedIndex = resolvedIndexNames.computeIfAbsent(item.index(), key -> { @@ -541,9 +542,9 @@ private CachingAsyncSupplier(AsyncSupplier supplier) { } @Override - public synchronized void get(ActionListener listener) { + public synchronized void getAsync(ActionListener listener) { if (value == null) { - asyncSupplier.get(ActionListener.wrap(loaded -> { + asyncSupplier.getAsync(ActionListener.wrap(loaded -> { value = loaded; listener.onResponse(value); }, listener::onFailure)); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java index 48c11655bb5f8..03c78ed903e81 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java @@ -28,7 +28,7 @@ import org.elasticsearch.protocol.xpack.graph.GraphExploreRequest; import org.elasticsearch.transport.RemoteClusterAware; import org.elasticsearch.transport.TransportRequest; -import org.elasticsearch.xpack.core.security.authz.IndicesAndAliasesResolverField; +import org.elasticsearch.xpack.core.security.authz.ResolvedIndices; import java.util.ArrayList; import java.util.Arrays; @@ -456,100 +456,4 @@ ResolvedIndices splitLocalAndRemoteIndexNames(String... indices) { } } - /** - * Stores a collection of index names separated into "local" and "remote". - * This allows the resolution and categorization to take place exactly once per-request. - */ - public static class ResolvedIndices { - private final List local; - private final List remote; - - ResolvedIndices(List local, List remote) { - this.local = Collections.unmodifiableList(local); - this.remote = Collections.unmodifiableList(remote); - } - - /** - * Returns the collection of index names that have been stored as "local" indices. - * This is a List because order may be important. For example [ "a*" , "-a1" ] is interpreted differently - * to [ "-a1", "a*" ]. As a consequence, this list may contain duplicates. - */ - public List getLocal() { - return local; - } - - /** - * Returns the collection of index names that have been stored as "remote" indices. - */ - public List getRemote() { - return remote; - } - - /** - * @return true if both the {@link #getLocal() local} and {@link #getRemote() remote} index lists are empty. - */ - public boolean isEmpty() { - return local.isEmpty() && remote.isEmpty(); - } - - /** - * @return true if the {@link #getRemote() remote} index lists is empty, and the local index list contains the - * {@link IndicesAndAliasesResolverField#NO_INDEX_PLACEHOLDER no-index-placeholder} and nothing else. - */ - public boolean isNoIndicesPlaceholder() { - return remote.isEmpty() && local.size() == 1 && local.contains(NO_INDEX_PLACEHOLDER); - } - - private String[] toArray() { - final String[] array = new String[local.size() + remote.size()]; - int i = 0; - for (String index : local) { - array[i++] = index; - } - for (String index : remote) { - array[i++] = index; - } - return array; - } - - /** - * Builder class for ResolvedIndices that allows for the building of a list of indices - * without the need to construct new objects and merging them together - */ - private static class Builder { - - private final List local = new ArrayList<>(); - private final List remote = new ArrayList<>(); - - /** add a local index name */ - private void addLocal(String index) { - local.add(index); - } - - /** adds the array of local index names */ - private void addLocal(String[] indices) { - local.addAll(Arrays.asList(indices)); - } - - /** adds the list of local index names */ - private void addLocal(List indices) { - local.addAll(indices); - } - - /** adds the list of remote index names */ - private void addRemote(List indices) { - remote.addAll(indices); - } - - /** @return true if both the local and remote index lists are empty. */ - private boolean isEmpty() { - return local.isEmpty() && remote.isEmpty(); - } - - /** @return a immutable ResolvedIndices instance with the local and remote index lists */ - private ResolvedIndices build() { - return new ResolvedIndices(local, remote); - } - } - } } 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 861975bcf4d6f..1c7273b90bc63 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 @@ -34,6 +34,8 @@ import org.elasticsearch.xpack.core.security.action.user.UserRequest; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine; +import org.elasticsearch.xpack.core.security.authz.ResolvedIndices; import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsCache; import org.elasticsearch.xpack.core.security.authz.permission.Role; @@ -42,7 +44,6 @@ import org.elasticsearch.xpack.core.security.support.Automatons; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; -import org.elasticsearch.xpack.security.authz.IndicesAndAliasesResolver.ResolvedIndices; import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; import org.elasticsearch.xpack.security.support.SecurityIndexManager; @@ -229,7 +230,7 @@ public void authorizeIndexAction(RequestInfo requestInfo, AuthorizationInfo auth } else if (request instanceof IndicesRequest && IndicesAndAliasesResolver.allowsRemoteIndices((IndicesRequest) request)) { // remote indices are allowed - indicesAsyncSupplier.get(ActionListener.wrap(resolvedIndices -> { + indicesAsyncSupplier.getAsync(ActionListener.wrap(resolvedIndices -> { assert !resolvedIndices.isEmpty() : "every indices request needs to have its indices set thus the resolved indices must not be empty"; //all wildcard expressions have been resolved and only the security plugin could have set '-*' here. @@ -246,7 +247,7 @@ public void authorizeIndexAction(RequestInfo requestInfo, AuthorizationInfo auth authorizeIndexActionName(action, authorizationInfo, IndicesAccessControl.ALLOW_NO_INDICES, ActionListener.wrap(indexAuthorizationResult -> { if (indexAuthorizationResult.isGranted()) { - indicesAsyncSupplier.get(ActionListener.wrap(resolvedIndices -> { + indicesAsyncSupplier.getAsync(ActionListener.wrap(resolvedIndices -> { assert !resolvedIndices.isEmpty() : "every indices request needs to have its indices set thus the resolved indices must not be empty"; //all wildcard expressions have been resolved and only the security plugin could have set '-*' here. diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/SecuritySearchOperationListener.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/SecuritySearchOperationListener.java index c38053fcf30aa..5e0c2945caadc 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/SecuritySearchOperationListener.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/SecuritySearchOperationListener.java @@ -16,7 +16,7 @@ import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.AuthenticationField; import org.elasticsearch.xpack.security.audit.AuditUtil; -import org.elasticsearch.xpack.security.authz.AuthorizationEngine.AuthorizationInfo; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo; import static org.elasticsearch.xpack.security.authz.AuthorizationService.AUTHORIZATION_INFO_KEY; import static org.elasticsearch.xpack.security.authz.AuthorizationService.ORIGINATING_ACTION_KEY; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/AuditTrailServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/AuditTrailServiceTests.java index fe79ec099f363..fb194ecefc671 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/AuditTrailServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/AuditTrailServiceTests.java @@ -13,7 +13,7 @@ import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; import org.elasticsearch.xpack.core.security.user.User; -import org.elasticsearch.xpack.security.authz.AuthorizationEngine.AuthorizationInfo; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo; import org.elasticsearch.xpack.security.transport.filter.IPFilter; import org.elasticsearch.xpack.security.transport.filter.SecurityIpFilterRule; import org.junit.Before; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailMutedTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailMutedTests.java index de541f84061a2..a055840b29c59 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailMutedTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailMutedTests.java @@ -28,7 +28,7 @@ import org.elasticsearch.xpack.core.security.user.SystemUser; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.audit.index.IndexAuditTrail.State; -import org.elasticsearch.xpack.security.authz.AuthorizationEngine.EmptyAuthorizationInfo; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.EmptyAuthorizationInfo; import org.elasticsearch.xpack.security.transport.filter.SecurityIpFilterRule; import org.junit.After; import org.junit.Before; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailTests.java index e7626ac979a4d..765a70f8120d3 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailTests.java @@ -62,7 +62,7 @@ import org.elasticsearch.xpack.security.LocalStateSecurity; import org.elasticsearch.xpack.security.audit.index.IndexAuditTrail.Field; import org.elasticsearch.xpack.security.audit.index.IndexAuditTrail.Message; -import org.elasticsearch.xpack.security.authz.AuthorizationEngine.AuthorizationInfo; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo; import org.elasticsearch.xpack.security.support.SecurityIndexManager; import org.elasticsearch.xpack.security.transport.filter.IPFilter; import org.elasticsearch.xpack.security.transport.filter.SecurityIpFilterRule; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailFilterTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailFilterTests.java index ae2b6cad3204b..29a17d51ba7cc 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailFilterTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailFilterTests.java @@ -32,7 +32,7 @@ import org.elasticsearch.xpack.security.audit.logfile.LoggingAuditTrail.AuditEventMetaInfo; import org.elasticsearch.xpack.security.audit.logfile.LoggingAuditTrailTests.MockMessage; import org.elasticsearch.xpack.security.audit.logfile.LoggingAuditTrailTests.RestContent; -import org.elasticsearch.xpack.security.authz.AuthorizationEngine.AuthorizationInfo; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo; import org.elasticsearch.xpack.security.rest.RemoteHostHeader; import org.elasticsearch.xpack.security.transport.filter.SecurityIpFilterRule; import org.junit.Before; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java index 1446675535417..55d5bd579c12d 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java @@ -40,7 +40,7 @@ import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.audit.AuditTrail; import org.elasticsearch.xpack.security.audit.AuditUtil; -import org.elasticsearch.xpack.security.authz.AuthorizationEngine.AuthorizationInfo; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo; import org.elasticsearch.xpack.security.rest.RemoteHostHeader; import org.elasticsearch.xpack.security.transport.filter.IPFilter; import org.elasticsearch.xpack.security.transport.filter.SecurityIpFilterRule; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java index 74aae8119ca20..cf334619f5b48 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java @@ -64,7 +64,7 @@ import org.elasticsearch.xpack.security.audit.AuditUtil; import org.elasticsearch.xpack.security.authc.AuthenticationService.Authenticator; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; -import org.elasticsearch.xpack.security.authz.AuthorizationEngine.EmptyAuthorizationInfo; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.EmptyAuthorizationInfo; import org.elasticsearch.xpack.security.support.SecurityIndexManager; import org.junit.After; import org.junit.Before; 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 02da62c6ebec9..778865c15d268 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 @@ -111,7 +111,7 @@ import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.audit.AuditTrailService; import org.elasticsearch.xpack.security.audit.AuditUtil; -import org.elasticsearch.xpack.security.authz.AuthorizationEngine.AuthorizationInfo; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo; import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; import org.elasticsearch.xpack.security.authz.store.NativePrivilegeStore; import org.elasticsearch.xpack.sql.action.SqlQueryAction; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java index 2f1bc047554fd..44e8cf6d3e24c 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java @@ -52,6 +52,7 @@ import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.xpack.core.graph.action.GraphExploreAction; import org.elasticsearch.xpack.core.security.authz.IndicesAndAliasesResolverField; +import org.elasticsearch.xpack.core.security.authz.ResolvedIndices; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.IndicesPrivileges; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsCache; @@ -60,7 +61,6 @@ import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.user.XPackSecurityUser; import org.elasticsearch.xpack.core.security.user.XPackUser; -import org.elasticsearch.xpack.security.authz.IndicesAndAliasesResolver.ResolvedIndices; import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; import org.elasticsearch.xpack.security.support.SecurityIndexManager; import org.elasticsearch.xpack.security.test.SecurityTestUtils; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/SecuritySearchOperationListenerTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/SecuritySearchOperationListenerTests.java index b4cf84f414ada..5b73d6d212fc5 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/SecuritySearchOperationListenerTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/SecuritySearchOperationListenerTests.java @@ -24,7 +24,7 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationField; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.audit.AuditTrailService; -import org.elasticsearch.xpack.security.authz.AuthorizationEngine.AuthorizationInfo; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo; import java.util.Collections; From b9a2c81ae3a3446dbd7bee4fe601ab4de8bec7b2 Mon Sep 17 00:00:00 2001 From: jaymode Date: Tue, 22 Jan 2019 09:48:11 -0700 Subject: [PATCH 05/15] Fix resolving restricted indices after merging --- .../org/elasticsearch/xpack/security/authz/RBACEngine.java | 5 ----- 1 file changed, 5 deletions(-) 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 1c7273b90bc63..4dc855eddc409 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 @@ -304,11 +304,6 @@ static List resolveAuthorizedIndicesFromRole(Role role, String action, M indicesAndAliases.add(aliasOrIndex); } } - - if (Arrays.asList(role.names()).contains(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName()) == false) { - // we should filter out all of the security indices from wildcards - indicesAndAliases.removeAll(SecurityIndexManager.indexNames()); - } return Collections.unmodifiableList(indicesAndAliases); } From d628008d987ee50151655387899bce0e41989db6 Mon Sep 17 00:00:00 2001 From: jaymode Date: Tue, 29 Jan 2019 10:06:09 -0700 Subject: [PATCH 06/15] fix RBACEngine after restricted indices changes --- .../xpack/security/authz/RBACEngine.java | 35 +------------------ 1 file changed, 1 insertion(+), 34 deletions(-) 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 4dc855eddc409..c68fc7d0ef6ba 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,8 +6,6 @@ package org.elasticsearch.xpack.security.authz; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.CompositeIndicesRequest; import org.elasticsearch.action.IndicesRequest; @@ -39,16 +37,12 @@ import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsCache; import org.elasticsearch.xpack.core.security.authz.permission.Role; -import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; -import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; import org.elasticsearch.xpack.core.security.support.Automatons; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; -import org.elasticsearch.xpack.security.support.SecurityIndexManager; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; @@ -62,14 +56,11 @@ public class RBACEngine implements AuthorizationEngine { private static final Predicate SAME_USER_PRIVILEGE = Automatons.predicate( ChangePasswordAction.NAME, AuthenticateAction.NAME, HasPrivilegesAction.NAME, GetUserPrivilegesAction.NAME); - private static final Predicate MONITOR_INDEX_PREDICATE = IndexPrivilege.MONITOR.predicate(); private static final String INDEX_SUB_REQUEST_PRIMARY = IndexAction.NAME + "[p]"; private static final String INDEX_SUB_REQUEST_REPLICA = IndexAction.NAME + "[r]"; private static final String DELETE_SUB_REQUEST_PRIMARY = DeleteAction.NAME + "[p]"; private static final String DELETE_SUB_REQUEST_REPLICA = DeleteAction.NAME + "[r]"; - private static final Logger logger = LogManager.getLogger(RBACEngine.class); - private final CompositeRolesStore rolesStore; private final FieldPermissionsCache fieldPermissionsCache; @@ -314,37 +305,13 @@ private void buildIndicesAccessControl(Authentication authentication, String act if (authorizationInfo instanceof RBACAuthorizationInfo) { final Role role = ((RBACAuthorizationInfo) authorizationInfo).getRole(); final IndicesAccessControl accessControl = role.authorize(action, indices, aliasAndIndexLookup, fieldPermissionsCache); - if (accessControl.isGranted() && hasSecurityIndexAccess(accessControl) && MONITOR_INDEX_PREDICATE.test(action) == false - && isSuperuser(authentication.getUser()) == false) { - // only superusers are allowed to work with this index, but we should allow indices monitoring actions through - // for debugging - // purposes. These monitor requests also sometimes resolve indices concretely and then requests them - logger.debug("user [{}] attempted to directly perform [{}] against the security index [{}]", - authentication.getUser().principal(), action, SecurityIndexManager.SECURITY_INDEX_NAME); - listener.onResponse(new IndexAuthorizationResult(true, new IndicesAccessControl(false, Collections.emptyMap()))); - } else { - listener.onResponse(new IndexAuthorizationResult(true, accessControl)); - } + listener.onResponse(new IndexAuthorizationResult(true, accessControl)); } else { listener.onFailure(new IllegalArgumentException("unsupported authorization info:" + authorizationInfo.getClass().getSimpleName())); } } - private static boolean hasSecurityIndexAccess(IndicesAccessControl indicesAccessControl) { - for (String index : SecurityIndexManager.indexNames()) { - final IndicesAccessControl.IndexAccessControl indexPermissions = indicesAccessControl.getIndexPermissions(index); - if (indexPermissions != null && indexPermissions.isGranted()) { - return true; - } - } - return false; - } - - private static boolean isSuperuser(User user) { - return Arrays.asList(user.roles()).contains(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName()); - } - private static boolean checkChangePasswordAction(Authentication authentication) { // we need to verify that this user was authenticated by or looked up by a realm type that support password changes // otherwise we open ourselves up to issues where a user in a different realm could be created with the same username From 3280607410d67a53dbc39447e915d1a61d373f54 Mon Sep 17 00:00:00 2001 From: Jay Modi Date: Tue, 29 Jan 2019 14:17:37 -0700 Subject: [PATCH 07/15] Allow authorization engines as an extension (#37785) Authorization engines can now be registered by implementing a plugin, which also has a service implementation of a security extension. Only one extension may register an authorization engine and this engine will be used for all users except reserved realm users and internal users. --- build.gradle | 3 + .../build.gradle | 46 +++++ .../example/AuthorizationEnginePlugin.java | 30 +++ .../example/CustomAuthorizationEngine.java | 131 +++++++++++++ .../ExampleAuthorizationEngineExtension.java | 35 ++++ ...arch.xpack.core.security.SecurityExtension | 1 + .../example/CustomAuthorizationEngineIT.java | 163 ++++++++++++++++ .../CustomAuthorizationEngineTests.java | 177 ++++++++++++++++++ x-pack/docs/build.gradle | 1 - ...asciidoc => custom-authorization.asciidoc} | 91 ++++++--- .../authorization/managing-roles.asciidoc | 2 +- .../core/security/SecurityExtension.java | 13 ++ .../xpack/security/Security.java | 22 ++- .../security/authz/AuthorizationService.java | 33 ++-- .../authz/AuthorizationServiceTests.java | 79 +++++++- .../build.gradle | 2 +- 16 files changed, 781 insertions(+), 48 deletions(-) create mode 100644 plugins/examples/security-authorization-engine/build.gradle create mode 100644 plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/AuthorizationEnginePlugin.java create mode 100644 plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java create mode 100644 plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/ExampleAuthorizationEngineExtension.java create mode 100644 plugins/examples/security-authorization-engine/src/main/resources/META-INF/services/org.elasticsearch.xpack.core.security.SecurityExtension create mode 100644 plugins/examples/security-authorization-engine/src/test/java/org/elasticsearch/example/CustomAuthorizationEngineIT.java create mode 100644 plugins/examples/security-authorization-engine/src/test/java/org/elasticsearch/example/CustomAuthorizationEngineTests.java rename x-pack/docs/en/security/authorization/{custom-roles-provider.asciidoc => custom-authorization.asciidoc} (51%) diff --git a/build.gradle b/build.gradle index 4bd211a12b3b0..cd9aed4a259d3 100644 --- a/build.gradle +++ b/build.gradle @@ -232,6 +232,9 @@ allprojects { "org.elasticsearch.plugin:aggs-matrix-stats-client:${version}": ':modules:aggs-matrix-stats', "org.elasticsearch.plugin:percolator-client:${version}": ':modules:percolator', "org.elasticsearch.plugin:rank-eval-client:${version}": ':modules:rank-eval', + // for security example plugins + "org.elasticsearch.plugin:x-pack-core:${version}": ':x-pack:plugin:core', + "org.elasticsearch.client.x-pack-transport:${version}": ':x-pack:transport-client' ] /* diff --git a/plugins/examples/security-authorization-engine/build.gradle b/plugins/examples/security-authorization-engine/build.gradle new file mode 100644 index 0000000000000..d0d227e221b68 --- /dev/null +++ b/plugins/examples/security-authorization-engine/build.gradle @@ -0,0 +1,46 @@ +apply plugin: 'elasticsearch.esplugin' + +esplugin { + name 'security-authorization-engine' + description 'An example spi extension plugin for security that implements an Authorization Engine' + classname 'org.elasticsearch.example.AuthorizationEnginePlugin' + extendedPlugins = ['x-pack-security'] +} + +dependencies { + compileOnly "org.elasticsearch.plugin:x-pack-core:${version}" + testCompile "org.elasticsearch.client.x-pack-transport:${version}" +} + + +integTestRunner { + systemProperty 'tests.security.manager', 'false' +} + +integTestCluster { + dependsOn buildZip + setting 'xpack.security.enabled', 'true' + setting 'xpack.ilm.enabled', 'false' + setting 'xpack.ml.enabled', 'false' + setting 'xpack.monitoring.enabled', 'false' + setting 'xpack.license.self_generated.type', 'trial' + + // This is important, so that all the modules are available too. + // There are index templates that use token filters that are in analysis-module and + // processors are being used that are in ingest-common module. + distribution = 'default' + + setupCommand 'setupDummyUser', + 'bin/elasticsearch-users', 'useradd', 'test_user', '-p', 'x-pack-test-password', '-r', 'custom_superuser' + waitCondition = { node, ant -> + File tmpFile = new File(node.cwd, 'wait.success') + ant.get(src: "http://${node.httpUri()}/_cluster/health?wait_for_nodes=>=${numNodes}&wait_for_status=yellow", + dest: tmpFile.toString(), + username: 'test_user', + password: 'x-pack-test-password', + ignoreerrors: true, + retries: 10) + return tmpFile.exists() + } +} +check.dependsOn integTest diff --git a/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/AuthorizationEnginePlugin.java b/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/AuthorizationEnginePlugin.java new file mode 100644 index 0000000000000..1878bb90a0c85 --- /dev/null +++ b/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/AuthorizationEnginePlugin.java @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.example; + +import org.elasticsearch.plugins.ActionPlugin; +import org.elasticsearch.plugins.Plugin; + +/** + * Plugin class that is required so that the code contained here may be loaded as a plugin. + * Additional items such as settings and actions can be registered using this plugin class. + */ +public class AuthorizationEnginePlugin extends Plugin implements ActionPlugin { +} 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 new file mode 100644 index 0000000000000..84f9fddf738da --- /dev/null +++ b/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java @@ -0,0 +1,131 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.example; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.cluster.metadata.AliasOrIndex; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine; +import org.elasticsearch.xpack.core.security.authz.ResolvedIndices; +import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; +import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl.IndexAccessControl; +import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions; +import org.elasticsearch.xpack.core.security.user.User; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +/** + * A custom implementation of an authorization engine. This engine is extremely basic in that it + * authorizes based upon the name of a single role. If users have this role they are granted access. + */ +public class CustomAuthorizationEngine implements AuthorizationEngine { + + @Override + public void resolveAuthorizationInfo(RequestInfo requestInfo, ActionListener listener) { + final Authentication authentication = requestInfo.getAuthentication(); + if (authentication.getUser().isRunAs()) { + final CustomAuthorizationInfo authenticatedUserAuthzInfo = + new CustomAuthorizationInfo(authentication.getUser().authenticatedUser().roles(), null); + listener.onResponse(new CustomAuthorizationInfo(authentication.getUser().roles(), authenticatedUserAuthzInfo)); + } else { + listener.onResponse(new CustomAuthorizationInfo(authentication.getUser().roles(), null)); + } + } + + @Override + public void authorizeRunAs(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, ActionListener listener) { + if (isSuperuser(requestInfo.getAuthentication().getUser().authenticatedUser())) { + listener.onResponse(AuthorizationResult.granted()); + } else { + listener.onResponse(AuthorizationResult.deny()); + } + } + + @Override + public void authorizeClusterAction(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, + ActionListener listener) { + if (isSuperuser(requestInfo.getAuthentication().getUser())) { + listener.onResponse(AuthorizationResult.granted()); + } else { + listener.onResponse(AuthorizationResult.deny()); + } + } + + @Override + public void authorizeIndexAction(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, + AsyncSupplier indicesAsyncSupplier, + Function aliasOrIndexFunction, + ActionListener listener) { + if (isSuperuser(requestInfo.getAuthentication().getUser())) { + indicesAsyncSupplier.getAsync(ActionListener.wrap(resolvedIndices -> { + Map indexAccessControlMap = new HashMap<>(); + for (String name : resolvedIndices.getLocal()) { + indexAccessControlMap.put(name, new IndexAccessControl(true, FieldPermissions.DEFAULT, null)); + } + IndicesAccessControl indicesAccessControl = + new IndicesAccessControl(true, Collections.unmodifiableMap(indexAccessControlMap)); + listener.onResponse(new IndexAuthorizationResult(true, indicesAccessControl)); + }, listener::onFailure)); + } else { + listener.onResponse(new IndexAuthorizationResult(true, IndicesAccessControl.DENIED)); + } + } + + @Override + public void loadAuthorizedIndices(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, + Map aliasAndIndexLookup, ActionListener> listener) { + if (isSuperuser(requestInfo.getAuthentication().getUser())) { + listener.onResponse(new ArrayList<>(aliasAndIndexLookup.keySet())); + } else { + listener.onResponse(Collections.emptyList()); + } + } + + public static class CustomAuthorizationInfo implements AuthorizationInfo { + + private final String[] roles; + private final CustomAuthorizationInfo authenticatedAuthzInfo; + + CustomAuthorizationInfo(String[] roles, CustomAuthorizationInfo authenticatedAuthzInfo) { + this.roles = roles; + this.authenticatedAuthzInfo = authenticatedAuthzInfo; + } + + @Override + public Map asMap() { + return Collections.singletonMap("roles", roles); + } + + @Override + public CustomAuthorizationInfo getAuthenticatedUserAuthorizationInfo() { + return authenticatedAuthzInfo; + } + } + + private boolean isSuperuser(User user) { + return Arrays.binarySearch(user.roles(), "custom_superuser") > -1; + } +} diff --git a/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/ExampleAuthorizationEngineExtension.java b/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/ExampleAuthorizationEngineExtension.java new file mode 100644 index 0000000000000..cba064fae27ad --- /dev/null +++ b/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/ExampleAuthorizationEngineExtension.java @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.example; + +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.xpack.core.security.SecurityExtension; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine; + +/** + * Security extension class that registers the custom authorization engine to be used + */ +public class ExampleAuthorizationEngineExtension implements SecurityExtension { + + @Override + public AuthorizationEngine getAuthorizationEngine(Settings settings) { + return new CustomAuthorizationEngine(); + } +} diff --git a/plugins/examples/security-authorization-engine/src/main/resources/META-INF/services/org.elasticsearch.xpack.core.security.SecurityExtension b/plugins/examples/security-authorization-engine/src/main/resources/META-INF/services/org.elasticsearch.xpack.core.security.SecurityExtension new file mode 100644 index 0000000000000..73029aef8fd63 --- /dev/null +++ b/plugins/examples/security-authorization-engine/src/main/resources/META-INF/services/org.elasticsearch.xpack.core.security.SecurityExtension @@ -0,0 +1 @@ +org.elasticsearch.example.ExampleAuthorizationEngineExtension \ No newline at end of file diff --git a/plugins/examples/security-authorization-engine/src/test/java/org/elasticsearch/example/CustomAuthorizationEngineIT.java b/plugins/examples/security-authorization-engine/src/test/java/org/elasticsearch/example/CustomAuthorizationEngineIT.java new file mode 100644 index 0000000000000..9daf9bd01a8bc --- /dev/null +++ b/plugins/examples/security-authorization-engine/src/test/java/org/elasticsearch/example/CustomAuthorizationEngineIT.java @@ -0,0 +1,163 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.example; + +import org.apache.http.util.EntityUtils; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.Response; +import org.elasticsearch.client.ResponseException; +import org.elasticsearch.common.network.NetworkModule; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.test.ESIntegTestCase; +import org.elasticsearch.xpack.core.XPackClientPlugin; +import org.elasticsearch.xpack.core.security.authc.support.Hasher; +import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; +import org.elasticsearch.xpack.core.security.client.SecurityClient; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Collection; +import java.util.Collections; + +import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; + +/** + * Integration tests for the custom authorization engine. These tests are meant to be run against + * an external cluster with the custom authorization plugin installed to validate the functionality + * when running as a plugin + */ +public class CustomAuthorizationEngineIT extends ESIntegTestCase { + + @Override + protected Settings externalClusterClientSettings() { + final String token = "Basic " + + Base64.getEncoder().encodeToString(("test_user:x-pack-test-password").getBytes(StandardCharsets.UTF_8)); + return Settings.builder() + .put(ThreadContext.PREFIX + ".Authorization", token) + .put(NetworkModule.TRANSPORT_TYPE_KEY, "security4") + .build(); + } + + @Override + protected Collection> transportClientPlugins() { + return Collections.singleton(XPackClientPlugin.class); + } + + public void testClusterAction() throws IOException { + SecurityClient securityClient = new SecurityClient(client()); + securityClient.preparePutUser("custom_user", "x-pack-test-password".toCharArray(), Hasher.BCRYPT, "custom_superuser").get(); + + { + RequestOptions.Builder options = RequestOptions.DEFAULT.toBuilder(); + options.addHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, + basicAuthHeaderValue("custom_user", new SecureString("x-pack-test-password".toCharArray()))); + Request request = new Request("GET", "_cluster/health"); + request.setOptions(options); + Response response = getRestClient().performRequest(request); + assertThat(response.getStatusLine().getStatusCode(), is(200)); + } + + { + securityClient.preparePutUser("custom_user2", "x-pack-test-password".toCharArray(), Hasher.BCRYPT, "not_superuser").get(); + RequestOptions.Builder options = RequestOptions.DEFAULT.toBuilder(); + options.addHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, + basicAuthHeaderValue("custom_user2", new SecureString("x-pack-test-password".toCharArray()))); + Request request = new Request("GET", "_cluster/health"); + request.setOptions(options); + ResponseException e = expectThrows(ResponseException.class, () -> getRestClient().performRequest(request)); + assertThat(e.getResponse().getStatusLine().getStatusCode(), is(403)); + } + } + + public void testIndexAction() throws IOException { + SecurityClient securityClient = new SecurityClient(client()); + securityClient.preparePutUser("custom_user", "x-pack-test-password".toCharArray(), Hasher.BCRYPT, "custom_superuser").get(); + + { + RequestOptions.Builder options = RequestOptions.DEFAULT.toBuilder(); + options.addHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, + basicAuthHeaderValue("custom_user", new SecureString("x-pack-test-password".toCharArray()))); + Request request = new Request("PUT", "/index"); + request.setOptions(options); + Response response = getRestClient().performRequest(request); + assertThat(response.getStatusLine().getStatusCode(), is(200)); + } + + { + securityClient.preparePutUser("custom_user2", "x-pack-test-password".toCharArray(), Hasher.BCRYPT, "not_superuser").get(); + RequestOptions.Builder options = RequestOptions.DEFAULT.toBuilder(); + options.addHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, + basicAuthHeaderValue("custom_user2", new SecureString("x-pack-test-password".toCharArray()))); + Request request = new Request("PUT", "/index"); + request.setOptions(options); + ResponseException e = expectThrows(ResponseException.class, () -> getRestClient().performRequest(request)); + assertThat(e.getResponse().getStatusLine().getStatusCode(), is(403)); + } + } + + public void testRunAs() throws IOException { + SecurityClient securityClient = new SecurityClient(client()); + securityClient.preparePutUser("custom_user", "x-pack-test-password".toCharArray(), Hasher.BCRYPT, "custom_superuser").get(); + securityClient.preparePutUser("custom_user2", "x-pack-test-password".toCharArray(), Hasher.BCRYPT, "custom_superuser").get(); + securityClient.preparePutUser("custom_user3", "x-pack-test-password".toCharArray(), Hasher.BCRYPT, "not_superuser").get(); + + { + RequestOptions.Builder options = RequestOptions.DEFAULT.toBuilder(); + options.addHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, + basicAuthHeaderValue("custom_user", new SecureString("x-pack-test-password".toCharArray()))); + options.addHeader("es-security-runas-user", "custom_user2"); + Request request = new Request("GET", "/_security/_authenticate"); + request.setOptions(options); + Response response = getRestClient().performRequest(request); + assertThat(response.getStatusLine().getStatusCode(), is(200)); + String responseStr = EntityUtils.toString(response.getEntity()); + assertThat(responseStr, containsString("custom_user2")); + } + + { + RequestOptions.Builder options = RequestOptions.DEFAULT.toBuilder(); + options.addHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, + basicAuthHeaderValue("custom_user", new SecureString("x-pack-test-password".toCharArray()))); + options.addHeader("es-security-runas-user", "custom_user3"); + Request request = new Request("PUT", "/index"); + request.setOptions(options); + ResponseException e = expectThrows(ResponseException.class, () -> getRestClient().performRequest(request)); + assertThat(e.getResponse().getStatusLine().getStatusCode(), is(403)); + } + + { + RequestOptions.Builder options = RequestOptions.DEFAULT.toBuilder(); + options.addHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, + basicAuthHeaderValue("custom_user3", new SecureString("x-pack-test-password".toCharArray()))); + options.addHeader("es-security-runas-user", "custom_user2"); + Request request = new Request("PUT", "/index"); + request.setOptions(options); + ResponseException e = expectThrows(ResponseException.class, () -> getRestClient().performRequest(request)); + assertThat(e.getResponse().getStatusLine().getStatusCode(), is(403)); + } + } +} diff --git a/plugins/examples/security-authorization-engine/src/test/java/org/elasticsearch/example/CustomAuthorizationEngineTests.java b/plugins/examples/security-authorization-engine/src/test/java/org/elasticsearch/example/CustomAuthorizationEngineTests.java new file mode 100644 index 0000000000000..e24e490767988 --- /dev/null +++ b/plugins/examples/security-authorization-engine/src/test/java/org/elasticsearch/example/CustomAuthorizationEngineTests.java @@ -0,0 +1,177 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.example; + +import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.cluster.metadata.AliasOrIndex.Index; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.transport.TransportRequest; +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.AuthorizationInfo; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationResult; +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.ResolvedIndices; +import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; +import org.elasticsearch.xpack.core.security.user.User; + +import java.util.Collections; + +import static org.hamcrest.Matchers.is; + +/** + * Unit tests for the custom authorization engine. These are basic tests that validate the + * engine's functionality outside of being used by the AuthorizationService + */ +public class CustomAuthorizationEngineTests extends ESTestCase { + + public void testGetAuthorizationInfo() { + PlainActionFuture future = new PlainActionFuture<>(); + CustomAuthorizationEngine engine = new CustomAuthorizationEngine(); + engine.resolveAuthorizationInfo(getRequestInfo(), future); + assertNotNull(future.actionGet()); + } + + public void testAuthorizeRunAs() { + final String action = "cluster:monitor/foo"; + final TransportRequest request = new TransportRequest() {}; + CustomAuthorizationEngine engine = new CustomAuthorizationEngine(); + // unauthorized + { + Authentication authentication = + new Authentication(new User("joe", new String[]{"custom_superuser"}, new User("bar", "not_superuser")), + new RealmRef("test", "test", "node"), new RealmRef("test", "test", "node")); + RequestInfo info = new RequestInfo(authentication, request, action); + PlainActionFuture future = new PlainActionFuture<>(); + engine.resolveAuthorizationInfo(info, future); + AuthorizationInfo authzInfo = future.actionGet(); + + PlainActionFuture resultFuture = new PlainActionFuture<>(); + engine.authorizeRunAs(info, authzInfo, resultFuture); + AuthorizationResult result = resultFuture.actionGet(); + assertThat(result.isGranted(), is(false)); + assertThat(result.isAuditable(), is(true)); + } + + // authorized + { + Authentication authentication = + new Authentication(new User("joe", new String[]{"not_superuser"}, new User("bar", "custom_superuser")), + new RealmRef("test", "test", "node"), new RealmRef("test", "test", "node")); + RequestInfo info = new RequestInfo(authentication, request, action); + PlainActionFuture future = new PlainActionFuture<>(); + engine.resolveAuthorizationInfo(info, future); + AuthorizationInfo authzInfo = future.actionGet(); + PlainActionFuture resultFuture = new PlainActionFuture<>(); + engine.authorizeRunAs(info, authzInfo, resultFuture); + AuthorizationResult result = resultFuture.actionGet(); + assertThat(result.isGranted(), is(true)); + assertThat(result.isAuditable(), is(true)); + } + } + + public void testAuthorizeClusterAction() { + CustomAuthorizationEngine engine = new CustomAuthorizationEngine(); + RequestInfo requestInfo = getRequestInfo(); + // authorized + { + PlainActionFuture future = new PlainActionFuture<>(); + engine.resolveAuthorizationInfo(requestInfo, future); + AuthorizationInfo authzInfo = future.actionGet(); + + PlainActionFuture resultFuture = new PlainActionFuture<>(); + engine.authorizeClusterAction(requestInfo, authzInfo, resultFuture); + AuthorizationResult result = resultFuture.actionGet(); + assertThat(result.isGranted(), is(true)); + assertThat(result.isAuditable(), is(true)); + } + + // unauthorized + { + RequestInfo unauthReqInfo = + new RequestInfo(new Authentication(new User("joe", "not_superuser"), new RealmRef("test", "test", "node"), null), + requestInfo.getRequest(), requestInfo.getAction()); + PlainActionFuture future = new PlainActionFuture<>(); + engine.resolveAuthorizationInfo(unauthReqInfo, future); + AuthorizationInfo authzInfo = future.actionGet(); + + PlainActionFuture resultFuture = new PlainActionFuture<>(); + engine.authorizeClusterAction(unauthReqInfo, authzInfo, resultFuture); + AuthorizationResult result = resultFuture.actionGet(); + assertThat(result.isGranted(), is(false)); + assertThat(result.isAuditable(), is(true)); + } + } + + public void testAuthorizeIndexAction() { + CustomAuthorizationEngine engine = new CustomAuthorizationEngine(); + // authorized + { + RequestInfo requestInfo = + new RequestInfo(new Authentication(new User("joe", "custom_superuser"), new RealmRef("test", "test", "node"), null), + new SearchRequest(), "indices:data/read/search"); + PlainActionFuture future = new PlainActionFuture<>(); + engine.resolveAuthorizationInfo(requestInfo, future); + AuthorizationInfo authzInfo = future.actionGet(); + + PlainActionFuture resultFuture = new PlainActionFuture<>(); + engine.authorizeIndexAction(requestInfo, authzInfo, + listener -> listener.onResponse(new ResolvedIndices(Collections.singletonList("index"), Collections.emptyList())), + name -> name.equals("index") ? new Index(IndexMetaData.builder("index").build()) : null, resultFuture); + IndexAuthorizationResult result = resultFuture.actionGet(); + assertThat(result.isGranted(), is(true)); + assertThat(result.isAuditable(), is(true)); + IndicesAccessControl indicesAccessControl = result.getIndicesAccessControl(); + assertNotNull(indicesAccessControl.getIndexPermissions("index")); + assertThat(indicesAccessControl.getIndexPermissions("index").isGranted(), is(true)); + } + + // unauthorized + { + RequestInfo requestInfo = + new RequestInfo(new Authentication(new User("joe", "not_superuser"), new RealmRef("test", "test", "node"), null), + new SearchRequest(), "indices:data/read/search"); + PlainActionFuture future = new PlainActionFuture<>(); + engine.resolveAuthorizationInfo(requestInfo, future); + AuthorizationInfo authzInfo = future.actionGet(); + + PlainActionFuture resultFuture = new PlainActionFuture<>(); + engine.authorizeIndexAction(requestInfo, authzInfo, + listener -> listener.onResponse(new ResolvedIndices(Collections.singletonList("index"), Collections.emptyList())), + name -> name.equals("index") ? new Index(IndexMetaData.builder("index").build()) : null, resultFuture); + IndexAuthorizationResult result = resultFuture.actionGet(); + assertThat(result.isGranted(), is(false)); + assertThat(result.isAuditable(), is(true)); + IndicesAccessControl indicesAccessControl = result.getIndicesAccessControl(); + assertNull(indicesAccessControl.getIndexPermissions("index")); + } + } + + private RequestInfo getRequestInfo() { + final String action = "cluster:monitor/foo"; + final TransportRequest request = new TransportRequest() {}; + final Authentication authentication = + new Authentication(new User("joe", "custom_superuser"), new RealmRef("test", "test", "node"), null); + return new RequestInfo(authentication, request, action); + } +} diff --git a/x-pack/docs/build.gradle b/x-pack/docs/build.gradle index de2400c0e85f0..a818d5ac8eed4 100644 --- a/x-pack/docs/build.gradle +++ b/x-pack/docs/build.gradle @@ -13,7 +13,6 @@ buildRestTests.expectedUnconvertedCandidates = [ 'en/security/authentication/user-cache.asciidoc', 'en/security/authorization/run-as-privilege.asciidoc', 'en/security/ccs-clients-integrations/http.asciidoc', - 'en/security/authorization/custom-roles-provider.asciidoc', 'en/rest-api/watcher/stats.asciidoc', 'en/watcher/example-watches/watching-time-series-data.asciidoc', ] diff --git a/x-pack/docs/en/security/authorization/custom-roles-provider.asciidoc b/x-pack/docs/en/security/authorization/custom-authorization.asciidoc similarity index 51% rename from x-pack/docs/en/security/authorization/custom-roles-provider.asciidoc rename to x-pack/docs/en/security/authorization/custom-authorization.asciidoc index bb8942985b701..735fb26cc58a3 100644 --- a/x-pack/docs/en/security/authorization/custom-roles-provider.asciidoc +++ b/x-pack/docs/en/security/authorization/custom-authorization.asciidoc @@ -1,23 +1,23 @@ [role="xpack"] -[[custom-roles-provider]] -=== Custom roles provider extension +[[custom-roles-authorization]] +=== Customizing roles and authorization If you need to retrieve user roles from a system not supported out-of-the-box -by the {es} {security-features}, you can create a custom roles provider to -retrieve and resolve -roles. You implement a custom roles provider as an SPI loaded security extension -as part of an ordinary elasticsearch plugin. +or if the authorization system that is provided by the {es} {security-features} +does not meet your needs, a SPI loaded security extension can be implemented to +customize role retrieval and/or the authorization system. The SPI loaded +security extension is part of an ordinary elasticsearch plugin. [[implementing-custom-roles-provider]] ==== Implementing a custom roles provider -To create a custom roles provider: +To create a custom roles provider: . Implement the interface `BiConsumer, ActionListener>>`. That is to say, the implementation consists of one method that takes a set of strings, which are the role names to resolve, and an ActionListener, on which the set of resolved role descriptors are passed on as the response. -. The custom roles provider implementation must take special care to not block on any I/O +. The custom roles provider implementation must take special care to not block on any I/O operations. It is the responsibility of the implementation to ensure asynchronous behavior and non-blocking calls, which is made easier by the fact that the `ActionListener` is provided on which to send the response when the roles have been resolved and the response @@ -32,7 +32,7 @@ To package your custom roles provider as a plugin: [source,java] ---------------------------------------------------- @Override -public List, ActionListener>>> +public List, ActionListener>>> getRolesProviders(Settings settings, ResourceWatcherService resourceWatcherService) { ... } @@ -41,50 +41,81 @@ getRolesProviders(Settings settings, ResourceWatcherService resourceWatcherServi The `getRolesProviders` method is used to provide a list of custom roles providers that will be used to resolve role names, if the role names could not be resolved by the reserved roles or native roles stores. The list should be returned in the order that the custom role -providers should be invoked to resolve roles. For example, if `getRolesProviders` returns two -instances of roles providers, and both of them are able to resolve role `A`, then the resolved -role descriptor that will be used for role `A` will be the one resolved by the first roles +providers should be invoked to resolve roles. For example, if `getRolesProviders` returns two +instances of roles providers, and both of them are able to resolve role `A`, then the resolved +role descriptor that will be used for role `A` will be the one resolved by the first roles provider in the list. + +[[implementing-authorization-engine]] +==== Implementing an authorization engine + +To create an authorization engine, you need to: + +. Implement the `org.elasticsearch.xpack.core.security.authz.AuthorizationEngine` + interface in a class with the desired authorization behavior. +. Implement the `org.elasticsearch.xpack.core.security.authz.Authorization.AuthorizationInfo` + interface in a class that contains the necessary information to authorize the request. + +To package your authorization engine as a plugin: + +. Implement an extension class for your authorization engine that extends + `org.elasticsearch.xpack.core.security.SecurityExtension`. There you need to + override the following method: + [source,java] ---------------------------------------------------- @Override -public List getSettingsFilter() { +public AuthorizationEngine getAuthorizationEngine(Settings settings) { ... } ---------------------------------------------------- + -The `Plugin#getSettingsFilter` method returns a list of setting names that should be -filtered from the settings APIs as they may contain sensitive credentials. Note this method is not -part of the `SecurityExtension` interface, it's available as part of the elasticsearch plugin main class. +The `getAuthorizationEngine` method is used to provide the authorization engine +implementation. + +Sample code that illustrates the structure and implementation of a custom +authorization engine is provided in the +https://github.com/elastic/elasticsearch/tree/master/plugin/examples/security-example-authorization-engine[elasticsearch] +repository on GitHub. You can use this code as a starting point for creating your +own authorization engine. + +[[packing-extension-plugin]] +==== Implement an elasticsearch plugin + +In order to register the security extension for your custom roles provider or +authorization engine, you need to also implement an elasticsearch plugin that +contains the extension: +. Implement a plugin class that extends `org.elasticsearch.plugins.Plugin` . Create a build configuration file for the plugin; Gradle is our recommendation. +. Create a `plugin-descriptor.properties` file as described in + {plugins}/plugin-authors.html[Help for plugin authors]. . Create a `META-INF/services/org.elasticsearch.xpack.core.security.SecurityExtension` descriptor file for the extension that contains the fully qualified class name of your `org.elasticsearch.xpack.core.security.SecurityExtension` implementation . Bundle all in a single zip file. -[[using-custom-roles-provider]] -==== Using a custom roles provider to resolve roles +[[using-security-extension]] +==== Using the security extension -To use a custom roles provider: +To use a security extension: -. Install the roles provider extension on each node in the cluster. You run +. Install the plugin with the extension on each node in the cluster. You run `bin/elasticsearch-plugin` with the `install` sub-command and specify the URL pointing to the zip file that contains the extension. For example: + [source,shell] ---------------------------------------- -bin/elasticsearch-plugin install file:////my-roles-provider-1.0.zip +bin/elasticsearch-plugin install file:////my-extension-plugin-1.0.zip ---------------------------------------- -. Add any configuration parameters for any of the custom roles provider implementations -to `elasticsearch.yml`. The settings are not namespaced and you have access to any -settings when constructing the custom roles providers, although it is recommended to -have a namespacing convention for custom roles providers to keep your `elasticsearch.yml` -configuration easy to understand. +. Add any configuration parameters for implementations in the extension to the +`elasticsearch.yml` file. The settings are not namespaced and you have access to any +settings when constructing the extensions, although it is recommended to have a +namespacing convention for extensions to keep your `elasticsearch.yml` +configuration easy to understand. + -For example, if you have a custom roles provider that -resolves roles from reading a blob in an S3 bucket on AWS, then you would specify settings +For example, if you have a custom roles provider that +resolves roles from reading a blob in an S3 bucket on AWS, then you would specify settings in `elasticsearch.yml` such as: + [source,js] @@ -94,8 +125,8 @@ custom_roles_provider.s3_roles_provider.region: us-east-1 custom_roles_provider.s3_roles_provider.secret_key: xxx custom_roles_provider.s3_roles_provider.access_key: xxx ---------------------------------------- +// NOTCONSOLE + -These settings will be available as the first parameter in the `getRolesProviders` method, from -where you will create and return the custom roles provider instances. +These settings are passed as arguments to the methods in the `SecurityExtension` interface. . Restart Elasticsearch. diff --git a/x-pack/docs/en/security/authorization/managing-roles.asciidoc b/x-pack/docs/en/security/authorization/managing-roles.asciidoc index cac4eaac1fbfa..04fb12e19d75b 100644 --- a/x-pack/docs/en/security/authorization/managing-roles.asciidoc +++ b/x-pack/docs/en/security/authorization/managing-roles.asciidoc @@ -179,7 +179,7 @@ There are two available mechanisms to define roles: using the _Role Management A or in local files on the {es} nodes. You can also implement custom roles providers. If you need to integrate with another system to retrieve user roles, you can build a custom roles provider plugin. For more information, -see <>. +see <>. [float] [[roles-management-ui]] diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java index 82d31aa8a2993..9f0eb474a59c8 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java @@ -11,6 +11,7 @@ import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xpack.core.security.authc.AuthenticationFailureHandler; import org.elasticsearch.xpack.core.security.authc.Realm; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.store.RoleRetrievalResult; @@ -79,6 +80,18 @@ default AuthenticationFailureHandler getAuthenticationFailureHandler() { return Collections.emptyList(); } + /** + * Returns a authorization engine for authorizing requests, or null to use the default authorization mechanism. + * + * Only one installed extension may have an authorization engine. If more than + * one extension returns a non-null authorization engine, an error is raised. + * + * @param settings The configured settings for the node + */ + default AuthorizationEngine getAuthorizationEngine(Settings settings) { + return null; + } + /** * Loads the XPackSecurityExtensions from the given class loader */ 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 665f3ed6d35c0..948b9b7143e96 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 @@ -111,6 +111,7 @@ import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.RealmSettings; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine; import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField; import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; import org.elasticsearch.xpack.core.security.authz.accesscontrol.SecurityIndexSearcherWrapper; @@ -437,7 +438,7 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste // minimal getLicenseState().addListener(allRolesStore::invalidateAll); final AuthorizationService authzService = new AuthorizationService(settings, allRolesStore, clusterService, - auditTrailService, failureHandler, threadPool, anonymousUser); + auditTrailService, failureHandler, threadPool, anonymousUser, getAuthorizationEngine()); components.add(nativeRolesStore); // used by roles actions components.add(reservedRolesStore); // used by roles actions components.add(allRolesStore); // for SecurityFeatureSet and clear roles cache @@ -467,6 +468,25 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste return components; } + private AuthorizationEngine getAuthorizationEngine() { + AuthorizationEngine authorizationEngine = null; + String extensionName = null; + for (SecurityExtension extension : securityExtensions) { + final AuthorizationEngine extensionEngine = extension.getAuthorizationEngine(settings); + if (extensionEngine != null && authorizationEngine != null) { + throw new IllegalStateException("Extensions [" + extensionName + "] and [" + extension.toString() + "] " + + "both set an authorization engine"); + } + authorizationEngine = extensionEngine; + extensionName = extension.toString(); + } + + if (authorizationEngine != null) { + logger.debug("Using authorization engine from extension [" + extensionName + "]"); + } + return authorizationEngine; + } + private AuthenticationFailureHandler createAuthenticationFailureHandler(final Realms realms) { AuthenticationFailureHandler failureHandler = null; String extensionName = null; 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 3e9f8db284042..98186af0503ba 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 @@ -24,6 +24,7 @@ import org.elasticsearch.action.update.UpdateAction; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.Nullable; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Setting.Property; @@ -90,12 +91,13 @@ public class AuthorizationService { private final ThreadContext threadContext; private final AnonymousUser anonymousUser; private final AuthorizationEngine rbacEngine; + private final AuthorizationEngine authorizationEngine; private final boolean isAnonymousEnabled; private final boolean anonymousAuthzExceptionEnabled; public AuthorizationService(Settings settings, CompositeRolesStore rolesStore, ClusterService clusterService, AuditTrailService auditTrail, AuthenticationFailureHandler authcFailureHandler, - ThreadPool threadPool, AnonymousUser anonymousUser) { + ThreadPool threadPool, AnonymousUser anonymousUser, @Nullable AuthorizationEngine authorizationEngine) { this.clusterService = clusterService; this.auditTrail = auditTrail; this.indicesAndAliasesResolver = new IndicesAndAliasesResolver(settings, clusterService); @@ -105,6 +107,7 @@ public AuthorizationService(Settings settings, CompositeRolesStore rolesStore, C this.isAnonymousEnabled = AnonymousUser.isAnonymousEnabled(settings); this.anonymousAuthzExceptionEnabled = ANONYMOUS_AUTHORIZATION_EXCEPTION_SETTING.get(settings); this.rbacEngine = new RBACEngine(settings, rolesStore); + this.authorizationEngine = authorizationEngine == null ? this.rbacEngine : authorizationEngine; this.settings = settings; } @@ -293,14 +296,22 @@ private void authorizeAction(final RequestInfo requestInfo, final String request } } - private AuthorizationEngine getRunAsAuthorizationEngine(final Authentication authentication) { - return ClientReservedRealm.isReserved(authentication.getUser().authenticatedUser().principal(), settings) ? - rbacEngine : rbacEngine; + // pkg-private for testing + AuthorizationEngine getRunAsAuthorizationEngine(final Authentication authentication) { + return getAuthorizationEngineForUser(authentication.getUser().authenticatedUser()); } - private AuthorizationEngine getAuthorizationEngine(final Authentication authentication) { - return ClientReservedRealm.isReserved(authentication.getUser().principal(), settings) ? - rbacEngine : rbacEngine; + // pkg-private for testing + AuthorizationEngine getAuthorizationEngine(final Authentication authentication) { + return getAuthorizationEngineForUser(authentication.getUser()); + } + + private AuthorizationEngine getAuthorizationEngineForUser(final User user) { + if (ClientReservedRealm.isReserved(user.principal(), settings) || isInternalUser(user)) { + return rbacEngine; + } else { + return authorizationEngine; + } } private void authorizeSystemUser(final Authentication authentication, final String action, final String requestId, @@ -490,13 +501,13 @@ private void putTransientIfNonExisting(String key, Object value) { } } - ElasticsearchSecurityException denial(String auditRequestId, Authentication authentication, String action, TransportRequest request, - AuthorizationInfo authzInfo) { + private ElasticsearchSecurityException denial(String auditRequestId, Authentication authentication, String action, + TransportRequest request, AuthorizationInfo authzInfo) { return denial(auditRequestId, authentication, action, request, authzInfo, null); } - ElasticsearchSecurityException denial(String auditRequestId, Authentication authentication, String action, TransportRequest request, - AuthorizationInfo authzInfo, Exception cause) { + 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); } 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 7bf07cb906b5b..2ab2bc4c657b7 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 @@ -71,6 +71,7 @@ import org.elasticsearch.action.update.UpdateAction; import org.elasticsearch.action.update.UpdateRequest; import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.AliasOrIndex; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.cluster.node.DiscoveryNode; @@ -94,8 +95,10 @@ import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; import org.elasticsearch.xpack.core.security.authc.DefaultAuthenticationFailureHandler; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine; import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField; import org.elasticsearch.xpack.core.security.authz.IndicesAndAliasesResolverField; +import org.elasticsearch.xpack.core.security.authz.ResolvedIndices; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.IndicesPrivileges; import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; @@ -107,8 +110,11 @@ import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; import org.elasticsearch.xpack.core.security.user.AnonymousUser; import org.elasticsearch.xpack.core.security.user.ElasticUser; +import org.elasticsearch.xpack.core.security.user.KibanaUser; import org.elasticsearch.xpack.core.security.user.SystemUser; import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.core.security.user.XPackSecurityUser; +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.AuthorizationInfo; @@ -132,6 +138,7 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.CountDownLatch; +import java.util.function.Function; import java.util.function.Predicate; import static java.util.Arrays.asList; @@ -218,7 +225,7 @@ 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)); + auditTrail, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(settings), null); } private void authorize(Authentication authentication, String action, TransportRequest request) { @@ -642,7 +649,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); + new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, anonymousUser, null); RoleDescriptor role = new RoleDescriptor("a_all", null, new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a").privileges("all").build() }, null); @@ -669,7 +676,7 @@ 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)); + new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(settings), null); RoleDescriptor role = new RoleDescriptor("a_all", null, new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null); @@ -1292,6 +1299,72 @@ public void testProxyRequestAuthenticationDeniedWithReadPrivileges() { authzInfoRoles(new String[]{role.getName()})); } + public void testAuthorizationEngineSelection() { + final AuthorizationEngine engine = new AuthorizationEngine() { + @Override + public void resolveAuthorizationInfo(RequestInfo requestInfo, ActionListener listener) { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public void authorizeRunAs(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, + ActionListener listener) { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public void authorizeClusterAction(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, + ActionListener listener) { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public void authorizeIndexAction(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, + AsyncSupplier indicesAsyncSupplier, + Function aliasOrIndexFunction, + ActionListener listener) { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public void loadAuthorizedIndices(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, + Map aliasAndIndexLookup, 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); + Authentication authentication = createAuthentication(new User("test user", "a_all")); + assertEquals(engine, authorizationService.getAuthorizationEngine(authentication)); + + authentication = createAuthentication(new User("runas", new String[] { "runas_role" }, new User("runner", "runner_role"))); + assertEquals(engine, authorizationService.getAuthorizationEngine(authentication)); + assertEquals(engine, authorizationService.getRunAsAuthorizationEngine(authentication)); + + authentication = createAuthentication(new User("runas", new String[] { "runas_role" }, new ElasticUser(true))); + assertEquals(engine, authorizationService.getAuthorizationEngine(authentication)); + assertNotEquals(engine, authorizationService.getRunAsAuthorizationEngine(authentication)); + assertThat(authorizationService.getRunAsAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + + authentication = createAuthentication(new User("elastic", new String[] { "superuser" }, new User("runner", "runner_role"))); + assertNotEquals(engine, authorizationService.getAuthorizationEngine(authentication)); + assertThat(authorizationService.getAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + assertEquals(engine, authorizationService.getRunAsAuthorizationEngine(authentication)); + + authentication = createAuthentication(new User("kibana", new String[] { "kibana_system" }, new ElasticUser(true))); + assertNotEquals(engine, authorizationService.getAuthorizationEngine(authentication)); + assertThat(authorizationService.getAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + assertNotEquals(engine, authorizationService.getRunAsAuthorizationEngine(authentication)); + assertThat(authorizationService.getRunAsAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + + authentication = createAuthentication(randomFrom(XPackUser.INSTANCE, XPackSecurityUser.INSTANCE, + new ElasticUser(true), new KibanaUser(true))); + assertNotEquals(engine, authorizationService.getRunAsAuthorizationEngine(authentication)); + assertThat(authorizationService.getRunAsAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + } + static AuthorizationInfo authzInfoRoles(String[] expectedRoles) { return Matchers.argThat(new RBACAuthorizationInfoRoleMatcher(expectedRoles)); } diff --git a/x-pack/qa/security-example-spi-extension/build.gradle b/x-pack/qa/security-example-spi-extension/build.gradle index 664e5f715bbb1..1ff65519c367d 100644 --- a/x-pack/qa/security-example-spi-extension/build.gradle +++ b/x-pack/qa/security-example-spi-extension/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'elasticsearch.esplugin' esplugin { name 'spi-extension' - description 'An example spi extension pluing for xpack security' + description 'An example spi extension plugin for security' classname 'org.elasticsearch.example.SpiExtensionPlugin' extendedPlugins = ['x-pack-security'] } From e5615d2dd36e0a34348c123ecf0aafeebaa02d69 Mon Sep 17 00:00:00 2001 From: Jay Modi Date: Mon, 4 Feb 2019 07:53:37 -0700 Subject: [PATCH 08/15] Move request interceptors to AuthorizationService (#38137) This change moves the RequestInterceptor iteration from the action filter to the AuthorizationService. This is done to remove the need for the use of a role within the request interceptors and replace it with the AuthorizationEngine. The AuthorizationEngine interface was also enhanced with a new method that is used to determine if a users permission on one index is a subset of their permissions on a list of indices or aliases. Additionally, this change addresses some leftover cleanups. --- .../example/CustomAuthorizationEngine.java | 11 + .../security/authz/AuthorizationEngine.java | 22 ++ .../xpack/security/Security.java | 44 +-- .../action/filter/SecurityActionFilter.java | 29 +- ...cumentLevelSecurityRequestInterceptor.java | 61 ---- .../IndicesAliasesRequestInterceptor.java | 92 ------ .../interceptor/RequestInterceptor.java | 28 -- .../interceptor/ResizeRequestInterceptor.java | 77 ----- .../security/authz/AuthorizationService.java | 294 ++++++++++-------- .../xpack/security/authz/RBACEngine.java | 35 ++- .../BulkShardRequestInterceptor.java | 40 ++- ...cumentLevelSecurityRequestInterceptor.java | 68 ++++ .../IndicesAliasesRequestInterceptor.java | 105 +++++++ .../authz/interceptor/RequestInterceptor.java | 24 ++ .../interceptor/ResizeRequestInterceptor.java | 85 +++++ .../interceptor/SearchRequestInterceptor.java | 28 +- .../interceptor/UpdateRequestInterceptor.java | 16 +- .../DocumentLevelSecurityTests.java | 2 +- .../integration/FieldLevelSecurityTests.java | 2 +- .../filter/SecurityActionFilterTests.java | 4 +- .../authz/AuthorizationServiceTests.java | 19 +- ...IndicesAliasesRequestInterceptorTests.java | 69 +++- .../ResizeRequestInterceptorTests.java | 61 +++- 23 files changed, 720 insertions(+), 496 deletions(-) delete mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/FieldAndDocumentLevelSecurityRequestInterceptor.java delete mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/IndicesAliasesRequestInterceptor.java delete mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/RequestInterceptor.java delete mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/ResizeRequestInterceptor.java rename x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/{action => authz}/interceptor/BulkShardRequestInterceptor.java (58%) create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/FieldAndDocumentLevelSecurityRequestInterceptor.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/IndicesAliasesRequestInterceptor.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/RequestInterceptor.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/interceptor/ResizeRequestInterceptor.java rename x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/{action => authz}/interceptor/SearchRequestInterceptor.java (50%) rename x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/{action => authz}/interceptor/UpdateRequestInterceptor.java (65%) rename x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/{action => authz}/interceptor/IndicesAliasesRequestInterceptorTests.java (65%) rename x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/{action => authz}/interceptor/ResizeRequestInterceptorTests.java (61%) 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(); + } } } From 34aa55a9e54f04ba064148634e71a406fd1396b8 Mon Sep 17 00:00:00 2001 From: Jay Modi Date: Mon, 4 Feb 2019 11:45:01 -0700 Subject: [PATCH 09/15] Authorization engines evaluate privileges for APIs (#38219) This commit moves the evaluation of privileges from a few transport actions into the authorization engine. The APIs are used by other applications for making decisions and if a different authorization engine is used that is not role based, we should still allow these APIs to work. By moving this evaluation out of the transport action, the transport actions no longer have a dependency on roles. --- .../example/CustomAuthorizationEngine.java | 97 +++ .../security/authz/AuthorizationEngine.java | 36 ++ .../TransportGetUserPrivilegesAction.java | 99 +--- .../user/TransportHasPrivilegesAction.java | 155 +---- .../security/authz/AuthorizationService.java | 22 + .../xpack/security/authz/RBACEngine.java | 224 ++++++- ...TransportGetUserPrivilegesActionTests.java | 86 --- .../TransportHasPrivilegesActionTests.java | 526 ----------------- .../authz/AuthorizationServiceTests.java | 19 + .../xpack/security/authz/RBACEngineTests.java | 554 ++++++++++++++++++ 10 files changed, 967 insertions(+), 851 deletions(-) delete mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportGetUserPrivilegesActionTests.java delete mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesActionTests.java 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 c16e21b984d6f..da8bcf8b4ef44 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 @@ -21,21 +21,35 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.cluster.metadata.AliasOrIndex; +import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesRequest; +import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse; +import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse.Indices; +import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesRequest; +import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesResponse; +import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesResponse.ResourcePrivileges; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine; import org.elasticsearch.xpack.core.security.authz.ResolvedIndices; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.IndicesPrivileges; import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl.IndexAccessControl; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; +import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege; import org.elasticsearch.xpack.core.security.user.User; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Function; +import java.util.stream.Collectors; /** * A custom implementation of an authorization engine. This engine is extremely basic in that it @@ -115,6 +129,89 @@ public void validateIndexPermissionsAreSubset(RequestInfo requestInfo, Authoriza } } + @Override + public void checkPrivileges(Authentication authentication, AuthorizationInfo authorizationInfo, + HasPrivilegesRequest hasPrivilegesRequest, + Collection applicationPrivilegeDescriptors, + ActionListener listener) { + if (isSuperuser(authentication.getUser())) { + listener.onResponse(getHasPrivilegesResponse(authentication, hasPrivilegesRequest, true)); + } else { + listener.onResponse(getHasPrivilegesResponse(authentication, hasPrivilegesRequest, false)); + } + } + + @Override + public void getUserPrivileges(Authentication authentication, AuthorizationInfo authorizationInfo, GetUserPrivilegesRequest request, + ActionListener listener) { + if (isSuperuser(authentication.getUser())) { + listener.onResponse(getUserPrivilegesResponse(true)); + } else { + listener.onResponse(getUserPrivilegesResponse(false)); + } + } + + private HasPrivilegesResponse getHasPrivilegesResponse(Authentication authentication, HasPrivilegesRequest hasPrivilegesRequest, + boolean authorized) { + Map clusterPrivMap = new HashMap<>(); + for (String clusterPriv : hasPrivilegesRequest.clusterPrivileges()) { + clusterPrivMap.put(clusterPriv, authorized); + } + final Map indices = new LinkedHashMap<>(); + for (IndicesPrivileges check : hasPrivilegesRequest.indexPrivileges()) { + for (String index : check.getIndices()) { + final Map privileges = new HashMap<>(); + final HasPrivilegesResponse.ResourcePrivileges existing = indices.get(index); + if (existing != null) { + privileges.putAll(existing.getPrivileges()); + } + for (String privilege : check.getPrivileges()) { + privileges.put(privilege, authorized); + } + indices.put(index, new ResourcePrivileges(index, privileges)); + } + } + final Map> privilegesByApplication = new HashMap<>(); + Set applicationNames = Arrays.stream(hasPrivilegesRequest.applicationPrivileges()) + .map(RoleDescriptor.ApplicationResourcePrivileges::getApplication) + .collect(Collectors.toSet()); + for (String applicationName : applicationNames) { + final Map appPrivilegesByResource = new LinkedHashMap<>(); + for (RoleDescriptor.ApplicationResourcePrivileges p : hasPrivilegesRequest.applicationPrivileges()) { + if (applicationName.equals(p.getApplication())) { + for (String resource : p.getResources()) { + final Map privileges = new HashMap<>(); + final HasPrivilegesResponse.ResourcePrivileges existing = appPrivilegesByResource.get(resource); + if (existing != null) { + privileges.putAll(existing.getPrivileges()); + } + for (String privilege : p.getPrivileges()) { + privileges.put(privilege, authorized); + } + appPrivilegesByResource.put(resource, new HasPrivilegesResponse.ResourcePrivileges(resource, privileges)); + } + } + } + privilegesByApplication.put(applicationName, appPrivilegesByResource.values()); + } + return new HasPrivilegesResponse(authentication.getUser().principal(), authorized, clusterPrivMap, indices.values(), + privilegesByApplication); + } + + private GetUserPrivilegesResponse getUserPrivilegesResponse(boolean isSuperuser) { + final Set cluster = isSuperuser ? Collections.singleton("ALL") : Collections.emptySet(); + final Set conditionalCluster = Collections.emptySet(); + final Set indices = isSuperuser ? Collections.singleton(new Indices(Collections.singleton("*"), + Collections.singleton("*"), Collections.emptySet(), Collections.emptySet(), true)) : Collections.emptySet(); + + final Set application = isSuperuser ? + Collections.singleton( + RoleDescriptor.ApplicationResourcePrivileges.builder().application("*").privileges("*").resources("*").build()) : + Collections.emptySet(); + final Set runAs = isSuperuser ? Collections.singleton("*") : Collections.emptySet(); + return new GetUserPrivilegesResponse(cluster, conditionalCluster, indices, application, runAs); + } + 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 51e1f409771d8..19fb501f5843b 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 @@ -9,10 +9,16 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.cluster.metadata.AliasOrIndex; import org.elasticsearch.transport.TransportRequest; +import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesRequest; +import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse; +import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesRequest; +import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesResponse; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; import org.elasticsearch.xpack.core.security.user.User; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; @@ -141,6 +147,7 @@ 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 @@ -161,6 +168,35 @@ void loadAuthorizedIndices(RequestInfo requestInfo, AuthorizationInfo authorizat void validateIndexPermissionsAreSubset(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, Map> indexNameToNewNames, ActionListener listener); + /** + * Checks the current user's privileges against those that being requested to check in the + * request. This provides a way for an application to ask if a user has permission to perform + * an action or if they have permissions to an application resource. + * + * @param authentication the authentication that is associated with this request + * @param authorizationInfo information needed from authorization that was previously retrieved + * from {@link #resolveAuthorizationInfo(RequestInfo, ActionListener)} + * @param hasPrivilegesRequest the request that contains the privileges to check for the user + * @param applicationPrivilegeDescriptors a collection of application privilege descriptors + * @param listener the listener to be notified of the has privileges response + */ + void checkPrivileges(Authentication authentication, AuthorizationInfo authorizationInfo, HasPrivilegesRequest hasPrivilegesRequest, + Collection applicationPrivilegeDescriptors, + ActionListener listener); + + /** + * Retrieve's the current user's privileges in a standard format that can be rendered via an + * API for an application to understand the privileges that the current user has. + * + * @param authentication the authentication that is associated with this request + * @param authorizationInfo information needed from authorization that was previously retrieved + * from {@link #resolveAuthorizationInfo(RequestInfo, ActionListener)} + * @param request the request for retrieving the user's privileges + * @param listener the listener to be notified of the has privileges response + */ + void getUserPrivileges(Authentication authentication, AuthorizationInfo authorizationInfo, GetUserPrivilegesRequest request, + 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/action/user/TransportGetUserPrivilegesAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportGetUserPrivilegesAction.java index e85b3870c95f3..00232033b89ef 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportGetUserPrivilegesAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportGetUserPrivilegesAction.java @@ -5,15 +5,10 @@ */ package org.elasticsearch.xpack.security.action.user; -import org.apache.logging.log4j.message.ParameterizedMessage; -import org.apache.lucene.util.automaton.Operations; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.HandledTransportAction; -import org.elasticsearch.common.bytes.BytesReference; -import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.inject.Inject; -import org.elasticsearch.common.settings.Settings; import org.elasticsearch.tasks.Task; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; @@ -21,26 +16,8 @@ import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesRequest; import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse; import org.elasticsearch.xpack.core.security.authc.Authentication; -import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; -import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsCache; -import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsDefinition; -import org.elasticsearch.xpack.core.security.authz.permission.IndicesPermission; -import org.elasticsearch.xpack.core.security.authz.permission.Role; -import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; -import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege; -import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege; -import org.elasticsearch.xpack.core.security.authz.privilege.Privilege; import org.elasticsearch.xpack.core.security.user.User; -import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; - -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.Set; -import java.util.TreeSet; - -import static org.elasticsearch.common.Strings.arrayToCommaDelimitedString; +import org.elasticsearch.xpack.security.authz.AuthorizationService; /** * Transport action for {@link GetUserPrivilegesAction} @@ -48,89 +25,27 @@ public class TransportGetUserPrivilegesAction extends HandledTransportAction { private final ThreadPool threadPool; - private final CompositeRolesStore rolesStore; + private final AuthorizationService authorizationService; @Inject public TransportGetUserPrivilegesAction(ThreadPool threadPool, TransportService transportService, - ActionFilters actionFilters, CompositeRolesStore rolesStore) { + ActionFilters actionFilters, AuthorizationService authorizationService) { super(GetUserPrivilegesAction.NAME, transportService, actionFilters, GetUserPrivilegesRequest::new); this.threadPool = threadPool; - this.rolesStore = rolesStore; + this.authorizationService = authorizationService; } @Override protected void doExecute(Task task, GetUserPrivilegesRequest request, ActionListener listener) { final String username = request.username(); - final User user = Authentication.getAuthentication(threadPool.getThreadContext()).getUser(); + final Authentication authentication = Authentication.getAuthentication(threadPool.getThreadContext()); + final User user = authentication.getUser(); if (user.principal().equals(username) == false) { listener.onFailure(new IllegalArgumentException("users may only list the privileges of their own account")); return; } - // FIXME reuse field permissions cache! - rolesStore.getRoles(user, new FieldPermissionsCache(Settings.EMPTY), ActionListener.wrap( - role -> listener.onResponse(buildResponseObject(role)), - listener::onFailure)); - } - - // package protected for testing - GetUserPrivilegesResponse buildResponseObject(Role userRole) { - logger.trace(() -> new ParameterizedMessage("List privileges for role [{}]", arrayToCommaDelimitedString(userRole.names()))); - - // We use sorted sets for Strings because they will typically be small, and having a predictable order allows for simpler testing - final Set cluster = new TreeSet<>(); - // But we don't have a meaningful ordering for objects like ConditionalClusterPrivilege, so the tests work with "random" ordering - final Set conditionalCluster = new HashSet<>(); - for (Tuple tup : userRole.cluster().privileges()) { - if (tup.v2() == null) { - if (ClusterPrivilege.NONE.equals(tup.v1()) == false) { - cluster.addAll(tup.v1().name()); - } - } else { - conditionalCluster.add(tup.v2()); - } - } - - final Set indices = new LinkedHashSet<>(); - for (IndicesPermission.Group group : userRole.indices().groups()) { - final Set queries = group.getQuery() == null ? Collections.emptySet() : group.getQuery(); - final Set fieldSecurity = group.getFieldPermissions().hasFieldLevelSecurity() - ? group.getFieldPermissions().getFieldPermissionsDefinition().getFieldGrantExcludeGroups() : Collections.emptySet(); - indices.add(new GetUserPrivilegesResponse.Indices( - Arrays.asList(group.indices()), - group.privilege().name(), - fieldSecurity, - queries, - group.allowRestrictedIndices() - )); - } - - final Set application = new LinkedHashSet<>(); - for (String applicationName : userRole.application().getApplicationNames()) { - for (ApplicationPrivilege privilege : userRole.application().getPrivileges(applicationName)) { - final Set resources = userRole.application().getResourcePatterns(privilege); - if (resources.isEmpty()) { - logger.trace("No resources defined in application privilege {}", privilege); - } else { - application.add(RoleDescriptor.ApplicationResourcePrivileges.builder() - .application(applicationName) - .privileges(privilege.name()) - .resources(resources) - .build()); - } - } - } - - final Privilege runAsPrivilege = userRole.runAs().getPrivilege(); - final Set runAs; - if (Operations.isEmpty(runAsPrivilege.getAutomaton())) { - runAs = Collections.emptySet(); - } else { - runAs = runAsPrivilege.name(); - } - - return new GetUserPrivilegesResponse(cluster, conditionalCluster, indices, application, runAs); + authorizationService.retrieveUserPrivileges(authentication, request, listener); } - } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesAction.java index deb3a37ee2e9d..ae400172bf110 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesAction.java @@ -5,15 +5,10 @@ */ package org.elasticsearch.xpack.security.action.user; -import org.apache.logging.log4j.message.ParameterizedMessage; -import org.apache.lucene.util.automaton.Automaton; -import org.apache.lucene.util.automaton.Operations; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.HandledTransportAction; -import org.elasticsearch.common.Strings; import org.elasticsearch.common.inject.Inject; -import org.elasticsearch.common.settings.Settings; import org.elasticsearch.tasks.Task; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; @@ -22,27 +17,13 @@ import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesResponse; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; -import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsCache; -import org.elasticsearch.xpack.core.security.authz.permission.IndicesPermission; -import org.elasticsearch.xpack.core.security.authz.permission.Role; -import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; -import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege; -import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; -import org.elasticsearch.xpack.core.security.authz.privilege.Privilege; -import org.elasticsearch.xpack.core.security.support.Automatons; import org.elasticsearch.xpack.core.security.user.User; -import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; +import org.elasticsearch.xpack.security.authz.AuthorizationService; import org.elasticsearch.xpack.security.authz.store.NativePrivilegeStore; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -53,16 +34,16 @@ public class TransportHasPrivilegesAction extends HandledTransportAction { private final ThreadPool threadPool; - private final CompositeRolesStore rolesStore; + private final AuthorizationService authorizationService; private final NativePrivilegeStore privilegeStore; @Inject public TransportHasPrivilegesAction(ThreadPool threadPool, TransportService transportService, - ActionFilters actionFilters, CompositeRolesStore rolesStore, + ActionFilters actionFilters, AuthorizationService authorizationService, NativePrivilegeStore privilegeStore) { super(HasPrivilegesAction.NAME, transportService, actionFilters, HasPrivilegesRequest::new); this.threadPool = threadPool; - this.rolesStore = rolesStore; + this.authorizationService = authorizationService; this.privilegeStore = privilegeStore; } @@ -70,17 +51,15 @@ public TransportHasPrivilegesAction(ThreadPool threadPool, TransportService tran protected void doExecute(Task task, HasPrivilegesRequest request, ActionListener listener) { final String username = request.username(); - final User user = Authentication.getAuthentication(threadPool.getThreadContext()).getUser(); + final Authentication authentication = Authentication.getAuthentication(threadPool.getThreadContext()); + final User user = authentication.getUser(); if (user.principal().equals(username) == false) { listener.onFailure(new IllegalArgumentException("users may only check the privileges of their own account")); return; } - // FIXME - rolesStore.getRoles(user, new FieldPermissionsCache(Settings.EMPTY), ActionListener.wrap( - role -> resolveApplicationPrivileges(request, ActionListener.wrap( - applicationPrivilegeLookup -> checkPrivileges(request, role, applicationPrivilegeLookup, listener), - listener::onFailure)), + resolveApplicationPrivileges(request, ActionListener.wrap(applicationPrivilegeDescriptors -> + authorizationService.checkPrivileges(authentication, request, applicationPrivilegeDescriptors, listener), listener::onFailure)); } @@ -90,125 +69,9 @@ private void resolveApplicationPrivileges(HasPrivilegesRequest request, privilegeStore.getPrivileges(applications, null, listener); } - private Set getApplicationNames(HasPrivilegesRequest request) { + public static Set getApplicationNames(HasPrivilegesRequest request) { return Arrays.stream(request.applicationPrivileges()) .map(RoleDescriptor.ApplicationResourcePrivileges::getApplication) .collect(Collectors.toSet()); } - - private void checkPrivileges(HasPrivilegesRequest request, Role userRole, - Collection applicationPrivileges, - ActionListener listener) { - logger.trace(() -> new ParameterizedMessage("Check whether role [{}] has privileges cluster=[{}] index=[{}] application=[{}]", - Strings.arrayToCommaDelimitedString(userRole.names()), - Strings.arrayToCommaDelimitedString(request.clusterPrivileges()), - Strings.arrayToCommaDelimitedString(request.indexPrivileges()), - Strings.arrayToCommaDelimitedString(request.applicationPrivileges()) - )); - - Map cluster = new HashMap<>(); - for (String checkAction : request.clusterPrivileges()) { - final ClusterPrivilege checkPrivilege = ClusterPrivilege.get(Collections.singleton(checkAction)); - final ClusterPrivilege rolePrivilege = userRole.cluster().privilege(); - cluster.put(checkAction, testPrivilege(checkPrivilege, rolePrivilege.getAutomaton())); - } - boolean allMatch = cluster.values().stream().allMatch(Boolean::booleanValue); - - final Map predicateCache = new HashMap<>(); - - final Map indices = new LinkedHashMap<>(); - for (RoleDescriptor.IndicesPrivileges check : request.indexPrivileges()) { - for (String index : check.getIndices()) { - final Map privileges = new HashMap<>(); - final HasPrivilegesResponse.ResourcePrivileges existing = indices.get(index); - if (existing != null) { - privileges.putAll(existing.getPrivileges()); - } - for (String privilege : check.getPrivileges()) { - if (testIndexMatch(index, check.allowRestrictedIndices(), privilege, userRole, predicateCache)) { - logger.debug(() -> new ParameterizedMessage("Role [{}] has [{}] on index [{}]", - Strings.arrayToCommaDelimitedString(userRole.names()), privilege, index)); - privileges.put(privilege, true); - } else { - logger.debug(() -> new ParameterizedMessage("Role [{}] does not have [{}] on index [{}]", - Strings.arrayToCommaDelimitedString(userRole.names()), privilege, index)); - privileges.put(privilege, false); - allMatch = false; - } - } - indices.put(index, new HasPrivilegesResponse.ResourcePrivileges(index, privileges)); - } - } - - final Map> privilegesByApplication = new HashMap<>(); - for (String applicationName : getApplicationNames(request)) { - logger.debug("Checking privileges for application {}", applicationName); - final Map appPrivilegesByResource = new LinkedHashMap<>(); - for (RoleDescriptor.ApplicationResourcePrivileges p : request.applicationPrivileges()) { - if (applicationName.equals(p.getApplication())) { - for (String resource : p.getResources()) { - final Map privileges = new HashMap<>(); - final HasPrivilegesResponse.ResourcePrivileges existing = appPrivilegesByResource.get(resource); - if (existing != null) { - privileges.putAll(existing.getPrivileges()); - } - for (String privilege : p.getPrivileges()) { - if (testResourceMatch(applicationName, resource, privilege, userRole, applicationPrivileges)) { - logger.debug(() -> new ParameterizedMessage("Role [{}] has [{} {}] on resource [{}]", - Strings.arrayToCommaDelimitedString(userRole.names()), applicationName, privilege, resource)); - privileges.put(privilege, true); - } else { - logger.debug(() -> new ParameterizedMessage("Role [{}] does not have [{} {}] on resource [{}]", - Strings.arrayToCommaDelimitedString(userRole.names()), applicationName, privilege, resource)); - privileges.put(privilege, false); - allMatch = false; - } - } - appPrivilegesByResource.put(resource, new HasPrivilegesResponse.ResourcePrivileges(resource, privileges)); - } - } - } - privilegesByApplication.put(applicationName, appPrivilegesByResource.values()); - } - - listener.onResponse(new HasPrivilegesResponse(request.username(), allMatch, cluster, indices.values(), privilegesByApplication)); - } - - private boolean testIndexMatch(String checkIndexPattern, boolean allowRestrictedIndices, String checkPrivilegeName, Role userRole, - Map predicateCache) { - final IndexPrivilege checkPrivilege = IndexPrivilege.get(Collections.singleton(checkPrivilegeName)); - - final Automaton checkIndexAutomaton = IndicesPermission.Group.buildIndexMatcherAutomaton(allowRestrictedIndices, checkIndexPattern); - - List privilegeAutomatons = new ArrayList<>(); - for (IndicesPermission.Group group : userRole.indices().groups()) { - final Automaton groupIndexAutomaton = predicateCache.computeIfAbsent(group, - g -> IndicesPermission.Group.buildIndexMatcherAutomaton(g.allowRestrictedIndices(), g.indices())); - if (Operations.subsetOf(checkIndexAutomaton, groupIndexAutomaton)) { - final IndexPrivilege rolePrivilege = group.privilege(); - if (rolePrivilege.name().contains(checkPrivilegeName)) { - return true; - } - privilegeAutomatons.add(rolePrivilege.getAutomaton()); - } - } - return testPrivilege(checkPrivilege, Automatons.unionAndMinimize(privilegeAutomatons)); - } - - private static boolean testPrivilege(Privilege checkPrivilege, Automaton roleAutomaton) { - return Operations.subsetOf(checkPrivilege.getAutomaton(), roleAutomaton); - } - - private boolean testResourceMatch(String application, String checkResource, String checkPrivilegeName, Role userRole, - Collection privileges) { - final Set nameSet = Collections.singleton(checkPrivilegeName); - final ApplicationPrivilege checkPrivilege = ApplicationPrivilege.get(application, nameSet, privileges); - assert checkPrivilege.getApplication().equals(application) - : "Privilege " + checkPrivilege + " should have application " + application; - assert checkPrivilege.name().equals(nameSet) - : "Privilege " + checkPrivilege + " should have name " + nameSet; - - return userRole.application().grants(checkPrivilege, checkResource); - } - } 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 bd27bd24fdd85..ec302f1003af8 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 @@ -35,6 +35,10 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportActionProxy; import org.elasticsearch.transport.TransportRequest; +import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesRequest; +import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse; +import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesRequest; +import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesResponse; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.AuthenticationFailureHandler; import org.elasticsearch.xpack.core.security.authc.esnative.ClientReservedRealm; @@ -48,6 +52,7 @@ 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; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.user.AnonymousUser; @@ -68,6 +73,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; @@ -118,6 +124,22 @@ public AuthorizationService(Settings settings, CompositeRolesStore rolesStore, C this.settings = settings; } + public void checkPrivileges(Authentication authentication, HasPrivilegesRequest request, + Collection applicationPrivilegeDescriptors, + ActionListener listener) { + getAuthorizationEngine(authentication).checkPrivileges(authentication, getAuthorizationInfoFromContext(), request, + applicationPrivilegeDescriptors, wrapPreservingContext(listener, threadContext)); + } + + public void retrieveUserPrivileges(Authentication authentication, GetUserPrivilegesRequest request, + ActionListener listener) { + getAuthorizationEngine(authentication).getUserPrivileges(authentication, getAuthorizationInfoFromContext(), request, listener); + } + + private AuthorizationInfo getAuthorizationInfoFromContext() { + return Objects.requireNonNull(threadContext.getTransient(AUTHORIZATION_INFO_KEY), "authorization info is missing from context"); + } + /** * Verifies that the given user can execute the given request (and action). If the user doesn't * have the appropriate privileges for this action/request, an {@link ElasticsearchSecurityException} 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 0d2974e4c8abc..8a8cd3dbe93af 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,9 @@ package org.elasticsearch.xpack.security.authz; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.ParameterizedMessage; import org.apache.lucene.util.automaton.Automaton; import org.apache.lucene.util.automaton.Operations; import org.elasticsearch.action.ActionListener; @@ -23,6 +26,9 @@ import org.elasticsearch.action.search.SearchTransportService; import org.elasticsearch.action.termvectors.MultiTermVectorsAction; import org.elasticsearch.cluster.metadata.AliasOrIndex; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.transport.TransportActionProxy; @@ -30,30 +36,51 @@ import org.elasticsearch.xpack.core.security.action.user.AuthenticateAction; import org.elasticsearch.xpack.core.security.action.user.ChangePasswordAction; import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesAction; +import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesRequest; +import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse; import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesAction; +import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesRequest; +import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesResponse; import org.elasticsearch.xpack.core.security.action.user.UserRequest; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings; import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine; import org.elasticsearch.xpack.core.security.authz.ResolvedIndices; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsCache; +import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsDefinition; +import org.elasticsearch.xpack.core.security.authz.permission.IndicesPermission; import org.elasticsearch.xpack.core.security.authz.permission.Role; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; +import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.Privilege; import org.elasticsearch.xpack.core.security.support.Automatons; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.TreeSet; import java.util.function.Function; import java.util.function.Predicate; +import static org.elasticsearch.common.Strings.arrayToCommaDelimitedString; +import static org.elasticsearch.xpack.security.action.user.TransportHasPrivilegesAction.getApplicationNames; import static org.elasticsearch.xpack.security.audit.logfile.LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME; public class RBACEngine implements AuthorizationEngine { @@ -65,10 +92,12 @@ public class RBACEngine implements AuthorizationEngine { private static final String DELETE_SUB_REQUEST_PRIMARY = DeleteAction.NAME + "[p]"; private static final String DELETE_SUB_REQUEST_REPLICA = DeleteAction.NAME + "[r]"; + private static final Logger logger = LogManager.getLogger(RBACEngine.class); + private final CompositeRolesStore rolesStore; private final FieldPermissionsCache fieldPermissionsCache; - RBACEngine(Settings settings, CompositeRolesStore rolesStore) { + public RBACEngine(Settings settings, CompositeRolesStore rolesStore) { this.rolesStore = rolesStore; this.fieldPermissionsCache = new FieldPermissionsCache(settings); } @@ -310,7 +339,200 @@ public void validateIndexPermissionsAreSubset(RequestInfo requestInfo, Authoriza listener.onFailure( new IllegalArgumentException("unsupported authorization info:" + authorizationInfo.getClass().getSimpleName())); } + } + + @Override + public void checkPrivileges(Authentication authentication, AuthorizationInfo authorizationInfo, + HasPrivilegesRequest request, + Collection applicationPrivileges, + ActionListener listener) { + if (authorizationInfo instanceof RBACAuthorizationInfo == false) { + listener.onFailure( + new IllegalArgumentException("unsupported authorization info:" + authorizationInfo.getClass().getSimpleName())); + return; + } + final Role userRole = ((RBACAuthorizationInfo) authorizationInfo).getRole(); + logger.trace(() -> new ParameterizedMessage("Check whether role [{}] has privileges cluster=[{}] index=[{}] application=[{}]", + Strings.arrayToCommaDelimitedString(userRole.names()), + Strings.arrayToCommaDelimitedString(request.clusterPrivileges()), + Strings.arrayToCommaDelimitedString(request.indexPrivileges()), + Strings.arrayToCommaDelimitedString(request.applicationPrivileges()) + )); + + Map cluster = new HashMap<>(); + for (String checkAction : request.clusterPrivileges()) { + final ClusterPrivilege checkPrivilege = ClusterPrivilege.get(Collections.singleton(checkAction)); + final ClusterPrivilege rolePrivilege = userRole.cluster().privilege(); + cluster.put(checkAction, testPrivilege(checkPrivilege, rolePrivilege.getAutomaton())); + } + boolean allMatch = cluster.values().stream().allMatch(Boolean::booleanValue); + + final Map predicateCache = new HashMap<>(); + + final Map indices = new LinkedHashMap<>(); + for (RoleDescriptor.IndicesPrivileges check : request.indexPrivileges()) { + for (String index : check.getIndices()) { + final Map privileges = new HashMap<>(); + final HasPrivilegesResponse.ResourcePrivileges existing = indices.get(index); + if (existing != null) { + privileges.putAll(existing.getPrivileges()); + } + for (String privilege : check.getPrivileges()) { + if (testIndexMatch(index, check.allowRestrictedIndices(), privilege, userRole, predicateCache)) { + logger.debug(() -> new ParameterizedMessage("Role [{}] has [{}] on index [{}]", + Strings.arrayToCommaDelimitedString(userRole.names()), privilege, index)); + privileges.put(privilege, true); + } else { + logger.debug(() -> new ParameterizedMessage("Role [{}] does not have [{}] on index [{}]", + Strings.arrayToCommaDelimitedString(userRole.names()), privilege, index)); + privileges.put(privilege, false); + allMatch = false; + } + } + indices.put(index, new HasPrivilegesResponse.ResourcePrivileges(index, privileges)); + } + } + + final Map> privilegesByApplication = new HashMap<>(); + for (String applicationName : getApplicationNames(request)) { + logger.debug("Checking privileges for application {}", applicationName); + final Map appPrivilegesByResource = new LinkedHashMap<>(); + for (RoleDescriptor.ApplicationResourcePrivileges p : request.applicationPrivileges()) { + if (applicationName.equals(p.getApplication())) { + for (String resource : p.getResources()) { + final Map privileges = new HashMap<>(); + final HasPrivilegesResponse.ResourcePrivileges existing = appPrivilegesByResource.get(resource); + if (existing != null) { + privileges.putAll(existing.getPrivileges()); + } + for (String privilege : p.getPrivileges()) { + if (testResourceMatch(applicationName, resource, privilege, userRole, applicationPrivileges)) { + logger.debug(() -> new ParameterizedMessage("Role [{}] has [{} {}] on resource [{}]", + Strings.arrayToCommaDelimitedString(userRole.names()), applicationName, privilege, resource)); + privileges.put(privilege, true); + } else { + logger.debug(() -> new ParameterizedMessage("Role [{}] does not have [{} {}] on resource [{}]", + Strings.arrayToCommaDelimitedString(userRole.names()), applicationName, privilege, resource)); + privileges.put(privilege, false); + allMatch = false; + } + } + appPrivilegesByResource.put(resource, new HasPrivilegesResponse.ResourcePrivileges(resource, privileges)); + } + } + } + privilegesByApplication.put(applicationName, appPrivilegesByResource.values()); + } + + listener.onResponse(new HasPrivilegesResponse(request.username(), allMatch, cluster, indices.values(), privilegesByApplication)); + } + + + @Override + public void getUserPrivileges(Authentication authentication, AuthorizationInfo authorizationInfo, GetUserPrivilegesRequest request, + ActionListener listener) { + if (authorizationInfo instanceof RBACAuthorizationInfo == false) { + listener.onFailure( + new IllegalArgumentException("unsupported authorization info:" + authorizationInfo.getClass().getSimpleName())); + } else { + final Role role = ((RBACAuthorizationInfo) authorizationInfo).getRole(); + listener.onResponse(buildUserPrivilegesResponseObject(role)); + } + } + + GetUserPrivilegesResponse buildUserPrivilegesResponseObject(Role userRole) { + logger.trace(() -> new ParameterizedMessage("List privileges for role [{}]", arrayToCommaDelimitedString(userRole.names()))); + + // We use sorted sets for Strings because they will typically be small, and having a predictable order allows for simpler testing + final Set cluster = new TreeSet<>(); + // But we don't have a meaningful ordering for objects like ConditionalClusterPrivilege, so the tests work with "random" ordering + final Set conditionalCluster = new HashSet<>(); + for (Tuple tup : userRole.cluster().privileges()) { + if (tup.v2() == null) { + if (ClusterPrivilege.NONE.equals(tup.v1()) == false) { + cluster.addAll(tup.v1().name()); + } + } else { + conditionalCluster.add(tup.v2()); + } + } + + final Set indices = new LinkedHashSet<>(); + for (IndicesPermission.Group group : userRole.indices().groups()) { + final Set queries = group.getQuery() == null ? Collections.emptySet() : group.getQuery(); + final Set fieldSecurity = group.getFieldPermissions().hasFieldLevelSecurity() + ? group.getFieldPermissions().getFieldPermissionsDefinition().getFieldGrantExcludeGroups() : Collections.emptySet(); + indices.add(new GetUserPrivilegesResponse.Indices( + Arrays.asList(group.indices()), + group.privilege().name(), + fieldSecurity, + queries, + group.allowRestrictedIndices() + )); + } + + final Set application = new LinkedHashSet<>(); + for (String applicationName : userRole.application().getApplicationNames()) { + for (ApplicationPrivilege privilege : userRole.application().getPrivileges(applicationName)) { + final Set resources = userRole.application().getResourcePatterns(privilege); + if (resources.isEmpty()) { + logger.trace("No resources defined in application privilege {}", privilege); + } else { + application.add(RoleDescriptor.ApplicationResourcePrivileges.builder() + .application(applicationName) + .privileges(privilege.name()) + .resources(resources) + .build()); + } + } + } + + final Privilege runAsPrivilege = userRole.runAs().getPrivilege(); + final Set runAs; + if (Operations.isEmpty(runAsPrivilege.getAutomaton())) { + runAs = Collections.emptySet(); + } else { + runAs = runAsPrivilege.name(); + } + + return new GetUserPrivilegesResponse(cluster, conditionalCluster, indices, application, runAs); + } + + private boolean testIndexMatch(String checkIndexPattern, boolean allowRestrictedIndices, String checkPrivilegeName, Role userRole, + Map predicateCache) { + final IndexPrivilege checkPrivilege = IndexPrivilege.get(Collections.singleton(checkPrivilegeName)); + + final Automaton checkIndexAutomaton = IndicesPermission.Group.buildIndexMatcherAutomaton(allowRestrictedIndices, checkIndexPattern); + + List privilegeAutomatons = new ArrayList<>(); + for (IndicesPermission.Group group : userRole.indices().groups()) { + final Automaton groupIndexAutomaton = predicateCache.computeIfAbsent(group, + g -> IndicesPermission.Group.buildIndexMatcherAutomaton(g.allowRestrictedIndices(), g.indices())); + if (Operations.subsetOf(checkIndexAutomaton, groupIndexAutomaton)) { + final IndexPrivilege rolePrivilege = group.privilege(); + if (rolePrivilege.name().contains(checkPrivilegeName)) { + return true; + } + privilegeAutomatons.add(rolePrivilege.getAutomaton()); + } + } + return testPrivilege(checkPrivilege, Automatons.unionAndMinimize(privilegeAutomatons)); + } + + private static boolean testPrivilege(Privilege checkPrivilege, Automaton roleAutomaton) { + return Operations.subsetOf(checkPrivilege.getAutomaton(), roleAutomaton); + } + + private boolean testResourceMatch(String application, String checkResource, String checkPrivilegeName, Role userRole, + Collection privileges) { + final Set nameSet = Collections.singleton(checkPrivilegeName); + final ApplicationPrivilege checkPrivilege = ApplicationPrivilege.get(application, nameSet, privileges); + assert checkPrivilege.getApplication().equals(application) + : "Privilege " + checkPrivilege + " should have application " + application; + assert checkPrivilege.name().equals(nameSet) + : "Privilege " + checkPrivilege + " should have name " + nameSet; + return userRole.application().grants(checkPrivilege, checkResource); } static List resolveAuthorizedIndicesFromRole(Role role, String action, Map aliasAndIndexLookup) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportGetUserPrivilegesActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportGetUserPrivilegesActionTests.java deleted file mode 100644 index 68cd414e0441d..0000000000000 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportGetUserPrivilegesActionTests.java +++ /dev/null @@ -1,86 +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.user; - -import org.elasticsearch.action.support.ActionFilters; -import org.elasticsearch.common.bytes.BytesArray; -import org.elasticsearch.common.util.set.Sets; -import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.threadpool.ThreadPool; -import org.elasticsearch.transport.TransportService; -import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse; -import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; -import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions; -import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsDefinition; -import org.elasticsearch.xpack.core.security.authz.permission.Role; -import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; -import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivileges.ManageApplicationPrivileges; -import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; -import org.elasticsearch.xpack.core.security.authz.privilege.Privilege; -import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; - -import java.util.Collections; -import java.util.Set; - -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.emptyIterable; -import static org.hamcrest.Matchers.iterableWithSize; -import static org.mockito.Mockito.mock; - -public class TransportGetUserPrivilegesActionTests extends ESTestCase { - - public void testBuildResponseObject() { - final ManageApplicationPrivileges manageApplicationPrivileges = new ManageApplicationPrivileges(Sets.newHashSet("app01", "app02")); - final BytesArray query = new BytesArray("{\"term\":{\"public\":true}}"); - final Role role = Role.builder("test", "role") - .cluster(Sets.newHashSet("monitor", "manage_watcher"), Collections.singleton(manageApplicationPrivileges)) - .add(IndexPrivilege.get(Sets.newHashSet("read", "write")), "index-1") - .add(IndexPrivilege.ALL, "index-2", "index-3") - .add( - new FieldPermissions(new FieldPermissionsDefinition(new String[]{ "public.*" }, new String[0])), - Collections.singleton(query), - IndexPrivilege.READ, randomBoolean(), "index-4", "index-5") - .addApplicationPrivilege(new ApplicationPrivilege("app01", "read", "data:read"), Collections.singleton("*")) - .runAs(new Privilege(Sets.newHashSet("user01", "user02"), "user01", "user02")) - .build(); - - final TransportGetUserPrivilegesAction action = new TransportGetUserPrivilegesAction(mock(ThreadPool.class), - mock(TransportService.class), mock(ActionFilters.class), mock(CompositeRolesStore.class)); - final GetUserPrivilegesResponse response = action.buildResponseObject(role); - - assertThat(response.getClusterPrivileges(), containsInAnyOrder("monitor", "manage_watcher")); - assertThat(response.getConditionalClusterPrivileges(), containsInAnyOrder(manageApplicationPrivileges)); - - assertThat(response.getIndexPrivileges(), iterableWithSize(3)); - final GetUserPrivilegesResponse.Indices index1 = findIndexPrivilege(response.getIndexPrivileges(), "index-1"); - assertThat(index1.getIndices(), containsInAnyOrder("index-1")); - assertThat(index1.getPrivileges(), containsInAnyOrder("read", "write")); - assertThat(index1.getFieldSecurity(), emptyIterable()); - assertThat(index1.getQueries(), emptyIterable()); - final GetUserPrivilegesResponse.Indices index2 = findIndexPrivilege(response.getIndexPrivileges(), "index-2"); - assertThat(index2.getIndices(), containsInAnyOrder("index-2", "index-3")); - assertThat(index2.getPrivileges(), containsInAnyOrder("all")); - assertThat(index2.getFieldSecurity(), emptyIterable()); - assertThat(index2.getQueries(), emptyIterable()); - final GetUserPrivilegesResponse.Indices index4 = findIndexPrivilege(response.getIndexPrivileges(), "index-4"); - assertThat(index4.getIndices(), containsInAnyOrder("index-4", "index-5")); - assertThat(index4.getPrivileges(), containsInAnyOrder("read")); - assertThat(index4.getFieldSecurity(), containsInAnyOrder( - new FieldPermissionsDefinition.FieldGrantExcludeGroup(new String[]{ "public.*" }, new String[0]))); - assertThat(index4.getQueries(), containsInAnyOrder(query)); - - assertThat(response.getApplicationPrivileges(), containsInAnyOrder( - RoleDescriptor.ApplicationResourcePrivileges.builder().application("app01").privileges("read").resources("*").build()) - ); - - assertThat(response.getRunAs(), containsInAnyOrder("user01", "user02")); - } - - private GetUserPrivilegesResponse.Indices findIndexPrivilege(Set indices, String name) { - return indices.stream().filter(i -> i.getIndices().contains(name)).findFirst().get(); - } -} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesActionTests.java deleted file mode 100644 index 8d734d5d79925..0000000000000 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesActionTests.java +++ /dev/null @@ -1,526 +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.user; - -import org.elasticsearch.action.ActionListener; -import org.elasticsearch.action.admin.cluster.health.ClusterHealthAction; -import org.elasticsearch.action.delete.DeleteAction; -import org.elasticsearch.action.index.IndexAction; -import org.elasticsearch.action.support.ActionFilters; -import org.elasticsearch.action.support.PlainActionFuture; -import org.elasticsearch.common.Strings; -import org.elasticsearch.common.collect.MapBuilder; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.util.concurrent.ThreadContext; -import org.elasticsearch.common.util.set.Sets; -import org.elasticsearch.mock.orig.Mockito; -import org.elasticsearch.tasks.Task; -import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.test.junit.annotations.TestLogging; -import org.elasticsearch.threadpool.ThreadPool; -import org.elasticsearch.transport.Transport; -import org.elasticsearch.transport.TransportService; -import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesRequest; -import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesResponse; -import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesResponse.ResourcePrivileges; -import org.elasticsearch.xpack.core.security.authc.Authentication; -import org.elasticsearch.xpack.core.security.authc.AuthenticationField; -import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; -import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsCache; -import org.elasticsearch.xpack.core.security.authz.permission.Role; -import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; -import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; -import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege; -import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; -import org.elasticsearch.xpack.core.security.user.User; -import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; -import org.elasticsearch.xpack.security.authz.store.NativePrivilegeStore; -import org.hamcrest.Matchers; -import org.junit.Before; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Locale; -import java.util.Set; - -import static java.util.Collections.emptyMap; -import static org.elasticsearch.common.util.set.Sets.newHashSet; -import static org.hamcrest.Matchers.arrayWithSize; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.iterableWithSize; -import static org.hamcrest.Matchers.notNullValue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -@TestLogging("org.elasticsearch.xpack.security.action.user.TransportHasPrivilegesAction:TRACE," + - "org.elasticsearch.xpack.core.security.authz.permission.ApplicationPermission:DEBUG") -public class TransportHasPrivilegesActionTests extends ESTestCase { - - private User user; - private Role role; - private TransportHasPrivilegesAction action; - private List applicationPrivileges; - - @Before - public void setup() { - user = new User(randomAlphaOfLengthBetween(4, 12)); - final ThreadPool threadPool = mock(ThreadPool.class); - final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); - final TransportService transportService = new TransportService(Settings.EMPTY, mock(Transport.class), null, - TransportService.NOOP_TRANSPORT_INTERCEPTOR, x -> null, null, Collections.emptySet()); - - final Authentication authentication = mock(Authentication.class); - threadContext.putTransient(AuthenticationField.AUTHENTICATION_KEY, authentication); - when(threadPool.getThreadContext()).thenReturn(threadContext); - - when(authentication.getUser()).thenReturn(user); - - CompositeRolesStore rolesStore = mock(CompositeRolesStore.class); - Mockito.doAnswer(invocationOnMock -> { - ActionListener listener = (ActionListener) invocationOnMock.getArguments()[2]; - listener.onResponse(role); - return null; - }).when(rolesStore).getRoles(eq(user), any(FieldPermissionsCache.class), any(ActionListener.class)); - - applicationPrivileges = new ArrayList<>(); - NativePrivilegeStore privilegeStore = mock(NativePrivilegeStore.class); - Mockito.doAnswer(inv -> { - assertThat(inv.getArguments(), arrayWithSize(3)); - ActionListener> listener - = (ActionListener>) inv.getArguments()[2]; - logger.info("Privileges for ({}) are {}", Arrays.toString(inv.getArguments()), applicationPrivileges); - listener.onResponse(applicationPrivileges); - return null; - }).when(privilegeStore).getPrivileges(any(Collection.class), any(Collection.class), any(ActionListener.class)); - - action = new TransportHasPrivilegesAction(threadPool, transportService, mock(ActionFilters.class), rolesStore, - privilegeStore); - } - - /** - * This tests that action names in the request are considered "matched" by the relevant named privilege - * (in this case that {@link DeleteAction} and {@link IndexAction} are satisfied by {@link IndexPrivilege#WRITE}). - */ - public void testNamedIndexPrivilegesMatchApplicableActions() throws Exception { - role = Role.builder("test1") - .cluster(Collections.singleton("all"), Collections.emptyList()) - .add(IndexPrivilege.WRITE, "academy") - .build(); - - final HasPrivilegesRequest request = new HasPrivilegesRequest(); - request.username(user.principal()); - request.clusterPrivileges(ClusterHealthAction.NAME); - request.indexPrivileges(RoleDescriptor.IndicesPrivileges.builder() - .indices("academy") - .privileges(DeleteAction.NAME, IndexAction.NAME) - .build()); - request.applicationPrivileges(new RoleDescriptor.ApplicationResourcePrivileges[0]); - final PlainActionFuture future = new PlainActionFuture(); - action.doExecute(mock(Task.class), request, future); - - final HasPrivilegesResponse response = future.get(); - assertThat(response, notNullValue()); - assertThat(response.getUsername(), is(user.principal())); - assertThat(response.isCompleteMatch(), is(true)); - - assertThat(response.getClusterPrivileges().size(), equalTo(1)); - assertThat(response.getClusterPrivileges().get(ClusterHealthAction.NAME), equalTo(true)); - - assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(1)); - final ResourcePrivileges result = response.getIndexPrivileges().iterator().next(); - assertThat(result.getResource(), equalTo("academy")); - assertThat(result.getPrivileges().size(), equalTo(2)); - assertThat(result.getPrivileges().get(DeleteAction.NAME), equalTo(true)); - assertThat(result.getPrivileges().get(IndexAction.NAME), equalTo(true)); - } - - /** - * This tests that the action responds correctly when the user/role has some, but not all - * of the privileges being checked. - */ - public void testMatchSubsetOfPrivileges() throws Exception { - role = Role.builder("test2") - .cluster(ClusterPrivilege.MONITOR) - .add(IndexPrivilege.INDEX, "academy") - .add(IndexPrivilege.WRITE, "initiative") - .build(); - - final HasPrivilegesRequest request = new HasPrivilegesRequest(); - request.username(user.principal()); - request.clusterPrivileges("monitor", "manage"); - request.indexPrivileges(RoleDescriptor.IndicesPrivileges.builder() - .indices("academy", "initiative", "school") - .privileges("delete", "index", "manage") - .build()); - request.applicationPrivileges(new RoleDescriptor.ApplicationResourcePrivileges[0]); - final PlainActionFuture future = new PlainActionFuture(); - action.doExecute(mock(Task.class), request, future); - - final HasPrivilegesResponse response = future.get(); - assertThat(response, notNullValue()); - assertThat(response.getUsername(), is(user.principal())); - assertThat(response.isCompleteMatch(), is(false)); - assertThat(response.getClusterPrivileges().size(), equalTo(2)); - assertThat(response.getClusterPrivileges().get("monitor"), equalTo(true)); - assertThat(response.getClusterPrivileges().get("manage"), equalTo(false)); - assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(3)); - - final Iterator indexPrivilegesIterator = response.getIndexPrivileges().iterator(); - final ResourcePrivileges academy = indexPrivilegesIterator.next(); - final ResourcePrivileges initiative = indexPrivilegesIterator.next(); - final ResourcePrivileges school = indexPrivilegesIterator.next(); - - assertThat(academy.getResource(), equalTo("academy")); - assertThat(academy.getPrivileges().size(), equalTo(3)); - assertThat(academy.getPrivileges().get("index"), equalTo(true)); // explicit - assertThat(academy.getPrivileges().get("delete"), equalTo(false)); - assertThat(academy.getPrivileges().get("manage"), equalTo(false)); - - assertThat(initiative.getResource(), equalTo("initiative")); - assertThat(initiative.getPrivileges().size(), equalTo(3)); - assertThat(initiative.getPrivileges().get("index"), equalTo(true)); // implied by write - assertThat(initiative.getPrivileges().get("delete"), equalTo(true)); // implied by write - assertThat(initiative.getPrivileges().get("manage"), equalTo(false)); - - assertThat(school.getResource(), equalTo("school")); - assertThat(school.getPrivileges().size(), equalTo(3)); - assertThat(school.getPrivileges().get("index"), equalTo(false)); - assertThat(school.getPrivileges().get("delete"), equalTo(false)); - assertThat(school.getPrivileges().get("manage"), equalTo(false)); - } - - /** - * This tests that the action responds correctly when the user/role has none - * of the privileges being checked. - */ - public void testMatchNothing() throws Exception { - role = Role.builder("test3") - .cluster(ClusterPrivilege.MONITOR) - .build(); - - final HasPrivilegesResponse response = hasPrivileges(RoleDescriptor.IndicesPrivileges.builder() - .indices("academy") - .privileges("read", "write") - .build(), Strings.EMPTY_ARRAY); - assertThat(response.getUsername(), is(user.principal())); - assertThat(response.isCompleteMatch(), is(false)); - assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(1)); - final ResourcePrivileges result = response.getIndexPrivileges().iterator().next(); - assertThat(result.getResource(), equalTo("academy")); - assertThat(result.getPrivileges().size(), equalTo(2)); - assertThat(result.getPrivileges().get("read"), equalTo(false)); - assertThat(result.getPrivileges().get("write"), equalTo(false)); - } - - /** - * Wildcards in the request are treated as - * does the user have ___ privilege on every possible index that matches this pattern? - * Or, expressed differently, - * does the user have ___ privilege on a wildcard that covers (is a superset of) this pattern? - */ - public void testWildcardHandling() throws Exception { - final ApplicationPrivilege kibanaRead = defineApplicationPrivilege("kibana", "read", - "data:read/*", "action:login", "action:view/dashboard"); - final ApplicationPrivilege kibanaWrite = defineApplicationPrivilege("kibana", "write", - "data:write/*", "action:login", "action:view/dashboard"); - final ApplicationPrivilege kibanaAdmin = defineApplicationPrivilege("kibana", "admin", - "action:login", "action:manage/*"); - final ApplicationPrivilege kibanaViewSpace = defineApplicationPrivilege("kibana", "view-space", - "action:login", "space:view/*"); - role = Role.builder("test3") - .add(IndexPrivilege.ALL, "logstash-*", "foo?") - .add(IndexPrivilege.READ, "abc*") - .add(IndexPrivilege.WRITE, "*xyz") - .addApplicationPrivilege(kibanaRead, Collections.singleton("*")) - .addApplicationPrivilege(kibanaViewSpace, newHashSet("space/engineering/*", "space/builds")) - .build(); - - final HasPrivilegesRequest request = new HasPrivilegesRequest(); - request.username(user.principal()); - request.clusterPrivileges(Strings.EMPTY_ARRAY); - request.indexPrivileges( - RoleDescriptor.IndicesPrivileges.builder() - .indices("logstash-2016-*") - .privileges("write") // Yes, because (ALL,"logstash-*") - .build(), - RoleDescriptor.IndicesPrivileges.builder() - .indices("logstash-*") - .privileges("read") // Yes, because (ALL,"logstash-*") - .build(), - RoleDescriptor.IndicesPrivileges.builder() - .indices("log*") - .privileges("manage") // No, because "log*" includes indices that "logstash-*" does not - .build(), - RoleDescriptor.IndicesPrivileges.builder() - .indices("foo*", "foo?") - .privileges("read") // Yes, "foo?", but not "foo*", because "foo*" > "foo?" - .build(), - RoleDescriptor.IndicesPrivileges.builder() - .indices("abcd*") - .privileges("read", "write") // read = Yes, because (READ, "abc*"), write = No - .build(), - RoleDescriptor.IndicesPrivileges.builder() - .indices("abc*xyz") - .privileges("read", "write", "manage") // read = Yes ( READ "abc*"), write = Yes (WRITE, "*xyz"), manage = No - .build(), - RoleDescriptor.IndicesPrivileges.builder() - .indices("a*xyz") - .privileges("read", "write", "manage") // read = No, write = Yes (WRITE, "*xyz"), manage = No - .build() - ); - - request.applicationPrivileges( - RoleDescriptor.ApplicationResourcePrivileges.builder() - .resources("*") - .application("kibana") - .privileges(Sets.union(kibanaRead.name(), kibanaWrite.name())) // read = Yes, write = No - .build(), - RoleDescriptor.ApplicationResourcePrivileges.builder() - .resources("space/engineering/project-*", "space/*") // project-* = Yes, space/* = Not - .application("kibana") - .privileges("space:view/dashboard") - .build() - ); - - final PlainActionFuture future = new PlainActionFuture(); - action.doExecute(mock(Task.class), request, future); - - final HasPrivilegesResponse response = future.get(); - assertThat(response, notNullValue()); - assertThat(response.getUsername(), is(user.principal())); - assertThat(response.isCompleteMatch(), is(false)); - assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(8)); - assertThat(response.getIndexPrivileges(), containsInAnyOrder( - new ResourcePrivileges("logstash-2016-*", Collections.singletonMap("write", true)), - new ResourcePrivileges("logstash-*", Collections.singletonMap("read", true)), - new ResourcePrivileges("log*", Collections.singletonMap("manage", false)), - new ResourcePrivileges("foo?", Collections.singletonMap("read", true)), - new ResourcePrivileges("foo*", Collections.singletonMap("read", false)), - new ResourcePrivileges("abcd*", mapBuilder().put("read", true).put("write", false).map()), - new ResourcePrivileges("abc*xyz", mapBuilder().put("read", true).put("write", true).put("manage", false).map()), - new ResourcePrivileges("a*xyz", mapBuilder().put("read", false).put("write", true).put("manage", false).map()) - )); - assertThat(response.getApplicationPrivileges().entrySet(), Matchers.iterableWithSize(1)); - final Set kibanaPrivileges = response.getApplicationPrivileges().get("kibana"); - assertThat(kibanaPrivileges, Matchers.iterableWithSize(3)); - assertThat(Strings.collectionToCommaDelimitedString(kibanaPrivileges), kibanaPrivileges, containsInAnyOrder( - new ResourcePrivileges("*", mapBuilder().put("read", true).put("write", false).map()), - new ResourcePrivileges("space/engineering/project-*", Collections.singletonMap("space:view/dashboard", true)), - new ResourcePrivileges("space/*", Collections.singletonMap("space:view/dashboard", false)) - )); - } - - private ApplicationPrivilege defineApplicationPrivilege(String app, String name, String ... actions) { - this.applicationPrivileges.add(new ApplicationPrivilegeDescriptor(app, name, newHashSet(actions), emptyMap())); - return new ApplicationPrivilege(app, name, actions); - } - - public void testCheckingIndexPermissionsDefinedOnDifferentPatterns() throws Exception { - role = Role.builder("test-write") - .add(IndexPrivilege.INDEX, "apache-*") - .add(IndexPrivilege.DELETE, "apache-2016-*") - .build(); - - final HasPrivilegesResponse response = hasPrivileges(RoleDescriptor.IndicesPrivileges.builder() - .indices("apache-2016-12", "apache-2017-01") - .privileges("index", "delete") - .build(), Strings.EMPTY_ARRAY); - assertThat(response.isCompleteMatch(), is(false)); - assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(2)); - assertThat(response.getIndexPrivileges(), containsInAnyOrder( - new ResourcePrivileges("apache-2016-12", - MapBuilder.newMapBuilder(new LinkedHashMap()) - .put("index", true).put("delete", true).map()), - new ResourcePrivileges("apache-2017-01", - MapBuilder.newMapBuilder(new LinkedHashMap()) - .put("index", true).put("delete", false).map() - ) - )); - } - - public void testCheckingApplicationPrivilegesOnDifferentApplicationsAndResources() throws Exception { - final ApplicationPrivilege app1Read = defineApplicationPrivilege("app1", "read", "data:read/*"); - final ApplicationPrivilege app1Write = defineApplicationPrivilege("app1", "write", "data:write/*"); - final ApplicationPrivilege app1All = defineApplicationPrivilege("app1", "all", "*"); - final ApplicationPrivilege app2Read = defineApplicationPrivilege("app2", "read", "data:read/*"); - final ApplicationPrivilege app2Write = defineApplicationPrivilege("app2", "write", "data:write/*"); - final ApplicationPrivilege app2All = defineApplicationPrivilege("app2", "all", "*"); - - role = Role.builder("test-role") - .addApplicationPrivilege(app1Read, Collections.singleton("foo/*")) - .addApplicationPrivilege(app1All, Collections.singleton("foo/bar/baz")) - .addApplicationPrivilege(app2Read, Collections.singleton("foo/bar/*")) - .addApplicationPrivilege(app2Write, Collections.singleton("*/bar/*")) - .build(); - - final HasPrivilegesResponse response = hasPrivileges(new RoleDescriptor.IndicesPrivileges[0], - new RoleDescriptor.ApplicationResourcePrivileges[]{ - RoleDescriptor.ApplicationResourcePrivileges.builder() - .application("app1") - .resources("foo/1", "foo/bar/2", "foo/bar/baz", "baz/bar/foo") - .privileges("read", "write", "all") - .build(), - RoleDescriptor.ApplicationResourcePrivileges.builder() - .application("app2") - .resources("foo/1", "foo/bar/2", "foo/bar/baz", "baz/bar/foo") - .privileges("read", "write", "all") - .build() - }, Strings.EMPTY_ARRAY); - - assertThat(response.isCompleteMatch(), is(false)); - assertThat(response.getIndexPrivileges(), Matchers.emptyIterable()); - assertThat(response.getApplicationPrivileges().entrySet(), Matchers.iterableWithSize(2)); - final Set app1 = response.getApplicationPrivileges().get("app1"); - assertThat(app1, Matchers.iterableWithSize(4)); - assertThat(Strings.collectionToCommaDelimitedString(app1), app1, containsInAnyOrder( - new ResourcePrivileges("foo/1", MapBuilder.newMapBuilder(new LinkedHashMap()) - .put("read", true).put("write", false).put("all", false).map()), - new ResourcePrivileges("foo/bar/2", MapBuilder.newMapBuilder(new LinkedHashMap()) - .put("read", true).put("write", false).put("all", false).map()), - new ResourcePrivileges("foo/bar/baz", MapBuilder.newMapBuilder(new LinkedHashMap()) - .put("read", true).put("write", true).put("all", true).map()), - new ResourcePrivileges("baz/bar/foo", MapBuilder.newMapBuilder(new LinkedHashMap()) - .put("read", false).put("write", false).put("all", false).map()) - )); - final Set app2 = response.getApplicationPrivileges().get("app2"); - assertThat(app2, Matchers.iterableWithSize(4)); - assertThat(Strings.collectionToCommaDelimitedString(app2), app2, containsInAnyOrder( - new ResourcePrivileges("foo/1", MapBuilder.newMapBuilder(new LinkedHashMap()) - .put("read", false).put("write", false).put("all", false).map()), - new ResourcePrivileges("foo/bar/2", MapBuilder.newMapBuilder(new LinkedHashMap()) - .put("read", true).put("write", true).put("all", false).map()), - new ResourcePrivileges("foo/bar/baz", MapBuilder.newMapBuilder(new LinkedHashMap()) - .put("read", true).put("write", true).put("all", false).map()), - new ResourcePrivileges("baz/bar/foo", MapBuilder.newMapBuilder(new LinkedHashMap()) - .put("read", false).put("write", true).put("all", false).map()) - )); - } - - public void testCheckingApplicationPrivilegesWithComplexNames() throws Exception { - final String appName = randomAlphaOfLength(1).toLowerCase(Locale.ROOT) + randomAlphaOfLengthBetween(3, 10); - final String action1 = randomAlphaOfLength(1).toLowerCase(Locale.ROOT) + randomAlphaOfLengthBetween(2, 5); - final String action2 = randomAlphaOfLength(1).toLowerCase(Locale.ROOT) + randomAlphaOfLengthBetween(6, 9); - - final ApplicationPrivilege priv1 = defineApplicationPrivilege(appName, action1, "DATA:read/*", "ACTION:" + action1); - final ApplicationPrivilege priv2 = defineApplicationPrivilege(appName, action2, "DATA:read/*", "ACTION:" + action2); - - role = Role.builder("test-write") - .addApplicationPrivilege(priv1, Collections.singleton("user/*/name")) - .build(); - - final HasPrivilegesResponse response = hasPrivileges( - new RoleDescriptor.IndicesPrivileges[0], - new RoleDescriptor.ApplicationResourcePrivileges[]{ - RoleDescriptor.ApplicationResourcePrivileges.builder() - .application(appName) - .resources("user/hawkeye/name") - .privileges("DATA:read/user/*", "ACTION:" + action1, "ACTION:" + action2, action1, action2) - .build() - }, - "monitor"); - assertThat(response.isCompleteMatch(), is(false)); - assertThat(response.getApplicationPrivileges().keySet(), containsInAnyOrder(appName)); - assertThat(response.getApplicationPrivileges().get(appName), iterableWithSize(1)); - assertThat(response.getApplicationPrivileges().get(appName), containsInAnyOrder( - new ResourcePrivileges("user/hawkeye/name", MapBuilder.newMapBuilder(new LinkedHashMap()) - .put("DATA:read/user/*", true) - .put("ACTION:" + action1, true) - .put("ACTION:" + action2, false) - .put(action1, true) - .put(action2, false) - .map()) - )); - } - - public void testIsCompleteMatch() throws Exception { - final ApplicationPrivilege kibanaRead = defineApplicationPrivilege("kibana", "read", "data:read/*"); - final ApplicationPrivilege kibanaWrite = defineApplicationPrivilege("kibana", "write", "data:write/*"); - role = Role.builder("test-write") - .cluster(ClusterPrivilege.MONITOR) - .add(IndexPrivilege.READ, "read-*") - .add(IndexPrivilege.ALL, "all-*") - .addApplicationPrivilege(kibanaRead, Collections.singleton("*")) - .build(); - - assertThat(hasPrivileges(indexPrivileges("read", "read-123", "read-456", "all-999"), "monitor").isCompleteMatch(), is(true)); - assertThat(hasPrivileges(indexPrivileges("read", "read-123", "read-456", "all-999"), "manage").isCompleteMatch(), is(false)); - assertThat(hasPrivileges(indexPrivileges("write", "read-123", "read-456", "all-999"), "monitor").isCompleteMatch(), is(false)); - assertThat(hasPrivileges(indexPrivileges("write", "read-123", "read-456", "all-999"), "manage").isCompleteMatch(), is(false)); - assertThat(hasPrivileges( - new RoleDescriptor.IndicesPrivileges[]{ - RoleDescriptor.IndicesPrivileges.builder() - .indices("read-a") - .privileges("read") - .build(), - RoleDescriptor.IndicesPrivileges.builder() - .indices("all-b") - .privileges("read", "write") - .build() - }, - new RoleDescriptor.ApplicationResourcePrivileges[]{ - RoleDescriptor.ApplicationResourcePrivileges.builder() - .application("kibana") - .resources("*") - .privileges("read") - .build() - }, - "monitor").isCompleteMatch(), is(true)); - assertThat(hasPrivileges( - new RoleDescriptor.IndicesPrivileges[]{indexPrivileges("read", "read-123", "read-456", "all-999")}, - new RoleDescriptor.ApplicationResourcePrivileges[]{ - RoleDescriptor.ApplicationResourcePrivileges.builder() - .application("kibana").resources("*").privileges("read").build(), - RoleDescriptor.ApplicationResourcePrivileges.builder() - .application("kibana").resources("*").privileges("write").build() - }, - "monitor").isCompleteMatch(), is(false)); - } - - private RoleDescriptor.IndicesPrivileges indexPrivileges(String priv, String... indices) { - return RoleDescriptor.IndicesPrivileges.builder() - .indices(indices) - .privileges(priv) - .build(); - } - - private HasPrivilegesResponse hasPrivileges(RoleDescriptor.IndicesPrivileges indicesPrivileges, String... clusterPrivileges) - throws Exception { - return hasPrivileges( - new RoleDescriptor.IndicesPrivileges[]{indicesPrivileges}, - new RoleDescriptor.ApplicationResourcePrivileges[0], - clusterPrivileges - ); - } - - private HasPrivilegesResponse hasPrivileges(RoleDescriptor.IndicesPrivileges[] indicesPrivileges, - RoleDescriptor.ApplicationResourcePrivileges[] appPrivileges, - String... clusterPrivileges) throws Exception { - final HasPrivilegesRequest request = new HasPrivilegesRequest(); - request.username(user.principal()); - request.clusterPrivileges(clusterPrivileges); - request.indexPrivileges(indicesPrivileges); - request.applicationPrivileges(appPrivileges); - final PlainActionFuture future = new PlainActionFuture(); - action.doExecute(mock(Task.class), request, future); - final HasPrivilegesResponse response = future.get(); - assertThat(response, notNullValue()); - return response; - } - - private static MapBuilder mapBuilder() { - return MapBuilder.newMapBuilder(); - } - -} 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 55d6f4342b147..79316df55c4e8 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 @@ -93,6 +93,10 @@ import org.elasticsearch.xpack.core.security.action.privilege.DeletePrivilegesRequest; import org.elasticsearch.xpack.core.security.action.user.AuthenticateAction; import org.elasticsearch.xpack.core.security.action.user.AuthenticateRequest; +import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesRequest; +import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse; +import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesRequest; +import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesResponse; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; import org.elasticsearch.xpack.core.security.authc.DefaultAuthenticationFailureHandler; @@ -107,6 +111,7 @@ import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsCache; import org.elasticsearch.xpack.core.security.authz.permission.Role; import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege; import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; @@ -1370,6 +1375,20 @@ public void validateIndexPermissionsAreSubset(RequestInfo requestInfo, Authoriza ActionListener listener) { throw new UnsupportedOperationException("not implemented"); } + + @Override + public void checkPrivileges(Authentication authentication, AuthorizationInfo authorizationInfo, + HasPrivilegesRequest hasPrivilegesRequest, + Collection applicationPrivilegeDescriptors, + ActionListener listener) { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public void getUserPrivileges(Authentication authentication, AuthorizationInfo authorizationInfo, + GetUserPrivilegesRequest request, ActionListener listener) { + throw new UnsupportedOperationException("not implemented"); + } }; authorizationService = new AuthorizationService(Settings.EMPTY, rolesStore, clusterService, diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java index cf0a5a909a1d4..f7403ce74313f 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java @@ -9,8 +9,15 @@ import org.elasticsearch.action.admin.cluster.health.ClusterHealthAction; import org.elasticsearch.action.admin.cluster.state.ClusterStateAction; import org.elasticsearch.action.admin.cluster.stats.ClusterStatsAction; +import org.elasticsearch.action.delete.DeleteAction; +import org.elasticsearch.action.index.IndexAction; +import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.client.Client; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.collect.MapBuilder; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.license.GetLicenseAction; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.transport.TransportRequest; @@ -21,6 +28,10 @@ import org.elasticsearch.xpack.core.security.action.user.ChangePasswordRequest; import org.elasticsearch.xpack.core.security.action.user.ChangePasswordRequestBuilder; import org.elasticsearch.xpack.core.security.action.user.DeleteUserAction; +import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse; +import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesRequest; +import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesResponse; +import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesResponse.ResourcePrivileges; import org.elasticsearch.xpack.core.security.action.user.PutUserAction; import org.elasticsearch.xpack.core.security.action.user.UserRequest; import org.elasticsearch.xpack.core.security.authc.Authentication; @@ -28,12 +39,41 @@ import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings; import org.elasticsearch.xpack.core.security.authc.ldap.LdapRealmSettings; import org.elasticsearch.xpack.core.security.authc.pki.PkiRealmSettings; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions; +import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsDefinition; +import org.elasticsearch.xpack.core.security.authz.permission.Role; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; +import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivileges.ManageApplicationPrivileges; +import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.Privilege; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; +import org.elasticsearch.xpack.security.authz.RBACEngine.RBACAuthorizationInfo; import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; +import org.hamcrest.Matchers; import org.junit.Before; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +import static java.util.Collections.emptyMap; +import static org.elasticsearch.common.util.set.Sets.newHashSet; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.emptyIterable; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.iterableWithSize; +import static org.hamcrest.Matchers.notNullValue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -191,4 +231,518 @@ public void testSameUserPermissionDoesNotAllowChangePasswordForLookedUpByOtherRe verify(lookedUpBy).getType(); verifyNoMoreInteractions(authentication, lookedUpBy, authenticatedBy); } + + /** + * This tests that action names in the request are considered "matched" by the relevant named privilege + * (in this case that {@link DeleteAction} and {@link IndexAction} are satisfied by {@link IndexPrivilege#WRITE}). + */ + public void testNamedIndexPrivilegesMatchApplicableActions() throws Exception { + User user = new User(randomAlphaOfLengthBetween(4, 12)); + Authentication authentication = mock(Authentication.class); + when(authentication.getUser()).thenReturn(user); + Role role = Role.builder("test1") + .cluster(Collections.singleton("all"), Collections.emptyList()) + .add(IndexPrivilege.WRITE, "academy") + .build(); + RBACAuthorizationInfo authzInfo = new RBACAuthorizationInfo(role, null); + + final HasPrivilegesRequest request = new HasPrivilegesRequest(); + request.username(user.principal()); + request.clusterPrivileges(ClusterHealthAction.NAME); + request.indexPrivileges(RoleDescriptor.IndicesPrivileges.builder() + .indices("academy") + .privileges(DeleteAction.NAME, IndexAction.NAME) + .build()); + request.applicationPrivileges(new RoleDescriptor.ApplicationResourcePrivileges[0]); + + final PlainActionFuture future = new PlainActionFuture<>(); + engine.checkPrivileges(authentication, authzInfo, request, Collections.emptyList(), future); + + final HasPrivilegesResponse response = future.get(); + assertThat(response, notNullValue()); + assertThat(response.getUsername(), is(user.principal())); + assertThat(response.isCompleteMatch(), is(true)); + + assertThat(response.getClusterPrivileges().size(), equalTo(1)); + assertThat(response.getClusterPrivileges().get(ClusterHealthAction.NAME), equalTo(true)); + + assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(1)); + final ResourcePrivileges result = response.getIndexPrivileges().iterator().next(); + assertThat(result.getResource(), equalTo("academy")); + assertThat(result.getPrivileges().size(), equalTo(2)); + assertThat(result.getPrivileges().get(DeleteAction.NAME), equalTo(true)); + assertThat(result.getPrivileges().get(IndexAction.NAME), equalTo(true)); + } + + /** + * This tests that the action responds correctly when the user/role has some, but not all + * of the privileges being checked. + */ + public void testMatchSubsetOfPrivileges() throws Exception { + User user = new User(randomAlphaOfLengthBetween(4, 12)); + Authentication authentication = mock(Authentication.class); + when(authentication.getUser()).thenReturn(user); + Role role = Role.builder("test2") + .cluster(ClusterPrivilege.MONITOR) + .add(IndexPrivilege.INDEX, "academy") + .add(IndexPrivilege.WRITE, "initiative") + .build(); + RBACAuthorizationInfo authzInfo = new RBACAuthorizationInfo(role, null); + + final HasPrivilegesRequest request = new HasPrivilegesRequest(); + request.username(user.principal()); + request.clusterPrivileges("monitor", "manage"); + request.indexPrivileges(RoleDescriptor.IndicesPrivileges.builder() + .indices("academy", "initiative", "school") + .privileges("delete", "index", "manage") + .build()); + request.applicationPrivileges(new RoleDescriptor.ApplicationResourcePrivileges[0]); + final PlainActionFuture future = new PlainActionFuture<>(); + engine.checkPrivileges(authentication, authzInfo, request, Collections.emptyList(), future); + + final HasPrivilegesResponse response = future.get(); + assertThat(response, notNullValue()); + assertThat(response.getUsername(), is(user.principal())); + assertThat(response.isCompleteMatch(), is(false)); + assertThat(response.getClusterPrivileges().size(), equalTo(2)); + assertThat(response.getClusterPrivileges().get("monitor"), equalTo(true)); + assertThat(response.getClusterPrivileges().get("manage"), equalTo(false)); + assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(3)); + + final Iterator indexPrivilegesIterator = response.getIndexPrivileges().iterator(); + final ResourcePrivileges academy = indexPrivilegesIterator.next(); + final ResourcePrivileges initiative = indexPrivilegesIterator.next(); + final ResourcePrivileges school = indexPrivilegesIterator.next(); + + assertThat(academy.getResource(), equalTo("academy")); + assertThat(academy.getPrivileges().size(), equalTo(3)); + assertThat(academy.getPrivileges().get("index"), equalTo(true)); // explicit + assertThat(academy.getPrivileges().get("delete"), equalTo(false)); + assertThat(academy.getPrivileges().get("manage"), equalTo(false)); + + assertThat(initiative.getResource(), equalTo("initiative")); + assertThat(initiative.getPrivileges().size(), equalTo(3)); + assertThat(initiative.getPrivileges().get("index"), equalTo(true)); // implied by write + assertThat(initiative.getPrivileges().get("delete"), equalTo(true)); // implied by write + assertThat(initiative.getPrivileges().get("manage"), equalTo(false)); + + assertThat(school.getResource(), equalTo("school")); + assertThat(school.getPrivileges().size(), equalTo(3)); + assertThat(school.getPrivileges().get("index"), equalTo(false)); + assertThat(school.getPrivileges().get("delete"), equalTo(false)); + assertThat(school.getPrivileges().get("manage"), equalTo(false)); + } + + /** + * This tests that the action responds correctly when the user/role has none + * of the privileges being checked. + */ + public void testMatchNothing() throws Exception { + User user = new User(randomAlphaOfLengthBetween(4, 12)); + Authentication authentication = mock(Authentication.class); + when(authentication.getUser()).thenReturn(user); + Role role = Role.builder("test3") + .cluster(ClusterPrivilege.MONITOR) + .build(); + RBACAuthorizationInfo authzInfo = new RBACAuthorizationInfo(role, null); + + final HasPrivilegesResponse response = hasPrivileges(RoleDescriptor.IndicesPrivileges.builder() + .indices("academy") + .privileges("read", "write") + .build(), + authentication, authzInfo, Collections.emptyList(), Strings.EMPTY_ARRAY); + assertThat(response.getUsername(), is(user.principal())); + assertThat(response.isCompleteMatch(), is(false)); + assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(1)); + final ResourcePrivileges result = response.getIndexPrivileges().iterator().next(); + assertThat(result.getResource(), equalTo("academy")); + assertThat(result.getPrivileges().size(), equalTo(2)); + assertThat(result.getPrivileges().get("read"), equalTo(false)); + assertThat(result.getPrivileges().get("write"), equalTo(false)); + } + + /** + * Wildcards in the request are treated as + * does the user have ___ privilege on every possible index that matches this pattern? + * Or, expressed differently, + * does the user have ___ privilege on a wildcard that covers (is a superset of) this pattern? + */ + public void testWildcardHandling() throws Exception { + List privs = new ArrayList<>(); + final ApplicationPrivilege kibanaRead = defineApplicationPrivilege(privs, "kibana", "read", + "data:read/*", "action:login", "action:view/dashboard"); + final ApplicationPrivilege kibanaWrite = defineApplicationPrivilege(privs, "kibana", "write", + "data:write/*", "action:login", "action:view/dashboard"); + final ApplicationPrivilege kibanaAdmin = defineApplicationPrivilege(privs, "kibana", "admin", + "action:login", "action:manage/*"); + final ApplicationPrivilege kibanaViewSpace = defineApplicationPrivilege(privs, "kibana", "view-space", + "action:login", "space:view/*"); + User user = new User(randomAlphaOfLengthBetween(4, 12)); + Authentication authentication = mock(Authentication.class); + when(authentication.getUser()).thenReturn(user); + Role role = Role.builder("test3") + .add(IndexPrivilege.ALL, "logstash-*", "foo?") + .add(IndexPrivilege.READ, "abc*") + .add(IndexPrivilege.WRITE, "*xyz") + .addApplicationPrivilege(kibanaRead, Collections.singleton("*")) + .addApplicationPrivilege(kibanaViewSpace, newHashSet("space/engineering/*", "space/builds")) + .build(); + RBACAuthorizationInfo authzInfo = new RBACAuthorizationInfo(role, null); + + final HasPrivilegesRequest request = new HasPrivilegesRequest(); + request.username(user.principal()); + request.clusterPrivileges(Strings.EMPTY_ARRAY); + request.indexPrivileges( + RoleDescriptor.IndicesPrivileges.builder() + .indices("logstash-2016-*") + .privileges("write") // Yes, because (ALL,"logstash-*") + .build(), + RoleDescriptor.IndicesPrivileges.builder() + .indices("logstash-*") + .privileges("read") // Yes, because (ALL,"logstash-*") + .build(), + RoleDescriptor.IndicesPrivileges.builder() + .indices("log*") + .privileges("manage") // No, because "log*" includes indices that "logstash-*" does not + .build(), + RoleDescriptor.IndicesPrivileges.builder() + .indices("foo*", "foo?") + .privileges("read") // Yes, "foo?", but not "foo*", because "foo*" > "foo?" + .build(), + RoleDescriptor.IndicesPrivileges.builder() + .indices("abcd*") + .privileges("read", "write") // read = Yes, because (READ, "abc*"), write = No + .build(), + RoleDescriptor.IndicesPrivileges.builder() + .indices("abc*xyz") + .privileges("read", "write", "manage") // read = Yes ( READ "abc*"), write = Yes (WRITE, "*xyz"), manage = No + .build(), + RoleDescriptor.IndicesPrivileges.builder() + .indices("a*xyz") + .privileges("read", "write", "manage") // read = No, write = Yes (WRITE, "*xyz"), manage = No + .build() + ); + + request.applicationPrivileges( + RoleDescriptor.ApplicationResourcePrivileges.builder() + .resources("*") + .application("kibana") + .privileges(Sets.union(kibanaRead.name(), kibanaWrite.name())) // read = Yes, write = No + .build(), + RoleDescriptor.ApplicationResourcePrivileges.builder() + .resources("space/engineering/project-*", "space/*") // project-* = Yes, space/* = Not + .application("kibana") + .privileges("space:view/dashboard") + .build() + ); + + final PlainActionFuture future = new PlainActionFuture<>(); + engine.checkPrivileges(authentication, authzInfo, request, privs, future); + + final HasPrivilegesResponse response = future.get(); + assertThat(response, notNullValue()); + assertThat(response.getUsername(), is(user.principal())); + assertThat(response.isCompleteMatch(), is(false)); + assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(8)); + assertThat(response.getIndexPrivileges(), containsInAnyOrder( + new ResourcePrivileges("logstash-2016-*", Collections.singletonMap("write", true)), + new ResourcePrivileges("logstash-*", Collections.singletonMap("read", true)), + new ResourcePrivileges("log*", Collections.singletonMap("manage", false)), + new ResourcePrivileges("foo?", Collections.singletonMap("read", true)), + new ResourcePrivileges("foo*", Collections.singletonMap("read", false)), + new ResourcePrivileges("abcd*", mapBuilder().put("read", true).put("write", false).map()), + new ResourcePrivileges("abc*xyz", mapBuilder().put("read", true).put("write", true).put("manage", false).map()), + new ResourcePrivileges("a*xyz", mapBuilder().put("read", false).put("write", true).put("manage", false).map()) + )); + assertThat(response.getApplicationPrivileges().entrySet(), Matchers.iterableWithSize(1)); + final Set kibanaPrivileges = response.getApplicationPrivileges().get("kibana"); + assertThat(kibanaPrivileges, Matchers.iterableWithSize(3)); + assertThat(Strings.collectionToCommaDelimitedString(kibanaPrivileges), kibanaPrivileges, containsInAnyOrder( + new ResourcePrivileges("*", mapBuilder().put("read", true).put("write", false).map()), + new ResourcePrivileges("space/engineering/project-*", Collections.singletonMap("space:view/dashboard", true)), + new ResourcePrivileges("space/*", Collections.singletonMap("space:view/dashboard", false)) + )); + } + + public void testCheckingIndexPermissionsDefinedOnDifferentPatterns() throws Exception { + User user = new User(randomAlphaOfLengthBetween(4, 12)); + Authentication authentication = mock(Authentication.class); + when(authentication.getUser()).thenReturn(user); + Role role = Role.builder("test-write") + .add(IndexPrivilege.INDEX, "apache-*") + .add(IndexPrivilege.DELETE, "apache-2016-*") + .build(); + RBACAuthorizationInfo authzInfo = new RBACAuthorizationInfo(role, null); + + final HasPrivilegesResponse response = hasPrivileges(RoleDescriptor.IndicesPrivileges.builder() + .indices("apache-2016-12", "apache-2017-01") + .privileges("index", "delete") + .build(), authentication, authzInfo, Collections.emptyList(), Strings.EMPTY_ARRAY); + assertThat(response.isCompleteMatch(), is(false)); + assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(2)); + assertThat(response.getIndexPrivileges(), containsInAnyOrder( + new ResourcePrivileges("apache-2016-12", + MapBuilder.newMapBuilder(new LinkedHashMap()) + .put("index", true).put("delete", true).map()), + new ResourcePrivileges("apache-2017-01", + MapBuilder.newMapBuilder(new LinkedHashMap()) + .put("index", true).put("delete", false).map() + ) + )); + } + + public void testCheckingApplicationPrivilegesOnDifferentApplicationsAndResources() throws Exception { + List privs = new ArrayList<>(); + final ApplicationPrivilege app1Read = defineApplicationPrivilege(privs, "app1", "read", "data:read/*"); + final ApplicationPrivilege app1Write = defineApplicationPrivilege(privs, "app1", "write", "data:write/*"); + final ApplicationPrivilege app1All = defineApplicationPrivilege(privs, "app1", "all", "*"); + final ApplicationPrivilege app2Read = defineApplicationPrivilege(privs, "app2", "read", "data:read/*"); + final ApplicationPrivilege app2Write = defineApplicationPrivilege(privs, "app2", "write", "data:write/*"); + final ApplicationPrivilege app2All = defineApplicationPrivilege(privs, "app2", "all", "*"); + + User user = new User(randomAlphaOfLengthBetween(4, 12)); + Authentication authentication = mock(Authentication.class); + when(authentication.getUser()).thenReturn(user); + Role role = Role.builder("test-role") + .addApplicationPrivilege(app1Read, Collections.singleton("foo/*")) + .addApplicationPrivilege(app1All, Collections.singleton("foo/bar/baz")) + .addApplicationPrivilege(app2Read, Collections.singleton("foo/bar/*")) + .addApplicationPrivilege(app2Write, Collections.singleton("*/bar/*")) + .build(); + RBACAuthorizationInfo authzInfo = new RBACAuthorizationInfo(role, null); + + final HasPrivilegesResponse response = hasPrivileges(new RoleDescriptor.IndicesPrivileges[0], + new RoleDescriptor.ApplicationResourcePrivileges[]{ + RoleDescriptor.ApplicationResourcePrivileges.builder() + .application("app1") + .resources("foo/1", "foo/bar/2", "foo/bar/baz", "baz/bar/foo") + .privileges("read", "write", "all") + .build(), + RoleDescriptor.ApplicationResourcePrivileges.builder() + .application("app2") + .resources("foo/1", "foo/bar/2", "foo/bar/baz", "baz/bar/foo") + .privileges("read", "write", "all") + .build() + }, authentication, authzInfo, privs, Strings.EMPTY_ARRAY); + + assertThat(response.isCompleteMatch(), is(false)); + assertThat(response.getIndexPrivileges(), Matchers.emptyIterable()); + assertThat(response.getApplicationPrivileges().entrySet(), Matchers.iterableWithSize(2)); + final Set app1 = response.getApplicationPrivileges().get("app1"); + assertThat(app1, Matchers.iterableWithSize(4)); + assertThat(Strings.collectionToCommaDelimitedString(app1), app1, containsInAnyOrder( + new ResourcePrivileges("foo/1", MapBuilder.newMapBuilder(new LinkedHashMap()) + .put("read", true).put("write", false).put("all", false).map()), + new ResourcePrivileges("foo/bar/2", MapBuilder.newMapBuilder(new LinkedHashMap()) + .put("read", true).put("write", false).put("all", false).map()), + new ResourcePrivileges("foo/bar/baz", MapBuilder.newMapBuilder(new LinkedHashMap()) + .put("read", true).put("write", true).put("all", true).map()), + new ResourcePrivileges("baz/bar/foo", MapBuilder.newMapBuilder(new LinkedHashMap()) + .put("read", false).put("write", false).put("all", false).map()) + )); + final Set app2 = response.getApplicationPrivileges().get("app2"); + assertThat(app2, Matchers.iterableWithSize(4)); + assertThat(Strings.collectionToCommaDelimitedString(app2), app2, containsInAnyOrder( + new ResourcePrivileges("foo/1", MapBuilder.newMapBuilder(new LinkedHashMap()) + .put("read", false).put("write", false).put("all", false).map()), + new ResourcePrivileges("foo/bar/2", MapBuilder.newMapBuilder(new LinkedHashMap()) + .put("read", true).put("write", true).put("all", false).map()), + new ResourcePrivileges("foo/bar/baz", MapBuilder.newMapBuilder(new LinkedHashMap()) + .put("read", true).put("write", true).put("all", false).map()), + new ResourcePrivileges("baz/bar/foo", MapBuilder.newMapBuilder(new LinkedHashMap()) + .put("read", false).put("write", true).put("all", false).map()) + )); + } + + public void testCheckingApplicationPrivilegesWithComplexNames() throws Exception { + final String appName = randomAlphaOfLength(1).toLowerCase(Locale.ROOT) + randomAlphaOfLengthBetween(3, 10); + final String action1 = randomAlphaOfLength(1).toLowerCase(Locale.ROOT) + randomAlphaOfLengthBetween(2, 5); + final String action2 = randomAlphaOfLength(1).toLowerCase(Locale.ROOT) + randomAlphaOfLengthBetween(6, 9); + + final List privs = new ArrayList<>(); + final ApplicationPrivilege priv1 = defineApplicationPrivilege(privs, appName, action1, "DATA:read/*", "ACTION:" + action1); + final ApplicationPrivilege priv2 = defineApplicationPrivilege(privs, appName, action2, "DATA:read/*", "ACTION:" + action2); + + User user = new User(randomAlphaOfLengthBetween(4, 12)); + Authentication authentication = mock(Authentication.class); + when(authentication.getUser()).thenReturn(user); + Role role = Role.builder("test-write") + .addApplicationPrivilege(priv1, Collections.singleton("user/*/name")) + .build(); + RBACAuthorizationInfo authzInfo = new RBACAuthorizationInfo(role, null); + + final HasPrivilegesResponse response = hasPrivileges( + new RoleDescriptor.IndicesPrivileges[0], + new RoleDescriptor.ApplicationResourcePrivileges[]{ + RoleDescriptor.ApplicationResourcePrivileges.builder() + .application(appName) + .resources("user/hawkeye/name") + .privileges("DATA:read/user/*", "ACTION:" + action1, "ACTION:" + action2, action1, action2) + .build() + }, authentication, authzInfo, privs, "monitor"); + assertThat(response.isCompleteMatch(), is(false)); + assertThat(response.getApplicationPrivileges().keySet(), containsInAnyOrder(appName)); + assertThat(response.getApplicationPrivileges().get(appName), iterableWithSize(1)); + assertThat(response.getApplicationPrivileges().get(appName), containsInAnyOrder( + new ResourcePrivileges("user/hawkeye/name", MapBuilder.newMapBuilder(new LinkedHashMap()) + .put("DATA:read/user/*", true) + .put("ACTION:" + action1, true) + .put("ACTION:" + action2, false) + .put(action1, true) + .put(action2, false) + .map()) + )); + } + + public void testIsCompleteMatch() throws Exception { + final List privs = new ArrayList<>(); + final ApplicationPrivilege kibanaRead = defineApplicationPrivilege(privs, "kibana", "read", "data:read/*"); + final ApplicationPrivilege kibanaWrite = defineApplicationPrivilege(privs, "kibana", "write", "data:write/*"); + User user = new User(randomAlphaOfLengthBetween(4, 12)); + Authentication authentication = mock(Authentication.class); + when(authentication.getUser()).thenReturn(user); + Role role = Role.builder("test-write") + .cluster(ClusterPrivilege.MONITOR) + .add(IndexPrivilege.READ, "read-*") + .add(IndexPrivilege.ALL, "all-*") + .addApplicationPrivilege(kibanaRead, Collections.singleton("*")) + .build(); + RBACAuthorizationInfo authzInfo = new RBACAuthorizationInfo(role, null); + + + assertThat(hasPrivileges( + indexPrivileges("read", "read-123", "read-456", "all-999"), authentication, authzInfo, privs, "monitor").isCompleteMatch(), + is(true)); + assertThat(hasPrivileges( + indexPrivileges("read", "read-123", "read-456", "all-999"), authentication, authzInfo, privs, "manage").isCompleteMatch(), + is(false)); + assertThat(hasPrivileges( + indexPrivileges("write", "read-123", "read-456", "all-999"), authentication, authzInfo, privs, "monitor").isCompleteMatch(), + is(false)); + assertThat(hasPrivileges( + indexPrivileges("write", "read-123", "read-456", "all-999"), authentication, authzInfo, privs, "manage").isCompleteMatch(), + is(false)); + assertThat(hasPrivileges( + new RoleDescriptor.IndicesPrivileges[]{ + RoleDescriptor.IndicesPrivileges.builder() + .indices("read-a") + .privileges("read") + .build(), + RoleDescriptor.IndicesPrivileges.builder() + .indices("all-b") + .privileges("read", "write") + .build() + }, + new RoleDescriptor.ApplicationResourcePrivileges[]{ + RoleDescriptor.ApplicationResourcePrivileges.builder() + .application("kibana") + .resources("*") + .privileges("read") + .build() + }, authentication, authzInfo, privs, "monitor").isCompleteMatch(), is(true)); + assertThat(hasPrivileges( + new RoleDescriptor.IndicesPrivileges[]{indexPrivileges("read", "read-123", "read-456", "all-999")}, + new RoleDescriptor.ApplicationResourcePrivileges[]{ + RoleDescriptor.ApplicationResourcePrivileges.builder() + .application("kibana").resources("*").privileges("read").build(), + RoleDescriptor.ApplicationResourcePrivileges.builder() + .application("kibana").resources("*").privileges("write").build() + }, authentication, authzInfo, privs, "monitor").isCompleteMatch(), is(false)); + } + + public void testBuildUserPrivilegeResponse() { + final ManageApplicationPrivileges manageApplicationPrivileges = new ManageApplicationPrivileges(Sets.newHashSet("app01", "app02")); + final BytesArray query = new BytesArray("{\"term\":{\"public\":true}}"); + final Role role = Role.builder("test", "role") + .cluster(Sets.newHashSet("monitor", "manage_watcher"), Collections.singleton(manageApplicationPrivileges)) + .add(IndexPrivilege.get(Sets.newHashSet("read", "write")), "index-1") + .add(IndexPrivilege.ALL, "index-2", "index-3") + .add( + new FieldPermissions(new FieldPermissionsDefinition(new String[]{ "public.*" }, new String[0])), + Collections.singleton(query), + IndexPrivilege.READ, randomBoolean(), "index-4", "index-5") + .addApplicationPrivilege(new ApplicationPrivilege("app01", "read", "data:read"), Collections.singleton("*")) + .runAs(new Privilege(Sets.newHashSet("user01", "user02"), "user01", "user02")) + .build(); + + final GetUserPrivilegesResponse response = engine.buildUserPrivilegesResponseObject(role); + + assertThat(response.getClusterPrivileges(), containsInAnyOrder("monitor", "manage_watcher")); + assertThat(response.getConditionalClusterPrivileges(), containsInAnyOrder(manageApplicationPrivileges)); + + assertThat(response.getIndexPrivileges(), iterableWithSize(3)); + final GetUserPrivilegesResponse.Indices index1 = findIndexPrivilege(response.getIndexPrivileges(), "index-1"); + assertThat(index1.getIndices(), containsInAnyOrder("index-1")); + assertThat(index1.getPrivileges(), containsInAnyOrder("read", "write")); + assertThat(index1.getFieldSecurity(), emptyIterable()); + assertThat(index1.getQueries(), emptyIterable()); + final GetUserPrivilegesResponse.Indices index2 = findIndexPrivilege(response.getIndexPrivileges(), "index-2"); + assertThat(index2.getIndices(), containsInAnyOrder("index-2", "index-3")); + assertThat(index2.getPrivileges(), containsInAnyOrder("all")); + assertThat(index2.getFieldSecurity(), emptyIterable()); + assertThat(index2.getQueries(), emptyIterable()); + final GetUserPrivilegesResponse.Indices index4 = findIndexPrivilege(response.getIndexPrivileges(), "index-4"); + assertThat(index4.getIndices(), containsInAnyOrder("index-4", "index-5")); + assertThat(index4.getPrivileges(), containsInAnyOrder("read")); + assertThat(index4.getFieldSecurity(), containsInAnyOrder( + new FieldPermissionsDefinition.FieldGrantExcludeGroup(new String[]{ "public.*" }, new String[0]))); + assertThat(index4.getQueries(), containsInAnyOrder(query)); + + assertThat(response.getApplicationPrivileges(), containsInAnyOrder( + RoleDescriptor.ApplicationResourcePrivileges.builder().application("app01").privileges("read").resources("*").build()) + ); + + assertThat(response.getRunAs(), containsInAnyOrder("user01", "user02")); + } + + private GetUserPrivilegesResponse.Indices findIndexPrivilege(Set indices, String name) { + return indices.stream().filter(i -> i.getIndices().contains(name)).findFirst().get(); + } + + private RoleDescriptor.IndicesPrivileges indexPrivileges(String priv, String... indices) { + return RoleDescriptor.IndicesPrivileges.builder() + .indices(indices) + .privileges(priv) + .build(); + } + + private ApplicationPrivilege defineApplicationPrivilege(List privs, String app, String name, + String ... actions) { + privs.add(new ApplicationPrivilegeDescriptor(app, name, newHashSet(actions), emptyMap())); + return new ApplicationPrivilege(app, name, actions); + } + + private HasPrivilegesResponse hasPrivileges(RoleDescriptor.IndicesPrivileges indicesPrivileges, Authentication authentication, + AuthorizationInfo authorizationInfo, + List applicationPrivilegeDescriptors, + String... clusterPrivileges) throws Exception { + return hasPrivileges( + new RoleDescriptor.IndicesPrivileges[]{indicesPrivileges}, + new RoleDescriptor.ApplicationResourcePrivileges[0], + authentication, authorizationInfo, applicationPrivilegeDescriptors, + clusterPrivileges + ); + } + + private HasPrivilegesResponse hasPrivileges(RoleDescriptor.IndicesPrivileges[] indicesPrivileges, + RoleDescriptor.ApplicationResourcePrivileges[] appPrivileges, + Authentication authentication, + AuthorizationInfo authorizationInfo, + List applicationPrivilegeDescriptors, + String... clusterPrivileges) throws Exception { + final HasPrivilegesRequest request = new HasPrivilegesRequest(); + request.username(authentication.getUser().principal()); + request.clusterPrivileges(clusterPrivileges); + request.indexPrivileges(indicesPrivileges); + request.applicationPrivileges(appPrivileges); + final PlainActionFuture future = new PlainActionFuture<>(); + engine.checkPrivileges(authentication, authorizationInfo, request, applicationPrivilegeDescriptors, future); + final HasPrivilegesResponse response = future.get(); + assertThat(response, notNullValue()); + return response; + } + + private static MapBuilder mapBuilder() { + return MapBuilder.newMapBuilder(); + } } From 1c9a8e137352c4303ae0a2ffb9cd224e7502625f Mon Sep 17 00:00:00 2001 From: jaymode Date: Mon, 4 Feb 2019 12:17:54 -0700 Subject: [PATCH 10/15] fix inconsistency in parameter name/type --- .../example/CustomAuthorizationEngine.java | 7 +++--- .../CustomAuthorizationEngineTests.java | 9 ++++++-- .../security/authz/AuthorizationEngine.java | 17 +++++++------- .../authz/permission/IndicesPermission.java | 5 ++--- .../core/security/authz/permission/Role.java | 2 +- .../authz/store/ReservedRolesStoreTests.java | 12 +++++----- .../security/authz/AuthorizationService.java | 6 ++--- .../xpack/security/authz/RBACEngine.java | 17 +++++++------- .../authz/AuthorizationServiceTests.java | 4 ++-- .../accesscontrol/IndicesPermissionTests.java | 22 +++++++++---------- .../authz/store/CompositeRolesStoreTests.java | 2 +- 11 files changed, 52 insertions(+), 51 deletions(-) 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 da8bcf8b4ef44..77eec1e183bc1 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 @@ -48,7 +48,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.function.Function; import java.util.stream.Collectors; /** @@ -91,7 +90,7 @@ public void authorizeClusterAction(RequestInfo requestInfo, AuthorizationInfo au @Override public void authorizeIndexAction(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, AsyncSupplier indicesAsyncSupplier, - Function aliasOrIndexFunction, + Map aliasOrIndexLookup, ActionListener listener) { if (isSuperuser(requestInfo.getAuthentication().getUser())) { indicesAsyncSupplier.getAsync(ActionListener.wrap(resolvedIndices -> { @@ -110,9 +109,9 @@ public void authorizeIndexAction(RequestInfo requestInfo, AuthorizationInfo auth @Override public void loadAuthorizedIndices(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, - Map aliasAndIndexLookup, ActionListener> listener) { + Map aliasOrIndexLookup, ActionListener> listener) { if (isSuperuser(requestInfo.getAuthentication().getUser())) { - listener.onResponse(new ArrayList<>(aliasAndIndexLookup.keySet())); + listener.onResponse(new ArrayList<>(aliasOrIndexLookup.keySet())); } else { listener.onResponse(Collections.emptyList()); } diff --git a/plugins/examples/security-authorization-engine/src/test/java/org/elasticsearch/example/CustomAuthorizationEngineTests.java b/plugins/examples/security-authorization-engine/src/test/java/org/elasticsearch/example/CustomAuthorizationEngineTests.java index e24e490767988..d34a4251b2163 100644 --- a/plugins/examples/security-authorization-engine/src/test/java/org/elasticsearch/example/CustomAuthorizationEngineTests.java +++ b/plugins/examples/security-authorization-engine/src/test/java/org/elasticsearch/example/CustomAuthorizationEngineTests.java @@ -21,6 +21,7 @@ import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.cluster.metadata.AliasOrIndex; import org.elasticsearch.cluster.metadata.AliasOrIndex.Index; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.test.ESTestCase; @@ -36,6 +37,8 @@ import org.elasticsearch.xpack.core.security.user.User; import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import static org.hamcrest.Matchers.is; @@ -125,6 +128,8 @@ public void testAuthorizeClusterAction() { public void testAuthorizeIndexAction() { CustomAuthorizationEngine engine = new CustomAuthorizationEngine(); + Map aliasOrIndexMap = new HashMap<>(); + aliasOrIndexMap.put("index", new Index(IndexMetaData.builder("index").build())); // authorized { RequestInfo requestInfo = @@ -137,7 +142,7 @@ public void testAuthorizeIndexAction() { PlainActionFuture resultFuture = new PlainActionFuture<>(); engine.authorizeIndexAction(requestInfo, authzInfo, listener -> listener.onResponse(new ResolvedIndices(Collections.singletonList("index"), Collections.emptyList())), - name -> name.equals("index") ? new Index(IndexMetaData.builder("index").build()) : null, resultFuture); + aliasOrIndexMap, resultFuture); IndexAuthorizationResult result = resultFuture.actionGet(); assertThat(result.isGranted(), is(true)); assertThat(result.isAuditable(), is(true)); @@ -158,7 +163,7 @@ public void testAuthorizeIndexAction() { PlainActionFuture resultFuture = new PlainActionFuture<>(); engine.authorizeIndexAction(requestInfo, authzInfo, listener -> listener.onResponse(new ResolvedIndices(Collections.singletonList("index"), Collections.emptyList())), - name -> name.equals("index") ? new Index(IndexMetaData.builder("index").build()) : null, resultFuture); + aliasOrIndexMap, resultFuture); IndexAuthorizationResult result = resultFuture.actionGet(); assertThat(result.isGranted(), is(false)); assertThat(result.isAuditable(), is(true)); 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 19fb501f5843b..fd5e6fba9c55e 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 @@ -22,7 +22,6 @@ import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.function.Function; /** *

@@ -57,7 +56,7 @@ * can actually impersonate the user running the request. *

  • {@link #authorizeClusterAction(RequestInfo, AuthorizationInfo, ActionListener)} if the * request is a cluster level operation.
  • - *
  • {@link #authorizeIndexAction(RequestInfo, AuthorizationInfo, AsyncSupplier, Function, ActionListener)} if + *
  • {@link #authorizeIndexAction(RequestInfo, AuthorizationInfo, AsyncSupplier, Map, ActionListener)} if * the request is a an index action. This method may be called multiple times for a single * request as the request may be made up of sub-requests that also need to be authorized. The async supplier * for resolved indices will invoke the @@ -66,7 +65,7 @@ * *

    * NOTE: the {@link #loadAuthorizedIndices(RequestInfo, AuthorizationInfo, Map, ActionListener)} - * method may be called prior to {@link #authorizeIndexAction(RequestInfo, AuthorizationInfo, AsyncSupplier, Function, ActionListener)} + * method may be called prior to {@link #authorizeIndexAction(RequestInfo, AuthorizationInfo, AsyncSupplier, Map, ActionListener)} * in cases where wildcards need to be expanded. *


    * Authorization engines can be called from various threads including network threads that should @@ -124,12 +123,12 @@ public interface AuthorizationEngine { * from {@link #resolveAuthorizationInfo(RequestInfo, ActionListener)} * @param indicesAsyncSupplier the asynchronous supplier for the indices that this request is * attempting to operate on - * @param aliasOrIndexFunction a function that when given a string name, returns the cluster - * metadata specific to that alias or index + * @param aliasOrIndexLookup a map of a string name to the cluster metadata specific to that + * alias or index * @param listener the listener to be notified of the authorization result */ void authorizeIndexAction(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, - AsyncSupplier indicesAsyncSupplier, Function aliasOrIndexFunction, + AsyncSupplier indicesAsyncSupplier, Map aliasOrIndexLookup, ActionListener listener); /** @@ -140,12 +139,12 @@ void authorizeIndexAction(RequestInfo requestInfo, AuthorizationInfo authorizati * and associated user(s) * @param authorizationInfo information needed from authorization that was previously retrieved * from {@link #resolveAuthorizationInfo(RequestInfo, ActionListener)} - * @param aliasAndIndexLookup a function that when given a string name, returns the cluster - * metadata specific to that alias or index + * @param aliasOrIndexLookup a map of a string name to the cluster metadata specific to that + * alias or index * @param listener the listener to be notified of the authorization result */ void loadAuthorizedIndices(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, - Map aliasAndIndexLookup, ActionListener> listener); + Map aliasOrIndexLookup, ActionListener> listener); /** diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 4f689d59d5095..90145e3288366 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -30,7 +30,6 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import java.util.function.Function; import java.util.function.Predicate; import static java.util.Collections.unmodifiableMap; @@ -136,7 +135,7 @@ public Automaton allowedActionsMatcher(String index) { * Authorizes the provided action against the provided indices, given the current cluster metadata */ public Map authorize(String action, Set requestedIndicesOrAliases, - Function allAliasesAndIndices, + Map allAliasesAndIndices, FieldPermissionsCache fieldPermissionsCache) { // now... every index that is associated with the request, must be granted // by at least one indices permission group @@ -147,7 +146,7 @@ public Map authorize(String act for (String indexOrAlias : requestedIndicesOrAliases) { boolean granted = false; Set concreteIndices = new HashSet<>(); - AliasOrIndex aliasOrIndex = allAliasesAndIndices.apply(indexOrAlias); + AliasOrIndex aliasOrIndex = allAliasesAndIndices.get(indexOrAlias); if (aliasOrIndex != null) { for (IndexMetaData indexMetaData : aliasOrIndex.getIndices()) { concreteIndices.add(indexMetaData.getIndex().getName()); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java index 2af3b508774d6..3805d8283bbd7 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java @@ -79,7 +79,7 @@ public static Builder builder(RoleDescriptor rd, FieldPermissionsCache fieldPerm * is configured for any group also the allowed fields and role queries are resolved. */ public IndicesAccessControl authorize(String action, Set requestedIndicesOrAliases, - Function aliasAndIndexLookup, + Map aliasAndIndexLookup, FieldPermissionsCache fieldPermissionsCache) { Map indexPermissions = indices.authorize( action, requestedIndicesOrAliases, aliasAndIndexLookup, fieldPermissionsCache diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java index 7f4af70e89f3e..8b877732cf197 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java @@ -589,7 +589,7 @@ private void assertMonitoringOnRestrictedIndices(Role role) { for (final String indexMonitoringActionName : indexMonitoringActionNamesList) { final Map authzMap = role.indices().authorize(indexMonitoringActionName, Sets.newHashSet(RestrictedIndicesNames.INTERNAL_SECURITY_INDEX, RestrictedIndicesNames.SECURITY_INDEX_NAME), - metaData.getAliasAndIndexLookup()::get, fieldPermissionsCache); + metaData.getAliasAndIndexLookup(), fieldPermissionsCache); assertThat(authzMap.get(RestrictedIndicesNames.INTERNAL_SECURITY_INDEX).isGranted(), is(true)); assertThat(authzMap.get(RestrictedIndicesNames.SECURITY_INDEX_NAME).isGranted(), is(true)); } @@ -708,22 +708,22 @@ public void testSuperuserRole() { FieldPermissionsCache fieldPermissionsCache = new FieldPermissionsCache(Settings.EMPTY); SortedMap lookup = metaData.getAliasAndIndexLookup(); Map authzMap = - superuserRole.indices().authorize(SearchAction.NAME, Sets.newHashSet("a1", "ba"), lookup::get, fieldPermissionsCache); + superuserRole.indices().authorize(SearchAction.NAME, Sets.newHashSet("a1", "ba"), lookup, fieldPermissionsCache); assertThat(authzMap.get("a1").isGranted(), is(true)); assertThat(authzMap.get("b").isGranted(), is(true)); authzMap = - superuserRole.indices().authorize(DeleteIndexAction.NAME, Sets.newHashSet("a1", "ba"), lookup::get, fieldPermissionsCache); + superuserRole.indices().authorize(DeleteIndexAction.NAME, Sets.newHashSet("a1", "ba"), lookup, fieldPermissionsCache); assertThat(authzMap.get("a1").isGranted(), is(true)); assertThat(authzMap.get("b").isGranted(), is(true)); - authzMap = superuserRole.indices().authorize(IndexAction.NAME, Sets.newHashSet("a2", "ba"), lookup::get, fieldPermissionsCache); + authzMap = superuserRole.indices().authorize(IndexAction.NAME, Sets.newHashSet("a2", "ba"), lookup, fieldPermissionsCache); assertThat(authzMap.get("a2").isGranted(), is(true)); assertThat(authzMap.get("b").isGranted(), is(true)); authzMap = superuserRole.indices() - .authorize(UpdateSettingsAction.NAME, Sets.newHashSet("aaaaaa", "ba"), lookup::get, fieldPermissionsCache); + .authorize(UpdateSettingsAction.NAME, Sets.newHashSet("aaaaaa", "ba"), lookup, fieldPermissionsCache); assertThat(authzMap.get("aaaaaa").isGranted(), is(true)); assertThat(authzMap.get("b").isGranted(), is(true)); authzMap = superuserRole.indices().authorize(randomFrom(IndexAction.NAME, DeleteIndexAction.NAME, SearchAction.NAME), - Sets.newHashSet(RestrictedIndicesNames.SECURITY_INDEX_NAME), lookup::get, fieldPermissionsCache); + Sets.newHashSet(RestrictedIndicesNames.SECURITY_INDEX_NAME), lookup, fieldPermissionsCache); assertThat(authzMap.get(RestrictedIndicesNames.SECURITY_INDEX_NAME).isGranted(), is(true)); assertThat(authzMap.get(RestrictedIndicesNames.INTERNAL_SECURITY_INDEX).isGranted(), is(true)); assertTrue(superuserRole.indices().check(SearchAction.NAME)); 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 ec302f1003af8..e12b4c41b9d70 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 @@ -252,7 +252,7 @@ private void authorizeAction(final RequestInfo requestInfo, final String request })); }); authzEngine.authorizeIndexAction(requestInfo, authzInfo, resolvedIndicesAsyncSupplier, - metaData.getAliasAndIndexLookup()::get, wrapPreservingContext(new AuthorizationResultListener<>(result -> + metaData.getAliasAndIndexLookup(), wrapPreservingContext(new AuthorizationResultListener<>(result -> handleIndexActionAuthorizationResult(result, requestInfo, requestId, authzInfo, authzEngine, authorizedIndicesSupplier, resolvedIndicesAsyncSupplier, metaData, listener), listener::onFailure, requestInfo, requestId, authzInfo), threadContext)); @@ -296,7 +296,7 @@ private void handleIndexActionAuthorizationResult(final IndexAuthorizationResult ril.onResponse(withAliases); }, ril::onFailure)); }, - metaData.getAliasAndIndexLookup()::get, + metaData.getAliasAndIndexLookup(), wrapPreservingContext(new AuthorizationResultListener<>( authorizationResult -> runRequestInterceptors(requestInfo, authzInfo, authorizationEngine, listener), listener::onFailure, aliasesRequestInfo, requestId, authzInfo), threadContext)); @@ -506,7 +506,7 @@ private void authorizeBulkItems(RequestInfo requestInfo, AuthorizationInfo authz new RequestInfo(requestInfo.getAuthentication(), requestInfo.getRequest(), bulkItemAction); authzEngine.authorizeIndexAction(bulkItemInfo, authzInfo, ril -> ril.onResponse(new ResolvedIndices(new ArrayList<>(indices), Collections.emptyList())), - metaData.getAliasAndIndexLookup()::get, ActionListener.wrap(indexAuthorizationResult -> + metaData.getAliasAndIndexLookup(), ActionListener.wrap(indexAuthorizationResult -> groupedActionListener.onResponse(new Tuple<>(bulkItemAction, indexAuthorizationResult)), groupedActionListener::onFailure)); }); 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 8a8cd3dbe93af..448f9981f01f3 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 @@ -76,7 +76,6 @@ import java.util.Map.Entry; import java.util.Set; import java.util.TreeSet; -import java.util.function.Function; import java.util.function.Predicate; import static org.elasticsearch.common.Strings.arrayToCommaDelimitedString; @@ -213,7 +212,7 @@ private static boolean shouldAuthorizeIndexActionNameOnly(String action, Transpo @Override public void authorizeIndexAction(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, AsyncSupplier indicesAsyncSupplier, - Function aliasOrIndexFunction, + Map aliasOrIndexLookup, ActionListener listener) { final String action = requestInfo.getAction(); final TransportRequest request = requestInfo.getRequest(); @@ -264,7 +263,7 @@ public void authorizeIndexAction(RequestInfo requestInfo, AuthorizationInfo auth authorizeIndexActionName(action, authorizationInfo, IndicesAccessControl.ALLOW_NO_INDICES, listener); } else { buildIndicesAccessControl(authentication, action, authorizationInfo, - Sets.newHashSet(resolvedIndices.getLocal()), aliasOrIndexFunction, listener); + Sets.newHashSet(resolvedIndices.getLocal()), aliasOrIndexLookup, listener); } }, listener::onFailure)); } else { @@ -280,7 +279,7 @@ public void authorizeIndexAction(RequestInfo requestInfo, AuthorizationInfo auth listener.onResponse(new IndexAuthorizationResult(true, IndicesAccessControl.ALLOW_NO_INDICES)); } else { buildIndicesAccessControl(authentication, action, authorizationInfo, - Sets.newHashSet(resolvedIndices.getLocal()), aliasOrIndexFunction, listener); + Sets.newHashSet(resolvedIndices.getLocal()), aliasOrIndexLookup, listener); } }, listener::onFailure)); } else { @@ -307,10 +306,10 @@ private void authorizeIndexActionName(String action, AuthorizationInfo authoriza @Override public void loadAuthorizedIndices(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, - Map aliasAndIndexLookup, ActionListener> listener) { + Map aliasOrIndexLookup, ActionListener> listener) { if (authorizationInfo instanceof RBACAuthorizationInfo) { final Role role = ((RBACAuthorizationInfo) authorizationInfo).getRole(); - listener.onResponse(resolveAuthorizedIndicesFromRole(role, requestInfo.getAction(), aliasAndIndexLookup)); + listener.onResponse(resolveAuthorizedIndicesFromRole(role, requestInfo.getAction(), aliasOrIndexLookup)); } else { listener.onFailure( new IllegalArgumentException("unsupported authorization info:" + authorizationInfo.getClass().getSimpleName())); @@ -550,9 +549,9 @@ static List resolveAuthorizedIndicesFromRole(Role role, String action, M } private void buildIndicesAccessControl(Authentication authentication, String action, - AuthorizationInfo authorizationInfo, Set indices, - Function aliasAndIndexLookup, - ActionListener listener) { + AuthorizationInfo authorizationInfo, Set indices, + Map aliasAndIndexLookup, + ActionListener listener) { if (authorizationInfo instanceof RBACAuthorizationInfo) { final Role role = ((RBACAuthorizationInfo) authorizationInfo).getRole(); final IndicesAccessControl accessControl = role.authorize(action, indices, aliasAndIndexLookup, fieldPermissionsCache); 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 79316df55c4e8..19bb05b0b6bc6 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 @@ -1358,14 +1358,14 @@ public void authorizeClusterAction(RequestInfo requestInfo, AuthorizationInfo au @Override public void authorizeIndexAction(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, AsyncSupplier indicesAsyncSupplier, - Function aliasOrIndexFunction, + Map aliasOrIndexLookup, ActionListener listener) { throw new UnsupportedOperationException("not implemented"); } @Override public void loadAuthorizedIndices(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, - Map aliasAndIndexLookup, ActionListener> listener) { + Map aliasOrIndexLookup, ActionListener> listener) { throw new UnsupportedOperationException("not implemented"); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java index f81d7c9a792f6..e002cd1b7da0f 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java @@ -64,7 +64,7 @@ public void testAuthorize() { Role role = Role.builder("_role") .add(new FieldPermissions(fieldPermissionDef(fields, null)), query, IndexPrivilege.ALL, randomBoolean(), "_index") .build(); - IndicesAccessControl permissions = role.authorize(SearchAction.NAME, Sets.newHashSet("_index"), lookup::get, fieldPermissionsCache); + IndicesAccessControl permissions = role.authorize(SearchAction.NAME, Sets.newHashSet("_index"), lookup, fieldPermissionsCache); assertThat(permissions.getIndexPermissions("_index"), notNullValue()); assertTrue(permissions.getIndexPermissions("_index").getFieldPermissions().grantsAccessTo("_field")); assertTrue(permissions.getIndexPermissions("_index").getFieldPermissions().hasFieldLevelSecurity()); @@ -75,7 +75,7 @@ public void testAuthorize() { role = Role.builder("_role") .add(new FieldPermissions(fieldPermissionDef(fields, null)), null, IndexPrivilege.ALL, randomBoolean(), "_index") .build(); - permissions = role.authorize(SearchAction.NAME, Sets.newHashSet("_index"), lookup::get, fieldPermissionsCache); + permissions = role.authorize(SearchAction.NAME, Sets.newHashSet("_index"), lookup, fieldPermissionsCache); assertThat(permissions.getIndexPermissions("_index"), notNullValue()); assertTrue(permissions.getIndexPermissions("_index").getFieldPermissions().grantsAccessTo("_field")); assertTrue(permissions.getIndexPermissions("_index").getFieldPermissions().hasFieldLevelSecurity()); @@ -83,7 +83,7 @@ public void testAuthorize() { // no field level security: role = Role.builder("_role").add(new FieldPermissions(), query, IndexPrivilege.ALL, randomBoolean(), "_index").build(); - permissions = role.authorize(SearchAction.NAME, Sets.newHashSet("_index"), lookup::get, fieldPermissionsCache); + permissions = role.authorize(SearchAction.NAME, Sets.newHashSet("_index"), lookup, fieldPermissionsCache); assertThat(permissions.getIndexPermissions("_index"), notNullValue()); assertFalse(permissions.getIndexPermissions("_index").getFieldPermissions().hasFieldLevelSecurity()); assertThat(permissions.getIndexPermissions("_index").getQueries().size(), equalTo(1)); @@ -93,7 +93,7 @@ public void testAuthorize() { role = Role.builder("_role") .add(new FieldPermissions(fieldPermissionDef(fields, null)), query, IndexPrivilege.ALL, randomBoolean(), "_alias") .build(); - permissions = role.authorize(SearchAction.NAME, Sets.newHashSet("_alias"), lookup::get, fieldPermissionsCache); + permissions = role.authorize(SearchAction.NAME, Sets.newHashSet("_alias"), lookup, fieldPermissionsCache); assertThat(permissions.getIndexPermissions("_index"), notNullValue()); assertTrue(permissions.getIndexPermissions("_index").getFieldPermissions().grantsAccessTo("_field")); assertTrue(permissions.getIndexPermissions("_index").getFieldPermissions().hasFieldLevelSecurity()); @@ -112,7 +112,7 @@ public void testAuthorize() { role = Role.builder("_role") .add(new FieldPermissions(fieldPermissionDef(allFields, null)), query, IndexPrivilege.ALL, randomBoolean(), "_alias") .build(); - permissions = role.authorize(SearchAction.NAME, Sets.newHashSet("_alias"), lookup::get, fieldPermissionsCache); + permissions = role.authorize(SearchAction.NAME, Sets.newHashSet("_alias"), lookup, fieldPermissionsCache); assertThat(permissions.getIndexPermissions("_index"), notNullValue()); assertFalse(permissions.getIndexPermissions("_index").getFieldPermissions().hasFieldLevelSecurity()); assertThat(permissions.getIndexPermissions("_index").getQueries().size(), equalTo(1)); @@ -141,7 +141,7 @@ public void testAuthorize() { .add(new FieldPermissions(fieldPermissionDef(allFields, null)), fooQuery, IndexPrivilege.ALL, randomBoolean(), "_alias") .add(new FieldPermissions(fieldPermissionDef(allFields, null)), query, IndexPrivilege.ALL, randomBoolean(), "_alias") .build(); - permissions = role.authorize(SearchAction.NAME, Sets.newHashSet("_alias"), lookup::get, fieldPermissionsCache); + permissions = role.authorize(SearchAction.NAME, Sets.newHashSet("_alias"), lookup, fieldPermissionsCache); Set bothQueries = Sets.union(fooQuery, query); assertThat(permissions.getIndexPermissions("_index"), notNullValue()); assertFalse(permissions.getIndexPermissions("_index").getFieldPermissions().hasFieldLevelSecurity()); @@ -178,7 +178,7 @@ public void testAuthorizeMultipleGroupsMixedDls() { .add(new FieldPermissions(fieldPermissionDef(fields, null)), query, IndexPrivilege.ALL, randomBoolean(), "_index") .add(new FieldPermissions(fieldPermissionDef(null, null)), null, IndexPrivilege.ALL, randomBoolean(), "*") .build(); - IndicesAccessControl permissions = role.authorize(SearchAction.NAME, Sets.newHashSet("_index"), lookup::get, fieldPermissionsCache); + IndicesAccessControl permissions = role.authorize(SearchAction.NAME, Sets.newHashSet("_index"), lookup, fieldPermissionsCache); assertThat(permissions.getIndexPermissions("_index"), notNullValue()); assertTrue(permissions.getIndexPermissions("_index").getFieldPermissions().grantsAccessTo("_field")); assertFalse(permissions.getIndexPermissions("_index").getFieldPermissions().hasFieldLevelSecurity()); @@ -236,7 +236,7 @@ public void testCorePermissionAuthorize() { new FieldPermissions(fieldPermissionDef(null, new String[]{"denied_field"})), null, randomBoolean(), "a1"); IndicesPermission core = new IndicesPermission(group1, group2); Map authzMap = - core.authorize(SearchAction.NAME, Sets.newHashSet("a1", "ba"), lookup::get, fieldPermissionsCache); + core.authorize(SearchAction.NAME, Sets.newHashSet("a1", "ba"), lookup, fieldPermissionsCache); assertTrue(authzMap.get("a1").getFieldPermissions().grantsAccessTo("denied_field")); assertTrue(authzMap.get("a1").getFieldPermissions().grantsAccessTo(randomAlphaOfLength(5))); // did not define anything for ba so we allow all @@ -256,7 +256,7 @@ public void testCorePermissionAuthorize() { new FieldPermissions(fieldPermissionDef(new String[] { "*_field2" }, new String[] { "denied_field2" })), null, randomBoolean(), "a2"); core = new IndicesPermission(group1, group2, group3, group4); - authzMap = core.authorize(SearchAction.NAME, Sets.newHashSet("a1", "a2"), lookup::get, fieldPermissionsCache); + authzMap = core.authorize(SearchAction.NAME, Sets.newHashSet("a1", "a2"), lookup, fieldPermissionsCache); assertFalse(authzMap.get("a1").getFieldPermissions().hasFieldLevelSecurity()); assertFalse(authzMap.get("a2").getFieldPermissions().grantsAccessTo("denied_field2")); assertFalse(authzMap.get("a2").getFieldPermissions().grantsAccessTo("denied_field")); @@ -298,7 +298,7 @@ public void testSecurityIndicesPermissions() { // allow_restricted_indices: false IndicesPermission.Group group = new IndicesPermission.Group(IndexPrivilege.ALL, new FieldPermissions(), null, false, "*"); Map authzMap = new IndicesPermission(group).authorize(SearchAction.NAME, - Sets.newHashSet(RestrictedIndicesNames.INTERNAL_SECURITY_INDEX, RestrictedIndicesNames.SECURITY_INDEX_NAME), lookup::get, + Sets.newHashSet(RestrictedIndicesNames.INTERNAL_SECURITY_INDEX, RestrictedIndicesNames.SECURITY_INDEX_NAME), lookup, fieldPermissionsCache); assertThat(authzMap.get(RestrictedIndicesNames.INTERNAL_SECURITY_INDEX).isGranted(), is(false)); assertThat(authzMap.get(RestrictedIndicesNames.SECURITY_INDEX_NAME).isGranted(), is(false)); @@ -306,7 +306,7 @@ public void testSecurityIndicesPermissions() { // allow_restricted_indices: true group = new IndicesPermission.Group(IndexPrivilege.ALL, new FieldPermissions(), null, true, "*"); authzMap = new IndicesPermission(group).authorize(SearchAction.NAME, - Sets.newHashSet(RestrictedIndicesNames.INTERNAL_SECURITY_INDEX, RestrictedIndicesNames.SECURITY_INDEX_NAME), lookup::get, + Sets.newHashSet(RestrictedIndicesNames.INTERNAL_SECURITY_INDEX, RestrictedIndicesNames.SECURITY_INDEX_NAME), lookup, fieldPermissionsCache); assertThat(authzMap.get(RestrictedIndicesNames.INTERNAL_SECURITY_INDEX).isGranted(), is(true)); assertThat(authzMap.get(RestrictedIndicesNames.SECURITY_INDEX_NAME).isGranted(), is(true)); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java index 2e70d84dd6f8a..15b9932855891 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java @@ -468,7 +468,7 @@ public void testMergingRolesWithFls() { .numberOfShards(1).numberOfReplicas(0).build(), true) .build(); Map acls = role.indices().authorize("indices:data/read/search", - Collections.singleton("test"), metaData.getAliasAndIndexLookup()::get, cache); + Collections.singleton("test"), metaData.getAliasAndIndexLookup(), cache); assertFalse(acls.isEmpty()); assertTrue(acls.get("test").getFieldPermissions().grantsAccessTo("L1.foo")); assertFalse(acls.get("test").getFieldPermissions().grantsAccessTo("L2.foo")); From 3e60a91601e12b6beb864996e54d6ed867de5642 Mon Sep 17 00:00:00 2001 From: jaymode Date: Mon, 4 Feb 2019 12:34:23 -0700 Subject: [PATCH 11/15] add licensing for authorization engine --- .../license/XPackLicenseState.java | 20 +++++++++-- .../license/XPackLicenseStateTests.java | 6 ++-- .../xpack/security/Security.java | 3 +- .../security/authz/AuthorizationService.java | 15 +++++--- .../authz/AuthorizationServiceTests.java | 35 ++++++++++++++++--- 5 files changed, 63 insertions(+), 16 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java index 84dc4c9a5887b..7cb04a9e57a4b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java @@ -103,7 +103,8 @@ private static String[] securityAcknowledgementMessages(OperationMode currentMod "The following X-Pack security functionality will be disabled: authentication, authorization, " + "ip filtering, and auditing. Please restart your node after applying the license.", "Field and document level access control will be disabled.", - "Custom realms will be ignored." + "Custom realms will be ignored.", + "A custom authorization engine will be ignored." }; } break; @@ -116,7 +117,8 @@ private static String[] securityAcknowledgementMessages(OperationMode currentMod case PLATINUM: return new String[] { "Field and document level access control will be disabled.", - "Custom realms will be ignored." + "Custom realms will be ignored.", + "A custom authorization engine will be ignored." }; } break; @@ -131,7 +133,8 @@ private static String[] securityAcknowledgementMessages(OperationMode currentMod "Authentication will be limited to the native realms.", "IP filtering and auditing will be disabled.", "Field and document level access control will be disabled.", - "Custom realms will be ignored." + "Custom realms will be ignored.", + "A custom authorization engine will be ignored." }; } } @@ -433,6 +436,17 @@ public synchronized boolean isAuthorizationRealmAllowed() { && status.active; } + /** + * @return whether a custom authorization engine is allowed based on the license {@link OperationMode} + * @see org.elasticsearch.xpack.core.security.authc.support.DelegatedAuthorizationSettings + */ + public synchronized boolean isAuthorizationEngineAllowed() { + final boolean isSecurityCurrentlyEnabled = + isSecurityEnabled(status.mode, isSecurityExplicitlyEnabled, isSecurityEnabled); + return isSecurityCurrentlyEnabled && (status.mode == OperationMode.PLATINUM || status.mode == OperationMode.TRIAL) + && status.active; + } + /** * Determine if Watcher is available based on the current license. *

    diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/XPackLicenseStateTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/XPackLicenseStateTests.java index bbd5d950c8b9b..8ad42d5afe636 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/XPackLicenseStateTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/XPackLicenseStateTests.java @@ -226,17 +226,17 @@ public void testSecurityAckAnyToTrialOrPlatinum() { } public void testSecurityAckTrialStandardGoldOrPlatinumToBasic() { - assertAckMesssages(XPackField.SECURITY, randomTrialStandardGoldOrPlatinumMode(), BASIC, 3); + assertAckMesssages(XPackField.SECURITY, randomTrialStandardGoldOrPlatinumMode(), BASIC, 4); } public void testSecurityAckAnyToStandard() { OperationMode from = randomFrom(BASIC, GOLD, PLATINUM, TRIAL); - assertAckMesssages(XPackField.SECURITY, from, STANDARD, 4); + assertAckMesssages(XPackField.SECURITY, from, STANDARD, 5); } public void testSecurityAckBasicStandardTrialOrPlatinumToGold() { OperationMode from = randomFrom(BASIC, PLATINUM, TRIAL, STANDARD); - assertAckMesssages(XPackField.SECURITY, from, GOLD, 2); + assertAckMesssages(XPackField.SECURITY, from, GOLD, 3); } public void testMonitoringAckBasicToAny() { 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 cbda696ced517..1895ef9287e1a 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 @@ -451,7 +451,8 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste requestInterceptors = Collections.unmodifiableSet(requestInterceptors); final AuthorizationService authzService = new AuthorizationService(settings, allRolesStore, clusterService, - auditTrailService, failureHandler, threadPool, anonymousUser, getAuthorizationEngine(), requestInterceptors); + auditTrailService, failureHandler, threadPool, anonymousUser, getAuthorizationEngine(), requestInterceptors, + getLicenseState()); components.add(nativeRolesStore); // used by roles actions components.add(reservedRolesStore); // used by roles actions 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 e12b4c41b9d70..14c690de7413d 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 @@ -32,6 +32,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.index.IndexNotFoundException; +import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportActionProxy; import org.elasticsearch.transport.TransportRequest; @@ -103,13 +104,14 @@ public class AuthorizationService { private final AuthorizationEngine rbacEngine; private final AuthorizationEngine authorizationEngine; private final Set requestInterceptors; + private final XPackLicenseState licenseState; 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, - Set requestInterceptors) { + Set requestInterceptors, XPackLicenseState licenseState) { this.clusterService = clusterService; this.auditTrail = auditTrail; this.indicesAndAliasesResolver = new IndicesAndAliasesResolver(settings, clusterService); @@ -122,6 +124,7 @@ public AuthorizationService(Settings settings, CompositeRolesStore rolesStore, C this.authorizationEngine = authorizationEngine == null ? this.rbacEngine : authorizationEngine; this.requestInterceptors = requestInterceptors; this.settings = settings; + this.licenseState = licenseState; } public void checkPrivileges(Authentication authentication, HasPrivilegesRequest request, @@ -349,10 +352,14 @@ AuthorizationEngine getAuthorizationEngine(final Authentication authentication) } private AuthorizationEngine getAuthorizationEngineForUser(final User user) { - if (ClientReservedRealm.isReserved(user.principal(), settings) || isInternalUser(user)) { - return rbacEngine; + if (rbacEngine != authorizationEngine && licenseState.isAuthorizationEngineAllowed()) { + if (ClientReservedRealm.isReserved(user.principal(), settings) || isInternalUser(user)) { + return rbacEngine; + } else { + return authorizationEngine; + } } else { - return authorizationEngine; + return rbacEngine; } } 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 19bb05b0b6bc6..f1a8a113bff0d 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 @@ -85,6 +85,7 @@ import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.index.IndexNotFoundException; import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportActionProxy; @@ -144,7 +145,6 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.CountDownLatch; -import java.util.function.Function; import java.util.function.Predicate; import static java.util.Arrays.asList; @@ -233,7 +233,7 @@ public void setup() { 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, - Collections.emptySet()); + Collections.emptySet(), new XPackLicenseState(settings)); } private void authorize(Authentication authentication, String action, TransportRequest request) { @@ -659,7 +659,8 @@ 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, Collections.emptySet()); + new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, anonymousUser, null, Collections.emptySet(), + new XPackLicenseState(settings)); RoleDescriptor role = new RoleDescriptor("a_all", null, new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a").privileges("all").build() }, null); @@ -687,7 +688,7 @@ public void testDenialForAnonymousUserAuthorizationExceptionDisabled() { final Authentication authentication = createAuthentication(new AnonymousUser(settings)); authorizationService = new AuthorizationService(settings, rolesStore, clusterService, auditTrail, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(settings), null, - Collections.emptySet()); + Collections.emptySet(), new XPackLicenseState(settings)); RoleDescriptor role = new RoleDescriptor("a_all", null, new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null); @@ -1391,36 +1392,60 @@ public void getUserPrivileges(Authentication authentication, AuthorizationInfo a } }; + XPackLicenseState licenseState = mock(XPackLicenseState.class); + when(licenseState.isAuthorizationEngineAllowed()).thenReturn(true); authorizationService = new AuthorizationService(Settings.EMPTY, rolesStore, clusterService, auditTrail, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(Settings.EMPTY), - engine, Collections.emptySet()); + engine, Collections.emptySet(), licenseState); Authentication authentication = createAuthentication(new User("test user", "a_all")); assertEquals(engine, authorizationService.getAuthorizationEngine(authentication)); + when(licenseState.isAuthorizationEngineAllowed()).thenReturn(false); + assertThat(authorizationService.getAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + when(licenseState.isAuthorizationEngineAllowed()).thenReturn(true); authentication = createAuthentication(new User("runas", new String[] { "runas_role" }, new User("runner", "runner_role"))); assertEquals(engine, authorizationService.getAuthorizationEngine(authentication)); assertEquals(engine, authorizationService.getRunAsAuthorizationEngine(authentication)); + when(licenseState.isAuthorizationEngineAllowed()).thenReturn(false); + assertThat(authorizationService.getAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + assertThat(authorizationService.getRunAsAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + when(licenseState.isAuthorizationEngineAllowed()).thenReturn(true); authentication = createAuthentication(new User("runas", new String[] { "runas_role" }, new ElasticUser(true))); assertEquals(engine, authorizationService.getAuthorizationEngine(authentication)); assertNotEquals(engine, authorizationService.getRunAsAuthorizationEngine(authentication)); assertThat(authorizationService.getRunAsAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + when(licenseState.isAuthorizationEngineAllowed()).thenReturn(false); + assertThat(authorizationService.getAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + assertThat(authorizationService.getRunAsAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + when(licenseState.isAuthorizationEngineAllowed()).thenReturn(true); authentication = createAuthentication(new User("elastic", new String[] { "superuser" }, new User("runner", "runner_role"))); assertNotEquals(engine, authorizationService.getAuthorizationEngine(authentication)); assertThat(authorizationService.getAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); assertEquals(engine, authorizationService.getRunAsAuthorizationEngine(authentication)); + when(licenseState.isAuthorizationEngineAllowed()).thenReturn(false); + assertThat(authorizationService.getAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + assertThat(authorizationService.getRunAsAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + when(licenseState.isAuthorizationEngineAllowed()).thenReturn(true); authentication = createAuthentication(new User("kibana", new String[] { "kibana_system" }, new ElasticUser(true))); assertNotEquals(engine, authorizationService.getAuthorizationEngine(authentication)); assertThat(authorizationService.getAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); assertNotEquals(engine, authorizationService.getRunAsAuthorizationEngine(authentication)); assertThat(authorizationService.getRunAsAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + when(licenseState.isAuthorizationEngineAllowed()).thenReturn(false); + assertThat(authorizationService.getAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + assertThat(authorizationService.getRunAsAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + when(licenseState.isAuthorizationEngineAllowed()).thenReturn(true); authentication = createAuthentication(randomFrom(XPackUser.INSTANCE, XPackSecurityUser.INSTANCE, new ElasticUser(true), new KibanaUser(true))); assertNotEquals(engine, authorizationService.getRunAsAuthorizationEngine(authentication)); assertThat(authorizationService.getRunAsAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + when(licenseState.isAuthorizationEngineAllowed()).thenReturn(false); + assertThat(authorizationService.getAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + assertThat(authorizationService.getRunAsAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); } static AuthorizationInfo authzInfoRoles(String[] expectedRoles) { From 58e15aa844a80f3b92eec860c3cc3549ae597c62 Mon Sep 17 00:00:00 2001 From: jaymode Date: Mon, 4 Feb 2019 12:49:07 -0700 Subject: [PATCH 12/15] remove unused import --- .../elasticsearch/xpack/core/security/authz/permission/Role.java | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java index 3805d8283bbd7..d6de2364fdf73 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java @@ -25,7 +25,6 @@ import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.function.Function; public final class Role { From 2372c1747c52ca06056236df33071786a835554a Mon Sep 17 00:00:00 2001 From: jaymode Date: Mon, 4 Feb 2019 14:11:23 -0700 Subject: [PATCH 13/15] fix building of AliasOrIndex.Index --- .../example/CustomAuthorizationEngineTests.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/examples/security-authorization-engine/src/test/java/org/elasticsearch/example/CustomAuthorizationEngineTests.java b/plugins/examples/security-authorization-engine/src/test/java/org/elasticsearch/example/CustomAuthorizationEngineTests.java index d34a4251b2163..a4f3e90208695 100644 --- a/plugins/examples/security-authorization-engine/src/test/java/org/elasticsearch/example/CustomAuthorizationEngineTests.java +++ b/plugins/examples/security-authorization-engine/src/test/java/org/elasticsearch/example/CustomAuthorizationEngineTests.java @@ -19,11 +19,13 @@ package org.elasticsearch.example; +import org.elasticsearch.Version; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.cluster.metadata.AliasOrIndex; import org.elasticsearch.cluster.metadata.AliasOrIndex.Index; import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.xpack.core.security.authc.Authentication; @@ -129,7 +131,11 @@ public void testAuthorizeClusterAction() { public void testAuthorizeIndexAction() { CustomAuthorizationEngine engine = new CustomAuthorizationEngine(); Map aliasOrIndexMap = new HashMap<>(); - aliasOrIndexMap.put("index", new Index(IndexMetaData.builder("index").build())); + aliasOrIndexMap.put("index", new Index(IndexMetaData.builder("index") + .settings(Settings.builder().put("index.version.created", Version.CURRENT)) + .numberOfShards(1) + .numberOfReplicas(0) + .build())); // authorized { RequestInfo requestInfo = From 4c03fd523edbc92ba775d0c1e871191bd8d2672e Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Tue, 5 Feb 2019 07:22:11 -0700 Subject: [PATCH 14/15] Update plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java Co-Authored-By: jaymode --- .../org/elasticsearch/example/CustomAuthorizationEngine.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 77eec1e183bc1..f460dcaca364a 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 @@ -233,6 +233,6 @@ public CustomAuthorizationInfo getAuthenticatedUserAuthorizationInfo() { } private boolean isSuperuser(User user) { - return Arrays.binarySearch(user.roles(), "custom_superuser") > -1; + return Arrays.asList(user.roles()).contains("custom_superuser"); } } From de06e4ca0011b8681d21a22f348c8959333a9380 Mon Sep 17 00:00:00 2001 From: jaymode Date: Tue, 5 Feb 2019 11:08:21 -0700 Subject: [PATCH 15/15] fixes after merge --- .../authz/permission/LimitedRole.java | 36 +++++- .../core/security/authz/permission/Role.java | 21 +++- .../xpack/security/authz/RBACEngine.java | 118 ++++-------------- .../authz/store/CompositeRolesStore.java | 11 +- .../authz/AuthorizationServiceTests.java | 100 ++++++++------- .../xpack/security/authz/RBACEngineTests.java | 3 +- .../authz/store/CompositeRolesStoreTests.java | 20 ++- 7 files changed, 154 insertions(+), 155 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRole.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRole.java index b27a9435b473f..8c7491d0a9a3d 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRole.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRole.java @@ -6,11 +6,13 @@ package org.elasticsearch.xpack.core.security.authz.permission; +import org.apache.lucene.util.automaton.Automaton; import org.elasticsearch.cluster.metadata.AliasOrIndex; import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege; +import org.elasticsearch.xpack.core.security.support.Automatons; import java.util.Collection; import java.util.Map; @@ -37,6 +39,26 @@ public Role limitedBy() { return limitedBy; } + @Override + public ClusterPermission cluster() { + throw new UnsupportedOperationException("cannot retrieve cluster permission on limited role"); + } + + @Override + public IndicesPermission indices() { + throw new UnsupportedOperationException("cannot retrieve indices permission on limited role"); + } + + @Override + public ApplicationPermission application() { + throw new UnsupportedOperationException("cannot retrieve application permission on limited role"); + } + + @Override + public RunAsPermission runAs() { + throw new UnsupportedOperationException("cannot retrieve cluster permission on limited role"); + } + @Override public IndicesAccessControl authorize(String action, Set requestedIndicesOrAliases, Map aliasAndIndexLookup, @@ -55,11 +77,18 @@ public IndicesAccessControl authorize(String action, Set requestedIndice */ @Override public Predicate allowedIndicesMatcher(String action) { - Predicate predicate = indices().allowedIndicesMatcher(action); + Predicate predicate = super.indices().allowedIndicesMatcher(action); predicate = predicate.and(limitedBy.indices().allowedIndicesMatcher(action)); return predicate; } + @Override + public Automaton allowedActionsMatcher(String index) { + final Automaton allowedMatcher = super.allowedActionsMatcher(index); + final Automaton limitedByMatcher = super.allowedActionsMatcher(index); + return Automatons.intersectAndMinimize(allowedMatcher, limitedByMatcher); + } + /** * Check if indices permissions allow for the given action, also checks whether the limited by role allows the given actions * @@ -140,6 +169,11 @@ public ResourcePrivilegesMap checkApplicationResourcePrivileges(final String app return ResourcePrivilegesMap.intersection(resourcePrivilegesMap, resourcePrivilegesMapForLimitedRole); } + @Override + public boolean checkRunAs(String runAs) { + return super.checkRunAs(runAs) && limitedBy.checkRunAs(runAs); + } + /** * Create a new role defined by given role and the limited role. * diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java index 1c228814ff684..817a9e41eab71 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.core.security.authz.permission; +import org.apache.lucene.util.automaton.Automaton; import org.elasticsearch.cluster.metadata.AliasOrIndex; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.bytes.BytesReference; @@ -82,7 +83,15 @@ public static Builder builder(RoleDescriptor rd, FieldPermissionsCache fieldPerm * has the privilege for executing the given action on. */ public Predicate allowedIndicesMatcher(String action) { - return indices().allowedIndicesMatcher(action); + return indices.allowedIndicesMatcher(action); + } + + public Automaton allowedActionsMatcher(String index) { + return indices.allowedActionsMatcher(index); + } + + public boolean checkRunAs(String runAsName) { + return runAs.check(runAsName); } /** @@ -92,7 +101,7 @@ public Predicate allowedIndicesMatcher(String action) { * @return {@code true} if action is allowed else returns {@code false} */ public boolean checkIndicesAction(String action) { - return indices().check(action); + return indices.check(action); } @@ -108,7 +117,7 @@ public boolean checkIndicesAction(String action) { */ public ResourcePrivilegesMap checkIndicesPrivileges(Set checkForIndexPatterns, boolean allowRestrictedIndices, Set checkForPrivileges) { - return indices().checkResourcePrivileges(checkForIndexPatterns, allowRestrictedIndices, checkForPrivileges); + return indices.checkResourcePrivileges(checkForIndexPatterns, allowRestrictedIndices, checkForPrivileges); } /** @@ -119,7 +128,7 @@ public ResourcePrivilegesMap checkIndicesPrivileges(Set checkForIndexPat * @return {@code true} if action is allowed else returns {@code false} */ public boolean checkClusterAction(String action, TransportRequest request) { - return cluster().check(action, request); + return cluster.check(action, request); } /** @@ -129,7 +138,7 @@ public boolean checkClusterAction(String action, TransportRequest request) { * @return {@code true} if cluster privilege is allowed else returns {@code false} */ public boolean grants(ClusterPrivilege clusterPrivilege) { - return cluster().grants(clusterPrivilege); + return cluster.grants(clusterPrivilege); } /** @@ -147,7 +156,7 @@ public boolean grants(ClusterPrivilege clusterPrivilege) { public ResourcePrivilegesMap checkApplicationResourcePrivileges(final String applicationName, Set checkForResources, Set checkForPrivilegeNames, Collection storedPrivileges) { - return application().checkResourcePrivileges(applicationName, checkForResources, checkForPrivilegeNames, storedPrivileges); + return application.checkResourcePrivileges(applicationName, checkForResources, checkForPrivilegeNames, storedPrivileges); } /** 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 8354305a27c7f..e2824e74ecafe 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 @@ -52,12 +52,12 @@ import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsDefinition; import org.elasticsearch.xpack.core.security.authz.permission.IndicesPermission; import org.elasticsearch.xpack.core.security.authz.permission.ResourcePrivileges; +import org.elasticsearch.xpack.core.security.authz.permission.ResourcePrivilegesMap; import org.elasticsearch.xpack.core.security.authz.permission.Role; import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege; -import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.Privilege; import org.elasticsearch.xpack.core.security.support.Automatons; import org.elasticsearch.xpack.core.security.user.User; @@ -70,7 +70,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -125,7 +124,7 @@ public void authorizeRunAs(RequestInfo requestInfo, AuthorizationInfo authorizat if (authorizationInfo instanceof RBACAuthorizationInfo) { final Role role = ((RBACAuthorizationInfo) authorizationInfo).getAuthenticatedUserAuthorizationInfo().getRole(); listener.onResponse( - new AuthorizationResult(role.runAs().check(requestInfo.getAuthentication().getUser().principal()))); + new AuthorizationResult(role.checkRunAs(requestInfo.getAuthentication().getUser().principal()))); } else { listener.onFailure(new IllegalArgumentException("unsupported authorization info:" + authorizationInfo.getClass().getSimpleName())); @@ -137,7 +136,7 @@ public void authorizeClusterAction(RequestInfo requestInfo, AuthorizationInfo au ActionListener listener) { if (authorizationInfo instanceof RBACAuthorizationInfo) { final Role role = ((RBACAuthorizationInfo) authorizationInfo).getRole(); - if (role.cluster().check(requestInfo.getAction(), requestInfo.getRequest())) { + if (role.checkClusterAction(requestInfo.getAction(), requestInfo.getRequest())) { listener.onResponse(AuthorizationResult.granted()); } else if (checkSameUserPermissions(requestInfo.getAction(), requestInfo.getRequest(), requestInfo.getAuthentication())) { listener.onResponse(AuthorizationResult.granted()); @@ -294,7 +293,7 @@ private void authorizeIndexActionName(String action, AuthorizationInfo authoriza ActionListener listener) { if (authorizationInfo instanceof RBACAuthorizationInfo) { final Role role = ((RBACAuthorizationInfo) authorizationInfo).getRole(); - if (role.indices().check(action)) { + if (role.checkIndicesAction(action)) { listener.onResponse(new IndexAuthorizationResult(true, grantedValue)); } else { listener.onResponse(new IndexAuthorizationResult(true, IndicesAccessControl.DENIED)); @@ -325,9 +324,9 @@ public void validateIndexPermissionsAreSubset(RequestInfo requestInfo, Authoriza final Role role = ((RBACAuthorizationInfo) authorizationInfo).getRole(); Map permissionMap = new HashMap<>(); for (Entry> entry : indexNameToNewNames.entrySet()) { - Automaton existingPermissions = permissionMap.computeIfAbsent(entry.getKey(), role.indices()::allowedActionsMatcher); + Automaton existingPermissions = permissionMap.computeIfAbsent(entry.getKey(), role::allowedActionsMatcher); for (String alias : entry.getValue()) { - Automaton newNamePermissions = permissionMap.computeIfAbsent(alias, role.indices()::allowedActionsMatcher); + Automaton newNamePermissions = permissionMap.computeIfAbsent(alias, role::allowedActionsMatcher); if (Operations.subsetOf(newNamePermissions, existingPermissions) == false) { listener.onResponse(AuthorizationResult.deny()); return; @@ -362,69 +361,37 @@ public void checkPrivileges(Authentication authentication, AuthorizationInfo aut Map cluster = new HashMap<>(); for (String checkAction : request.clusterPrivileges()) { final ClusterPrivilege checkPrivilege = ClusterPrivilege.get(Collections.singleton(checkAction)); - final ClusterPrivilege rolePrivilege = userRole.cluster().privilege(); - cluster.put(checkAction, testPrivilege(checkPrivilege, rolePrivilege.getAutomaton())); + cluster.put(checkAction, userRole.grants(checkPrivilege)); } boolean allMatch = cluster.values().stream().allMatch(Boolean::booleanValue); - - final Map predicateCache = new HashMap<>(); - - final Map indices = new LinkedHashMap<>(); + ResourcePrivilegesMap.Builder combineIndicesResourcePrivileges = ResourcePrivilegesMap.builder(); for (RoleDescriptor.IndicesPrivileges check : request.indexPrivileges()) { - for (String index : check.getIndices()) { - final Map privileges = new HashMap<>(); - final ResourcePrivileges existing = indices.get(index); - if (existing != null) { - privileges.putAll(existing.getPrivileges()); - } - for (String privilege : check.getPrivileges()) { - if (testIndexMatch(index, check.allowRestrictedIndices(), privilege, userRole, predicateCache)) { - logger.debug(() -> new ParameterizedMessage("Role [{}] has [{}] on index [{}]", - Strings.arrayToCommaDelimitedString(userRole.names()), privilege, index)); - privileges.put(privilege, true); - } else { - logger.debug(() -> new ParameterizedMessage("Role [{}] does not have [{}] on index [{}]", - Strings.arrayToCommaDelimitedString(userRole.names()), privilege, index)); - privileges.put(privilege, false); - allMatch = false; - } - } - indices.put(index, ResourcePrivileges.builder(index).addPrivileges(privileges).build()); - } + ResourcePrivilegesMap resourcePrivileges = userRole.checkIndicesPrivileges(Sets.newHashSet(check.getIndices()), + check.allowRestrictedIndices(), Sets.newHashSet(check.getPrivileges())); + allMatch = allMatch && resourcePrivileges.allAllowed(); + combineIndicesResourcePrivileges.addResourcePrivilegesMap(resourcePrivileges); } + ResourcePrivilegesMap allIndices = combineIndicesResourcePrivileges.build(); + allMatch = allMatch && allIndices.allAllowed(); final Map> privilegesByApplication = new HashMap<>(); for (String applicationName : getApplicationNames(request)) { logger.debug("Checking privileges for application {}", applicationName); - final Map appPrivilegesByResource = new LinkedHashMap<>(); + ResourcePrivilegesMap.Builder builder = ResourcePrivilegesMap.builder(); for (RoleDescriptor.ApplicationResourcePrivileges p : request.applicationPrivileges()) { if (applicationName.equals(p.getApplication())) { - for (String resource : p.getResources()) { - final Map privileges = new HashMap<>(); - final ResourcePrivileges existing = appPrivilegesByResource.get(resource); - if (existing != null) { - privileges.putAll(existing.getPrivileges()); - } - for (String privilege : p.getPrivileges()) { - if (testResourceMatch(applicationName, resource, privilege, userRole, applicationPrivileges)) { - logger.debug(() -> new ParameterizedMessage("Role [{}] has [{} {}] on resource [{}]", - Strings.arrayToCommaDelimitedString(userRole.names()), applicationName, privilege, resource)); - privileges.put(privilege, true); - } else { - logger.debug(() -> new ParameterizedMessage("Role [{}] does not have [{} {}] on resource [{}]", - Strings.arrayToCommaDelimitedString(userRole.names()), applicationName, privilege, resource)); - privileges.put(privilege, false); - allMatch = false; - } - } - appPrivilegesByResource.put(resource, ResourcePrivileges.builder(resource).addPrivileges(privileges).build()); - } + ResourcePrivilegesMap appPrivsByResourceMap = userRole.checkApplicationResourcePrivileges(applicationName, + Sets.newHashSet(p.getResources()), Sets.newHashSet(p.getPrivileges()), applicationPrivileges); + builder.addResourcePrivilegesMap(appPrivsByResourceMap); } } - privilegesByApplication.put(applicationName, appPrivilegesByResource.values()); + ResourcePrivilegesMap resourcePrivsForApplication = builder.build(); + allMatch = allMatch && resourcePrivsForApplication.allAllowed(); + privilegesByApplication.put(applicationName, resourcePrivsForApplication.getResourceToResourcePrivileges().values()); } - listener.onResponse(new HasPrivilegesResponse(request.username(), allMatch, cluster, indices.values(), privilegesByApplication)); + listener.onResponse(new HasPrivilegesResponse(request.username(), allMatch, cluster, + allIndices.getResourceToResourcePrivileges().values(), privilegesByApplication)); } @@ -498,45 +465,8 @@ GetUserPrivilegesResponse buildUserPrivilegesResponseObject(Role userRole) { return new GetUserPrivilegesResponse(cluster, conditionalCluster, indices, application, runAs); } - private boolean testIndexMatch(String checkIndexPattern, boolean allowRestrictedIndices, String checkPrivilegeName, Role userRole, - Map predicateCache) { - final IndexPrivilege checkPrivilege = IndexPrivilege.get(Collections.singleton(checkPrivilegeName)); - - final Automaton checkIndexAutomaton = IndicesPermission.Group.buildIndexMatcherAutomaton(allowRestrictedIndices, checkIndexPattern); - - List privilegeAutomatons = new ArrayList<>(); - for (IndicesPermission.Group group : userRole.indices().groups()) { - final Automaton groupIndexAutomaton = predicateCache.computeIfAbsent(group, - g -> IndicesPermission.Group.buildIndexMatcherAutomaton(g.allowRestrictedIndices(), g.indices())); - if (Operations.subsetOf(checkIndexAutomaton, groupIndexAutomaton)) { - final IndexPrivilege rolePrivilege = group.privilege(); - if (rolePrivilege.name().contains(checkPrivilegeName)) { - return true; - } - privilegeAutomatons.add(rolePrivilege.getAutomaton()); - } - } - return testPrivilege(checkPrivilege, Automatons.unionAndMinimize(privilegeAutomatons)); - } - - private static boolean testPrivilege(Privilege checkPrivilege, Automaton roleAutomaton) { - return Operations.subsetOf(checkPrivilege.getAutomaton(), roleAutomaton); - } - - private boolean testResourceMatch(String application, String checkResource, String checkPrivilegeName, Role userRole, - Collection privileges) { - final Set nameSet = Collections.singleton(checkPrivilegeName); - final ApplicationPrivilege checkPrivilege = ApplicationPrivilege.get(application, nameSet, privileges); - assert checkPrivilege.getApplication().equals(application) - : "Privilege " + checkPrivilege + " should have application " + application; - assert checkPrivilege.name().equals(nameSet) - : "Privilege " + checkPrivilege + " should have name " + nameSet; - - return userRole.application().grants(checkPrivilege, checkResource); - } - static List resolveAuthorizedIndicesFromRole(Role role, String action, Map aliasAndIndexLookup) { - Predicate predicate = role.indices().allowedIndicesMatcher(action); + Predicate predicate = role.allowedIndicesMatcher(action); List indicesAndAliases = new ArrayList<>(); // TODO: can this be done smarter? I think there are usually more indices/aliases in the cluster then indices defined a roles? diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java index eec0cec5a3de2..2d1d4a98b4ba6 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java @@ -205,14 +205,13 @@ public void getRoles(User user, Authentication authentication, ActionListener { - buildAndCacheRoleFromDescriptors(apiKeyRoleDescriptors.getLimitedByRoleDescriptors(), - apiKeyRoleDescriptors.getApiKeyId(), ActionListener.wrap( + buildAndCacheRoleFromDescriptors(descriptors, apiKeyRoleDescriptors.getApiKeyId() + "_role_desc", + ActionListener.wrap(role -> buildAndCacheRoleFromDescriptors(apiKeyRoleDescriptors.getLimitedByRoleDescriptors(), + apiKeyRoleDescriptors.getApiKeyId() + "_limited_role_desc", ActionListener.wrap( limitedBy -> roleActionListener.onResponse(LimitedRole.createLimitedRole(role, limitedBy)), - roleActionListener::onFailure)); - }, roleActionListener::onFailure)); + roleActionListener::onFailure)), roleActionListener::onFailure)); } }, roleActionListener::onFailure)); } else { 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 efd8db980ba8d..7c4cd564e9993 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 @@ -959,7 +959,7 @@ public void testMonitoringOperationsAgainstSecurityIndexRequireAllowRestricted() final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); final Authentication restrictedUserAuthn = createAuthentication(new User("restricted_user", "restricted_monitor")); assertThrowsAuthorizationException(() -> authorize(restrictedUserAuthn, action, request), action, "restricted_user"); - verify(auditTrail).accessDenied(requestId, restrictedUserAuthn, action, request, + verify(auditTrail).accessDenied(eq(requestId), eq(restrictedUserAuthn), eq(action), eq(request), authzInfoRoles(new String[] { "restricted_monitor" })); verifyNoMoreInteractions(auditTrail); } @@ -967,7 +967,7 @@ public void testMonitoringOperationsAgainstSecurityIndexRequireAllowRestricted() final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); final Authentication unrestrictedUserAuthn = createAuthentication(new User("unrestricted_user", "unrestricted_monitor")); authorize(unrestrictedUserAuthn, action, request); - verify(auditTrail).accessGranted(requestId, unrestrictedUserAuthn, action, request, + verify(auditTrail).accessGranted(eq(requestId), eq(unrestrictedUserAuthn), eq(action), eq(request), authzInfoRoles(new String[] { "unrestricted_monitor" })); verifyNoMoreInteractions(auditTrail); } @@ -1016,7 +1016,8 @@ public void testSuperusersCanExecuteOperationAgainstSecurityIndex() throws IOExc try (ThreadContext.StoredContext ignore = threadContext.newStoredContext(false)) { final Authentication authentication = createAuthentication(superuser); authorize(authentication, action, request); - verify(auditTrail).accessGranted(requestId, authentication, action, request, authzInfoRoles(superuser.roles())); + verify(auditTrail).accessGranted(eq(requestId), eq(authentication), eq(action), eq(request), + authzInfoRoles(superuser.roles())); } } } @@ -1039,7 +1040,7 @@ public void testSuperusersCanExecuteOperationAgainstSecurityIndexWithWildcard() String action = SearchAction.NAME; SearchRequest request = new SearchRequest("_all"); - authorize(createAuthentication(superuser), action, request); + authorize(authentication, action, request); verify(auditTrail).accessGranted(eq(requestId), eq(authentication), eq(action), eq(request), authzInfoRoles(superuser.roles())); assertThat(request.indices(), arrayContainingInAnyOrder(INTERNAL_SECURITY_INDEX, SECURITY_INDEX_NAME)); } @@ -1417,55 +1418,68 @@ public void getUserPrivileges(Authentication authentication, AuthorizationInfo a authorizationService = new AuthorizationService(Settings.EMPTY, rolesStore, clusterService, auditTrail, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(Settings.EMPTY), engine, Collections.emptySet(), licenseState); - Authentication authentication = createAuthentication(new User("test user", "a_all")); - assertEquals(engine, authorizationService.getAuthorizationEngine(authentication)); - when(licenseState.isAuthorizationEngineAllowed()).thenReturn(false); - assertThat(authorizationService.getAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + Authentication authentication; + try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { + authentication = createAuthentication(new User("test user", "a_all")); + assertEquals(engine, authorizationService.getAuthorizationEngine(authentication)); + when(licenseState.isAuthorizationEngineAllowed()).thenReturn(false); + assertThat(authorizationService.getAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + } when(licenseState.isAuthorizationEngineAllowed()).thenReturn(true); - authentication = createAuthentication(new User("runas", new String[] { "runas_role" }, new User("runner", "runner_role"))); - assertEquals(engine, authorizationService.getAuthorizationEngine(authentication)); - assertEquals(engine, authorizationService.getRunAsAuthorizationEngine(authentication)); - when(licenseState.isAuthorizationEngineAllowed()).thenReturn(false); - assertThat(authorizationService.getAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); - assertThat(authorizationService.getRunAsAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { + authentication = createAuthentication(new User("runas", new String[]{"runas_role"}, new User("runner", "runner_role"))); + assertEquals(engine, authorizationService.getAuthorizationEngine(authentication)); + assertEquals(engine, authorizationService.getRunAsAuthorizationEngine(authentication)); + when(licenseState.isAuthorizationEngineAllowed()).thenReturn(false); + assertThat(authorizationService.getAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + assertThat(authorizationService.getRunAsAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + } when(licenseState.isAuthorizationEngineAllowed()).thenReturn(true); - authentication = createAuthentication(new User("runas", new String[] { "runas_role" }, new ElasticUser(true))); - assertEquals(engine, authorizationService.getAuthorizationEngine(authentication)); - assertNotEquals(engine, authorizationService.getRunAsAuthorizationEngine(authentication)); - assertThat(authorizationService.getRunAsAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); - when(licenseState.isAuthorizationEngineAllowed()).thenReturn(false); - assertThat(authorizationService.getAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); - assertThat(authorizationService.getRunAsAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { + authentication = createAuthentication(new User("runas", new String[]{"runas_role"}, new ElasticUser(true))); + assertEquals(engine, authorizationService.getAuthorizationEngine(authentication)); + assertNotEquals(engine, authorizationService.getRunAsAuthorizationEngine(authentication)); + assertThat(authorizationService.getRunAsAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + when(licenseState.isAuthorizationEngineAllowed()).thenReturn(false); + assertThat(authorizationService.getAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + assertThat(authorizationService.getRunAsAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + } when(licenseState.isAuthorizationEngineAllowed()).thenReturn(true); - authentication = createAuthentication(new User("elastic", new String[] { "superuser" }, new User("runner", "runner_role"))); - assertNotEquals(engine, authorizationService.getAuthorizationEngine(authentication)); - assertThat(authorizationService.getAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); - assertEquals(engine, authorizationService.getRunAsAuthorizationEngine(authentication)); - when(licenseState.isAuthorizationEngineAllowed()).thenReturn(false); - assertThat(authorizationService.getAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); - assertThat(authorizationService.getRunAsAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { + authentication = createAuthentication(new User("elastic", new String[]{"superuser"}, new User("runner", "runner_role"))); + assertNotEquals(engine, authorizationService.getAuthorizationEngine(authentication)); + assertThat(authorizationService.getAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + assertEquals(engine, authorizationService.getRunAsAuthorizationEngine(authentication)); + when(licenseState.isAuthorizationEngineAllowed()).thenReturn(false); + assertThat(authorizationService.getAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + assertThat(authorizationService.getRunAsAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + } when(licenseState.isAuthorizationEngineAllowed()).thenReturn(true); - authentication = createAuthentication(new User("kibana", new String[] { "kibana_system" }, new ElasticUser(true))); - assertNotEquals(engine, authorizationService.getAuthorizationEngine(authentication)); - assertThat(authorizationService.getAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); - assertNotEquals(engine, authorizationService.getRunAsAuthorizationEngine(authentication)); - assertThat(authorizationService.getRunAsAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); - when(licenseState.isAuthorizationEngineAllowed()).thenReturn(false); - assertThat(authorizationService.getAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); - assertThat(authorizationService.getRunAsAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { + authentication = createAuthentication(new User("kibana", new String[]{"kibana_system"}, new ElasticUser(true))); + assertNotEquals(engine, authorizationService.getAuthorizationEngine(authentication)); + assertThat(authorizationService.getAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + assertNotEquals(engine, authorizationService.getRunAsAuthorizationEngine(authentication)); + assertThat(authorizationService.getRunAsAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + when(licenseState.isAuthorizationEngineAllowed()).thenReturn(false); + assertThat(authorizationService.getAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + assertThat(authorizationService.getRunAsAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + } when(licenseState.isAuthorizationEngineAllowed()).thenReturn(true); - authentication = createAuthentication(randomFrom(XPackUser.INSTANCE, XPackSecurityUser.INSTANCE, - new ElasticUser(true), new KibanaUser(true))); - assertNotEquals(engine, authorizationService.getRunAsAuthorizationEngine(authentication)); - assertThat(authorizationService.getRunAsAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); - when(licenseState.isAuthorizationEngineAllowed()).thenReturn(false); - assertThat(authorizationService.getAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); - assertThat(authorizationService.getRunAsAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { + authentication = createAuthentication(randomFrom(XPackUser.INSTANCE, XPackSecurityUser.INSTANCE, + new ElasticUser(true), new KibanaUser(true))); + assertNotEquals(engine, authorizationService.getRunAsAuthorizationEngine(authentication)); + assertThat(authorizationService.getRunAsAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + when(licenseState.isAuthorizationEngineAllowed()).thenReturn(false); + assertThat(authorizationService.getAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + assertThat(authorizationService.getRunAsAuthorizationEngine(authentication), instanceOf(RBACEngine.class)); + } } static AuthorizationInfo authzInfoRoles(String[] expectedRoles) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java index 761c99afc3d18..e43ca6bbc0b6f 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java @@ -489,8 +489,7 @@ public void testCheckingIndexPermissionsDefinedOnDifferentPatterns() throws Exce .put("index", true).put("delete", true).map()).build(), ResourcePrivileges.builder("apache-2017-01") .addPrivileges(MapBuilder.newMapBuilder(new LinkedHashMap()) - .put("index", true).put("delete", false).map() - ) + .put("index", true).put("delete", false).map()).build() )); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java index 694bd4a8c64a3..5061d4c11edc1 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java @@ -911,9 +911,16 @@ public void testApiKeyAuthUsesApiKeyService() throws IOException { final ReservedRolesStore reservedRolesStore = spy(new ReservedRolesStore()); ThreadContext threadContext = new ThreadContext(SECURITY_ENABLED_SETTINGS); ApiKeyService apiKeyService = mock(ApiKeyService.class); + NativePrivilegeStore nativePrivStore = mock(NativePrivilegeStore.class); + doAnswer(invocationOnMock -> { + ActionListener> listener = + (ActionListener>) invocationOnMock.getArguments()[2]; + listener.onResponse(Collections.emptyList()); + return Void.TYPE; + }).when(nativePrivStore).getPrivileges(any(Collection.class), any(Collection.class), any(ActionListener.class)); final CompositeRolesStore compositeRolesStore = new CompositeRolesStore(SECURITY_ENABLED_SETTINGS, fileRolesStore, nativeRolesStore, reservedRolesStore, - mock(NativePrivilegeStore.class), Collections.emptyList(), new ThreadContext(SECURITY_ENABLED_SETTINGS), + nativePrivStore, Collections.emptyList(), new ThreadContext(SECURITY_ENABLED_SETTINGS), new XPackLicenseState(SECURITY_ENABLED_SETTINGS), cache, apiKeyService); AuditUtil.getOrGenerateRequestId(threadContext); final Authentication authentication = new Authentication(new User("test api key user", "superuser"), @@ -946,9 +953,16 @@ public void testApiKeyAuthUsesApiKeyServiceWithScopedRole() throws IOException { final ReservedRolesStore reservedRolesStore = spy(new ReservedRolesStore()); ThreadContext threadContext = new ThreadContext(SECURITY_ENABLED_SETTINGS); ApiKeyService apiKeyService = mock(ApiKeyService.class); + NativePrivilegeStore nativePrivStore = mock(NativePrivilegeStore.class); + doAnswer(invocationOnMock -> { + ActionListener> listener = + (ActionListener>) invocationOnMock.getArguments()[2]; + listener.onResponse(Collections.emptyList()); + return Void.TYPE; + }).when(nativePrivStore).getPrivileges(any(Collection.class), any(Collection.class), any(ActionListener.class)); final CompositeRolesStore compositeRolesStore = new CompositeRolesStore(SECURITY_ENABLED_SETTINGS, fileRolesStore, nativeRolesStore, reservedRolesStore, - mock(NativePrivilegeStore.class), Collections.emptyList(), new ThreadContext(SECURITY_ENABLED_SETTINGS), + nativePrivStore, Collections.emptyList(), new ThreadContext(SECURITY_ENABLED_SETTINGS), new XPackLicenseState(SECURITY_ENABLED_SETTINGS), cache, apiKeyService); AuditUtil.getOrGenerateRequestId(threadContext); final Authentication authentication = new Authentication(new User("test api key user", "api_key"), @@ -965,7 +979,7 @@ public void testApiKeyAuthUsesApiKeyServiceWithScopedRole() throws IOException { PlainActionFuture roleFuture = new PlainActionFuture<>(); compositeRolesStore.getRoles(authentication.getUser(), authentication, roleFuture); Role role = roleFuture.actionGet(); - assertThat(role.cluster().check("cluster:admin/foo", Empty.INSTANCE), is(false)); + assertThat(role.checkClusterAction("cluster:admin/foo", Empty.INSTANCE), is(false)); verify(apiKeyService).getRoleForApiKey(eq(authentication), any(ActionListener.class)); }