|
@@ -3,59 +3,59 @@
|
|
|
* 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.StepListener;
|
|
|
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;
|
|
|
+import org.elasticsearch.common.Nullable;
|
|
|
import org.elasticsearch.common.collect.Tuple;
|
|
|
import org.elasticsearch.common.settings.Setting;
|
|
|
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.license.XPackLicenseState;
|
|
|
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.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.NativeRealmSettings;
|
|
|
+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;
|
|
|
-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.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.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;
|
|
@@ -63,54 +63,55 @@ 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.security.authc.ApiKeyService;
|
|
|
-import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm;
|
|
|
-import org.elasticsearch.xpack.security.authz.IndicesAndAliasesResolver.ResolvedIndices;
|
|
|
+import org.elasticsearch.xpack.security.authz.interceptor.RequestInterceptor;
|
|
|
import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore;
|
|
|
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Collection;
|
|
|
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.Objects;
|
|
|
import java.util.Set;
|
|
|
-import java.util.function.Predicate;
|
|
|
+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;
|
|
|
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<Boolean> 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<String> SAME_USER_PRIVILEGE = Automatons.predicate(
|
|
|
- ChangePasswordAction.NAME, AuthenticateAction.NAME, HasPrivilegesAction.NAME, GetUserPrivilegesAction.NAME);
|
|
|
+ public static final String AUTHORIZATION_INFO_KEY = "_authz_info";
|
|
|
+ private 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 ApiKeyService apiKeyService;
|
|
|
+ private final AuthorizationEngine rbacEngine;
|
|
|
+ private final AuthorizationEngine authorizationEngine;
|
|
|
+ private final Set<RequestInterceptor> 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, ApiKeyService apiKeyService,
|
|
|
- FieldPermissionsCache fieldPermissionsCache) {
|
|
|
- this.rolesStore = rolesStore;
|
|
|
+ ThreadPool threadPool, AnonymousUser anonymousUser, @Nullable AuthorizationEngine authorizationEngine,
|
|
|
+ Set<RequestInterceptor> requestInterceptors, XPackLicenseState licenseState) {
|
|
|
this.clusterService = clusterService;
|
|
|
this.auditTrail = auditTrail;
|
|
|
this.indicesAndAliasesResolver = new IndicesAndAliasesResolver(settings, clusterService);
|
|
@@ -119,8 +120,27 @@ public class AuthorizationService {
|
|
|
this.anonymousUser = anonymousUser;
|
|
|
this.isAnonymousEnabled = AnonymousUser.isAnonymousEnabled(settings);
|
|
|
this.anonymousAuthzExceptionEnabled = ANONYMOUS_AUTHORIZATION_EXCEPTION_SETTING.get(settings);
|
|
|
- this.fieldPermissionsCache = fieldPermissionsCache;
|
|
|
- this.apiKeyService = apiKeyService;
|
|
|
+ this.rbacEngine = new RBACEngine(settings, rolesStore);
|
|
|
+ this.authorizationEngine = authorizationEngine == null ? this.rbacEngine : authorizationEngine;
|
|
|
+ this.requestInterceptors = requestInterceptors;
|
|
|
+ this.settings = settings;
|
|
|
+ this.licenseState = licenseState;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void checkPrivileges(Authentication authentication, HasPrivilegesRequest request,
|
|
|
+ Collection<ApplicationPrivilegeDescriptor> applicationPrivilegeDescriptors,
|
|
|
+ ActionListener<HasPrivilegesResponse> listener) {
|
|
|
+ getAuthorizationEngine(authentication).checkPrivileges(authentication, getAuthorizationInfoFromContext(), request,
|
|
|
+ applicationPrivilegeDescriptors, wrapPreservingContext(listener, threadContext));
|
|
|
+ }
|
|
|
+
|
|
|
+ public void retrieveUserPrivileges(Authentication authentication, GetUserPrivilegesRequest request,
|
|
|
+ ActionListener<GetUserPrivilegesResponse> 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");
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -128,14 +148,16 @@ public class AuthorizationService {
|
|
|
* 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<Void> 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,212 +166,259 @@ public class AuthorizationService {
|
|
|
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 RequestInfo requestInfo = new RequestInfo(authentication, unwrappedRequest, action);
|
|
|
+ final ActionListener<AuthorizationInfo> authzInfoListener = wrapPreservingContext(ActionListener.wrap(
|
|
|
+ authorizationInfo -> {
|
|
|
+ putTransientIfNonExisting(AUTHORIZATION_INFO_KEY, authorizationInfo);
|
|
|
+ maybeAuthorizeRunAs(requestInfo, finalAuditId, authorizationInfo, listener);
|
|
|
+ }, listener::onFailure), threadContext);
|
|
|
+ getAuthorizationEngine(authentication).resolveAuthorizationInfo(requestInfo, 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 RequestInfo requestInfo, final String requestId, final AuthorizationInfo authzInfo,
|
|
|
+ final ActionListener<Void> listener) {
|
|
|
+ final Authentication authentication = requestInfo.getAuthentication();
|
|
|
+ final TransportRequest request = requestInfo.getRequest();
|
|
|
+ final String action = requestInfo.getAction();
|
|
|
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<AuthorizationResult> runAsListener = wrapPreservingContext(ActionListener.wrap(result -> {
|
|
|
+ if (result.isGranted()) {
|
|
|
+ if (result.isAuditable()) {
|
|
|
+ auditTrail.runAsGranted(requestId, authentication, action, request,
|
|
|
+ authzInfo.getAuthenticatedUserAuthorizationInfo());
|
|
|
+ }
|
|
|
+ authorizeAction(requestInfo, requestId, authzInfo, listener);
|
|
|
+ } else {
|
|
|
+ if (result.isAuditable()) {
|
|
|
+ auditTrail.runAsDenied(requestId, authentication, action, request,
|
|
|
+ authzInfo.getAuthenticatedUserAuthorizationInfo());
|
|
|
+ }
|
|
|
+ listener.onFailure(denialException(authentication, action, null));
|
|
|
+ }
|
|
|
+ }, e -> {
|
|
|
+ auditTrail.runAsDenied(requestId, authentication, action, request,
|
|
|
+ authzInfo.getAuthenticatedUserAuthorizationInfo());
|
|
|
+ listener.onFailure(denialException(authentication, action, null));
|
|
|
+ }), threadContext);
|
|
|
+ authorizeRunAs(requestInfo, authzInfo, runAsListener);
|
|
|
+ } else {
|
|
|
+ authorizeAction(requestInfo, requestId, 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 RequestInfo requestInfo, final String requestId, final AuthorizationInfo authzInfo,
|
|
|
+ final ActionListener<Void> 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)) {
|
|
|
- if (permission.checkClusterAction(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());
|
|
|
+ final ActionListener<AuthorizationResult> 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();
|
|
|
+ final AsyncSupplier<List<String>> authorizedIndicesSupplier = new CachingAsyncSupplier<>(authzIndicesListener ->
|
|
|
+ authzEngine.loadAuthorizedIndices(requestInfo, authzInfo, metaData.getAliasAndIndexLookup(),
|
|
|
+ authzIndicesListener));
|
|
|
+ final AsyncSupplier<ResolvedIndices> resolvedIndicesAsyncSupplier = new CachingAsyncSupplier<>((resolvedIndicesListener) -> {
|
|
|
+ authorizedIndicesSupplier.getAsync(ActionListener.wrap(authorizedIndices -> {
|
|
|
+ resolveIndexNames(request, metaData, authorizedIndices, resolvedIndicesListener);
|
|
|
+ }, e -> {
|
|
|
+ auditTrail.accessDenied(requestId, authentication, action, request, authzInfo);
|
|
|
+ if (e instanceof IndexNotFoundException) {
|
|
|
+ listener.onFailure(e);
|
|
|
+ } else {
|
|
|
+ listener.onFailure(denialException(authentication, action, e));
|
|
|
+ }
|
|
|
+ }));
|
|
|
+ });
|
|
|
+ authzEngine.authorizeIndexAction(requestInfo, authzInfo, resolvedIndicesAsyncSupplier,
|
|
|
+ metaData.getAliasAndIndexLookup(), 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));
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- // 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());
|
|
|
+ private void handleIndexActionAuthorizationResult(final IndexAuthorizationResult result, final RequestInfo requestInfo,
|
|
|
+ final String requestId, final AuthorizationInfo authzInfo,
|
|
|
+ final AuthorizationEngine authzEngine,
|
|
|
+ final AsyncSupplier<List<String>> authorizedIndicesSupplier,
|
|
|
+ final AsyncSupplier<ResolvedIndices> resolvedIndicesAsyncSupplier,
|
|
|
+ final MetaData metaData,
|
|
|
+ final ActionListener<Void> 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());
|
|
|
}
|
|
|
-
|
|
|
- //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.checkIndicesAction(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.checkIndicesAction(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.checkIndicesAction(action)) {
|
|
|
- auditTrail.accessGranted(auditId, authentication, action, request, permission.names());
|
|
|
- return;
|
|
|
+ //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<Alias> aliases = ((CreateIndexRequest) request).aliases();
|
|
|
+ if (aliases.isEmpty()) {
|
|
|
+ runRequestInterceptors(requestInfo, authzInfo, authorizationEngine, listener);
|
|
|
} 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());
|
|
|
+ final RequestInfo aliasesRequestInfo = new RequestInfo(authentication, request, IndicesAliasesAction.NAME);
|
|
|
+ authzEngine.authorizeIndexAction(aliasesRequestInfo, authzInfo,
|
|
|
+ ril -> {
|
|
|
+ resolvedIndicesAsyncSupplier.getAsync(ActionListener.wrap(resolvedIndices -> {
|
|
|
+ List<String> 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(),
|
|
|
+ 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 {
|
|
|
+ runRequestInterceptors(requestInfo, authzInfo, authorizationEngine, listener);
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- // 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.checkIndicesAction(action) == false) {
|
|
|
- throw denial(auditId, authentication, action, request, permission.names());
|
|
|
- } 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;
|
|
|
- }
|
|
|
- } 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());
|
|
|
+ private void runRequestInterceptors(RequestInfo requestInfo, AuthorizationInfo authorizationInfo,
|
|
|
+ AuthorizationEngine authorizationEngine, ActionListener<Void> listener) {
|
|
|
+ if (requestInterceptors.isEmpty()) {
|
|
|
+ listener.onResponse(null);
|
|
|
+ } else {
|
|
|
+ Iterator<RequestInterceptor> requestInterceptorIterator = requestInterceptors.iterator();
|
|
|
+ final StepListener<Void> firstStepListener = new StepListener<>();
|
|
|
+ final RequestInterceptor first = requestInterceptorIterator.next();
|
|
|
+
|
|
|
+ StepListener<Void> prevListener = firstStepListener;
|
|
|
+ while (requestInterceptorIterator.hasNext()) {
|
|
|
+ final RequestInterceptor nextInterceptor = requestInterceptorIterator.next();
|
|
|
+ final StepListener<Void> 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);
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- final boolean allowsRemoteIndices = request instanceof IndicesRequest
|
|
|
- && IndicesAndAliasesResolver.allowsRemoteIndices((IndicesRequest) request);
|
|
|
|
|
|
- // 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.checkIndicesAction(action) == false) {
|
|
|
- throw denial(auditId, authentication, action, request, permission.names());
|
|
|
- }
|
|
|
+ // pkg-private for testing
|
|
|
+ AuthorizationEngine getRunAsAuthorizationEngine(final Authentication authentication) {
|
|
|
+ return getAuthorizationEngineForUser(authentication.getUser().authenticatedUser());
|
|
|
+ }
|
|
|
|
|
|
- final MetaData metaData = clusterService.state().metaData();
|
|
|
- final AuthorizedIndices authorizedIndices = new AuthorizedIndices(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.checkIndicesAction(action) == false) {
|
|
|
- throw denial(auditId, authentication, action, request, permission.names());
|
|
|
- }
|
|
|
+ // pkg-private for testing
|
|
|
+ AuthorizationEngine getAuthorizationEngine(final Authentication authentication) {
|
|
|
+ return getAuthorizationEngineForUser(authentication.getUser());
|
|
|
+ }
|
|
|
|
|
|
- //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 AuthorizationEngine getAuthorizationEngineForUser(final User user) {
|
|
|
+ if (rbacEngine != authorizationEngine && licenseState.isAuthorizationEngineAllowed()) {
|
|
|
+ if (ClientReservedRealm.isReserved(user.principal(), settings) || isInternalUser(user)) {
|
|
|
+ return rbacEngine;
|
|
|
+ } else {
|
|
|
+ return authorizationEngine;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ return rbacEngine;
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- final Set<String> localIndices = new HashSet<>(resolvedIndices.getLocal());
|
|
|
- IndicesAccessControl indicesAccessControl = permission.authorize(action, localIndices, metaData, fieldPermissionsCache);
|
|
|
- if (indicesAccessControl.isGranted()) {
|
|
|
- putTransientIfNonExisting(AuthorizationServiceField.INDICES_PERMISSIONS_KEY, indicesAccessControl);
|
|
|
+ private void authorizeSystemUser(final Authentication authentication, final String action, final String requestId,
|
|
|
+ final TransportRequest request, final ActionListener<Void> 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 {
|
|
|
- throw denial(auditId, authentication, action, request, permission.names());
|
|
|
+ auditTrail.accessDenied(requestId, authentication, action, request, SYSTEM_AUTHZ_INFO);
|
|
|
+ listener.onFailure(denialException(authentication, action, null));
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- //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<Alias> aliases = ((CreateIndexRequest) request).aliases();
|
|
|
- if (!aliases.isEmpty()) {
|
|
|
- Set<String> 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
|
|
|
+ private TransportRequest maybeUnwrapRequest(Authentication authentication, TransportRequest originalRequest, String action,
|
|
|
+ String requestId) {
|
|
|
+ final TransportRequest request;
|
|
|
+ if (originalRequest instanceof ConcreteShardRequest) {
|
|
|
+ request = ((ConcreteShardRequest<?>) originalRequest).getRequest();
|
|
|
+ assert TransportActionProxy.isProxyRequest(request) == false : "expected non-proxy request for action: " + action;
|
|
|
+ } else {
|
|
|
+ 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");
|
|
|
+ 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");
|
|
|
+ auditTrail.accessDenied(requestId, authentication, action, request, EmptyAuthorizationInfo.INSTANCE);
|
|
|
+ throw denialException(authentication, action, 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 void authorizeRunAs(final RequestInfo requestInfo, final AuthorizationInfo authzInfo,
|
|
|
+ final ActionListener<AuthorizationResult> listener) {
|
|
|
+ final Authentication authentication = requestInfo.getAuthentication();
|
|
|
+ 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
|
|
|
+ listener.onResponse(AuthorizationResult.deny());
|
|
|
+ } else {
|
|
|
+ final AuthorizationEngine runAsAuthzEngine = getRunAsAuthorizationEngine(authentication);
|
|
|
+ runAsAuthzEngine.authorizeRunAs(requestInfo, authzInfo, listener);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* Performs authorization checks on the items within a {@link BulkShardRequest}.
|
|
|
* This inspects the {@link BulkItemRequest items} within the request, computes
|
|
@@ -357,48 +426,99 @@ public class AuthorizationService {
|
|
|
* 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 #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
|
|
|
* 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<String> indices, AuthorizedIndices authorizedIndices) {
|
|
|
+ private void authorizeBulkItems(RequestInfo requestInfo, AuthorizationInfo authzInfo,
|
|
|
+ AuthorizationEngine authzEngine, AsyncSupplier<ResolvedIndices> resolvedIndicesAsyncSupplier,
|
|
|
+ AsyncSupplier<List<String>> authorizedIndicesSupplier,
|
|
|
+ MetaData metaData, String requestId, ActionListener<Void> 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<String, String> resolvedIndexNames = new HashMap<>();
|
|
|
- // Maps (resolved-index , action) -> is-granted
|
|
|
- final Map<Tuple<String, String>, 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()));
|
|
|
- }
|
|
|
- if (resolvedIndices.getLocal().size() != 1) {
|
|
|
- throw illegalArgument("Bulk item should write to exactly 1 index, but request writes to "
|
|
|
- + String.join(",", resolvedIndices.getLocal()));
|
|
|
+ // Maps action -> resolved indices set
|
|
|
+ final Map<String, Set<String>> actionToIndicesMap = new HashMap<>();
|
|
|
+
|
|
|
+ authorizedIndicesSupplier.getAsync(ActionListener.wrap(authorizedIndices -> {
|
|
|
+ resolvedIndicesAsyncSupplier.getAsync(ActionListener.wrap(overallResolvedIndices -> {
|
|
|
+ final Set<String> 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<String> localSet = resolvedIndicesSet != null ? resolvedIndicesSet : new HashSet<>();
|
|
|
+ localSet.add(resolvedIndex);
|
|
|
+ return localSet;
|
|
|
+ });
|
|
|
}
|
|
|
- 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<String, String> 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<Collection<Tuple<String, IndexAuthorizationResult>>> bulkAuthzListener =
|
|
|
+ ActionListener.wrap(collection -> {
|
|
|
+ final Map<String, IndicesAccessControl> 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) {
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ listener.onResponse(null);
|
|
|
+ }, listener::onFailure);
|
|
|
+ final ActionListener<Tuple<String, IndexAuthorizationResult>> groupedActionListener = wrapPreservingContext(
|
|
|
+ new GroupedActionListener<>(bulkAuthzListener, actionToIndicesMap.size(), Collections.emptyList()), threadContext);
|
|
|
+
|
|
|
+ actionToIndicesMap.forEach((bulkItemAction, indices) -> {
|
|
|
+ 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(), ActionListener.wrap(indexAuthorizationResult ->
|
|
|
+ groupedActionListener.onResponse(new Tuple<>(bulkItemAction, indexAuthorizationResult)),
|
|
|
+ groupedActionListener::onFailure));
|
|
|
+ });
|
|
|
+ }, listener::onFailure));
|
|
|
+ }, listener::onFailure));
|
|
|
}
|
|
|
|
|
|
private IllegalArgumentException illegalArgument(String message) {
|
|
@@ -420,14 +540,9 @@ public class AuthorizationService {
|
|
|
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, List<String> authorizedIndices,
|
|
|
+ ActionListener<ResolvedIndices> listener) {
|
|
|
+ listener.onResponse(indicesAndAliasesResolver.resolve(request, metaData, authorizedIndices));
|
|
|
}
|
|
|
|
|
|
private void putTransientIfNonExisting(String key, Object value) {
|
|
@@ -437,155 +552,93 @@ public class AuthorizationService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- public void roles(User user, Authentication authentication, ActionListener<Role> 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");
|
|
|
+ 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) {
|
|
|
+ return authcFailureHandler.authenticationRequired(action, threadContext);
|
|
|
+ }
|
|
|
}
|
|
|
- if (XPackUser.is(user)) {
|
|
|
- assert XPackUser.INSTANCE.roles().length == 1;
|
|
|
- roleActionListener.onResponse(XPackUser.ROLE);
|
|
|
- return;
|
|
|
+ // 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 [{}]", cause, action, authUser.principal(),
|
|
|
+ authentication.getUser().principal());
|
|
|
}
|
|
|
- if (XPackSecurityUser.is(user)) {
|
|
|
- roleActionListener.onResponse(ReservedRolesStore.SUPERUSER_ROLE);
|
|
|
- return;
|
|
|
+ logger.debug("action [{}] is unauthorized for user [{}]", action, authUser.principal());
|
|
|
+ return authorizationError("action [{}] is unauthorized for user [{}]", cause, action, authUser.principal());
|
|
|
+ }
|
|
|
+
|
|
|
+ private class AuthorizationResultListener<T extends AuthorizationResult> implements ActionListener<T> {
|
|
|
+
|
|
|
+ private final Consumer<T> responseConsumer;
|
|
|
+ private final Consumer<Exception> failureConsumer;
|
|
|
+ private final RequestInfo requestInfo;
|
|
|
+ private final String requestId;
|
|
|
+ private final AuthorizationInfo authzInfo;
|
|
|
+
|
|
|
+ private AuthorizationResultListener(Consumer<T> responseConsumer, Consumer<Exception> failureConsumer, RequestInfo requestInfo,
|
|
|
+ String requestId, AuthorizationInfo authzInfo) {
|
|
|
+ this.responseConsumer = responseConsumer;
|
|
|
+ this.failureConsumer = failureConsumer;
|
|
|
+ this.requestInfo = requestInfo;
|
|
|
+ this.requestId = requestId;
|
|
|
+ this.authzInfo = authzInfo;
|
|
|
}
|
|
|
|
|
|
- final Authentication.AuthenticationType authType = authentication.getAuthenticationType();
|
|
|
- if (authType == Authentication.AuthenticationType.API_KEY) {
|
|
|
- apiKeyService.getRoleForApiKey(authentication, rolesStore, roleActionListener);
|
|
|
- } else {
|
|
|
- Set<String> 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");
|
|
|
+ @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);
|
|
|
}
|
|
|
- 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, roleActionListener);
|
|
|
+ handleFailure(result.isAuditable(), null);
|
|
|
}
|
|
|
}
|
|
|
- }
|
|
|
-
|
|
|
- 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);
|
|
|
- }
|
|
|
+ @Override
|
|
|
+ public void onFailure(Exception e) {
|
|
|
+ handleFailure(true, e);
|
|
|
+ }
|
|
|
|
|
|
- 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);
|
|
|
+ private void handleFailure(boolean audit, @Nullable Exception e) {
|
|
|
+ if (audit) {
|
|
|
+ auditTrail.accessDenied(requestId, requestInfo.getAuthentication(), requestInfo.getAction(), requestInfo.getRequest(),
|
|
|
+ authzInfo);
|
|
|
}
|
|
|
-
|
|
|
- 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;
|
|
|
+ failureConsumer.accept(denialException(requestInfo.getAuthentication(), requestInfo.getAction(), e));
|
|
|
}
|
|
|
- return false;
|
|
|
}
|
|
|
|
|
|
- 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);
|
|
|
- }
|
|
|
+ private static class CachingAsyncSupplier<V> implements AsyncSupplier<V> {
|
|
|
|
|
|
- ElasticsearchSecurityException denial(String auditRequestId, Authentication authentication, String action, TransportRequest request,
|
|
|
- String[] roleNames) {
|
|
|
- auditTrail.accessDenied(auditRequestId, authentication, action, request, roleNames);
|
|
|
- return denialException(authentication, action);
|
|
|
- }
|
|
|
+ private final AsyncSupplier<V> asyncSupplier;
|
|
|
+ private V value = null;
|
|
|
|
|
|
- private ElasticsearchSecurityException denyRunAs(String auditRequestId, Authentication authentication, String action,
|
|
|
- TransportRequest request, String[] roleNames) {
|
|
|
- auditTrail.runAsDenied(auditRequestId, authentication, action, request, roleNames);
|
|
|
- return denialException(authentication, action);
|
|
|
- }
|
|
|
+ private CachingAsyncSupplier(AsyncSupplier<V> supplier) {
|
|
|
+ this.asyncSupplier = supplier;
|
|
|
+ }
|
|
|
|
|
|
- private ElasticsearchSecurityException denialException(Authentication authentication, String action) {
|
|
|
- final User authUser = authentication.getUser().authenticatedUser();
|
|
|
- // Special case for anonymous user
|
|
|
- if (isAnonymousEnabled && anonymousUser.equals(authUser)) {
|
|
|
- if (anonymousAuthzExceptionEnabled == false) {
|
|
|
- throw authcFailureHandler.authenticationRequired(action, threadContext);
|
|
|
+ @Override
|
|
|
+ public synchronized void getAsync(ActionListener<V> listener) {
|
|
|
+ if (value == null) {
|
|
|
+ asyncSupplier.getAsync(ActionListener.wrap(loaded -> {
|
|
|
+ value = loaded;
|
|
|
+ listener.onResponse(value);
|
|
|
+ }, listener::onFailure));
|
|
|
+ } else {
|
|
|
+ listener.onResponse(value);
|
|
|
}
|
|
|
}
|
|
|
- // 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());
|
|
|
- }
|
|
|
- logger.debug("action [{}] is unauthorized for user [{}]", action, authUser.principal());
|
|
|
- return authorizationError("action [{}] is unauthorized for user [{}]", action, authUser.principal());
|
|
|
}
|
|
|
|
|
|
public static void addSettings(List<Setting<?>> settings) {
|