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

org.elasticsearch.xpack.security.authz.AuthorizationDenialMessages 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.elasticsearch.common.Strings;
import org.elasticsearch.common.util.set.Sets;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.transport.TransportRequest;
import org.elasticsearch.xpack.core.security.authc.Authentication;
import org.elasticsearch.xpack.core.security.authc.AuthenticationField;
import org.elasticsearch.xpack.core.security.authc.Subject;
import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine;
import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilegeResolver;
import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege;

import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;

import static org.elasticsearch.common.Strings.collectionToCommaDelimitedString;
import static org.elasticsearch.xpack.security.audit.logfile.LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME;
import static org.elasticsearch.xpack.security.authz.AuthorizationService.isIndexAction;

public interface AuthorizationDenialMessages {
    String actionDenied(
        Authentication authentication,
        @Nullable AuthorizationEngine.AuthorizationInfo authorizationInfo,
        String action,
        TransportRequest request,
        @Nullable String context
    );

    String runAsDenied(Authentication authentication, @Nullable AuthorizationEngine.AuthorizationInfo authorizationInfo, String action);

    String remoteActionDenied(
        Authentication authentication,
        @Nullable AuthorizationEngine.AuthorizationInfo authorizationInfo,
        String action,
        String clusterAlias
    );

    class Default implements AuthorizationDenialMessages {
        public Default() {}

        @Override
        public String runAsDenied(
            Authentication authentication,
            @Nullable AuthorizationEngine.AuthorizationInfo authorizationInfo,
            String action
        ) {
            assert authentication.isRunAs() : "constructing run as denied message but authentication for action was not run as";

            String userText = authenticatedUserDescription(authentication);
            String actionIsUnauthorizedMessage = actionIsUnauthorizedMessage(action, userText);

            String unauthorizedToRunAsMessage = "because "
                + userText
                + " is unauthorized to run as ["
                + authentication.getEffectiveSubject().getUser().principal()
                + "]";

            return actionIsUnauthorizedMessage
                + rolesDescription(authentication.getAuthenticatingSubject(), authorizationInfo.getAuthenticatedUserAuthorizationInfo())
                + ", "
                + unauthorizedToRunAsMessage;
        }

        @Override
        public String actionDenied(
            Authentication authentication,
            @Nullable AuthorizationEngine.AuthorizationInfo authorizationInfo,
            String action,
            TransportRequest request,
            @Nullable String context
        ) {
            String userText = successfulAuthenticationDescription(authentication, authorizationInfo);
            String remoteClusterText = authentication.isCrossClusterAccess() ? remoteClusterText(null) : "";
            String message = actionIsUnauthorizedMessage(action, remoteClusterText, userText);
            if (context != null) {
                message = message + " " + context;
            }

            if (ClusterPrivilegeResolver.isClusterAction(action)) {
                final Collection privileges = findClusterPrivilegesThatGrant(authentication, action, request);
                if (privileges != null && privileges.size() > 0) {
                    message = message
                        + ", this action is granted by the cluster privileges ["
                        + collectionToCommaDelimitedString(privileges)
                        + "]";
                }
            } else if (isIndexAction(action)) {
                final Collection privileges = findIndexPrivilegesThatGrant(action);
                if (privileges != null && privileges.size() > 0) {
                    message = message
                        + ", this action is granted by the index privileges ["
                        + collectionToCommaDelimitedString(privileges)
                        + "]";
                }
            }

            return message;
        }

        @Override
        public String remoteActionDenied(
            Authentication authentication,
            @Nullable AuthorizationEngine.AuthorizationInfo authorizationInfo,
            String action,
            String clusterAlias
        ) {
            String userText = successfulAuthenticationDescription(authentication, authorizationInfo);
            String remoteClusterText = remoteClusterText(clusterAlias);
            String message = isIndexAction(action)
                ? " because no remote indices privileges apply for the target cluster"
                : " because no remote cluster privileges apply for the target cluster";
            return actionIsUnauthorizedMessage(action, remoteClusterText, userText) + message;
        }

        protected Collection findClusterPrivilegesThatGrant(
            Authentication authentication,
            String action,
            TransportRequest request
        ) {
            return ClusterPrivilegeResolver.findPrivilegesThatGrant(action, request, authentication);
        }

        protected Collection findIndexPrivilegesThatGrant(String action) {
            return IndexPrivilege.findPrivilegesThatGrant(action);
        }

        private String remoteClusterText(@Nullable String clusterAlias) {
            return Strings.format("towards remote cluster%s ", clusterAlias == null ? "" : " [" + clusterAlias + "]");
        }

        private String authenticatedUserDescription(Authentication authentication) {
            String userText = (authentication.isServiceAccount() ? "service account" : "user")
                + " ["
                + authentication.getAuthenticatingSubject().getUser().principal()
                + "]";
            if (authentication.isAuthenticatedAsApiKey() || authentication.isCrossClusterAccess()) {
                final String apiKeyId = (String) authentication.getAuthenticatingSubject()
                    .getMetadata()
                    .get(AuthenticationField.API_KEY_ID_KEY);
                assert apiKeyId != null : "api key id must be present in the metadata";
                userText = "API key id [" + apiKeyId + "] of " + userText;
                if (authentication.isCrossClusterAccess()) {
                    final Authentication crossClusterAccessAuthentication = (Authentication) authentication.getAuthenticatingSubject()
                        .getMetadata()
                        .get(AuthenticationField.CROSS_CLUSTER_ACCESS_AUTHENTICATION_KEY);
                    assert crossClusterAccessAuthentication != null : "cross cluster access authentication must be present in the metadata";
                    userText = successfulAuthenticationDescription(crossClusterAccessAuthentication, null)
                        + " authenticated by "
                        + userText;
                }
            }
            return userText;
        }

        // package-private for tests
        String rolesDescription(Subject subject, @Nullable AuthorizationEngine.AuthorizationInfo authorizationInfo) {
            // We cannot print the roles if it's an API key or a service account (both do not have roles, but privileges)
            if (subject.getType() != Subject.Type.USER) {
                return "";
            }

            final StringBuilder sb = new StringBuilder();
            final List effectiveRoleNames = extractEffectiveRoleNames(authorizationInfo);
            if (effectiveRoleNames == null) {
                sb.append(" with assigned roles [").append(Strings.arrayToCommaDelimitedString(subject.getUser().roles())).append("]");
            } else {
                sb.append(" with effective roles [").append(Strings.collectionToCommaDelimitedString(effectiveRoleNames)).append("]");

                final Set assignedRoleNames = Set.of(subject.getUser().roles());
                final SortedSet unfoundedRoleNames = Sets.sortedDifference(assignedRoleNames, Set.copyOf(effectiveRoleNames));
                if (false == unfoundedRoleNames.isEmpty()) {
                    sb.append(" (assigned roles [")
                        .append(Strings.collectionToCommaDelimitedString(unfoundedRoleNames))
                        .append("] were not found)");
                }
            }
            return sb.toString();
        }

        // package-private for tests
        String successfulAuthenticationDescription(
            Authentication authentication,
            @Nullable AuthorizationEngine.AuthorizationInfo authorizationInfo
        ) {
            String userText = authenticatedUserDescription(authentication);

            if (authentication.isRunAs()) {
                userText = userText + " run as [" + authentication.getEffectiveSubject().getUser().principal() + "]";
            }

            userText += rolesDescription(authentication.getEffectiveSubject(), authorizationInfo);
            return userText;
        }

        private List extractEffectiveRoleNames(@Nullable AuthorizationEngine.AuthorizationInfo authorizationInfo) {
            if (authorizationInfo == null) {
                return null;
            }

            final Map info = authorizationInfo.asMap();
            final Object roleNames = info.get(PRINCIPAL_ROLES_FIELD_NAME);
            // AuthorizationInfo from custom authorization engine may not have this field or have it as a different data type
            if (false == roleNames instanceof String[]) {
                assert false == authorizationInfo instanceof RBACEngine.RBACAuthorizationInfo
                    : "unexpected user.roles field [" + roleNames + "] for RBACAuthorizationInfo";
                return null;
            }
            return Arrays.stream((String[]) roleNames).sorted().toList();
        }

        private String actionIsUnauthorizedMessage(String action, String userText) {
            return actionIsUnauthorizedMessage(action, "", userText);
        }

        private String actionIsUnauthorizedMessage(String action, String remoteClusterText, String userText) {
            return "action [" + action + "] " + remoteClusterText + "is unauthorized for " + userText;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy