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

org.elasticsearch.xpack.core.security.authz.permission.IndicesPermission Maven / Gradle / Ivy

There is a newer version: 8.13.2
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.core.security.authz.permission;

import org.apache.lucene.util.automaton.Automaton;
import org.apache.lucene.util.automaton.Operations;
import org.elasticsearch.action.admin.indices.mapping.put.AutoPutMappingAction;
import org.elasticsearch.action.admin.indices.mapping.put.PutMappingAction;
import org.elasticsearch.cluster.metadata.IndexAbstraction;
import org.elasticsearch.cluster.metadata.IndexMetadata;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.logging.DeprecationCategory;
import org.elasticsearch.common.logging.DeprecationLogger;
import org.elasticsearch.common.regex.Regex;
import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl;
import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege;
import org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames;
import org.elasticsearch.xpack.core.security.support.Automatons;
import org.elasticsearch.xpack.core.security.support.StringMatcher;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;

import static java.util.Collections.unmodifiableMap;
import static java.util.Collections.unmodifiableSet;

/**
 * A permission that is based on privileges for index related actions executed
 * on specific indices
 */
public final class IndicesPermission {

    private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(IndicesPermission.class);

    public static final IndicesPermission NONE = new IndicesPermission();

    private static final Set PRIVILEGE_NAME_SET_BWC_ALLOW_MAPPING_UPDATE =
            Collections.unmodifiableSet(new HashSet<>(Arrays.asList("create", "create_doc", "index", "write")));

    private final Map> allowedIndicesMatchersForAction = new ConcurrentHashMap<>();

    private final Group[] groups;

    public IndicesPermission(Group... groups) {
        this.groups = groups;
    }

    private static StringMatcher indexMatcher(Collection ordinaryIndices, Collection restrictedIndices) {
        StringMatcher matcher;
        if (ordinaryIndices.isEmpty()) {
            matcher = StringMatcher.of(restrictedIndices);
        } else {
            matcher = StringMatcher.of(ordinaryIndices)
                    .and("", index -> false == RestrictedIndicesNames.isRestricted(index));
            if (restrictedIndices.isEmpty() == false) {
                matcher = StringMatcher.of(restrictedIndices).or(matcher);
            }
        }
        return matcher;
    }

    public Group[] groups() {
        return groups;
    }

    /**
     * @return A predicate that will match all the indices that this permission
     * has the privilege for executing the given action on.
     */
    public Predicate allowedIndicesMatcher(String action) {
        return allowedIndicesMatchersForAction.computeIfAbsent(action, a -> Group.buildIndexMatcherPredicateForAction(a, groups));
    }

    /**
     * Checks if the permission matches the provided action, without looking at indices.
     * To be used in very specific cases where indices actions need to be authorized regardless of their indices.
     * The usecase for this is composite actions that are initially only authorized based on the action name (indices are not
     * checked on the coordinating node), and properly authorized later at the shard level checking their indices as well.
     */
    public boolean check(String action) {
        final boolean isMappingUpdateAction = isMappingUpdateAction(action);
        for (Group group : groups) {
            if (group.checkAction(action) || (isMappingUpdateAction && containsPrivilegeThatGrantsMappingUpdatesForBwc(group))) {
                return true;
            }
        }
        return false;
    }

    /**
     * For given index patterns and index privileges determines allowed privileges and creates an instance of {@link ResourcePrivilegesMap}
     * holding a map of resource to {@link ResourcePrivileges} where resource is index pattern and the map of index privilege to whether it
     * is allowed or not.
     *
     * @param checkForIndexPatterns check permission grants for the set of index patterns
     * @param allowRestrictedIndices if {@code true} then checks permission grants even for restricted indices by index matching
     * @param checkForPrivileges check permission grants for the set of index privileges
     * @return an instance of {@link ResourcePrivilegesMap}
     */
    public ResourcePrivilegesMap checkResourcePrivileges(Set checkForIndexPatterns, boolean allowRestrictedIndices,
                                                         Set checkForPrivileges) {
        final ResourcePrivilegesMap.Builder resourcePrivilegesMapBuilder = ResourcePrivilegesMap.builder();
        final Map predicateCache = new HashMap<>();
        for (String forIndexPattern : checkForIndexPatterns) {
            Automaton checkIndexAutomaton = Automatons.patterns(forIndexPattern);
            if (false == allowRestrictedIndices && false == isConcreteRestrictedIndex(forIndexPattern)) {
                checkIndexAutomaton = Automatons.minusAndMinimize(checkIndexAutomaton, RestrictedIndicesNames.NAMES_AUTOMATON);
            }
            if (false == Operations.isEmpty(checkIndexAutomaton)) {
                Automaton allowedIndexPrivilegesAutomaton = null;
                for (Group group : groups) {
                    final Automaton groupIndexAutomaton = predicateCache.computeIfAbsent(group,
                            g -> IndicesPermission.Group.buildIndexMatcherAutomaton(g.allowRestrictedIndices(), g.indices()));
                    if (Operations.subsetOf(checkIndexAutomaton, groupIndexAutomaton)) {
                        if (allowedIndexPrivilegesAutomaton != null) {
                            allowedIndexPrivilegesAutomaton = Automatons
                                    .unionAndMinimize(Arrays.asList(allowedIndexPrivilegesAutomaton, group.privilege().getAutomaton()));
                        } else {
                            allowedIndexPrivilegesAutomaton = group.privilege().getAutomaton();
                        }
                    }
                }
                for (String privilege : checkForPrivileges) {
                    IndexPrivilege indexPrivilege = IndexPrivilege.get(Collections.singleton(privilege));
                    if (allowedIndexPrivilegesAutomaton != null
                            && Operations.subsetOf(indexPrivilege.getAutomaton(), allowedIndexPrivilegesAutomaton)) {
                        resourcePrivilegesMapBuilder.addResourcePrivilege(forIndexPattern, privilege, Boolean.TRUE);
                    } else {
                        resourcePrivilegesMapBuilder.addResourcePrivilege(forIndexPattern, privilege, Boolean.FALSE);
                    }
                }
            } else {
                // the index pattern produced the empty automaton, presumably because the requested pattern expands exclusively inside the
                // restricted indices namespace - a namespace of indices that are normally hidden when granting/checking privileges - and
                // the pattern was not marked as `allowRestrictedIndices`. We try to anticipate this by considering _explicit_ restricted
                // indices even if `allowRestrictedIndices` is false.
                // TODO The `false` result is a _safe_ default but this is actually an error. Make it an error.
                for (String privilege : checkForPrivileges) {
                    resourcePrivilegesMapBuilder.addResourcePrivilege(forIndexPattern, privilege, Boolean.FALSE);
                }
            }
        }
        return resourcePrivilegesMapBuilder.build();
    }

    public Automaton allowedActionsMatcher(String index) {
        List automatonList = new ArrayList<>();
        for (Group group : groups) {
            if (group.indexNameMatcher.test(index)) {
                automatonList.add(group.privilege.getAutomaton());
            }
        }
        return automatonList.isEmpty() ? Automatons.EMPTY : Automatons.unionAndMinimize(automatonList);
    }

    /**
     * Authorizes the provided action against the provided indices, given the current cluster metadata
     */
    public Map authorize(String action, Set requestedIndicesOrAliases,
                                                                          Map lookup,
                                                                          FieldPermissionsCache fieldPermissionsCache) {
        // now... every index that is associated with the request, must be granted
        // by at least one indices permission group
        Map> fieldPermissionsByIndex = new HashMap<>();
        Map roleQueriesByIndex = new HashMap<>();
        Map grantedBuilder = new HashMap<>();

        final boolean isMappingUpdateAction = isMappingUpdateAction(action);

        for (String indexOrAlias : requestedIndicesOrAliases) {
            final boolean isBackingIndex;
            final boolean isDataStream;
            final Set concreteIndices = new HashSet<>();
            final IndexAbstraction indexAbstraction = lookup.get(indexOrAlias);
            if (indexAbstraction != null) {
                for (IndexMetadata indexMetadata : indexAbstraction.getIndices()) {
                    concreteIndices.add(indexMetadata.getIndex().getName());
                }
                isBackingIndex = indexAbstraction.getType() == IndexAbstraction.Type.CONCRETE_INDEX &&
                        indexAbstraction.getParentDataStream() != null;
                isDataStream = indexAbstraction.getType() == IndexAbstraction.Type.DATA_STREAM;
            } else {
                isBackingIndex = isDataStream = false;
            }

            // true if ANY group covers the given index AND the given action
            boolean granted = false;
            // true if ANY group, which contains certain ingest privileges, covers the given index AND the action is a mapping update for
            // an index or an alias (but not for a data stream)
            boolean bwcGrantMappingUpdate = false;
            final List bwcDeprecationLogActions = new ArrayList<>();

            for (Group group : groups) {
                // the group covers the given index OR the given index is a backing index and the group covers the parent data stream
                final boolean indexCheck = group.checkIndex(indexOrAlias) ||
                        (isBackingIndex && group.checkIndex(indexAbstraction.getParentDataStream().getName()));
                if (indexCheck) {
                    boolean actionCheck = group.checkAction(action);
                    granted = granted || actionCheck;
                    // mapping updates are allowed for certain privileges on indices and aliases (but not on data streams),
                    // outside of the privilege definition
                    boolean bwcMappingActionCheck = isMappingUpdateAction && false == isDataStream && false == isBackingIndex &&
                            containsPrivilegeThatGrantsMappingUpdatesForBwc(group);
                    bwcGrantMappingUpdate = bwcGrantMappingUpdate || bwcMappingActionCheck;
                    if (actionCheck || bwcMappingActionCheck) {
                        // propagate DLS and FLS permissions over the concrete indices
                        for (String index : concreteIndices) {
                            Set fieldPermissions = fieldPermissionsByIndex.computeIfAbsent(index, (k) -> new HashSet<>());
                            fieldPermissionsByIndex.put(indexOrAlias, fieldPermissions);
                            fieldPermissions.add(group.getFieldPermissions());
                            DocumentLevelPermissions permissions =
                                    roleQueriesByIndex.computeIfAbsent(index, (k) -> new DocumentLevelPermissions());
                            roleQueriesByIndex.putIfAbsent(indexOrAlias, permissions);
                            if (group.hasQuery()) {
                                permissions.addAll(group.getQuery());
                            } else {
                                // if more than one permission matches for a concrete index here and if
                                // a single permission doesn't have a role query then DLS will not be
                                // applied even when other permissions do have a role query
                                permissions.setAllowAll(true);
                            }
                        }
                        if (false == actionCheck) {
                            for (String privilegeName : group.privilege.name()) {
                                if (PRIVILEGE_NAME_SET_BWC_ALLOW_MAPPING_UPDATE.contains(privilegeName)) {
                                    bwcDeprecationLogActions.add(() -> {
                                        deprecationLogger.deprecate(
                                            DeprecationCategory.SECURITY,
                                            "[" + indexOrAlias + "] mapping update for ingest privilege [" + privilegeName + "]",
                                            "the index privilege ["
                                                + privilegeName
                                                + "] allowed the update mapping action ["
                                                + action
                                                + "] on index ["
                                                + indexOrAlias
                                                + "], this "
                                                + "privilege will not permit mapping updates in the next major release - users who require "
                                                + "access to update mappings must be granted explicit privileges"
                                        );
                                    });
                                }
                            }
                        }
                    }
                }
            }

            if (false == granted && bwcGrantMappingUpdate) {
                // the action is granted only due to the deprecated behaviour of certain privileges
                granted = true;
                bwcDeprecationLogActions.forEach(deprecationLogAction -> deprecationLogAction.run());
            }

            if (concreteIndices.isEmpty()) {
                grantedBuilder.put(indexOrAlias, granted);
            } else {
                grantedBuilder.put(indexOrAlias, granted);
                for (String concreteIndex : concreteIndices) {
                    grantedBuilder.put(concreteIndex, granted);
                }
            }
        }

        Map indexPermissions = new HashMap<>();
        for (Map.Entry entry : grantedBuilder.entrySet()) {
            String index = entry.getKey();
            DocumentLevelPermissions permissions = roleQueriesByIndex.get(index);
            final Set roleQueries;
            if (permissions != null && permissions.isAllowAll() == false) {
                roleQueries = unmodifiableSet(permissions.queries);
            } else {
                roleQueries = null;
            }

            final FieldPermissions fieldPermissions;
            final Set indexFieldPermissions = fieldPermissionsByIndex.get(index);
            if (indexFieldPermissions != null && indexFieldPermissions.isEmpty() == false) {
                fieldPermissions = indexFieldPermissions.size() == 1 ? indexFieldPermissions.iterator().next() :
                        fieldPermissionsCache.getFieldPermissions(indexFieldPermissions);
            } else {
                fieldPermissions = FieldPermissions.DEFAULT;
            }
            indexPermissions.put(index, new IndicesAccessControl.IndexAccessControl(entry.getValue(), fieldPermissions,
                    (roleQueries != null) ? DocumentPermissions.filteredBy(roleQueries) : DocumentPermissions.allowAll()));
        }
        return unmodifiableMap(indexPermissions);
    }

    private boolean isConcreteRestrictedIndex(String indexPattern) {
        if (Regex.isSimpleMatchPattern(indexPattern) || Automatons.isLuceneRegex(indexPattern)) {
            return false;
        }
        return RestrictedIndicesNames.isRestricted(indexPattern);
    }

    private static boolean isMappingUpdateAction(String action) {
        return action.equals(PutMappingAction.NAME) || action.equals(AutoPutMappingAction.NAME);
    }

    private static boolean containsPrivilegeThatGrantsMappingUpdatesForBwc(Group group) {
        return group.privilege().name().stream().anyMatch(PRIVILEGE_NAME_SET_BWC_ALLOW_MAPPING_UPDATE::contains);
    }

    public static class Group {
        private final IndexPrivilege privilege;
        private final Predicate actionMatcher;
        private final String[] indices;
        private final Predicate indexNameMatcher;
        private final FieldPermissions fieldPermissions;
        private final Set query;
        // by default certain restricted indices are exempted when granting privileges, as they should generally be hidden for ordinary
        // users. Setting this flag true eliminates the special status for the purpose of this permission - restricted indices still have
        // to be covered by the "indices"
        private final boolean allowRestrictedIndices;

        public Group(IndexPrivilege privilege, FieldPermissions fieldPermissions, @Nullable Set query,
                boolean allowRestrictedIndices, String... indices) {
            assert indices.length != 0;
            this.privilege = privilege;
            this.actionMatcher = privilege.predicate();
            this.indices = indices;
            this.indexNameMatcher = StringMatcher.of(Arrays.asList(indices));
            this.fieldPermissions = Objects.requireNonNull(fieldPermissions);
            this.query = query;
            this.allowRestrictedIndices = allowRestrictedIndices;
        }

        public IndexPrivilege privilege() {
            return privilege;
        }

        public String[] indices() {
            return indices;
        }

        @Nullable
        public Set getQuery() {
            return query;
        }

        public FieldPermissions getFieldPermissions() {
            return fieldPermissions;
        }

        private boolean checkAction(String action) {
            return actionMatcher.test(action);
        }

        private boolean checkIndex(String index) {
            assert index != null;
            return indexNameMatcher.test(index) && (allowRestrictedIndices || (false == RestrictedIndicesNames.isRestricted(index)));
        }

        boolean hasQuery() {
            return query != null;
        }

        public boolean allowRestrictedIndices() {
            return allowRestrictedIndices;
        }

        public static Automaton buildIndexMatcherAutomaton(boolean allowRestrictedIndices, String... indices) {
            final Automaton indicesAutomaton = Automatons.patterns(indices);
            if (allowRestrictedIndices) {
                return indicesAutomaton;
            } else {
                return Automatons.minusAndMinimize(indicesAutomaton, RestrictedIndicesNames.NAMES_AUTOMATON);
            }
        }

        private static Predicate buildIndexMatcherPredicateForAction(String action, Group... groups) {
            final Set ordinaryIndices = new HashSet<>();
            final Set restrictedIndices = new HashSet<>();
            final Set grantMappingUpdatesOnIndices = new HashSet<>();
            final Set grantMappingUpdatesOnRestrictedIndices = new HashSet<>();
            final boolean isMappingUpdateAction = isMappingUpdateAction(action);
            for (final Group group : groups) {
                if (group.actionMatcher.test(action)) {
                    if (group.allowRestrictedIndices) {
                        restrictedIndices.addAll(Arrays.asList(group.indices()));
                    } else {
                        ordinaryIndices.addAll(Arrays.asList(group.indices()));
                    }
                } else if (isMappingUpdateAction && containsPrivilegeThatGrantsMappingUpdatesForBwc(group)) {
                    // special BWC case for certain privileges: allow put mapping on indices and aliases (but not on data streams), even if
                    // the privilege definition does not currently allow it
                    if (group.allowRestrictedIndices) {
                        grantMappingUpdatesOnRestrictedIndices.addAll(Arrays.asList(group.indices()));
                    } else {
                        grantMappingUpdatesOnIndices.addAll(Arrays.asList(group.indices()));
                    }
                }
            }
            final StringMatcher nameMatcher = indexMatcher(ordinaryIndices, restrictedIndices);
            final StringMatcher bwcSpecialCaseMatcher = indexMatcher(grantMappingUpdatesOnIndices,
                    grantMappingUpdatesOnRestrictedIndices);
            return indexAbstraction -> {
                return nameMatcher.test(indexAbstraction.getName()) ||
                        (indexAbstraction.getType() != IndexAbstraction.Type.DATA_STREAM &&
                                (indexAbstraction.getParentDataStream() == null) &&
                                bwcSpecialCaseMatcher.test(indexAbstraction.getName()));
            };
        }
    }

    private static class DocumentLevelPermissions {

        private Set queries = null;
        private boolean allowAll = false;

        private void addAll(Set query) {
            if (allowAll == false) {
                if (queries == null) {
                    queries = new HashSet<>();
                }
                queries.addAll(query);
            }
        }

        private boolean isAllowAll() {
            return allowAll;
        }

        private void setAllowAll(boolean allowAll) {
            this.allowAll = allowAll;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy