All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.elasticsearch.xpack.security.authz.AuthorizationService Maven / Gradle / Ivy

There is a newer version: 8.16.1
Show newest version
/*
 * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
 * or more contributor license agreements. Licensed under the Elastic License
 * 2.0; you may not use this file except in compliance with the Elastic License
 * 2.0.
 */

package org.elasticsearch.xpack.security.authz;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.ElasticsearchRoleRestrictionException;
import org.elasticsearch.ElasticsearchSecurityException;
import org.elasticsearch.TransportVersion;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.DelegatingActionListener;
import org.elasticsearch.action.DocWriteRequest;
import org.elasticsearch.action.admin.indices.alias.Alias;
import org.elasticsearch.action.admin.indices.alias.TransportIndicesAliasesAction;
import org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
import org.elasticsearch.action.bulk.BulkItemRequest;
import org.elasticsearch.action.bulk.BulkShardRequest;
import org.elasticsearch.action.bulk.TransportShardBulkAction;
import org.elasticsearch.action.datastreams.CreateDataStreamAction;
import org.elasticsearch.action.datastreams.MigrateToDataStreamAction;
import org.elasticsearch.action.delete.TransportDeleteAction;
import org.elasticsearch.action.index.TransportIndexAction;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.support.GroupedActionListener;
import org.elasticsearch.action.support.replication.TransportReplicationAction.ConcreteShardRequest;
import org.elasticsearch.action.update.TransportUpdateAction;
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
import org.elasticsearch.cluster.metadata.Metadata;
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.Strings;
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.ListenableFuture;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.core.Tuple;
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.SecurityContext;
import org.elasticsearch.xpack.core.security.action.apikey.QueryApiKeyAction;
import org.elasticsearch.xpack.core.security.action.apikey.QueryApiKeyRequest;
import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse;
import org.elasticsearch.xpack.core.security.authc.Authentication;
import org.elasticsearch.xpack.core.security.authc.AuthenticationFailureHandler;
import org.elasticsearch.xpack.core.security.authc.Subject;
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.AuthorizationContext;
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.ParentActionAuthorization;
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.RestrictedIndices;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptorsIntersection;
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.privilege.ApplicationPrivilegeDescriptor;
import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilegeResolver;
import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege;
import org.elasticsearch.xpack.core.security.user.AnonymousUser;
import org.elasticsearch.xpack.core.security.user.InternalUser;
import org.elasticsearch.xpack.core.security.user.SystemUser;
import org.elasticsearch.xpack.core.security.user.User;
import org.elasticsearch.xpack.security.Security;
import org.elasticsearch.xpack.security.audit.AuditLevel;
import org.elasticsearch.xpack.security.audit.AuditTrail;
import org.elasticsearch.xpack.security.audit.AuditTrailService;
import org.elasticsearch.xpack.security.audit.AuditUtil;
import org.elasticsearch.xpack.security.authz.accesscontrol.wrapper.DlsFlsFeatureTrackingIndicesAccessControlWrapper;
import org.elasticsearch.xpack.security.authz.interceptor.RequestInterceptor;
import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore;
import org.elasticsearch.xpack.security.operator.OperatorPrivileges.OperatorPrivilegesService;

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.Set;
import java.util.function.Consumer;
import java.util.function.Supplier;

import static org.elasticsearch.action.support.ContextPreservingActionListener.wrapPreservingContext;
import static org.elasticsearch.xpack.core.security.SecurityField.setting;
import static org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField.ACTION_SCOPE_AUTHORIZATION_KEYS;
import static org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField.AUTHORIZATION_INFO_KEY;
import static org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField.ORIGINATING_ACTION_KEY;
import static org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl.allowAll;
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
    );
    private static final AuthorizationInfo SYSTEM_AUTHZ_INFO = () -> Collections.singletonMap(
        PRINCIPAL_ROLES_FIELD_NAME,
        new String[] { SystemUser.ROLE_NAME }
    );
    private static final String IMPLIED_INDEX_ACTION = TransportIndexAction.NAME + ":op_type/index";
    private static final String IMPLIED_CREATE_ACTION = TransportIndexAction.NAME + ":op_type/create";

    private static final Logger logger = LogManager.getLogger(AuthorizationService.class);

    private final Settings settings;
    private final ClusterService clusterService;
    private final AuditTrailService auditTrailService;
    private final IndicesAndAliasesResolver indicesAndAliasesResolver;
    private final AuthenticationFailureHandler authcFailureHandler;
    private final ThreadContext threadContext;
    private final SecurityContext securityContext;
    private final AnonymousUser anonymousUser;
    private final AuthorizationEngine rbacEngine;
    private final AuthorizationEngine authorizationEngine;
    private final Set requestInterceptors;
    private final XPackLicenseState licenseState;
    private final OperatorPrivilegesService operatorPrivilegesService;
    private final RestrictedIndices restrictedIndices;
    private final DlsFlsFeatureTrackingIndicesAccessControlWrapper indicesAccessControlWrapper;

    private final boolean isAnonymousEnabled;
    private final boolean anonymousAuthzExceptionEnabled;
    private final AuthorizationDenialMessages authorizationDenialMessages;

    public AuthorizationService(
        Settings settings,
        CompositeRolesStore rolesStore,
        FieldPermissionsCache fieldPermissionsCache,
        ClusterService clusterService,
        AuditTrailService auditTrailService,
        AuthenticationFailureHandler authcFailureHandler,
        ThreadPool threadPool,
        AnonymousUser anonymousUser,
        @Nullable AuthorizationEngine authorizationEngine,
        Set requestInterceptors,
        XPackLicenseState licenseState,
        IndexNameExpressionResolver resolver,
        OperatorPrivilegesService operatorPrivilegesService,
        RestrictedIndices restrictedIndices,
        AuthorizationDenialMessages authorizationDenialMessages
    ) {
        this.clusterService = clusterService;
        this.auditTrailService = auditTrailService;
        this.restrictedIndices = restrictedIndices;
        this.indicesAndAliasesResolver = new IndicesAndAliasesResolver(settings, clusterService, resolver);
        this.authcFailureHandler = authcFailureHandler;
        this.threadContext = threadPool.getThreadContext();
        this.securityContext = new SecurityContext(settings, this.threadContext);
        this.anonymousUser = anonymousUser;
        this.isAnonymousEnabled = AnonymousUser.isAnonymousEnabled(settings);
        this.anonymousAuthzExceptionEnabled = ANONYMOUS_AUTHORIZATION_EXCEPTION_SETTING.get(settings);
        this.rbacEngine = new RBACEngine(
            settings,
            rolesStore,
            fieldPermissionsCache,
            new LoadAuthorizedIndicesTimeChecker.Factory(logger, settings, clusterService.getClusterSettings())
        );
        this.authorizationEngine = authorizationEngine == null ? this.rbacEngine : authorizationEngine;
        this.requestInterceptors = requestInterceptors;
        this.settings = settings;
        this.licenseState = licenseState;
        this.operatorPrivilegesService = operatorPrivilegesService;
        this.indicesAccessControlWrapper = new DlsFlsFeatureTrackingIndicesAccessControlWrapper(settings, licenseState);
        this.authorizationDenialMessages = authorizationDenialMessages;
    }

    public void checkPrivileges(
        Subject subject,
        AuthorizationEngine.PrivilegesToCheck privilegesToCheck,
        Collection applicationPrivilegeDescriptors,
        ActionListener listener
    ) {
        final AuthorizationEngine authorizationEngine = getAuthorizationEngineForSubject(subject);
        authorizationEngine.resolveAuthorizationInfo(
            subject,
            wrapPreservingContext(
                listener.delegateFailure(
                    (delegateListener, authorizationInfo) -> authorizationEngine.checkPrivileges(
                        authorizationInfo,
                        privilegesToCheck,
                        applicationPrivilegeDescriptors,
                        wrapPreservingContext(delegateListener, threadContext)
                    )
                ),
                threadContext
            )
        );
    }

    public void retrieveUserPrivileges(
        Subject subject,
        AuthorizationInfo authorizationInfo,
        ActionListener listener
    ) {
        final AuthorizationEngine authorizationEngine = getAuthorizationEngineForSubject(subject);
        // TODO the AuthorizationInfo is associated to the Subject; the argument is redundant and a possible source of conflict
        authorizationEngine.getUserPrivileges(authorizationInfo, wrapPreservingContext(listener, threadContext));
    }

    public void getRoleDescriptorsIntersectionForRemoteCluster(
        final String remoteClusterAlias,
        final TransportVersion remoteClusterVersion,
        final Subject subject,
        final ActionListener listener
    ) {
        if (subject.getUser() instanceof InternalUser) {
            final String message = "the user ["
                + subject.getUser().principal()
                + "] is an internal user and we should never try to retrieve its roles descriptors towards a remote cluster";
            assert false : message;
            logger.warn(message);
            listener.onFailure(new IllegalArgumentException(message));
            return;
        }

        final AuthorizationEngine authorizationEngine = getAuthorizationEngineForSubject(subject);
        // AuthZ info can be null for persistent tasks
        if (threadContext.getTransient(AUTHORIZATION_INFO_KEY) == null) {
            logger.debug("authorization info not available in thread context, resolving it for subject [{}]", subject);
        }
        authorizationEngine.resolveAuthorizationInfo(
            subject,
            wrapPreservingContext(
                listener.delegateFailure(
                    (delegatedLister, resolvedAuthzInfo) -> authorizationEngine.getRoleDescriptorsIntersectionForRemoteCluster(
                        remoteClusterAlias,
                        remoteClusterVersion,
                        resolvedAuthzInfo,
                        wrapPreservingContext(delegatedLister, threadContext)
                    )
                ),
                threadContext
            )
        );
    }

    /**
     * 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}
     * will be thrown.
     *
     * @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
     */
    public void authorize(
        final Authentication authentication,
        final String action,
        final TransportRequest originalRequest,
        final ActionListener listener
    ) {

        final AuthorizationContext enclosingContext = extractAuthorizationContext(threadContext, action);
        final ParentActionAuthorization parentAuthorization = securityContext.getParentAuthorization();

        /* authorization fills in certain transient headers, which must be observed in the listener (action handler execution)
         * as well, but which must not bleed across different action context (eg parent-child action contexts).
         * 

* Therefore we begin by clearing the existing ones up, as they might already be set during the authorization of a * previous parent action that ran under the same thread context (also on the same node). * When the returned {@code StoredContext} is closed, ALL the original headers are restored. */ try ( ThreadContext.StoredContext ignore = threadContext.newStoredContext( ACTION_SCOPE_AUTHORIZATION_KEYS, List.of(ParentActionAuthorization.THREAD_CONTEXT_KEY) ) ) { // this does not clear {@code AuthorizationServiceField.ORIGINATING_ACTION_KEY} // prior to doing any authorization lets set the originating action in the thread context // the originating action is the current action if no originating action has yet been set in the current thread context // if there is already an original action, that stays put (eg. the current action is a child action) putTransientIfNonExisting(ORIGINATING_ACTION_KEY, action); final String auditId; try { auditId = requireAuditId(authentication, action, originalRequest); } catch (ElasticsearchSecurityException e) { listener.onFailure(e); return; } // 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); try { checkOperatorPrivileges(authentication, action, originalRequest); } catch (ElasticsearchException e) { listener.onFailure(e); return; } if (SystemUser.is(authentication.getEffectiveSubject().getUser())) { // this never goes async so no need to wrap the listener authorizeSystemUser(authentication, action, auditId, unwrappedRequest, listener); } else { final RequestInfo requestInfo = new RequestInfo( authentication, unwrappedRequest, action, enclosingContext, parentAuthorization ); final AuthorizationEngine engine = getAuthorizationEngine(authentication); final ActionListener authzInfoListener = wrapPreservingContext(ActionListener.wrap(authorizationInfo -> { threadContext.putTransient(AUTHORIZATION_INFO_KEY, authorizationInfo); maybeAuthorizeRunAs(requestInfo, auditId, authorizationInfo, listener); }, e -> { if (e instanceof ElasticsearchRoleRestrictionException) { logger.debug( () -> Strings.format( "denying action [%s] due to role restriction for authentication [%s]", action, authentication ), e ); listener.onFailure(actionDenied(authentication, EmptyAuthorizationInfo.INSTANCE, action, unwrappedRequest, e)); } else { listener.onFailure(e); } }), threadContext); engine.resolveAuthorizationInfo(requestInfo, authzInfoListener); } } } @Nullable private static AuthorizationContext extractAuthorizationContext(ThreadContext threadContext, String childAction) { final String originatingAction = threadContext.getTransient(ORIGINATING_ACTION_KEY); if (Strings.isNullOrEmpty(originatingAction)) { // No parent action return null; } AuthorizationInfo authorizationInfo = threadContext.getTransient(AUTHORIZATION_INFO_KEY); if (authorizationInfo == null) { throw internalError( "While attempting to authorize action [" + childAction + "], found originating action [" + originatingAction + "] but no authorization info" ); } final IndicesAccessControl parentAccessControl = threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY); return new AuthorizationContext(originatingAction, authorizationInfo, parentAccessControl); } private String requireAuditId(Authentication authentication, String action, TransportRequest originalRequest) { String auditId = AuditUtil.extractRequestId(threadContext); if (auditId == null) { // We would like to assert that there is an existing request-id, but if this is a system action, then that might not be // true because the request-id is generated during authentication if (authentication.getEffectiveSubject().getUser() instanceof InternalUser) { auditId = AuditUtil.getOrGenerateRequestId(threadContext); } else { auditTrailService.get().tamperedRequest(null, authentication, action, originalRequest); throw internalError( "Attempt to authorize action [" + action + "] for [" + authentication.getEffectiveSubject().getUser().principal() + "] without an existing request-id" ); } } return auditId; } private static ElasticsearchSecurityException internalError(String message) { // When running with assertions enabled (testing) kill the node so that there is a hard failure in CI assert false : message; // Otherwise (production) just throw an exception so that we don't authorize something incorrectly return new ElasticsearchSecurityException(message); } private void checkOperatorPrivileges(Authentication authentication, String action, TransportRequest originalRequest) throws ElasticsearchSecurityException { // Check operator privileges // TODO: audit? final ElasticsearchSecurityException operatorException = operatorPrivilegesService.check( authentication, action, originalRequest, threadContext ); if (operatorException != null) { throw actionDenied(authentication, null, action, originalRequest, "because it requires operator privileges", operatorException); } operatorPrivilegesService.maybeInterceptRequest(threadContext, originalRequest); } 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.isRunAs(); final AuditTrail auditTrail = auditTrailService.get(); if (isRunAs) { ActionListener runAsListener = wrapPreservingContext(ActionListener.wrap(result -> { if (result.isGranted()) { auditTrail.runAsGranted(requestId, authentication, action, request, authzInfo.getAuthenticatedUserAuthorizationInfo()); authorizeAction(requestInfo, requestId, authzInfo, listener); } else { auditTrail.runAsDenied(requestId, authentication, action, request, authzInfo.getAuthenticatedUserAuthorizationInfo()); listener.onFailure(runAsDenied(authentication, authzInfo, action)); } }, e -> { auditTrail.runAsDenied(requestId, authentication, action, request, authzInfo.getAuthenticatedUserAuthorizationInfo()); listener.onFailure(actionDenied(authentication, authzInfo, action, request)); }), threadContext); authorizeRunAs(requestInfo, authzInfo, runAsListener); } else { authorizeAction(requestInfo, requestId, authzInfo, listener); } } 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); final AuditTrail auditTrail = auditTrailService.get(); if (ClusterPrivilegeResolver.isClusterAction(action)) { final ActionListener clusterAuthzListener = wrapPreservingContext( new AuthorizationResultListener<>(result -> { securityContext.putIndicesAccessControl(allowAll()); listener.onResponse(null); }, listener::onFailure, requestInfo, requestId, authzInfo), threadContext ); authzEngine.authorizeClusterAction(requestInfo, authzInfo, clusterAuthzListener.delegateFailureAndWrap((l, result) -> { if (false == result.isGranted() && QueryApiKeyAction.NAME.equals(action)) { assert request instanceof QueryApiKeyRequest : "request does not match action"; final QueryApiKeyRequest queryApiKeyRequest = (QueryApiKeyRequest) request; if (false == queryApiKeyRequest.isFilterForCurrentUser()) { queryApiKeyRequest.setFilterForCurrentUser(); authzEngine.authorizeClusterAction(requestInfo, authzInfo, l); return; } } l.onResponse(result); })); } else if (isIndexAction(action)) { final Metadata metadata = clusterService.state().metadata(); final AsyncSupplier resolvedIndicesAsyncSupplier = new CachingAsyncSupplier<>(resolvedIndicesListener -> { if (request instanceof SearchRequest searchRequest && searchRequest.pointInTimeBuilder() != null) { var resolvedIndices = indicesAndAliasesResolver.resolvePITIndices(searchRequest); resolvedIndicesListener.onResponse(resolvedIndices); return; } final ResolvedIndices resolvedIndices = indicesAndAliasesResolver.tryResolveWithoutWildcards(action, request); if (resolvedIndices != null) { resolvedIndicesListener.onResponse(resolvedIndices); } else { authzEngine.loadAuthorizedIndices( requestInfo, authzInfo, metadata.getIndicesLookup(), ActionListener.wrap( authorizedIndices -> resolvedIndicesListener.onResponse( indicesAndAliasesResolver.resolve(action, request, metadata, authorizedIndices) ), e -> { auditTrail.accessDenied(requestId, authentication, action, request, authzInfo); if (e instanceof IndexNotFoundException) { listener.onFailure(e); } else { listener.onFailure(actionDenied(authentication, authzInfo, action, request, e)); } } ) ); } }); authzEngine.authorizeIndexAction( requestInfo, authzInfo, resolvedIndicesAsyncSupplier, metadata.getIndicesLookup(), wrapPreservingContext( new AuthorizationResultListener<>( result -> handleIndexActionAuthorizationResult( result, requestInfo, requestId, authzInfo, authzEngine, resolvedIndicesAsyncSupplier, metadata, listener ), listener::onFailure, requestInfo, requestId, authzInfo ), threadContext ) ); } else { logger.warn("denying access for [{}] as action [{}] is not an index or cluster action", authentication, action); auditTrail.accessDenied(requestId, authentication, action, request, authzInfo); listener.onFailure(actionDenied(authentication, authzInfo, action, request)); } } private void handleIndexActionAuthorizationResult( final IndexAuthorizationResult result, final RequestInfo requestInfo, final String requestId, final AuthorizationInfo authzInfo, final AuthorizationEngine authzEngine, final AsyncSupplier resolvedIndicesAsyncSupplier, final Metadata metadata, final ActionListener listener ) { final IndicesAccessControl indicesAccessControl = indicesAccessControlWrapper.wrap(result.getIndicesAccessControl()); final Authentication authentication = requestInfo.getAuthentication(); final TransportRequest request = requestInfo.getRequest(); final String action = requestInfo.getAction(); securityContext.putIndicesAccessControl(indicesAccessControl); final AuthorizationContext authzContext = new AuthorizationContext(action, authzInfo, indicesAccessControl); PreAuthorizationUtils.maybeSkipChildrenActionAuthorization(securityContext, authzContext); // 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) || (request instanceof MigrateToDataStreamAction.Request) || (request instanceof CreateDataStreamAction.Request); if (request instanceof CreateDataStreamAction.Request || (request instanceof MigrateToDataStreamAction.Request) || ((CreateIndexRequest) request).aliases().isEmpty()) { runRequestInterceptors(requestInfo, authzInfo, authorizationEngine, listener); } else { Set aliases = ((CreateIndexRequest) request).aliases(); final RequestInfo aliasesRequestInfo = new RequestInfo( authentication, request, TransportIndicesAliasesAction.NAME, authzContext ); authzEngine.authorizeIndexAction( aliasesRequestInfo, authzInfo, ril -> resolvedIndicesAsyncSupplier.getAsync(ril.delegateFailureAndWrap((l, resolvedIndices) -> { List aliasesAndIndices = new ArrayList<>(resolvedIndices.getLocal()); for (Alias alias : aliases) { aliasesAndIndices.add(alias.name()); } ResolvedIndices withAliases = new ResolvedIndices(aliasesAndIndices, Collections.emptyList()); l.onResponse(withAliases); })), metadata.getIndicesLookup(), 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, authzContext, authzEngine, resolvedIndicesAsyncSupplier, metadata, requestId, wrapPreservingContext( listener.delegateFailureAndWrap((l, ignore) -> runRequestInterceptors(requestInfo, authzInfo, authorizationEngine, l)), threadContext ) ); } else { runRequestInterceptors(requestInfo, authzInfo, authorizationEngine, listener); } } private void runRequestInterceptors( RequestInfo requestInfo, AuthorizationInfo authorizationInfo, AuthorizationEngine authorizationEngine, ActionListener listener ) { if (requestInterceptors.isEmpty()) { listener.onResponse(null); } else { final Iterator requestInterceptorIterator = requestInterceptors.iterator(); requestInterceptorIterator.next() .intercept(requestInfo, authorizationEngine, authorizationInfo, new DelegatingActionListener<>(listener) { @Override public void onResponse(Void unused) { if (requestInterceptorIterator.hasNext()) { requestInterceptorIterator.next().intercept(requestInfo, authorizationEngine, authorizationInfo, this); } else { listener.onResponse(null); } } }); } } // pkg-private for testing AuthorizationEngine getRunAsAuthorizationEngine(final Authentication authentication) { return getAuthorizationEngineForSubject(authentication.getAuthenticatingSubject()); } // pkg-private for testing AuthorizationEngine getAuthorizationEngine(final Authentication authentication) { return getAuthorizationEngineForSubject(authentication.getEffectiveSubject()); } AuthorizationEngine getAuthorizationEngineForSubject(final Subject subject) { return getAuthorizationEngineForUser(subject.getUser()); } private AuthorizationEngine getAuthorizationEngineForUser(final User user) { if (rbacEngine != authorizationEngine && Security.AUTHORIZATION_ENGINE_FEATURE.check(licenseState)) { if (ClientReservedRealm.isReserved(user.principal(), settings) || user instanceof InternalUser) { return rbacEngine; } else { return authorizationEngine; } } else { return rbacEngine; } } private void authorizeSystemUser( final Authentication authentication, final String action, final String requestId, final TransportRequest request, final ActionListener listener ) { final AuditTrail auditTrail = auditTrailService.get(); if (SystemUser.isAuthorized(action)) { securityContext.putIndicesAccessControl(allowAll()); threadContext.putTransient(AUTHORIZATION_INFO_KEY, SYSTEM_AUTHZ_INFO); auditTrail.accessGranted(requestId, authentication, action, request, SYSTEM_AUTHZ_INFO); listener.onResponse(null); } else { auditTrail.accessDenied(requestId, authentication, action, request, SYSTEM_AUTHZ_INFO); listener.onFailure(actionDenied(authentication, SYSTEM_AUTHZ_INFO, action, request)); } } 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); final AuditTrail auditTrail = auditTrailService.get(); 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 actionDenied(authentication, null, action, request, 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 actionDenied(authentication, null, action, request, cause); } } return request; } private void authorizeRunAs( final RequestInfo requestInfo, final AuthorizationInfo authzInfo, final ActionListener listener ) { final Authentication authentication = requestInfo.getAuthentication(); assert authentication.isRunAs() : "authentication must have run-as for run-as to be authorized"; if (authentication.getEffectiveSubject().getRealm() == 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 * an implied action for each item's {@link DocWriteRequest#opType()}, * 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 #actionDenied(Authentication, AuthorizationInfo, String, TransportRequest, 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( RequestInfo requestInfo, AuthorizationContext bulkAuthzContext, AuthorizationEngine authzEngine, AsyncSupplier resolvedIndicesAsyncSupplier, Metadata metadata, String requestId, ActionListener listener ) { final Authentication authentication = requestInfo.getAuthentication(); final AuthorizationInfo authzInfo = bulkAuthzContext.getAuthorizationInfo(); 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 (there are 4 action types total) final Map> actionToIndicesMap = new HashMap<>(4); final AuditTrail auditTrail = auditTrailService.get(); resolvedIndicesAsyncSupplier.getAsync(ActionListener.wrap(overallResolvedIndices -> { final Set localIndices = new HashSet<>(overallResolvedIndices.getLocal()); for (BulkItemRequest item : request.items()) { final String itemAction = getAction(item); final String resolvedIndex = resolvedIndexNames.computeIfAbsent(item.index(), key -> { final String resolved = resolveIndexNameDateMath(item); if (localIndices.contains(resolved) == false) { throw illegalArgument( "Found bulk item that writes to index " + resolved + " but the request writes to " + localIndices ); } return resolved; }); actionToIndicesMap.computeIfAbsent(itemAction, k -> new HashSet<>()).add(resolvedIndex); } final ActionListener>> bulkAuthzListener = ActionListener.wrap( collection -> { final Map actionToIndicesAccessControl = new HashMap<>(4); 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()); } }); final Map> actionToGrantedIndicesMap = new HashMap<>(4); final Map> actionToDeniedIndicesMap = new HashMap<>(4); for (BulkItemRequest item : request.items()) { final String resolvedIndex = resolvedIndexNames.get(item.index()); final String itemAction = getAction(item); if (actionToIndicesAccessControl.get(itemAction).hasIndexPermissions(resolvedIndex)) { actionToGrantedIndicesMap.computeIfAbsent(itemAction, ignore -> new HashSet<>()).add(resolvedIndex); } else { actionToDeniedIndicesMap.computeIfAbsent(itemAction, ignore -> new HashSet<>()).add(resolvedIndex); item.abort( resolvedIndex, actionDenied( authentication, authzInfo, itemAction, request, IndexAuthorizationResult.getFailureDescription(List.of(resolvedIndex), restrictedIndices), null ) ); } } actionToDeniedIndicesMap.forEach((action, resolvedIndicesSet) -> { auditTrail.explicitIndexAccessEvent( requestId, AuditLevel.ACCESS_DENIED, authentication, action, resolvedIndicesSet.toArray(new String[0]), BulkItemRequest.class.getSimpleName(), request.remoteAddress(), authzInfo ); }); actionToGrantedIndicesMap.forEach((action, resolvedIndicesSet) -> { auditTrail.explicitIndexAccessEvent( requestId, AuditLevel.ACCESS_GRANTED, authentication, action, resolvedIndicesSet.toArray(new String[0]), BulkItemRequest.class.getSimpleName(), request.remoteAddress(), authzInfo ); }); listener.onResponse(null); }, listener::onFailure ); final ActionListener> groupedActionListener = wrapPreservingContext( new GroupedActionListener<>(actionToIndicesMap.size(), bulkAuthzListener), threadContext ); actionToIndicesMap.forEach((bulkItemAction, indices) -> { final RequestInfo bulkItemInfo = new RequestInfo( requestInfo.getAuthentication(), requestInfo.getRequest(), bulkItemAction, bulkAuthzContext, requestInfo.getParentAuthorization() ); authzEngine.authorizeIndexAction( bulkItemInfo, authzInfo, ril -> ril.onResponse(new ResolvedIndices(new ArrayList<>(indices), Collections.emptyList())), metadata.getIndicesLookup(), groupedActionListener.delegateFailureAndWrap( (l, indexAuthorizationResult) -> l.onResponse(new Tuple<>(bulkItemAction, indexAuthorizationResult)) ) ); }); }, listener::onFailure)); } private String resolveIndexNameDateMath(BulkItemRequest bulkItemRequest) { final ResolvedIndices resolvedIndices = indicesAndAliasesResolver.resolveIndicesAndAliasesWithoutWildcards( getAction(bulkItemRequest), bulkItemRequest.request() ); 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()) ); } return resolvedIndices.getLocal().get(0); } private static IllegalArgumentException illegalArgument(String message) { assert false : message; return new IllegalArgumentException(message); } static boolean isIndexAction(String action) { return IndexPrivilege.ACTION_MATCHER.test(action); } private static String getAction(BulkItemRequest item) { final DocWriteRequest docWriteRequest = item.request(); return switch (docWriteRequest.opType()) { case INDEX -> IMPLIED_INDEX_ACTION; case CREATE -> IMPLIED_CREATE_ACTION; case UPDATE -> TransportUpdateAction.NAME; case DELETE -> TransportDeleteAction.NAME; }; } private void putTransientIfNonExisting(String key, Object value) { Object existing = threadContext.getTransient(key); if (existing == null) { threadContext.putTransient(key, value); } } private ElasticsearchSecurityException runAsDenied( Authentication authentication, @Nullable AuthorizationInfo authorizationInfo, String action ) { return denialException( authentication, action, () -> authorizationDenialMessages.runAsDenied(authentication, authorizationInfo, action), null ); } public ElasticsearchSecurityException remoteActionDenied(Authentication authentication, String action, String clusterAlias) { final AuthorizationInfo authorizationInfo = threadContext.getTransient(AUTHORIZATION_INFO_KEY); return denialException( authentication, action, () -> authorizationDenialMessages.remoteActionDenied(authentication, authorizationInfo, action, clusterAlias), null ); } ElasticsearchSecurityException actionDenied( Authentication authentication, @Nullable AuthorizationInfo authorizationInfo, String action, TransportRequest request ) { return actionDenied(authentication, authorizationInfo, action, request, null); } private ElasticsearchSecurityException actionDenied( Authentication authentication, @Nullable AuthorizationInfo authorizationInfo, String action, TransportRequest request, Exception cause ) { return actionDenied(authentication, authorizationInfo, action, request, null, cause); } private ElasticsearchSecurityException actionDenied( Authentication authentication, @Nullable AuthorizationInfo authorizationInfo, String action, TransportRequest request, @Nullable String context, Exception cause ) { return denialException( authentication, action, () -> authorizationDenialMessages.actionDenied(authentication, authorizationInfo, action, request, context), cause ); } private ElasticsearchSecurityException denialException( Authentication authentication, String action, Supplier authzDenialMessageSupplier, Exception cause ) { // Special case for anonymous user if (isAnonymousEnabled && anonymousUser.equals(authentication.getAuthenticatingSubject().getUser()) && anonymousAuthzExceptionEnabled == false) { return authcFailureHandler.authenticationRequired(action, threadContext); } String message = authzDenialMessageSupplier.get(); logger.debug(message); return authorizationError(message, cause); } 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()) { auditTrailService.get() .accessGranted( requestId, requestInfo.getAuthentication(), requestInfo.getAction(), requestInfo.getRequest(), authzInfo ); try { responseConsumer.accept(result); } catch (Exception e) { failureConsumer.accept(e); } } else { handleFailure(result.getFailureContext(requestInfo, restrictedIndices), null); } } @Override public void onFailure(Exception e) { handleFailure(null, e); } private void handleFailure(@Nullable String context, @Nullable Exception e) { Authentication authentication = requestInfo.getAuthentication(); String action = requestInfo.getAction(); TransportRequest request = requestInfo.getRequest(); auditTrailService.get().accessDenied(requestId, authentication, action, request, authzInfo); failureConsumer.accept(actionDenied(authentication, authzInfo, action, request, context, e)); } } private static class CachingAsyncSupplier implements AsyncSupplier { private final AsyncSupplier asyncSupplier; private volatile ListenableFuture valueFuture = null; private CachingAsyncSupplier(AsyncSupplier supplier) { this.asyncSupplier = supplier; } @Override public void getAsync(ActionListener listener) { if (valueFuture == null) { boolean firstInvocation = false; synchronized (this) { if (valueFuture == null) { valueFuture = new ListenableFuture<>(); firstInvocation = true; } } if (firstInvocation) { asyncSupplier.getAsync(valueFuture); } } valueFuture.addListener(listener); } } public static void addSettings(List> settings) { settings.add(ANONYMOUS_AUTHORIZATION_EXCEPTION_SETTING); settings.addAll(LoadAuthorizedIndicesTimeChecker.Factory.getSettings()); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy