From 0ff0bf5e344d09271273120f1f3b0f0f25a7a1aa Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Thu, 13 Feb 2025 11:31:34 +0100 Subject: [PATCH 001/131] WIP --- .../core/security/authz/privilege/IndexPrivilege.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java index 7174b2f616c2a..bd8d085aaab43 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java @@ -178,6 +178,12 @@ public final class IndexPrivilege extends Privilege { public static final IndexPrivilege NONE = new IndexPrivilege("none", Automatons.EMPTY); public static final IndexPrivilege ALL = new IndexPrivilege("all", ALL_AUTOMATON); + // TODO explain + public static final IndexPrivilege READ_FAILURE_STORE = new IndexPrivilege("read_failure_store", READ_AUTOMATON); + public static final IndexPrivilege MANAGE_FAILURE_STORE_INTERNAL = new IndexPrivilege( + "manage_failure_store_internal", + MANAGE_AUTOMATON + ); public static final IndexPrivilege READ = new IndexPrivilege("read", READ_AUTOMATON); public static final IndexPrivilege READ_CROSS_CLUSTER = new IndexPrivilege("read_cross_cluster", READ_CROSS_CLUSTER_AUTOMATON); public static final IndexPrivilege CREATE = new IndexPrivilege("create", CREATE_AUTOMATON); @@ -236,7 +242,9 @@ public final class IndexPrivilege extends Privilege { entry("maintenance", MAINTENANCE), entry("auto_configure", AUTO_CONFIGURE), entry("cross_cluster_replication", CROSS_CLUSTER_REPLICATION), - entry("cross_cluster_replication_internal", CROSS_CLUSTER_REPLICATION_INTERNAL) + entry("cross_cluster_replication_internal", CROSS_CLUSTER_REPLICATION_INTERNAL), + entry("read_failure_store", READ_FAILURE_STORE), + entry("manage_failure_store_internal", MANAGE_FAILURE_STORE_INTERNAL) ).filter(Objects::nonNull).collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)) ); From 78170dca53a92f340d6db121d686cf25285cfee4 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Thu, 13 Feb 2025 15:59:55 +0100 Subject: [PATCH 002/131] More --- .../IndexComponentSelectorPrivilege.java | 53 +++++++++++++++++++ .../authz/privilege/IndexPrivilege.java | 5 +- .../authz/store/CompositeRolesStore.java | 8 +++ 3 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java new file mode 100644 index 0000000000000..d53fa272d4252 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java @@ -0,0 +1,53 @@ +/* + * 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.privilege; + +import org.elasticsearch.action.support.IndexComponentSelector; + +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +public enum IndexComponentSelectorPrivilege { + ALL("all", (selector) -> true), + DATA("data", IndexComponentSelector.DATA::equals), + FAILURES("failures", IndexComponentSelector.FAILURES::equals); + + private final String name; + private final Predicate grants; + + IndexComponentSelectorPrivilege(String name, Predicate grants) { + this.name = name; + this.grants = grants; + } + + public String getName() { + return name; + } + + public boolean grants(IndexComponentSelector selector) { + return grants.test(selector); + } + + public static Set get(Set indexPrivileges) { + return indexPrivileges.stream().map(IndexComponentSelectorPrivilege::get).collect(Collectors.toSet()); + } + + private static IndexComponentSelectorPrivilege get(String indexPrivilegeName) { + final IndexPrivilege indexPrivilege = IndexPrivilege.getNamedOrNull(indexPrivilegeName); + if (indexPrivilege == null) { + return DATA; + } else if (indexPrivilege == IndexPrivilege.ALL) { + return ALL; + } else if (indexPrivilege == IndexPrivilege.READ_FAILURE_STORE || indexPrivilege == IndexPrivilege.MANAGE_FAILURE_STORE_INTERNAL) { + return FAILURES; + } else { + return DATA; + } + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java index bd8d085aaab43..0e4185e9e3455 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java @@ -31,6 +31,7 @@ import org.elasticsearch.action.datastreams.PromoteDataStreamAction; import org.elasticsearch.action.fieldcaps.TransportFieldCapabilitiesAction; import org.elasticsearch.action.search.TransportSearchShardsAction; +import org.elasticsearch.cluster.metadata.DataStream; import org.elasticsearch.common.Strings; import org.elasticsearch.core.Nullable; import org.elasticsearch.index.seqno.RetentionLeaseActions; @@ -243,8 +244,8 @@ public final class IndexPrivilege extends Privilege { entry("auto_configure", AUTO_CONFIGURE), entry("cross_cluster_replication", CROSS_CLUSTER_REPLICATION), entry("cross_cluster_replication_internal", CROSS_CLUSTER_REPLICATION_INTERNAL), - entry("read_failure_store", READ_FAILURE_STORE), - entry("manage_failure_store_internal", MANAGE_FAILURE_STORE_INTERNAL) + DataStream.isFailureStoreFeatureFlagEnabled() ? entry("read_failure_store", READ_FAILURE_STORE) : null, + DataStream.isFailureStoreFeatureFlagEnabled() ? entry("manage_failure_store_internal", MANAGE_FAILURE_STORE_INTERNAL) : null ).filter(Objects::nonNull).collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)) ); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java index 2e1a643bf4f4f..02458ff32f120 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java @@ -11,6 +11,7 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionRunnable; +import org.elasticsearch.action.support.IndexComponentSelector; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.cache.Cache; @@ -538,6 +539,7 @@ public static void buildRoleFromDescriptors( final Role.Builder builder = Role.builder(restrictedIndices, roleNames.toArray(Strings.EMPTY_ARRAY)) .cluster(clusterPrivileges, configurableClusterPrivileges) .runAs(runAsPrivilege); + indicesPrivilegesMap.forEach( (key, privilege) -> builder.add( fieldPermissionsCache.getFieldPermissions(privilege.fieldPermissionsDefinition), @@ -604,6 +606,12 @@ public static void buildRoleFromDescriptors( } } + private Map, Map, MergeableIndicesPrivilege>> splitBySelectorAccess( + Map, MergeableIndicesPrivilege> indicesPrivilegeMap + ) { + return Map.of(); + } + public void invalidateAll() { numInvalidation.incrementAndGet(); negativeLookupCache.invalidateAll(); From 4c0d584bcecc6f668ed46b1269e47dd788a05b37 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Thu, 13 Feb 2025 18:18:37 +0100 Subject: [PATCH 003/131] Moar --- .../authz/permission/IndicesPermission.java | 34 ++++- .../permission/RemoteIndicesPermission.java | 3 + .../core/security/authz/permission/Role.java | 42 +++-- .../ConfigurableClusterPrivileges.java | 2 + .../IndexComponentSelectorPrivilege.java | 31 ++++ .../authz/permission/LimitedRoleTests.java | 19 ++- .../authz/store/CompositeRolesStore.java | 60 +++++--- .../authz/AuthorizedIndicesTests.java | 5 +- .../xpack/security/authz/RBACEngineTests.java | 143 ++++++++++++------ .../accesscontrol/IndicesPermissionTests.java | 120 ++++++++++++--- 10 files changed, 344 insertions(+), 115 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index b91db5ca34366..296da8c0600be 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -27,6 +27,7 @@ import org.elasticsearch.index.Index; import org.elasticsearch.xpack.core.security.authz.RestrictedIndices; import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; +import org.elasticsearch.xpack.core.security.authz.privilege.IndexComponentSelectorPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.support.Automatons; import org.elasticsearch.xpack.core.security.support.StringMatcher; @@ -80,9 +81,12 @@ public Builder addGroup( FieldPermissions fieldPermissions, @Nullable Set query, boolean allowRestrictedIndices, + IndexComponentSelectorPrivilege selectorPrivilege, String... indices ) { - groups.add(new Group(privilege, fieldPermissions, query, allowRestrictedIndices, restrictedIndices, indices)); + groups.add( + new Group(privilege, fieldPermissions, query, allowRestrictedIndices, restrictedIndices, selectorPrivilege, indices) + ); return this; } @@ -803,6 +807,26 @@ public static class Group { // 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; + private final IndexComponentSelectorPrivilege selectorPrivilege; + + public Group( + IndexPrivilege privilege, + FieldPermissions fieldPermissions, + @Nullable Set query, + boolean allowRestrictedIndices, + RestrictedIndices restrictedIndices, + String... indices + ) { + this( + privilege, + fieldPermissions, + query, + allowRestrictedIndices, + restrictedIndices, + IndexComponentSelectorPrivilege.DATA, + indices + ); + } public Group( IndexPrivilege privilege, @@ -810,6 +834,7 @@ public Group( @Nullable Set query, boolean allowRestrictedIndices, RestrictedIndices restrictedIndices, + IndexComponentSelectorPrivilege selectorPrivilege, String... indices ) { assert indices.length != 0; @@ -830,6 +855,7 @@ public Group( } this.fieldPermissions = Objects.requireNonNull(fieldPermissions); this.query = query; + this.selectorPrivilege = selectorPrivilege; } public IndexPrivilege privilege() { @@ -875,7 +901,9 @@ boolean isTotal() { && indexNameMatcher.isTotal() && privilege == IndexPrivilege.ALL && query == null - && false == fieldPermissions.hasFieldLevelSecurity(); + && false == fieldPermissions.hasFieldLevelSecurity() + // TODO do we want this? + && selectorPrivilege.isTotal(); } @Override @@ -891,6 +919,8 @@ public String toString() { + query + ", allowRestrictedIndices=" + allowRestrictedIndices + + ", selectorPrivilege=" + + selectorPrivilege + '}'; } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/RemoteIndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/RemoteIndicesPermission.java index 2abc93997fa9d..b33a5b47f58fc 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/RemoteIndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/RemoteIndicesPermission.java @@ -10,6 +10,7 @@ import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.core.Nullable; import org.elasticsearch.xpack.core.security.authz.RestrictedIndices; +import org.elasticsearch.xpack.core.security.authz.privilege.IndexComponentSelectorPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.support.Automatons; import org.elasticsearch.xpack.core.security.support.StringMatcher; @@ -66,6 +67,8 @@ public Builder addGroup( // Deliberately passing EMPTY here since *which* indices are restricted is determined not on the querying cluster // but rather on the fulfilling cluster new RestrictedIndices(Automatons.EMPTY), + // TODO + IndexComponentSelectorPrivilege.DATA, indices ) ); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java index fe97b152a2ee7..10ae7900e955a 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java @@ -26,6 +26,7 @@ import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilegeResolver; import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.IndexComponentSelectorPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.Privilege; import org.elasticsearch.xpack.core.security.authz.restriction.WorkflowResolver; @@ -252,8 +253,7 @@ public Builder runAs(Privilege privilege) { } public Builder add(IndexPrivilege privilege, String... indices) { - groups.add(new IndicesPermissionGroupDefinition(privilege, FieldPermissions.DEFAULT, null, false, indices)); - return this; + return add(FieldPermissions.DEFAULT, null, privilege, false, IndexComponentSelectorPrivilege.DATA, indices); } public Builder add( @@ -261,9 +261,12 @@ public Builder add( Set query, IndexPrivilege privilege, boolean allowRestrictedIndices, + IndexComponentSelectorPrivilege selectorPrivilege, String... indices ) { - groups.add(new IndicesPermissionGroupDefinition(privilege, fieldPermissions, query, allowRestrictedIndices, indices)); + groups.add( + new IndicesPermissionGroupDefinition(privilege, fieldPermissions, query, allowRestrictedIndices, selectorPrivilege, indices) + ); return this; } @@ -276,7 +279,17 @@ public Builder addRemoteIndicesGroup( final String... indices ) { remoteIndicesGroups.computeIfAbsent(remoteClusterAliases, k -> new ArrayList<>()) - .add(new IndicesPermissionGroupDefinition(privilege, fieldPermissions, query, allowRestrictedIndices, indices)); + .add( + new IndicesPermissionGroupDefinition( + privilege, + fieldPermissions, + query, + allowRestrictedIndices, + // TODO + IndexComponentSelectorPrivilege.DATA, + indices + ) + ); return this; } @@ -316,6 +329,7 @@ public SimpleRole build() { group.fieldPermissions, group.query, group.allowRestrictedIndices, + group.selectorPrivilege, group.indices ); } @@ -364,6 +378,7 @@ private static class IndicesPermissionGroupDefinition { private final FieldPermissions fieldPermissions; private final @Nullable Set query; private final boolean allowRestrictedIndices; + private final IndexComponentSelectorPrivilege selectorPrivilege; private final String[] indices; private IndicesPermissionGroupDefinition( @@ -371,12 +386,14 @@ private IndicesPermissionGroupDefinition( FieldPermissions fieldPermissions, @Nullable Set query, boolean allowRestrictedIndices, + IndexComponentSelectorPrivilege selectorPrivilege, String... indices ) { this.privilege = privilege; this.fieldPermissions = fieldPermissions; this.query = query; this.allowRestrictedIndices = allowRestrictedIndices; + this.selectorPrivilege = selectorPrivilege; this.indices = indices; } } @@ -406,13 +423,18 @@ static SimpleRole buildFromRoleDescriptor( ); for (RoleDescriptor.IndicesPrivileges indexPrivilege : roleDescriptor.getIndicesPrivileges()) { + FieldPermissions fieldPermissions = fieldPermissionsCache.getFieldPermissions( + new FieldPermissionsDefinition(indexPrivilege.getGrantedFields(), indexPrivilege.getDeniedFields()) + ); + Set query = indexPrivilege.getQuery() == null ? null : Collections.singleton(indexPrivilege.getQuery()); + IndexPrivilege privilege = IndexPrivilege.get(Sets.newHashSet(indexPrivilege.getPrivileges())); + boolean allowRestrictedIndices = indexPrivilege.allowRestrictedIndices(); builder.add( - fieldPermissionsCache.getFieldPermissions( - new FieldPermissionsDefinition(indexPrivilege.getGrantedFields(), indexPrivilege.getDeniedFields()) - ), - indexPrivilege.getQuery() == null ? null : Collections.singleton(indexPrivilege.getQuery()), - IndexPrivilege.get(Sets.newHashSet(indexPrivilege.getPrivileges())), - indexPrivilege.allowRestrictedIndices(), + fieldPermissions, + query, + privilege, + allowRestrictedIndices, + IndexComponentSelectorPrivilege.DATA, indexPrivilege.getIndices() ); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java index 148fdf21fd2df..b8f5c3b67ab41 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java @@ -419,6 +419,8 @@ public ManageRolesPrivilege(List manageRolesInd FieldPermissions.DEFAULT, null, false, + // TODO + IndexComponentSelectorPrivilege.DATA, indexPatternPrivilege.indexPatterns() ); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java index d53fa272d4252..26fa660f07008 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java @@ -9,6 +9,8 @@ import org.elasticsearch.action.support.IndexComponentSelector; +import java.util.HashSet; +import java.util.Map; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -34,10 +36,39 @@ public boolean grants(IndexComponentSelector selector) { return grants.test(selector); } + public boolean isTotal() { + return this == ALL; + } + public static Set get(Set indexPrivileges) { return indexPrivileges.stream().map(IndexComponentSelectorPrivilege::get).collect(Collectors.toSet()); } + public static Map> split(Set indexPrivileges) { + final var data = new HashSet(); + final var failures = new HashSet(); + + for (String indexPrivilege : indexPrivileges) { + final var privilege = get(indexPrivilege); + switch (privilege) { + case DATA -> data.add(indexPrivilege); + case FAILURES -> failures.add(indexPrivilege); + // If we ever hit all, we can return early since we don't need to split + case ALL -> { + return Map.of(ALL, indexPrivileges); + } + } + } + + if (data.isEmpty()) { + return Map.of(FAILURES, failures); + } else if (failures.isEmpty()) { + return Map.of(DATA, data); + } else { + return Map.of(DATA, data, FAILURES, failures); + } + } + private static IndexComponentSelectorPrivilege get(String indexPrivilegeName) { final IndexPrivilege indexPrivilege = IndexPrivilege.getNamedOrNull(indexPrivilegeName); if (indexPrivilege == null) { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java index a4646c0d736c5..9544cf30fdc08 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java @@ -32,11 +32,7 @@ import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.IndicesPrivileges; import org.elasticsearch.xpack.core.security.authz.RoleDescriptorsIntersection; import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; -import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; -import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; -import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeTests; -import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilegeResolver; -import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.*; import org.elasticsearch.xpack.core.security.authz.restriction.Workflow; import org.elasticsearch.xpack.core.security.authz.restriction.WorkflowResolver; import org.elasticsearch.xpack.core.security.support.Automatons; @@ -760,9 +756,16 @@ public void testHasPrivilegesForIndexPatterns() { ); } { - fromRole = Role.builder(EMPTY_RESTRICTED_INDICES, "a-role") - .add(FieldPermissions.DEFAULT, Collections.emptySet(), IndexPrivilege.READ, true, "ind-1*", ".security") - .build(); + Role.Builder builder = Role.builder(EMPTY_RESTRICTED_INDICES, "a-role"); + fromRole = builder.add( + FieldPermissions.DEFAULT, + Collections.emptySet(), + IndexPrivilege.READ, + true, + IndexComponentSelectorPrivilege.DATA, + "ind-1*", + ".security" + ).build(); verifyResourcesPrivileges( fromRole, diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java index 02458ff32f120..610e40cfb3deb 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java @@ -11,7 +11,6 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionRunnable; -import org.elasticsearch.action.support.IndexComponentSelector; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.cache.Cache; @@ -32,6 +31,7 @@ import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.IndicesPrivileges; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.RemoteIndicesPrivileges; import org.elasticsearch.xpack.core.security.authz.accesscontrol.DocumentSubsetBitsetCache; +import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsCache; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsDefinition; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsDefinition.FieldGrantExcludeGroup; @@ -40,6 +40,7 @@ import org.elasticsearch.xpack.core.security.authz.permission.Role; import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.IndexComponentSelectorPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.Privilege; import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; @@ -540,24 +541,38 @@ public static void buildRoleFromDescriptors( .cluster(clusterPrivileges, configurableClusterPrivileges) .runAs(runAsPrivilege); - indicesPrivilegesMap.forEach( - (key, privilege) -> builder.add( - fieldPermissionsCache.getFieldPermissions(privilege.fieldPermissionsDefinition), - privilege.query, - IndexPrivilege.get(privilege.privileges), - false, - privilege.indices.toArray(Strings.EMPTY_ARRAY) - ) - ); - restrictedIndicesPrivilegesMap.forEach( - (key, privilege) -> builder.add( - fieldPermissionsCache.getFieldPermissions(privilege.fieldPermissionsDefinition), - privilege.query, - IndexPrivilege.get(privilege.privileges), - true, - privilege.indices.toArray(Strings.EMPTY_ARRAY) - ) - ); + for (Map.Entry, MergeableIndicesPrivilege> entry : indicesPrivilegesMap.entrySet()) { + MergeableIndicesPrivilege indicesPrivilege = entry.getValue(); + Map> split = IndexComponentSelectorPrivilege.split(indicesPrivilege.privileges); + FieldPermissions fieldPermissions = fieldPermissionsCache.getFieldPermissions(indicesPrivilege.fieldPermissionsDefinition); + String[] indices = indicesPrivilege.indices.toArray(Strings.EMPTY_ARRAY); + for (Map.Entry> privilegesBySelector : split.entrySet()) { + builder.add( + fieldPermissions, + indicesPrivilege.query, + IndexPrivilege.get(privilegesBySelector.getValue()), + false, + privilegesBySelector.getKey(), + indices + ); + } + } + for (Map.Entry, MergeableIndicesPrivilege> entry : restrictedIndicesPrivilegesMap.entrySet()) { + MergeableIndicesPrivilege indicesPrivilege = entry.getValue(); + Map> split = IndexComponentSelectorPrivilege.split(indicesPrivilege.privileges); + FieldPermissions fieldPermissions = fieldPermissionsCache.getFieldPermissions(indicesPrivilege.fieldPermissionsDefinition); + String[] indices = indicesPrivilege.indices.toArray(Strings.EMPTY_ARRAY); + for (Map.Entry> privilegesBySelector : split.entrySet()) { + builder.add( + fieldPermissions, + indicesPrivilege.query, + IndexPrivilege.get(privilegesBySelector.getValue()), + true, + privilegesBySelector.getKey(), + indices + ); + } + } remoteIndicesPrivilegesByCluster.forEach((clusterAliasKey, remoteIndicesPrivilegesForCluster) -> { remoteIndicesPrivilegesForCluster.forEach( @@ -606,12 +621,6 @@ public static void buildRoleFromDescriptors( } } - private Map, Map, MergeableIndicesPrivilege>> splitBySelectorAccess( - Map, MergeableIndicesPrivilege> indicesPrivilegeMap - ) { - return Map.of(); - } - public void invalidateAll() { numInvalidation.incrementAndGet(); negativeLookupCache.invalidateAll(); @@ -734,6 +743,7 @@ private static void collatePrivilegesByIndices( final Set key = newHashSet(indicesPrivilege.getIndices()); indicesPrivilegesMap.compute(key, (k, value) -> { if (value == null) { + // TODO do we need to worry about FLS and DLS combining incorrectly for different selectors? return new MergeableIndicesPrivilege( indicesPrivilege.getIndices(), indicesPrivilege.getPrivileges(), diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java index e9408fd34c3ed..4bce2af8c727c 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java @@ -30,6 +30,7 @@ import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions; 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.IndexComponentSelectorPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; import org.elasticsearch.xpack.core.security.test.TestRestrictedIndices; @@ -187,8 +188,8 @@ public void testSecurityIndicesAreRestrictedForDefaultRole() { } public void testSecurityIndicesAreNotRemovedFromUnrestrictedRole() { - Role role = Role.builder(RESTRICTED_INDICES, randomAlphaOfLength(8)) - .add(FieldPermissions.DEFAULT, null, IndexPrivilege.ALL, true, "*") + Role.Builder builder = Role.builder(RESTRICTED_INDICES, randomAlphaOfLength(8)); + Role role = builder.add(FieldPermissions.DEFAULT, null, IndexPrivilege.ALL, true, IndexComponentSelectorPrivilege.DATA, "*") .cluster(Set.of("all"), Set.of()) .build(); Settings indexSettings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersion.current()).build(); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java index 482715bb74c83..856f20a6967bc 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java @@ -90,13 +90,8 @@ import org.elasticsearch.xpack.core.security.authz.permission.Role; import org.elasticsearch.xpack.core.security.authz.permission.RunAsPermission; import org.elasticsearch.xpack.core.security.authz.permission.SimpleRole; -import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; -import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; -import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeTests; -import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilegeResolver; +import org.elasticsearch.xpack.core.security.authz.privilege.*; import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivileges.ManageApplicationPrivileges; -import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; -import org.elasticsearch.xpack.core.security.authz.privilege.Privilege; import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; import org.elasticsearch.xpack.core.security.support.Automatons; import org.elasticsearch.xpack.core.security.test.TestRestrictedIndices; @@ -780,9 +775,15 @@ public void testCheckRestrictedIndexPatternPermission() throws Exception { 0, randomIntBetween(2, XPackPlugin.ASYNC_RESULTS_INDEX.length() - 2) ); - Role role = Role.builder(RESTRICTED_INDICES, "role") - .add(FieldPermissions.DEFAULT, null, IndexPrivilege.INDEX, false, patternPrefix + "*") - .build(); + Role.Builder builder1 = Role.builder(RESTRICTED_INDICES, "role"); + Role role = builder1.add( + FieldPermissions.DEFAULT, + null, + IndexPrivilege.INDEX, + false, + IndexComponentSelectorPrivilege.DATA, + patternPrefix + "*" + ).build(); RBACAuthorizationInfo authzInfo = new RBACAuthorizationInfo(role, null); String prePatternPrefix = patternPrefix.substring(0, randomIntBetween(1, patternPrefix.length() - 1)) + "*"; @@ -891,9 +892,15 @@ public void testCheckRestrictedIndexPatternPermission() throws Exception { containsInAnyOrder(ResourcePrivileges.builder(restrictedIndexMatchingWildcard).addPrivileges(Map.of("index", false)).build()) ); - role = Role.builder(RESTRICTED_INDICES, "role") - .add(FieldPermissions.DEFAULT, null, IndexPrivilege.INDEX, true, patternPrefix + "*") - .build(); + Role.Builder builder = Role.builder(RESTRICTED_INDICES, "role"); + role = builder.add( + FieldPermissions.DEFAULT, + null, + IndexPrivilege.INDEX, + true, + IndexComponentSelectorPrivilege.DATA, + patternPrefix + "*" + ).build(); authzInfo = new RBACAuthorizationInfo(role, null); response = hasPrivileges( IndicesPrivileges.builder() @@ -916,10 +923,23 @@ public void testCheckRestrictedIndexPatternPermission() throws Exception { public void testCheckExplicitRestrictedIndexPermissions() throws Exception { final boolean restrictedIndexPermission = randomBoolean(); final boolean restrictedMonitorPermission = randomBoolean(); - Role role = Role.builder(RESTRICTED_INDICES, "role") - .add(FieldPermissions.DEFAULT, null, IndexPrivilege.INDEX, restrictedIndexPermission, ".sec*") - .add(FieldPermissions.DEFAULT, null, IndexPrivilege.MONITOR, restrictedMonitorPermission, ".security*") - .build(); + Role.Builder builder = Role.builder(RESTRICTED_INDICES, "role"); + Role.Builder builder1 = builder.add( + FieldPermissions.DEFAULT, + null, + IndexPrivilege.INDEX, + restrictedIndexPermission, + IndexComponentSelectorPrivilege.DATA, + ".sec*" + ); + Role role = builder1.add( + FieldPermissions.DEFAULT, + null, + IndexPrivilege.MONITOR, + restrictedMonitorPermission, + IndexComponentSelectorPrivilege.DATA, + ".security*" + ).build(); RBACAuthorizationInfo authzInfo = new RBACAuthorizationInfo(role, null); String explicitRestrictedIndex = randomFrom(TestRestrictedIndices.SAMPLE_RESTRICTED_NAMES); @@ -974,10 +994,23 @@ public void testCheckExplicitRestrictedIndexPermissions() throws Exception { } public void testCheckRestrictedIndexWildcardPermissions() throws Exception { - Role role = Role.builder(RESTRICTED_INDICES, "role") - .add(FieldPermissions.DEFAULT, null, IndexPrivilege.INDEX, false, ".sec*") - .add(FieldPermissions.DEFAULT, null, IndexPrivilege.MONITOR, true, ".security*") - .build(); + Role.Builder builder2 = Role.builder(RESTRICTED_INDICES, "role"); + Role.Builder builder3 = builder2.add( + FieldPermissions.DEFAULT, + null, + IndexPrivilege.INDEX, + false, + IndexComponentSelectorPrivilege.DATA, + ".sec*" + ); + Role role = builder3.add( + FieldPermissions.DEFAULT, + null, + IndexPrivilege.MONITOR, + true, + IndexComponentSelectorPrivilege.DATA, + ".security*" + ).build(); RBACAuthorizationInfo authzInfo = new RBACAuthorizationInfo(role, null); PrivilegesCheckResult response = hasPrivileges( @@ -1012,10 +1045,23 @@ public void testCheckRestrictedIndexWildcardPermissions() throws Exception { ) ); - role = Role.builder(RESTRICTED_INDICES, "role") - .add(FieldPermissions.DEFAULT, null, IndexPrivilege.INDEX, true, ".sec*") - .add(FieldPermissions.DEFAULT, null, IndexPrivilege.MONITOR, false, ".security*") - .build(); + Role.Builder builder = Role.builder(RESTRICTED_INDICES, "role"); + Role.Builder builder1 = builder.add( + FieldPermissions.DEFAULT, + null, + IndexPrivilege.INDEX, + true, + IndexComponentSelectorPrivilege.DATA, + ".sec*" + ); + role = builder1.add( + FieldPermissions.DEFAULT, + null, + IndexPrivilege.MONITOR, + false, + IndexComponentSelectorPrivilege.DATA, + ".security*" + ).build(); authzInfo = new RBACAuthorizationInfo(role, null); response = hasPrivileges( @@ -1288,18 +1334,23 @@ public void testBuildUserPrivilegeResponse() { final ManageApplicationPrivileges manageApplicationPrivileges = new ManageApplicationPrivileges(Sets.newHashSet("app01", "app02")); final BytesArray query = new BytesArray(""" {"term":{"public":true}}"""); - final Role role = Role.builder(RESTRICTED_INDICES, "test", "role") - .cluster(Sets.newHashSet("monitor", "manage_watcher"), Collections.singleton(manageApplicationPrivileges)) - .add(IndexPrivilege.get(Sets.newHashSet("read", "write")), "index-1") - .add(IndexPrivilege.ALL, "index-2", "index-3") - .add( - new FieldPermissions(new FieldPermissionsDefinition(new String[] { "public.*" }, new String[0])), - Collections.singleton(query), - IndexPrivilege.READ, - randomBoolean(), - "index-4", - "index-5" - ) + Role.Builder builder = Role.builder(RESTRICTED_INDICES, "test", "role") + .cluster(newHashSet("monitor", "manage_watcher"), Collections.singleton(manageApplicationPrivileges)) + .add(IndexPrivilege.get(newHashSet("read", "write")), "index-1") + .add(IndexPrivilege.ALL, "index-2", "index-3"); + FieldPermissions fieldPermissions = new FieldPermissions( + new FieldPermissionsDefinition(new String[] { "public.*" }, new String[0]) + ); + boolean allowRestrictedIndices = randomBoolean(); + final Role role = builder.add( + fieldPermissions, + Collections.singleton(query), + IndexPrivilege.READ, + allowRestrictedIndices, + IndexComponentSelectorPrivilege.DATA, + "index-4", + "index-5" + ) .addApplicationPrivilege(ApplicationPrivilegeTests.createPrivilege("app01", "read", "data:read"), Collections.singleton("*")) .runAs(new Privilege(Sets.newHashSet("user01", "user02"), "user01", "user02")) .addRemoteIndicesGroup(Set.of("remote-1"), FieldPermissions.DEFAULT, null, IndexPrivilege.READ, false, "remote-index-1") @@ -1905,16 +1956,18 @@ public void testChildSearchActionAuthorizationIsNotSkippedWhenRoleHasDLS() { final String[] indices = { "test-index" }; final BytesArray query = new BytesArray(""" {"term":{"foo":bar}}"""); + Role.Builder builder = Role.builder(RESTRICTED_INDICES, "test-role"); + FieldPermissions fieldPermissions = new FieldPermissions(new FieldPermissionsDefinition(new String[] { "foo" }, new String[0])); + boolean allowRestrictedIndices = randomBoolean(); final Role role = Mockito.spy( - Role.builder(RESTRICTED_INDICES, "test-role") - .add( - new FieldPermissions(new FieldPermissionsDefinition(new String[] { "foo" }, new String[0])), - Set.of(query), - IndexPrivilege.READ, - randomBoolean(), - indices - ) - .build() + builder.add( + fieldPermissions, + Set.of(query), + IndexPrivilege.READ, + allowRestrictedIndices, + IndexComponentSelectorPrivilege.DATA, + indices + ).build() ); final String action = randomFrom( diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java index 4488c28750dc0..3b5b3ba6d60dc 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java @@ -35,6 +35,7 @@ import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsDefinition; import org.elasticsearch.xpack.core.security.authz.permission.IndicesPermission; import org.elasticsearch.xpack.core.security.authz.permission.Role; +import org.elasticsearch.xpack.core.security.authz.privilege.IndexComponentSelectorPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.support.StringMatcher; import org.elasticsearch.xpack.core.security.test.TestRestrictedIndices; @@ -69,9 +70,17 @@ public void testAuthorize() { // basics: Set query = Collections.singleton(new BytesArray("{}")); String[] fields = new String[] { "_field" }; - Role role = Role.builder(RESTRICTED_INDICES, "_role") - .add(new FieldPermissions(fieldPermissionDef(fields, null)), query, IndexPrivilege.ALL, randomBoolean(), "_index") - .build(); + Role.Builder builder6 = Role.builder(RESTRICTED_INDICES, "_role"); + FieldPermissions fieldPermissions5 = new FieldPermissions(fieldPermissionDef(fields, null)); + boolean allowRestrictedIndices6 = randomBoolean(); + Role role = builder6.add( + fieldPermissions5, + query, + IndexPrivilege.ALL, + allowRestrictedIndices6, + IndexComponentSelectorPrivilege.DATA, + "_index" + ).build(); IndicesAccessControl permissions = role.authorize( TransportSearchAction.TYPE.name(), Sets.newHashSet("_index"), @@ -86,9 +95,17 @@ public void testAuthorize() { assertThat(permissions.getIndexPermissions("_index").getDocumentPermissions().getSingleSetOfQueries(), equalTo(query)); // no document level security: - role = Role.builder(RESTRICTED_INDICES, "_role") - .add(new FieldPermissions(fieldPermissionDef(fields, null)), null, IndexPrivilege.ALL, randomBoolean(), "_index") - .build(); + Role.Builder builder5 = Role.builder(RESTRICTED_INDICES, "_role"); + FieldPermissions fieldPermissions4 = new FieldPermissions(fieldPermissionDef(fields, null)); + boolean allowRestrictedIndices5 = randomBoolean(); + role = builder5.add( + fieldPermissions4, + null, + IndexPrivilege.ALL, + allowRestrictedIndices5, + IndexComponentSelectorPrivilege.DATA, + "_index" + ).build(); permissions = role.authorize(TransportSearchAction.TYPE.name(), Sets.newHashSet("_index"), md, fieldPermissionsCache); assertThat(permissions.getIndexPermissions("_index"), notNullValue()); assertTrue(permissions.getIndexPermissions("_index").getFieldPermissions().grantsAccessTo("_field")); @@ -97,9 +114,16 @@ public void testAuthorize() { assertThat(permissions.getIndexPermissions("_index").getDocumentPermissions().getListOfQueries(), nullValue()); // no field level security: - role = Role.builder(RESTRICTED_INDICES, "_role") - .add(FieldPermissions.DEFAULT, query, IndexPrivilege.ALL, randomBoolean(), "_index") - .build(); + Role.Builder builder4 = Role.builder(RESTRICTED_INDICES, "_role"); + boolean allowRestrictedIndices4 = randomBoolean(); + role = builder4.add( + FieldPermissions.DEFAULT, + query, + IndexPrivilege.ALL, + allowRestrictedIndices4, + IndexComponentSelectorPrivilege.DATA, + "_index" + ).build(); permissions = role.authorize(TransportSearchAction.TYPE.name(), Sets.newHashSet("_index"), md, fieldPermissionsCache); assertThat(permissions.getIndexPermissions("_index"), notNullValue()); assertFalse(permissions.getIndexPermissions("_index").getFieldPermissions().hasFieldLevelSecurity()); @@ -108,9 +132,17 @@ public void testAuthorize() { assertThat(permissions.getIndexPermissions("_index").getDocumentPermissions().getSingleSetOfQueries(), equalTo(query)); // index group associated with an alias: - role = Role.builder(RESTRICTED_INDICES, "_role") - .add(new FieldPermissions(fieldPermissionDef(fields, null)), query, IndexPrivilege.ALL, randomBoolean(), "_alias") - .build(); + Role.Builder builder3 = Role.builder(RESTRICTED_INDICES, "_role"); + FieldPermissions fieldPermissions3 = new FieldPermissions(fieldPermissionDef(fields, null)); + boolean allowRestrictedIndices3 = randomBoolean(); + role = builder3.add( + fieldPermissions3, + query, + IndexPrivilege.ALL, + allowRestrictedIndices3, + IndexComponentSelectorPrivilege.DATA, + "_alias" + ).build(); permissions = role.authorize(TransportSearchAction.TYPE.name(), Sets.newHashSet("_alias"), md, fieldPermissionsCache); assertThat(permissions.getIndexPermissions("_index"), notNullValue()); assertTrue(permissions.getIndexPermissions("_index").getFieldPermissions().grantsAccessTo("_field")); @@ -132,9 +164,17 @@ public void testAuthorize() { new String[] { "foo", "*" }, new String[] { randomAlphaOfLengthBetween(1, 10), "*" } ); - role = Role.builder(RESTRICTED_INDICES, "_role") - .add(new FieldPermissions(fieldPermissionDef(allFields, null)), query, IndexPrivilege.ALL, randomBoolean(), "_alias") - .build(); + Role.Builder builder2 = Role.builder(RESTRICTED_INDICES, "_role"); + FieldPermissions fieldPermissions2 = new FieldPermissions(fieldPermissionDef(allFields, null)); + boolean allowRestrictedIndices2 = randomBoolean(); + role = builder2.add( + fieldPermissions2, + query, + IndexPrivilege.ALL, + allowRestrictedIndices2, + IndexComponentSelectorPrivilege.DATA, + "_alias" + ).build(); permissions = role.authorize(TransportSearchAction.TYPE.name(), Sets.newHashSet("_alias"), md, fieldPermissionsCache); assertThat(permissions.getIndexPermissions("_index"), notNullValue()); assertFalse(permissions.getIndexPermissions("_index").getFieldPermissions().hasFieldLevelSecurity()); @@ -156,10 +196,27 @@ public void testAuthorize() { // match all fields with more than one permission Set fooQuery = Collections.singleton(new BytesArray("{foo}")); allFields = randomFrom(new String[] { "*" }, new String[] { "foo", "*" }, new String[] { randomAlphaOfLengthBetween(1, 10), "*" }); - role = Role.builder(RESTRICTED_INDICES, "_role") - .add(new FieldPermissions(fieldPermissionDef(allFields, null)), fooQuery, IndexPrivilege.ALL, randomBoolean(), "_alias") - .add(new FieldPermissions(fieldPermissionDef(allFields, null)), query, IndexPrivilege.ALL, randomBoolean(), "_alias") - .build(); + Role.Builder builder = Role.builder(RESTRICTED_INDICES, "_role"); + FieldPermissions fieldPermissions = new FieldPermissions(fieldPermissionDef(allFields, null)); + boolean allowRestrictedIndices = randomBoolean(); + Role.Builder builder1 = builder.add( + fieldPermissions, + fooQuery, + IndexPrivilege.ALL, + allowRestrictedIndices, + IndexComponentSelectorPrivilege.DATA, + "_alias" + ); + FieldPermissions fieldPermissions1 = new FieldPermissions(fieldPermissionDef(allFields, null)); + boolean allowRestrictedIndices1 = randomBoolean(); + role = builder1.add( + fieldPermissions1, + query, + IndexPrivilege.ALL, + allowRestrictedIndices1, + IndexComponentSelectorPrivilege.DATA, + "_alias" + ).build(); permissions = role.authorize(TransportSearchAction.TYPE.name(), Sets.newHashSet("_alias"), md, fieldPermissionsCache); Set bothQueries = Sets.union(fooQuery, query); assertThat(permissions.getIndexPermissions("_index"), notNullValue()); @@ -191,10 +248,27 @@ public void testAuthorizeMultipleGroupsMixedDls() { Set query = Collections.singleton(new BytesArray("{}")); String[] fields = new String[] { "_field" }; - Role role = Role.builder(RESTRICTED_INDICES, "_role") - .add(new FieldPermissions(fieldPermissionDef(fields, null)), query, IndexPrivilege.ALL, randomBoolean(), "_index") - .add(new FieldPermissions(fieldPermissionDef(null, null)), null, IndexPrivilege.ALL, randomBoolean(), "*") - .build(); + Role.Builder builder = Role.builder(RESTRICTED_INDICES, "_role"); + FieldPermissions fieldPermissions = new FieldPermissions(fieldPermissionDef(fields, null)); + boolean allowRestrictedIndices = randomBoolean(); + Role.Builder builder1 = builder.add( + fieldPermissions, + query, + IndexPrivilege.ALL, + allowRestrictedIndices, + IndexComponentSelectorPrivilege.DATA, + "_index" + ); + FieldPermissions fieldPermissions1 = new FieldPermissions(fieldPermissionDef(null, null)); + boolean allowRestrictedIndices1 = randomBoolean(); + Role role = builder1.add( + fieldPermissions1, + null, + IndexPrivilege.ALL, + allowRestrictedIndices1, + IndexComponentSelectorPrivilege.DATA, + "*" + ).build(); IndicesAccessControl permissions = role.authorize( TransportSearchAction.TYPE.name(), Sets.newHashSet("_index"), From 9976ca1c8366dde3e45b76df605eacde14853d26 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Sun, 16 Feb 2025 17:42:19 +0100 Subject: [PATCH 004/131] More --- .../core/security/authz/permission/Role.java | 19 ++++-- .../IndexComponentSelectorPrivilege.java | 66 +++++++++++-------- .../authz/privilege/IndexPrivilege.java | 8 ++- .../core/security/user/InternalUsers.java | 3 +- .../authz/store/CompositeRolesStore.java | 8 ++- 5 files changed, 64 insertions(+), 40 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java index 10ae7900e955a..eed77d752a070 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java @@ -429,14 +429,19 @@ static SimpleRole buildFromRoleDescriptor( Set query = indexPrivilege.getQuery() == null ? null : Collections.singleton(indexPrivilege.getQuery()); IndexPrivilege privilege = IndexPrivilege.get(Sets.newHashSet(indexPrivilege.getPrivileges())); boolean allowRestrictedIndices = indexPrivilege.allowRestrictedIndices(); - builder.add( - fieldPermissions, - query, - privilege, - allowRestrictedIndices, - IndexComponentSelectorPrivilege.DATA, - indexPrivilege.getIndices() + Map> split = IndexComponentSelectorPrivilege.splitBySelectors( + indexPrivilege.getPrivileges() ); + for (var entry : split.entrySet()) { + builder.add( + fieldPermissions, + query, + privilege, + allowRestrictedIndices, + entry.getKey(), + entry.getValue().toArray(String[]::new) + ); + } } for (RoleDescriptor.RemoteIndicesPrivileges remoteIndicesPrivileges : roleDescriptor.getRemoteIndicesPrivileges()) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java index 26fa660f07008..a6427004c3116 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java @@ -15,25 +15,24 @@ import java.util.function.Predicate; import java.util.stream.Collectors; -public enum IndexComponentSelectorPrivilege { - ALL("all", (selector) -> true), - DATA("data", IndexComponentSelector.DATA::equals), - FAILURES("failures", IndexComponentSelector.FAILURES::equals); +public record IndexComponentSelectorPrivilege(String name, Predicate predicate) { + public static final IndexComponentSelectorPrivilege ALL = new IndexComponentSelectorPrivilege("all", (selector) -> true); + public static final IndexComponentSelectorPrivilege DATA = new IndexComponentSelectorPrivilege( + "data", + IndexComponentSelector.DATA::equals + ); + public static final IndexComponentSelectorPrivilege FAILURES = new IndexComponentSelectorPrivilege( + "failures", + IndexComponentSelector.FAILURES::equals + ); - private final String name; - private final Predicate grants; + private static final Set FAILURE_STORE_PRIVILEGE_NAMES = Set.of( + IndexPrivilege.READ_FAILURE_STORE, + IndexPrivilege.MANAGE_FAILURE_STORE_INTERNAL + ); - IndexComponentSelectorPrivilege(String name, Predicate grants) { - this.name = name; - this.grants = grants; - } - - public String getName() { - return name; - } - - public boolean grants(IndexComponentSelector selector) { - return grants.test(selector); + public boolean test(IndexComponentSelector selector) { + return predicate.test(selector); } public boolean isTotal() { @@ -44,20 +43,29 @@ public static Set get(Set indexPrivileg return indexPrivileges.stream().map(IndexComponentSelectorPrivilege::get).collect(Collectors.toSet()); } - public static Map> split(Set indexPrivileges) { - final var data = new HashSet(); - final var failures = new HashSet(); + public static Map> splitBySelectors(String... indexPrivileges) { + return splitBySelectors(Set.of(indexPrivileges)); + } + + public static Map> splitBySelectors(Set indexPrivileges) { + final Set data = new HashSet<>(); + final Set failures = new HashSet<>(); for (String indexPrivilege : indexPrivileges) { - final var privilege = get(indexPrivilege); - switch (privilege) { - case DATA -> data.add(indexPrivilege); - case FAILURES -> failures.add(indexPrivilege); - // If we ever hit all, we can return early since we don't need to split - case ALL -> { - return Map.of(ALL, indexPrivileges); - } + final IndexComponentSelectorPrivilege privilege = get(indexPrivilege); + // If we ever hit all, we can return early since we don't need to split + if (privilege.equals(ALL)) { + return Map.of(ALL, indexPrivileges); + } + + if (privilege.equals(DATA)) { + data.add(indexPrivilege); + } else if (privilege.equals(FAILURES)) { + failures.add(indexPrivilege); + } else { + throw new IllegalArgumentException("Unknown index privilege: " + indexPrivilege); } + } if (data.isEmpty()) { @@ -75,7 +83,7 @@ private static IndexComponentSelectorPrivilege get(String indexPrivilegeName) { return DATA; } else if (indexPrivilege == IndexPrivilege.ALL) { return ALL; - } else if (indexPrivilege == IndexPrivilege.READ_FAILURE_STORE || indexPrivilege == IndexPrivilege.MANAGE_FAILURE_STORE_INTERNAL) { + } else if (FAILURE_STORE_PRIVILEGE_NAMES.contains(indexPrivilege)) { return FAILURES; } else { return DATA; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java index 0e4185e9e3455..68954b43f79ab 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java @@ -219,7 +219,6 @@ public final class IndexPrivilege extends Privilege { * If you are adding a new named index privilege, also add it to the * docs. */ - @SuppressWarnings("unchecked") private static final Map VALUES = sortByAccessLevel( Stream.of( entry("none", NONE), @@ -272,6 +271,13 @@ public static IndexPrivilege get(Set name) { }); } + public String getSingleName() { + if (name().size() != 1) { + throw new IllegalStateException("Expected a single name, but got: " + name()); + } + return name().iterator().next(); + } + @Nullable public static IndexPrivilege getNamedOrNull(String name) { return VALUES.get(name.toLowerCase(Locale.ROOT)); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/InternalUsers.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/InternalUsers.java index 9a0b17b22369c..5e5d7aa788db0 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/InternalUsers.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/InternalUsers.java @@ -30,6 +30,7 @@ import org.elasticsearch.index.reindex.ReindexAction; import org.elasticsearch.xpack.core.XPackPlugin; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.support.MetadataUtils; import java.util.Collection; @@ -244,7 +245,7 @@ public class InternalUsers { new RoleDescriptor.IndicesPrivileges[] { RoleDescriptor.IndicesPrivileges.builder() .indices("*") - .privileges(LazyRolloverAction.NAME) + .privileges(LazyRolloverAction.NAME, IndexPrivilege.MANAGE_FAILURE_STORE_INTERNAL.getSingleName()) .allowRestrictedIndices(true) .build() }, null, diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java index 610e40cfb3deb..c0b94c77fe88e 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java @@ -543,7 +543,9 @@ public static void buildRoleFromDescriptors( for (Map.Entry, MergeableIndicesPrivilege> entry : indicesPrivilegesMap.entrySet()) { MergeableIndicesPrivilege indicesPrivilege = entry.getValue(); - Map> split = IndexComponentSelectorPrivilege.split(indicesPrivilege.privileges); + Map> split = IndexComponentSelectorPrivilege.splitBySelectors( + indicesPrivilege.privileges + ); FieldPermissions fieldPermissions = fieldPermissionsCache.getFieldPermissions(indicesPrivilege.fieldPermissionsDefinition); String[] indices = indicesPrivilege.indices.toArray(Strings.EMPTY_ARRAY); for (Map.Entry> privilegesBySelector : split.entrySet()) { @@ -559,7 +561,9 @@ public static void buildRoleFromDescriptors( } for (Map.Entry, MergeableIndicesPrivilege> entry : restrictedIndicesPrivilegesMap.entrySet()) { MergeableIndicesPrivilege indicesPrivilege = entry.getValue(); - Map> split = IndexComponentSelectorPrivilege.split(indicesPrivilege.privileges); + Map> split = IndexComponentSelectorPrivilege.splitBySelectors( + indicesPrivilege.privileges + ); FieldPermissions fieldPermissions = fieldPermissionsCache.getFieldPermissions(indicesPrivilege.fieldPermissionsDefinition); String[] indices = indicesPrivilege.indices.toArray(Strings.EMPTY_ARRAY); for (Map.Entry> privilegesBySelector : split.entrySet()) { From bcd42c09da941a040aa9b9654fe86e4b585219b2 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Sun, 16 Feb 2025 21:07:58 +0100 Subject: [PATCH 005/131] Fix --- .../xpack/core/security/authz/permission/Role.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java index eed77d752a070..538f6fdb29126 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java @@ -427,7 +427,6 @@ static SimpleRole buildFromRoleDescriptor( new FieldPermissionsDefinition(indexPrivilege.getGrantedFields(), indexPrivilege.getDeniedFields()) ); Set query = indexPrivilege.getQuery() == null ? null : Collections.singleton(indexPrivilege.getQuery()); - IndexPrivilege privilege = IndexPrivilege.get(Sets.newHashSet(indexPrivilege.getPrivileges())); boolean allowRestrictedIndices = indexPrivilege.allowRestrictedIndices(); Map> split = IndexComponentSelectorPrivilege.splitBySelectors( indexPrivilege.getPrivileges() @@ -436,10 +435,10 @@ static SimpleRole buildFromRoleDescriptor( builder.add( fieldPermissions, query, - privilege, + IndexPrivilege.get(entry.getValue()), allowRestrictedIndices, entry.getKey(), - entry.getValue().toArray(String[]::new) + indexPrivilege.getIndices() ); } } From 6ceb40146ef1d943e5a5460a4f83e1e0b5667b3c Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 17 Feb 2025 09:26:34 +0100 Subject: [PATCH 006/131] Fix imports --- .../core/security/authz/permission/LimitedRoleTests.java | 7 ++++++- .../xpack/security/authz/RBACEngineTests.java | 8 +++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java index 9544cf30fdc08..630ff45136895 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java @@ -32,7 +32,12 @@ import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.IndicesPrivileges; import org.elasticsearch.xpack.core.security.authz.RoleDescriptorsIntersection; import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; -import org.elasticsearch.xpack.core.security.authz.privilege.*; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeTests; +import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilegeResolver; +import org.elasticsearch.xpack.core.security.authz.privilege.IndexComponentSelectorPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.authz.restriction.Workflow; import org.elasticsearch.xpack.core.security.authz.restriction.WorkflowResolver; import org.elasticsearch.xpack.core.security.support.Automatons; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java index 856f20a6967bc..b76a7b6ac5b7b 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java @@ -90,8 +90,14 @@ import org.elasticsearch.xpack.core.security.authz.permission.Role; import org.elasticsearch.xpack.core.security.authz.permission.RunAsPermission; import org.elasticsearch.xpack.core.security.authz.permission.SimpleRole; -import org.elasticsearch.xpack.core.security.authz.privilege.*; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeTests; +import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilegeResolver; import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivileges.ManageApplicationPrivileges; +import org.elasticsearch.xpack.core.security.authz.privilege.IndexComponentSelectorPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.Privilege; import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; import org.elasticsearch.xpack.core.security.support.Automatons; import org.elasticsearch.xpack.core.security.test.TestRestrictedIndices; From a531e5f68843220e215406425d0c42a6e43a457c Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 17 Feb 2025 10:14:33 +0100 Subject: [PATCH 007/131] More fixes --- .../authz/permission/IndicesPermission.java | 11 +++++++++++ .../xpack/core/security/user/InternalUsers.java | 9 ++++++--- .../authz/privilege/IndexPrivilegeTests.java | 16 +++++++++++----- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 296da8c0600be..88a4a77745cf8 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -76,6 +76,17 @@ public Builder(RestrictedIndices restrictedIndices) { this.restrictedIndices = restrictedIndices; } + // TODO remove me + public Builder addGroup( + IndexPrivilege privilege, + FieldPermissions fieldPermissions, + @Nullable Set query, + boolean allowRestrictedIndices, + String... indices + ) { + return addGroup(privilege, fieldPermissions, query, allowRestrictedIndices, IndexComponentSelectorPrivilege.DATA, indices); + } + public Builder addGroup( IndexPrivilege privilege, FieldPermissions fieldPermissions, diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/InternalUsers.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/InternalUsers.java index 5e5d7aa788db0..0b8270f5175fb 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/InternalUsers.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/InternalUsers.java @@ -162,7 +162,8 @@ public class InternalUsers { IndicesStatsAction.NAME + "*", TransportUpdateSettingsAction.TYPE.name(), DownsampleAction.NAME, - TransportAddIndexBlockAction.TYPE.name() + TransportAddIndexBlockAction.TYPE.name(), + IndexPrivilege.MANAGE_FAILURE_STORE_INTERNAL.getSingleName() ) .allowRestrictedIndices(false) .build(), @@ -181,7 +182,8 @@ public class InternalUsers { IndicesStatsAction.NAME + "*", TransportUpdateSettingsAction.TYPE.name(), DownsampleAction.NAME, - TransportAddIndexBlockAction.TYPE.name() + TransportAddIndexBlockAction.TYPE.name(), + IndexPrivilege.MANAGE_FAILURE_STORE_INTERNAL.getSingleName() ) .allowRestrictedIndices(true) .build() }, @@ -221,7 +223,8 @@ public class InternalUsers { TransportBulkAction.NAME, TransportIndexAction.NAME, TransportSearchScrollAction.TYPE.name(), - ModifyDataStreamsAction.NAME + ModifyDataStreamsAction.NAME, + IndexPrivilege.MANAGE_FAILURE_STORE_INTERNAL.getSingleName() ) .allowRestrictedIndices(false) .build() }, diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java index 073b3b92a43a5..4d9678f571f76 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java @@ -58,26 +58,32 @@ public void testOrderingOfPrivilegeNames() throws Exception { } public void testFindPrivilegesThatGrant() { - assertThat(findPrivilegesThatGrant(TransportSearchAction.TYPE.name()), equalTo(List.of("read", "all"))); + assertThat(findPrivilegesThatGrant(TransportSearchAction.TYPE.name()), equalTo(List.of("read", "read_failure_store", "all"))); assertThat(findPrivilegesThatGrant(TransportIndexAction.NAME), equalTo(List.of("create_doc", "create", "index", "write", "all"))); assertThat(findPrivilegesThatGrant(TransportUpdateAction.NAME), equalTo(List.of("index", "write", "all"))); assertThat(findPrivilegesThatGrant(TransportDeleteAction.NAME), equalTo(List.of("delete", "write", "all"))); assertThat( findPrivilegesThatGrant(IndicesStatsAction.NAME), - equalTo(List.of("monitor", "cross_cluster_replication", "manage", "all")) + equalTo(List.of("monitor", "manage", "manage_failure_store_internal", "cross_cluster_replication", "all")) + ); + assertThat( + findPrivilegesThatGrant(RefreshAction.NAME), + equalTo(List.of("maintenance", "manage", "manage_failure_store_internal", "all")) ); - assertThat(findPrivilegesThatGrant(RefreshAction.NAME), equalTo(List.of("maintenance", "manage", "all"))); } public void testPrivilegesForRollupFieldCapsAction() { final Collection privileges = findPrivilegesThatGrant(GetRollupIndexCapsAction.NAME); - assertThat(Set.copyOf(privileges), equalTo(Set.of("read", "view_index_metadata", "manage", "all"))); + assertThat( + Set.copyOf(privileges), + equalTo(Set.of("manage", "all", "read_failure_store", "view_index_metadata", "read", "manage_failure_store_internal")) + ); } public void testPrivilegesForGetCheckPointAction() { assertThat( findPrivilegesThatGrant(GetCheckpointAction.NAME), - containsInAnyOrder("monitor", "view_index_metadata", "manage", "all") + containsInAnyOrder("monitor", "view_index_metadata", "manage", "manage_failure_store_internal", "all") ); } From 2ccd9698be63318fd283052610aade80e80757ca Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 17 Feb 2025 10:20:29 +0100 Subject: [PATCH 008/131] Moar --- .../authz/permission/IndicesPermission.java | 2 +- .../core/security/authz/permission/Role.java | 2 +- .../IndexComponentSelectorPrivilege.java | 42 ++++++++++--------- .../authz/store/CompositeRolesStore.java | 4 +- 4 files changed, 26 insertions(+), 24 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 88a4a77745cf8..f7acdcf126ba4 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -913,7 +913,7 @@ boolean isTotal() { && privilege == IndexPrivilege.ALL && query == null && false == fieldPermissions.hasFieldLevelSecurity() - // TODO do we want this? + // TODO add selectorPrivilege here in a follow PR handling authorization && selectorPrivilege.isTotal(); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java index 538f6fdb29126..b4382fcd04e67 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java @@ -428,7 +428,7 @@ static SimpleRole buildFromRoleDescriptor( ); Set query = indexPrivilege.getQuery() == null ? null : Collections.singleton(indexPrivilege.getQuery()); boolean allowRestrictedIndices = indexPrivilege.allowRestrictedIndices(); - Map> split = IndexComponentSelectorPrivilege.splitBySelectors( + Map> split = IndexComponentSelectorPrivilege.groupBySelectors( indexPrivilege.getPrivileges() ); for (var entry : split.entrySet()) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java index a6427004c3116..9ed1589c5ffe4 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java @@ -31,7 +31,7 @@ public record IndexComponentSelectorPrivilege(String name, Predicate get(Set indexPrivileg return indexPrivileges.stream().map(IndexComponentSelectorPrivilege::get).collect(Collectors.toSet()); } - public static Map> splitBySelectors(String... indexPrivileges) { - return splitBySelectors(Set.of(indexPrivileges)); + public static Map> groupBySelectors(String... indexPrivileges) { + return groupBySelectors(Set.of(indexPrivileges)); } - public static Map> splitBySelectors(Set indexPrivileges) { - final Set data = new HashSet<>(); - final Set failures = new HashSet<>(); + public static Map> groupBySelectors(Set indexPrivileges) { + final Set dataAccessPrivileges = new HashSet<>(); + final Set failuresAccessPrivileges = new HashSet<>(); for (String indexPrivilege : indexPrivileges) { - final IndexComponentSelectorPrivilege privilege = get(indexPrivilege); - // If we ever hit all, we can return early since we don't need to split - if (privilege.equals(ALL)) { + final IndexComponentSelectorPrivilege selectorPrivilege = get(indexPrivilege); + // If we ever hit `all`, the entire group can be treated as granting "all" access and we can return early + if (selectorPrivilege.equals(ALL)) { return Map.of(ALL, indexPrivileges); } - if (privilege.equals(DATA)) { - data.add(indexPrivilege); - } else if (privilege.equals(FAILURES)) { - failures.add(indexPrivilege); + if (selectorPrivilege.equals(DATA)) { + dataAccessPrivileges.add(indexPrivilege); + } else if (selectorPrivilege.equals(FAILURES)) { + failuresAccessPrivileges.add(indexPrivilege); } else { - throw new IllegalArgumentException("Unknown index privilege: " + indexPrivilege); + assert false : "index privilege [" + indexPrivilege + "] mapped to an unexpected selector [" + selectorPrivilege + "]"; + throw new IllegalStateException( + "index privilege [" + indexPrivilege + "] mapped to an unexpected selector [" + selectorPrivilege + "]" + ); } - } - if (data.isEmpty()) { - return Map.of(FAILURES, failures); - } else if (failures.isEmpty()) { - return Map.of(DATA, data); + if (dataAccessPrivileges.isEmpty()) { + return Map.of(FAILURES, failuresAccessPrivileges); + } else if (failuresAccessPrivileges.isEmpty()) { + return Map.of(DATA, dataAccessPrivileges); } else { - return Map.of(DATA, data, FAILURES, failures); + return Map.of(DATA, dataAccessPrivileges, FAILURES, failuresAccessPrivileges); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java index c0b94c77fe88e..518e1645c8eb0 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java @@ -543,7 +543,7 @@ public static void buildRoleFromDescriptors( for (Map.Entry, MergeableIndicesPrivilege> entry : indicesPrivilegesMap.entrySet()) { MergeableIndicesPrivilege indicesPrivilege = entry.getValue(); - Map> split = IndexComponentSelectorPrivilege.splitBySelectors( + Map> split = IndexComponentSelectorPrivilege.groupBySelectors( indicesPrivilege.privileges ); FieldPermissions fieldPermissions = fieldPermissionsCache.getFieldPermissions(indicesPrivilege.fieldPermissionsDefinition); @@ -561,7 +561,7 @@ public static void buildRoleFromDescriptors( } for (Map.Entry, MergeableIndicesPrivilege> entry : restrictedIndicesPrivilegesMap.entrySet()) { MergeableIndicesPrivilege indicesPrivilege = entry.getValue(); - Map> split = IndexComponentSelectorPrivilege.splitBySelectors( + Map> split = IndexComponentSelectorPrivilege.groupBySelectors( indicesPrivilege.privileges ); FieldPermissions fieldPermissions = fieldPermissionsCache.getFieldPermissions(indicesPrivilege.fieldPermissionsDefinition); From 444112b0fb29b7d3b0ccf613630d7d827a50e0c1 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 17 Feb 2025 10:31:03 +0100 Subject: [PATCH 009/131] Superuser etc --- .../core/security/authz/permission/Role.java | 2 +- .../IndexComponentSelectorPrivilege.java | 23 ++++++++++--------- .../authz/store/ReservedRolesStore.java | 4 ++-- .../authz/store/CompositeRolesStore.java | 4 ++-- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java index b4382fcd04e67..80933180cb3c5 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java @@ -428,7 +428,7 @@ static SimpleRole buildFromRoleDescriptor( ); Set query = indexPrivilege.getQuery() == null ? null : Collections.singleton(indexPrivilege.getQuery()); boolean allowRestrictedIndices = indexPrivilege.allowRestrictedIndices(); - Map> split = IndexComponentSelectorPrivilege.groupBySelectors( + Map> split = IndexComponentSelectorPrivilege.groupBySelector( indexPrivilege.getPrivileges() ); for (var entry : split.entrySet()) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java index 9ed1589c5ffe4..4f8f6b66f9690 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java @@ -8,6 +8,7 @@ package org.elasticsearch.xpack.core.security.authz.privilege; import org.elasticsearch.action.support.IndexComponentSelector; +import org.elasticsearch.core.Predicates; import java.util.HashSet; import java.util.Map; @@ -16,7 +17,7 @@ import java.util.stream.Collectors; public record IndexComponentSelectorPrivilege(String name, Predicate predicate) { - public static final IndexComponentSelectorPrivilege ALL = new IndexComponentSelectorPrivilege("all", (selector) -> true); + public static final IndexComponentSelectorPrivilege ALL = new IndexComponentSelectorPrivilege("all", Predicates.always()); public static final IndexComponentSelectorPrivilege DATA = new IndexComponentSelectorPrivilege( "data", IndexComponentSelector.DATA::equals @@ -43,30 +44,29 @@ public static Set get(Set indexPrivileg return indexPrivileges.stream().map(IndexComponentSelectorPrivilege::get).collect(Collectors.toSet()); } - public static Map> groupBySelectors(String... indexPrivileges) { - return groupBySelectors(Set.of(indexPrivileges)); + public static Map> groupBySelector(String... indexPrivileges) { + return groupBySelector(Set.of(indexPrivileges)); } - public static Map> groupBySelectors(Set indexPrivileges) { + public static Map> groupBySelector(Set indexPrivileges) { final Set dataAccessPrivileges = new HashSet<>(); final Set failuresAccessPrivileges = new HashSet<>(); for (String indexPrivilege : indexPrivileges) { final IndexComponentSelectorPrivilege selectorPrivilege = get(indexPrivilege); // If we ever hit `all`, the entire group can be treated as granting "all" access and we can return early - if (selectorPrivilege.equals(ALL)) { + if (selectorPrivilege == ALL) { return Map.of(ALL, indexPrivileges); } - if (selectorPrivilege.equals(DATA)) { + if (selectorPrivilege == DATA) { dataAccessPrivileges.add(indexPrivilege); - } else if (selectorPrivilege.equals(FAILURES)) { + } else if (selectorPrivilege == FAILURES) { failuresAccessPrivileges.add(indexPrivilege); } else { - assert false : "index privilege [" + indexPrivilege + "] mapped to an unexpected selector [" + selectorPrivilege + "]"; - throw new IllegalStateException( - "index privilege [" + indexPrivilege + "] mapped to an unexpected selector [" + selectorPrivilege + "]" - ); + final var message = "index privilege [" + indexPrivilege + "] mapped to an unexpected selector [" + selectorPrivilege + "]"; + assert false : message; + throw new IllegalStateException(message); } } @@ -81,6 +81,7 @@ public static Map> groupBySelectors private static IndexComponentSelectorPrivilege get(String indexPrivilegeName) { final IndexPrivilege indexPrivilege = IndexPrivilege.getNamedOrNull(indexPrivilegeName); + // `null` means we got a raw action instead of a named privilege; all raw actions are treated as data access if (indexPrivilege == null) { return DATA; } else if (indexPrivilege == IndexPrivilege.ALL) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java index 3648d8a0c7daa..b0bffec41d35d 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java @@ -78,7 +78,7 @@ public class ReservedRolesStore implements BiConsumer, ActionListene RoleDescriptor.IndicesPrivileges.builder().indices("*").privileges("all").allowRestrictedIndices(false).build(), RoleDescriptor.IndicesPrivileges.builder() .indices("*") - .privileges("monitor", "read", "view_index_metadata", "read_cross_cluster") + .privileges("monitor", "read", "view_index_metadata", "read_cross_cluster", "read_failure_store") .allowRestrictedIndices(true) .build() }, new RoleDescriptor.ApplicationResourcePrivileges[] { @@ -95,7 +95,7 @@ public class ReservedRolesStore implements BiConsumer, ActionListene new RoleDescriptor.RemoteIndicesPrivileges( RoleDescriptor.IndicesPrivileges.builder() .indices("*") - .privileges("monitor", "read", "view_index_metadata", "read_cross_cluster") + .privileges("monitor", "read", "view_index_metadata", "read_cross_cluster", "read_failure_store") .allowRestrictedIndices(true) .build(), "*" diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java index 518e1645c8eb0..d8fd89bdd6061 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java @@ -543,7 +543,7 @@ public static void buildRoleFromDescriptors( for (Map.Entry, MergeableIndicesPrivilege> entry : indicesPrivilegesMap.entrySet()) { MergeableIndicesPrivilege indicesPrivilege = entry.getValue(); - Map> split = IndexComponentSelectorPrivilege.groupBySelectors( + Map> split = IndexComponentSelectorPrivilege.groupBySelector( indicesPrivilege.privileges ); FieldPermissions fieldPermissions = fieldPermissionsCache.getFieldPermissions(indicesPrivilege.fieldPermissionsDefinition); @@ -561,7 +561,7 @@ public static void buildRoleFromDescriptors( } for (Map.Entry, MergeableIndicesPrivilege> entry : restrictedIndicesPrivilegesMap.entrySet()) { MergeableIndicesPrivilege indicesPrivilege = entry.getValue(); - Map> split = IndexComponentSelectorPrivilege.groupBySelectors( + Map> split = IndexComponentSelectorPrivilege.groupBySelector( indicesPrivilege.privileges ); FieldPermissions fieldPermissions = fieldPermissionsCache.getFieldPermissions(indicesPrivilege.fieldPermissionsDefinition); From 79d15eba39097955bdb664b083d1e71eb8b7f0ef Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 17 Feb 2025 11:16:11 +0100 Subject: [PATCH 010/131] Composite role store tests --- .../authz/permission/IndicesPermission.java | 4 + .../authz/store/CompositeRolesStoreTests.java | 120 ++++++++++++++++++ 2 files changed, 124 insertions(+) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index f7acdcf126ba4..5b2c6360a5d5a 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -899,6 +899,10 @@ boolean hasQuery() { return query != null; } + public IndexComponentSelectorPrivilege getSelectorPrivilege() { + return selectorPrivilege; + } + public boolean allowRestrictedIndices() { return allowRestrictedIndices; } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java index ed173d8e2b127..bc5a65bff4d46 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java @@ -86,6 +86,7 @@ import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeTests; import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilegeResolver; import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.IndexComponentSelectorPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.authz.restriction.Workflow; import org.elasticsearch.xpack.core.security.authz.restriction.WorkflowResolver; @@ -150,6 +151,7 @@ import static org.elasticsearch.xpack.security.authc.ApiKeyServiceTests.Utils.createApiKeyAuthentication; import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.arrayContaining; +import static org.hamcrest.Matchers.arrayContainingInAnyOrder; import static org.hamcrest.Matchers.arrayWithSize; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; @@ -1268,6 +1270,7 @@ public void testBuildRoleWithFlsAndDlsInRemoteIndicesDefinition() { false, "{\"match\":{\"field\":\"a\"}}", new FieldPermissionsDefinition.FieldGrantExcludeGroup(new String[] { "field" }, null), + IndexComponentSelectorPrivilege.DATA, "index-1" ) ); @@ -1303,6 +1306,7 @@ public void testBuildRoleWithFlsAndDlsInRemoteIndicesDefinition() { false, "{\"match\":{\"field\":\"a\"}}", new FieldPermissionsDefinition.FieldGrantExcludeGroup(new String[] { "field" }, null), + IndexComponentSelectorPrivilege.DATA, "index-1" ), indexGroup( @@ -1310,6 +1314,7 @@ public void testBuildRoleWithFlsAndDlsInRemoteIndicesDefinition() { false, "{\"match\":{\"field\":\"b\"}}", new FieldPermissionsDefinition.FieldGrantExcludeGroup(new String[] { "other" }, null), + IndexComponentSelectorPrivilege.DATA, "index-1" ) ); @@ -1529,6 +1534,90 @@ public void testBuildRoleWithMultipleRemoteClusterMerged() { } } + public void testBuildRoleWithReadFailureStorePrivilegeOnly() { + String indexPattern = randomAlphanumericOfLength(10); + final Role role = buildRole( + roleDescriptorWithIndicesPrivileges( + "r1", + new IndicesPrivileges[] { IndicesPrivileges.builder().indices(indexPattern).privileges("read_failure_store").build() } + ) + ); + assertHasIndexGroups( + role.indices(), + indexGroup(IndexPrivilege.READ_FAILURE_STORE, false, IndexComponentSelectorPrivilege.FAILURES, indexPattern) + ); + } + + public void testBuildRoleWithReadFailureStorePrivilegeDuplicatesMerged() { + String indexPattern = randomAlphanumericOfLength(10); + final Role role = buildRole( + roleDescriptorWithIndicesPrivileges( + "r1", + new IndicesPrivileges[] { + IndicesPrivileges.builder().indices(indexPattern).privileges("read_failure_store").build(), + IndicesPrivileges.builder().indices(indexPattern).privileges("read_failure_store").build() } + ) + ); + assertHasIndexGroups( + role.indices(), + indexGroup(IndexPrivilege.READ_FAILURE_STORE, false, IndexComponentSelectorPrivilege.FAILURES, indexPattern) + ); + } + + public void testBuildRoleWithReadFailureStoreAndReadPrivilegeSplit() { + String indexPattern = randomAlphanumericOfLength(10); + final Role role = buildRole( + roleDescriptorWithIndicesPrivileges( + "r1", + new IndicesPrivileges[] { + IndicesPrivileges.builder().indices(indexPattern).privileges("read", "read_failure_store").build() } + ) + ); + assertHasIndexGroups( + role.indices(), + indexGroup(IndexPrivilege.READ_FAILURE_STORE, false, IndexComponentSelectorPrivilege.FAILURES, indexPattern), + indexGroup(IndexPrivilege.READ, false, IndexComponentSelectorPrivilege.DATA, indexPattern) + ); + } + + public void testBuildRoleWithMultipleReadFailureStoreAndReadPrivilegeSplit() { + String indexPattern = randomAlphanumericOfLength(10); + final Role role = buildRole( + roleDescriptorWithIndicesPrivileges( + "r1", + new IndicesPrivileges[] { + IndicesPrivileges.builder().indices(indexPattern).privileges("read").build(), + IndicesPrivileges.builder().indices(indexPattern).privileges("read_failure_store").build() } + ) + ); + assertHasIndexGroups( + role.indices(), + indexGroup(IndexPrivilege.READ_FAILURE_STORE, false, IndexComponentSelectorPrivilege.FAILURES, indexPattern), + indexGroup(IndexPrivilege.READ, false, IndexComponentSelectorPrivilege.DATA, indexPattern) + ); + } + + public void testBuildRoleWithAllPrivilegeIsNeverSplit() { + String indexPattern = randomAlphanumericOfLength(10); + final Role role = buildRole( + roleDescriptorWithIndicesPrivileges( + "r1", + new IndicesPrivileges[] { + IndicesPrivileges.builder().indices(indexPattern).privileges("read", "read_failure_store", "all").build(), + IndicesPrivileges.builder().indices(indexPattern).privileges("read_failure_store").build() } + ) + ); + assertHasIndexGroups( + role.indices(), + indexGroup( + IndexPrivilege.get(Set.of("read", "read_failure_store", "all")), + false, + IndexComponentSelectorPrivilege.ALL, + indexPattern + ) + ); + } + public void testCustomRolesProviderFailures() throws Exception { final FileRolesStore fileRolesStore = mock(FileRolesStore.class); doCallRealMethod().when(fileRolesStore).accept(anySet(), anyActionListener()); @@ -3257,6 +3346,10 @@ private RoleDescriptor roleDescriptorWithRemoteIndicesPrivileges( return roleDescriptorWithIndicesPrivileges(name, rips, null); } + private RoleDescriptor roleDescriptorWithIndicesPrivileges(final String name, final IndicesPrivileges[] ips) { + return roleDescriptorWithIndicesPrivileges(name, null, ips); + } + private RoleDescriptor roleDescriptorWithIndicesPrivileges( final String name, final RoleDescriptor.RemoteIndicesPrivileges[] rips, @@ -3357,6 +3450,12 @@ private void assertHasRemoteIndexGroupsForClusters( ); } + @SafeVarargs + @SuppressWarnings("varargs") + private void assertHasIndexGroups(final IndicesPermission permission, final Matcher... matchers) { + assertThat(permission.groups(), arrayContainingInAnyOrder(matchers)); + } + private static Matcher indexGroup(final String... indices) { return indexGroup(IndexPrivilege.READ, false, indices); } @@ -3371,6 +3470,23 @@ private static Matcher indexGroup( allowRestrictedIndices, null, new FieldPermissionsDefinition.FieldGrantExcludeGroup(null, null), + IndexComponentSelectorPrivilege.DATA, + indices + ); + } + + private static Matcher indexGroup( + final IndexPrivilege privilege, + final boolean allowRestrictedIndices, + final IndexComponentSelectorPrivilege selectorPrivilege, + final String... indices + ) { + return indexGroup( + privilege, + allowRestrictedIndices, + null, + new FieldPermissionsDefinition.FieldGrantExcludeGroup(null, null), + selectorPrivilege, indices ); } @@ -3380,6 +3496,7 @@ private static Matcher indexGroup( final boolean allowRestrictedIndices, @Nullable final String query, final FieldPermissionsDefinition.FieldGrantExcludeGroup flsGroup, + IndexComponentSelectorPrivilege selectorPrivilege, final String... indices ) { return new BaseMatcher<>() { @@ -3393,6 +3510,7 @@ public boolean matches(Object o) { && equalTo(privilege).matches(group.privilege()) && equalTo(allowRestrictedIndices).matches(group.allowRestrictedIndices()) && equalTo(new FieldPermissions(new FieldPermissionsDefinition(Set.of(flsGroup)))).matches(group.getFieldPermissions()) + && equalTo(selectorPrivilege).matches(group.getSelectorPrivilege()) && arrayContaining(indices).matches(group.indices()); } @@ -3410,6 +3528,8 @@ public void describeTo(Description description) { + query + ", fieldGrantExcludeGroup=" + flsGroup + + ", selectorPrivilege=" + + selectorPrivilege + '}' ); } From 125e808ea813ea86ebd93beca2b96025a9bf96a1 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 17 Feb 2025 12:54:29 +0100 Subject: [PATCH 011/131] Test fixes --- .../security/get-builtin-privileges.asciidoc | 2 + .../authz/permission/IndicesPermission.java | 14 +------ .../core/security/user/InternalUsers.java | 12 ++---- .../authz/AuthorizationServiceTests.java | 20 ++++++++-- .../xpack/security/authz/RBACEngineTests.java | 2 +- .../accesscontrol/IndicesPermissionTests.java | 37 ++++++++++++++----- 6 files changed, 52 insertions(+), 35 deletions(-) diff --git a/docs/reference/rest-api/security/get-builtin-privileges.asciidoc b/docs/reference/rest-api/security/get-builtin-privileges.asciidoc index 08a03a5b1e830..33d052a7ba2d1 100644 --- a/docs/reference/rest-api/security/get-builtin-privileges.asciidoc +++ b/docs/reference/rest-api/security/get-builtin-privileges.asciidoc @@ -148,6 +148,7 @@ A successful call returns an object with "cluster", "index", and "remote_cluster "maintenance", "manage", "manage_data_stream_lifecycle", + "manage_failure_store_internal", "manage_follow_index", "manage_ilm", "manage_leader_index", @@ -155,6 +156,7 @@ A successful call returns an object with "cluster", "index", and "remote_cluster "none", "read", "read_cross_cluster", + "read_failure_store", "view_index_metadata", "write" ], diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 5b2c6360a5d5a..85cfd7d8a9d25 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -76,17 +76,6 @@ public Builder(RestrictedIndices restrictedIndices) { this.restrictedIndices = restrictedIndices; } - // TODO remove me - public Builder addGroup( - IndexPrivilege privilege, - FieldPermissions fieldPermissions, - @Nullable Set query, - boolean allowRestrictedIndices, - String... indices - ) { - return addGroup(privilege, fieldPermissions, query, allowRestrictedIndices, IndexComponentSelectorPrivilege.DATA, indices); - } - public Builder addGroup( IndexPrivilege privilege, FieldPermissions fieldPermissions, @@ -95,6 +84,7 @@ public Builder addGroup( IndexComponentSelectorPrivilege selectorPrivilege, String... indices ) { + assert privilege != IndexPrivilege.ALL || selectorPrivilege.isTotal() : "all privilege must be associated with all selector"; groups.add( new Group(privilege, fieldPermissions, query, allowRestrictedIndices, restrictedIndices, selectorPrivilege, indices) ); @@ -917,7 +907,7 @@ boolean isTotal() { && privilege == IndexPrivilege.ALL && query == null && false == fieldPermissions.hasFieldLevelSecurity() - // TODO add selectorPrivilege here in a follow PR handling authorization + // TODO ensure we want this now, not in a follow up instead && selectorPrivilege.isTotal(); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/InternalUsers.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/InternalUsers.java index 0b8270f5175fb..9a0b17b22369c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/InternalUsers.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/InternalUsers.java @@ -30,7 +30,6 @@ import org.elasticsearch.index.reindex.ReindexAction; import org.elasticsearch.xpack.core.XPackPlugin; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; -import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.support.MetadataUtils; import java.util.Collection; @@ -162,8 +161,7 @@ public class InternalUsers { IndicesStatsAction.NAME + "*", TransportUpdateSettingsAction.TYPE.name(), DownsampleAction.NAME, - TransportAddIndexBlockAction.TYPE.name(), - IndexPrivilege.MANAGE_FAILURE_STORE_INTERNAL.getSingleName() + TransportAddIndexBlockAction.TYPE.name() ) .allowRestrictedIndices(false) .build(), @@ -182,8 +180,7 @@ public class InternalUsers { IndicesStatsAction.NAME + "*", TransportUpdateSettingsAction.TYPE.name(), DownsampleAction.NAME, - TransportAddIndexBlockAction.TYPE.name(), - IndexPrivilege.MANAGE_FAILURE_STORE_INTERNAL.getSingleName() + TransportAddIndexBlockAction.TYPE.name() ) .allowRestrictedIndices(true) .build() }, @@ -223,8 +220,7 @@ public class InternalUsers { TransportBulkAction.NAME, TransportIndexAction.NAME, TransportSearchScrollAction.TYPE.name(), - ModifyDataStreamsAction.NAME, - IndexPrivilege.MANAGE_FAILURE_STORE_INTERNAL.getSingleName() + ModifyDataStreamsAction.NAME ) .allowRestrictedIndices(false) .build() }, @@ -248,7 +244,7 @@ public class InternalUsers { new RoleDescriptor.IndicesPrivileges[] { RoleDescriptor.IndicesPrivileges.builder() .indices("*") - .privileges(LazyRolloverAction.NAME, IndexPrivilege.MANAGE_FAILURE_STORE_INTERNAL.getSingleName()) + .privileges(LazyRolloverAction.NAME) .allowRestrictedIndices(true) .build() }, null, diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java index 4f0e15eec7708..0f32b122dd9e5 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java @@ -987,7 +987,10 @@ public void testUnknownRoleCausesDenial() { ) ) ); - assertThat(securityException, throwableWithMessage(containsString("this action is granted by the index privileges [read,all]"))); + assertThat( + securityException, + throwableWithMessage(containsString("this action is granted by the index privileges [read,read_failure_store,all]")) + ); verify(auditTrail).accessDenied(eq(requestId), eq(authentication), eq(action), eq(request), authzInfoRoles(Role.EMPTY.names())); verifyNoMoreInteractions(auditTrail); @@ -1033,7 +1036,10 @@ public void testServiceAccountDenial() { throwableWithMessage(containsString("[" + action + "] is unauthorized for service account [" + serviceUser.principal() + "]")) ); verify(auditTrail).accessDenied(eq(requestId), eq(authentication), eq(action), eq(request), authzInfoRoles(role.names())); - assertThat(securityException, throwableWithMessage(containsString("this action is granted by the index privileges [read,all]"))); + assertThat( + securityException, + throwableWithMessage(containsString("this action is granted by the index privileges [read,read_failure_store,all]")) + ); verifyNoMoreInteractions(auditTrail); } @@ -1083,7 +1089,10 @@ public void testThatRoleWithNoIndicesIsDenied() { containsString("[" + action + "] is unauthorized" + " for user [test user]" + " with effective roles [no_indices]") ) ); - assertThat(securityException, throwableWithMessage(containsString("this action is granted by the index privileges [read,all]"))); + assertThat( + securityException, + throwableWithMessage(containsString("this action is granted by the index privileges [read,read_failure_store,all]")) + ); verify(auditTrail).accessDenied( eq(requestId), @@ -1536,7 +1545,10 @@ public void testDenialErrorMessagesForSearchAction() { assertThat(securityException, throwableWithMessage(containsString("other-4"))); assertThat(securityException, throwableWithMessage(not(containsString("all-1")))); assertThat(securityException, throwableWithMessage(not(containsString("read-2")))); - assertThat(securityException, throwableWithMessage(containsString(", this action is granted by the index privileges [read,all]"))); + assertThat( + securityException, + throwableWithMessage(containsString(", this action is granted by the index privileges [read,read_failure_store,all]")) + ); } public void testDenialErrorMessagesForBulkIngest() throws Exception { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java index b76a7b6ac5b7b..a3861c41606ed 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java @@ -1809,7 +1809,7 @@ public void testGetRoleDescriptorsForRemoteClusterForReservedRoles() { IndicesPrivileges.builder().indices("*").privileges("all").allowRestrictedIndices(false).build(), IndicesPrivileges.builder() .indices("*") - .privileges("monitor", "read", "read_cross_cluster", "view_index_metadata") + .privileges("monitor", "read", "read_cross_cluster", "read_failure_store", "view_index_metadata") .allowRestrictedIndices(true) .build() }, null, diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java index 3b5b3ba6d60dc..29e89ca4d635f 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java @@ -78,7 +78,7 @@ public void testAuthorize() { query, IndexPrivilege.ALL, allowRestrictedIndices6, - IndexComponentSelectorPrivilege.DATA, + IndexComponentSelectorPrivilege.ALL, "_index" ).build(); IndicesAccessControl permissions = role.authorize( @@ -103,7 +103,7 @@ public void testAuthorize() { null, IndexPrivilege.ALL, allowRestrictedIndices5, - IndexComponentSelectorPrivilege.DATA, + IndexComponentSelectorPrivilege.ALL, "_index" ).build(); permissions = role.authorize(TransportSearchAction.TYPE.name(), Sets.newHashSet("_index"), md, fieldPermissionsCache); @@ -121,7 +121,7 @@ public void testAuthorize() { query, IndexPrivilege.ALL, allowRestrictedIndices4, - IndexComponentSelectorPrivilege.DATA, + IndexComponentSelectorPrivilege.ALL, "_index" ).build(); permissions = role.authorize(TransportSearchAction.TYPE.name(), Sets.newHashSet("_index"), md, fieldPermissionsCache); @@ -140,7 +140,7 @@ public void testAuthorize() { query, IndexPrivilege.ALL, allowRestrictedIndices3, - IndexComponentSelectorPrivilege.DATA, + IndexComponentSelectorPrivilege.ALL, "_alias" ).build(); permissions = role.authorize(TransportSearchAction.TYPE.name(), Sets.newHashSet("_alias"), md, fieldPermissionsCache); @@ -172,7 +172,7 @@ public void testAuthorize() { query, IndexPrivilege.ALL, allowRestrictedIndices2, - IndexComponentSelectorPrivilege.DATA, + IndexComponentSelectorPrivilege.ALL, "_alias" ).build(); permissions = role.authorize(TransportSearchAction.TYPE.name(), Sets.newHashSet("_alias"), md, fieldPermissionsCache); @@ -204,7 +204,7 @@ public void testAuthorize() { fooQuery, IndexPrivilege.ALL, allowRestrictedIndices, - IndexComponentSelectorPrivilege.DATA, + IndexComponentSelectorPrivilege.ALL, "_alias" ); FieldPermissions fieldPermissions1 = new FieldPermissions(fieldPermissionDef(allFields, null)); @@ -214,7 +214,7 @@ public void testAuthorize() { query, IndexPrivilege.ALL, allowRestrictedIndices1, - IndexComponentSelectorPrivilege.DATA, + IndexComponentSelectorPrivilege.ALL, "_alias" ).build(); permissions = role.authorize(TransportSearchAction.TYPE.name(), Sets.newHashSet("_alias"), md, fieldPermissionsCache); @@ -256,7 +256,7 @@ public void testAuthorizeMultipleGroupsMixedDls() { query, IndexPrivilege.ALL, allowRestrictedIndices, - IndexComponentSelectorPrivilege.DATA, + IndexComponentSelectorPrivilege.ALL, "_index" ); FieldPermissions fieldPermissions1 = new FieldPermissions(fieldPermissionDef(null, null)); @@ -266,7 +266,7 @@ public void testAuthorizeMultipleGroupsMixedDls() { null, IndexPrivilege.ALL, allowRestrictedIndices1, - IndexComponentSelectorPrivilege.DATA, + IndexComponentSelectorPrivilege.ALL, "*" ).build(); IndicesAccessControl permissions = role.authorize( @@ -330,6 +330,7 @@ public void testCorePermissionAuthorize() { FieldPermissions.DEFAULT, null, randomBoolean(), + IndexComponentSelectorPrivilege.ALL, "a1" ) .addGroup( @@ -337,6 +338,7 @@ public void testCorePermissionAuthorize() { new FieldPermissions(fieldPermissionDef(null, new String[] { "denied_field" })), null, randomBoolean(), + IndexComponentSelectorPrivilege.DATA, "a1" ) .build(); @@ -362,6 +364,7 @@ public void testCorePermissionAuthorize() { FieldPermissions.DEFAULT, null, randomBoolean(), + IndexComponentSelectorPrivilege.ALL, "a1" ) .addGroup( @@ -369,6 +372,7 @@ public void testCorePermissionAuthorize() { new FieldPermissions(fieldPermissionDef(null, new String[] { "denied_field" })), null, randomBoolean(), + IndexComponentSelectorPrivilege.ALL, "a1" ) .addGroup( @@ -376,6 +380,7 @@ public void testCorePermissionAuthorize() { new FieldPermissions(fieldPermissionDef(new String[] { "*_field" }, new String[] { "denied_field" })), null, randomBoolean(), + IndexComponentSelectorPrivilege.ALL, "a2" ) .addGroup( @@ -383,6 +388,7 @@ public void testCorePermissionAuthorize() { new FieldPermissions(fieldPermissionDef(new String[] { "*_field2" }, new String[] { "denied_field2" })), null, randomBoolean(), + IndexComponentSelectorPrivilege.ALL, "a2" ) .build(); @@ -444,6 +450,7 @@ public void testSecurityIndicesPermissions() { FieldPermissions.DEFAULT, null, false, + IndexComponentSelectorPrivilege.ALL, "*" ).build(); IndicesAccessControl iac = indicesPermission.authorize( @@ -464,6 +471,7 @@ public void testSecurityIndicesPermissions() { FieldPermissions.DEFAULT, null, true, + IndexComponentSelectorPrivilege.ALL, "*" ).build(); iac = indicesPermission.authorize( @@ -494,6 +502,7 @@ public void testAsyncSearchIndicesPermissions() { FieldPermissions.DEFAULT, null, false, + IndexComponentSelectorPrivilege.ALL, "*" ).build(); IndicesAccessControl iac = indicesPermission.authorize( @@ -512,6 +521,7 @@ public void testAsyncSearchIndicesPermissions() { FieldPermissions.DEFAULT, null, true, + IndexComponentSelectorPrivilege.ALL, "*" ).build(); iac = indicesPermission.authorize( @@ -549,6 +559,7 @@ public void testAuthorizationForBackingIndices() { FieldPermissions.DEFAULT, null, false, + IndexComponentSelectorPrivilege.DATA, dataStreamName ).build(); IndicesAccessControl iac = indicesPermission.authorize( @@ -569,6 +580,7 @@ public void testAuthorizationForBackingIndices() { FieldPermissions.DEFAULT, null, false, + IndexComponentSelectorPrivilege.DATA, dataStreamName ).build(); iac = indicesPermission.authorize( @@ -614,6 +626,7 @@ public void testAuthorizationForMappingUpdates() { FieldPermissions.DEFAULT, null, randomBoolean(), + IndexComponentSelectorPrivilege.DATA, "test*" ) .addGroup( @@ -621,6 +634,7 @@ public void testAuthorizationForMappingUpdates() { new FieldPermissions(fieldPermissionDef(null, new String[] { "denied_field" })), null, randomBoolean(), + IndexComponentSelectorPrivilege.DATA, "test_write*" ) .build(); @@ -719,6 +733,7 @@ public void testIndicesPermissionHasFieldOrDocumentLevelSecurity() { fieldPermissions, queries, randomBoolean(), + IndexComponentSelectorPrivilege.ALL, "*" ).build(); assertThat(indicesPermission1.hasFieldOrDocumentLevelSecurity(), is(true)); @@ -729,6 +744,7 @@ public void testIndicesPermissionHasFieldOrDocumentLevelSecurity() { FieldPermissions.DEFAULT, null, true, + IndexComponentSelectorPrivilege.ALL, "*" ).build(); assertThat(indicesPermission2.hasFieldOrDocumentLevelSecurity(), is(false)); @@ -739,8 +755,9 @@ public void testIndicesPermissionHasFieldOrDocumentLevelSecurity() { FieldPermissions.DEFAULT, null, true, + IndexComponentSelectorPrivilege.ALL, "*" - ).addGroup(IndexPrivilege.NONE, fieldPermissions, queries, randomBoolean(), "*").build(); + ).addGroup(IndexPrivilege.NONE, fieldPermissions, queries, randomBoolean(), IndexComponentSelectorPrivilege.DATA, "*").build(); assertThat(indicesPermission3.hasFieldOrDocumentLevelSecurity(), is(false)); } From 671cf70adf5d463be567de1a3c01b9a90f178701 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 17 Feb 2025 13:09:39 +0100 Subject: [PATCH 012/131] Test fixes --- .../xpack/remotecluster/RemoteClusterSecurityEsqlIT.java | 6 +++--- .../RemoteClusterSecurityFcActionAuthorizationIT.java | 4 ++-- .../security/apikey/ApiKeyWorkflowsRestrictionRestIT.java | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityEsqlIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityEsqlIT.java index dcf993ea4ce7a..b88107f6b4bab 100644 --- a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityEsqlIT.java +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityEsqlIT.java @@ -702,7 +702,7 @@ public void testCrossClusterQueryWithOnlyRemotePrivs() throws Exception { error.getMessage(), containsString( "action [indices:data/read/esql] is unauthorized for user [remote_search_user] with effective roles [remote_search], " - + "this action is granted by the index privileges [read,read_cross_cluster,all]" + + "this action is granted by the index privileges [read,read_failure_store,read_cross_cluster,all]" ) ); @@ -718,7 +718,7 @@ public void testCrossClusterQueryWithOnlyRemotePrivs() throws Exception { error.getMessage(), containsString( "action [indices:data/read/esql] is unauthorized for user [remote_search_user] with effective roles " - + "[remote_search], this action is granted by the index privileges [read,read_cross_cluster,all]" + + "[remote_search], this action is granted by the index privileges [read,read_failure_store,read_cross_cluster,all]" ) ); @@ -733,7 +733,7 @@ public void testCrossClusterQueryWithOnlyRemotePrivs() throws Exception { error.getMessage(), containsString( "action [indices:data/read/esql] is unauthorized for user [remote_search_user] with effective roles " - + "[remote_search], this action is granted by the index privileges [read,read_cross_cluster,all]" + + "[remote_search], this action is granted by the index privileges [read,read_failure_store,read_cross_cluster,all]" ) ); } diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityFcActionAuthorizationIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityFcActionAuthorizationIT.java index 793313e238651..5477687d22cf6 100644 --- a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityFcActionAuthorizationIT.java +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityFcActionAuthorizationIT.java @@ -435,7 +435,7 @@ public void testUpdateCrossClusterApiKey() throws Exception { + "for user [foo] with assigned roles [role] authenticated by API key id [" + apiKeyId + "] of user [test_user] on indices [index], this action is granted by the index privileges " - + "[view_index_metadata,manage,read,all]" + + "[view_index_metadata,manage,manage_failure_store_internal,read,read_failure_store,all]" ) ); @@ -483,7 +483,7 @@ public void testUpdateCrossClusterApiKey() throws Exception { + "for user [foo] with assigned roles [role] authenticated by API key id [" + apiKeyId + "] of user [test_user] on indices [index], this action is granted by the index privileges " - + "[view_index_metadata,manage,read,all]" + + "[view_index_metadata,manage,manage_failure_store_internal,read,read_failure_store,all]" ) ); } diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyWorkflowsRestrictionRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyWorkflowsRestrictionRestIT.java index e0b9d9ecd0ede..bebc4a1e89d99 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyWorkflowsRestrictionRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyWorkflowsRestrictionRestIT.java @@ -187,7 +187,7 @@ public void testWorkflowsRestrictionAllowsAccess() throws IOException { + apiKeyId + "] of user [" + WORKFLOW_API_KEY_USER - + "] on indices [my-app-b], this action is granted by the index privileges [read,all]" + + "] on indices [my-app-b], this action is granted by the index privileges [read,read_failure_store,all]" ) ); assertThat(e.getMessage(), not(containsString("access restricted by workflow"))); From 1988a218eb687de4d639cfb71e8cfcc3d032975c Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 17 Feb 2025 13:10:51 +0100 Subject: [PATCH 013/131] Fix more --- .../search/56_search_application_search_with_apikey.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/search/56_search_application_search_with_apikey.yml b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/search/56_search_application_search_with_apikey.yml index b11b5fb4f3b6b..7a7fb19823860 100644 --- a/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/search/56_search_application_search_with_apikey.yml +++ b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/search/56_search_application_search_with_apikey.yml @@ -362,7 +362,7 @@ teardown: name: test-search-application - match: { status: 403 } - match: { error.type: security_exception } - - match: { error.reason: "action [indices:data/read/xpack/application/search_application/search] is unauthorized for API key id [${api_key_id_1}] of user [entsearch-user] on indices [test-search-application], this action is granted by the index privileges [read,all]" } + - match: { error.reason: "action [indices:data/read/xpack/application/search_application/search] is unauthorized for API key id [${api_key_id_1}] of user [entsearch-user] on indices [test-search-application], this action is granted by the index privileges [read,read_failure_store,all]" } # Query Search Application 'test-search-application-1' with new API key (api_key_encoded_1) should be allowed: - do: From c8fc1f73df2abb7efbabb2e04cbdcacbd9a39a96 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 17 Feb 2025 15:07:58 +0100 Subject: [PATCH 014/131] Keep selector with index privilege --- .../authz/permission/IndicesPermission.java | 36 +---- .../permission/RemoteIndicesPermission.java | 3 - .../core/security/authz/permission/Role.java | 37 +---- .../ConfigurableClusterPrivileges.java | 1 - .../IndexComponentSelectorPrivilege.java | 21 +-- .../authz/privilege/IndexPrivilege.java | 54 ++++++- .../authz/store/ReservedRolesStore.java | 10 +- .../authz/permission/LimitedRoleTests.java | 14 +- ..._search_application_search_with_apikey.yml | 2 +- .../RemoteClusterSecurityEsqlIT.java | 6 +- ...lusterSecurityFcActionAuthorizationIT.java | 4 +- .../ApiKeyWorkflowsRestrictionRestIT.java | 2 +- .../authz/store/CompositeRolesStore.java | 26 +--- .../authz/AuthorizationServiceTests.java | 20 +-- .../authz/AuthorizedIndicesTests.java | 5 +- .../xpack/security/authz/RBACEngineTests.java | 139 +++++------------- .../accesscontrol/IndicesPermissionTests.java | 139 +++--------------- .../QueryableBuiltInRolesUtilsTests.java | 7 +- 18 files changed, 163 insertions(+), 363 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 85cfd7d8a9d25..7aa249a956a56 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -81,13 +81,9 @@ public Builder addGroup( FieldPermissions fieldPermissions, @Nullable Set query, boolean allowRestrictedIndices, - IndexComponentSelectorPrivilege selectorPrivilege, String... indices ) { - assert privilege != IndexPrivilege.ALL || selectorPrivilege.isTotal() : "all privilege must be associated with all selector"; - groups.add( - new Group(privilege, fieldPermissions, query, allowRestrictedIndices, restrictedIndices, selectorPrivilege, indices) - ); + groups.add(new Group(privilege, fieldPermissions, query, allowRestrictedIndices, restrictedIndices, indices)); return this; } @@ -808,26 +804,6 @@ public static class Group { // 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; - private final IndexComponentSelectorPrivilege selectorPrivilege; - - public Group( - IndexPrivilege privilege, - FieldPermissions fieldPermissions, - @Nullable Set query, - boolean allowRestrictedIndices, - RestrictedIndices restrictedIndices, - String... indices - ) { - this( - privilege, - fieldPermissions, - query, - allowRestrictedIndices, - restrictedIndices, - IndexComponentSelectorPrivilege.DATA, - indices - ); - } public Group( IndexPrivilege privilege, @@ -835,7 +811,6 @@ public Group( @Nullable Set query, boolean allowRestrictedIndices, RestrictedIndices restrictedIndices, - IndexComponentSelectorPrivilege selectorPrivilege, String... indices ) { assert indices.length != 0; @@ -856,7 +831,6 @@ public Group( } this.fieldPermissions = Objects.requireNonNull(fieldPermissions); this.query = query; - this.selectorPrivilege = selectorPrivilege; } public IndexPrivilege privilege() { @@ -890,7 +864,7 @@ boolean hasQuery() { } public IndexComponentSelectorPrivilege getSelectorPrivilege() { - return selectorPrivilege; + return privilege.getSelectorPrivilege(); } public boolean allowRestrictedIndices() { @@ -906,9 +880,7 @@ boolean isTotal() { && indexNameMatcher.isTotal() && privilege == IndexPrivilege.ALL && query == null - && false == fieldPermissions.hasFieldLevelSecurity() - // TODO ensure we want this now, not in a follow up instead - && selectorPrivilege.isTotal(); + && false == fieldPermissions.hasFieldLevelSecurity(); } @Override @@ -924,8 +896,6 @@ public String toString() { + query + ", allowRestrictedIndices=" + allowRestrictedIndices - + ", selectorPrivilege=" - + selectorPrivilege + '}'; } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/RemoteIndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/RemoteIndicesPermission.java index b33a5b47f58fc..2abc93997fa9d 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/RemoteIndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/RemoteIndicesPermission.java @@ -10,7 +10,6 @@ import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.core.Nullable; import org.elasticsearch.xpack.core.security.authz.RestrictedIndices; -import org.elasticsearch.xpack.core.security.authz.privilege.IndexComponentSelectorPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.support.Automatons; import org.elasticsearch.xpack.core.security.support.StringMatcher; @@ -67,8 +66,6 @@ public Builder addGroup( // Deliberately passing EMPTY here since *which* indices are restricted is determined not on the querying cluster // but rather on the fulfilling cluster new RestrictedIndices(Automatons.EMPTY), - // TODO - IndexComponentSelectorPrivilege.DATA, indices ) ); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java index 80933180cb3c5..aeebb5608f31e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java @@ -253,7 +253,7 @@ public Builder runAs(Privilege privilege) { } public Builder add(IndexPrivilege privilege, String... indices) { - return add(FieldPermissions.DEFAULT, null, privilege, false, IndexComponentSelectorPrivilege.DATA, indices); + return add(FieldPermissions.DEFAULT, null, privilege, false, indices); } public Builder add( @@ -261,12 +261,9 @@ public Builder add( Set query, IndexPrivilege privilege, boolean allowRestrictedIndices, - IndexComponentSelectorPrivilege selectorPrivilege, String... indices ) { - groups.add( - new IndicesPermissionGroupDefinition(privilege, fieldPermissions, query, allowRestrictedIndices, selectorPrivilege, indices) - ); + groups.add(new IndicesPermissionGroupDefinition(privilege, fieldPermissions, query, allowRestrictedIndices, indices)); return this; } @@ -279,17 +276,7 @@ public Builder addRemoteIndicesGroup( final String... indices ) { remoteIndicesGroups.computeIfAbsent(remoteClusterAliases, k -> new ArrayList<>()) - .add( - new IndicesPermissionGroupDefinition( - privilege, - fieldPermissions, - query, - allowRestrictedIndices, - // TODO - IndexComponentSelectorPrivilege.DATA, - indices - ) - ); + .add(new IndicesPermissionGroupDefinition(privilege, fieldPermissions, query, allowRestrictedIndices, indices)); return this; } @@ -329,7 +316,6 @@ public SimpleRole build() { group.fieldPermissions, group.query, group.allowRestrictedIndices, - group.selectorPrivilege, group.indices ); } @@ -378,7 +364,6 @@ private static class IndicesPermissionGroupDefinition { private final FieldPermissions fieldPermissions; private final @Nullable Set query; private final boolean allowRestrictedIndices; - private final IndexComponentSelectorPrivilege selectorPrivilege; private final String[] indices; private IndicesPermissionGroupDefinition( @@ -386,14 +371,12 @@ private IndicesPermissionGroupDefinition( FieldPermissions fieldPermissions, @Nullable Set query, boolean allowRestrictedIndices, - IndexComponentSelectorPrivilege selectorPrivilege, String... indices ) { this.privilege = privilege; this.fieldPermissions = fieldPermissions; this.query = query; this.allowRestrictedIndices = allowRestrictedIndices; - this.selectorPrivilege = selectorPrivilege; this.indices = indices; } } @@ -428,18 +411,14 @@ static SimpleRole buildFromRoleDescriptor( ); Set query = indexPrivilege.getQuery() == null ? null : Collections.singleton(indexPrivilege.getQuery()); boolean allowRestrictedIndices = indexPrivilege.allowRestrictedIndices(); - Map> split = IndexComponentSelectorPrivilege.groupBySelector( + Map> split = IndexComponentSelectorPrivilege.partitionBySelectorPrivilege( indexPrivilege.getPrivileges() ); for (var entry : split.entrySet()) { - builder.add( - fieldPermissions, - query, - IndexPrivilege.get(entry.getValue()), - allowRestrictedIndices, - entry.getKey(), - indexPrivilege.getIndices() - ); + IndexPrivilege privilege = IndexPrivilege.get(entry.getValue()); + assert privilege.getSelectorPrivilege() == entry.getKey() + : "expected selector privilege to match the partitioned privilege"; + builder.add(fieldPermissions, query, privilege, allowRestrictedIndices, indexPrivilege.getIndices()); } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java index b8f5c3b67ab41..cf2ffbfba8692 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java @@ -420,7 +420,6 @@ public ManageRolesPrivilege(List manageRolesInd null, false, // TODO - IndexComponentSelectorPrivilege.DATA, indexPatternPrivilege.indexPatterns() ); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java index 4f8f6b66f9690..ce4410bcfe808 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java @@ -27,11 +27,6 @@ public record IndexComponentSelectorPrivilege(String name, Predicate FAILURE_STORE_PRIVILEGE_NAMES = Set.of( - IndexPrivilege.READ_FAILURE_STORE, - IndexPrivilege.MANAGE_FAILURE_STORE_INTERNAL - ); - public boolean grants(IndexComponentSelector selector) { return predicate.test(selector); } @@ -44,11 +39,11 @@ public static Set get(Set indexPrivileg return indexPrivileges.stream().map(IndexComponentSelectorPrivilege::get).collect(Collectors.toSet()); } - public static Map> groupBySelector(String... indexPrivileges) { - return groupBySelector(Set.of(indexPrivileges)); + public static Map> partitionBySelectorPrivilege(String... indexPrivileges) { + return partitionBySelectorPrivilege(Set.of(indexPrivileges)); } - public static Map> groupBySelector(Set indexPrivileges) { + public static Map> partitionBySelectorPrivilege(Set indexPrivileges) { final Set dataAccessPrivileges = new HashSet<>(); final Set failuresAccessPrivileges = new HashSet<>(); @@ -82,14 +77,6 @@ public static Map> groupBySelector( private static IndexComponentSelectorPrivilege get(String indexPrivilegeName) { final IndexPrivilege indexPrivilege = IndexPrivilege.getNamedOrNull(indexPrivilegeName); // `null` means we got a raw action instead of a named privilege; all raw actions are treated as data access - if (indexPrivilege == null) { - return DATA; - } else if (indexPrivilege == IndexPrivilege.ALL) { - return ALL; - } else if (FAILURE_STORE_PRIVILEGE_NAMES.contains(indexPrivilege)) { - return FAILURES; - } else { - return DATA; - } + return indexPrivilege == null ? DATA : indexPrivilege.getSelectorPrivilege(); } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java index 68954b43f79ab..53538cfe65343 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java @@ -178,12 +178,16 @@ public final class IndexPrivilege extends Privilege { ); public static final IndexPrivilege NONE = new IndexPrivilege("none", Automatons.EMPTY); - public static final IndexPrivilege ALL = new IndexPrivilege("all", ALL_AUTOMATON); - // TODO explain - public static final IndexPrivilege READ_FAILURE_STORE = new IndexPrivilege("read_failure_store", READ_AUTOMATON); + public static final IndexPrivilege ALL = new IndexPrivilege("all", ALL_AUTOMATON, IndexComponentSelectorPrivilege.ALL); + public static final IndexPrivilege READ_FAILURE_STORE = new IndexPrivilege( + "read_failure_store", + READ_AUTOMATON, + IndexComponentSelectorPrivilege.FAILURES + ); public static final IndexPrivilege MANAGE_FAILURE_STORE_INTERNAL = new IndexPrivilege( "manage_failure_store_internal", - MANAGE_AUTOMATON + MANAGE_AUTOMATON, + IndexComponentSelectorPrivilege.FAILURES ); public static final IndexPrivilege READ = new IndexPrivilege("read", READ_AUTOMATON); public static final IndexPrivilege READ_CROSS_CLUSTER = new IndexPrivilege("read_cross_cluster", READ_CROSS_CLUSTER_AUTOMATON); @@ -253,12 +257,23 @@ public final class IndexPrivilege extends Privilege { private static final ConcurrentHashMap, IndexPrivilege> CACHE = new ConcurrentHashMap<>(); + private final IndexComponentSelectorPrivilege selectorPrivilege; + private IndexPrivilege(String name, Automaton automaton) { - super(Collections.singleton(name), automaton); + this(Collections.singleton(name), automaton); + } + + private IndexPrivilege(String name, Automaton automaton, IndexComponentSelectorPrivilege selectorPrivilege) { + this(Collections.singleton(name), automaton, selectorPrivilege); } private IndexPrivilege(Set name, Automaton automaton) { + this(name, automaton, IndexComponentSelectorPrivilege.DATA); + } + + private IndexPrivilege(Set name, Automaton automaton, IndexComponentSelectorPrivilege selectorPrivilege) { super(name, automaton); + this.selectorPrivilege = selectorPrivilege; } public static IndexPrivilege get(Set name) { @@ -271,6 +286,10 @@ public static IndexPrivilege get(Set name) { }); } + public IndexComponentSelectorPrivilege getSelectorPrivilege() { + return selectorPrivilege; + } + public String getSingleName() { if (name().size() != 1) { throw new IllegalStateException("Expected a single name, but got: " + name()); @@ -291,6 +310,7 @@ private static IndexPrivilege resolve(Set name) { Set actions = new HashSet<>(); Set automata = new HashSet<>(); + Set selectorPrivileges = new HashSet<>(); for (String part : name) { part = part.toLowerCase(Locale.ROOT); if (ACTION_MATCHER.test(part)) { @@ -301,6 +321,7 @@ private static IndexPrivilege resolve(Set name) { return indexPrivilege; } else if (indexPrivilege != null) { automata.add(indexPrivilege.automaton); + selectorPrivileges.add(indexPrivilege.getSelectorPrivilege()); } else { String errorMessage = "unknown index privilege [" + part @@ -317,8 +338,20 @@ private static IndexPrivilege resolve(Set name) { if (actions.isEmpty() == false) { automata.add(patterns(actions)); + selectorPrivileges.add(IndexComponentSelectorPrivilege.DATA); + } + + for (IndexComponentSelectorPrivilege selectorPrivilege : selectorPrivileges) { + if (selectorPrivilege == IndexComponentSelectorPrivilege.ALL) { + return new IndexPrivilege(name, unionAndMinimize(automata), IndexComponentSelectorPrivilege.ALL); + } + } + if (selectorPrivileges.size() != 1) { + // TODO assertion and make this clearer + throw new IllegalArgumentException("Cannot mix different selector privileges in a single index privilege for [" + name + "]"); } - return new IndexPrivilege(name, unionAndMinimize(automata)); + + return new IndexPrivilege(name, unionAndMinimize(automata), selectorPrivileges.iterator().next()); } static Map values() { @@ -336,6 +369,13 @@ public static Set names() { * @see Privilege#sortByAccessLevel */ public static Collection findPrivilegesThatGrant(String action) { - return VALUES.entrySet().stream().filter(e -> e.getValue().predicate.test(action)).map(e -> e.getKey()).toList(); + return VALUES.entrySet() + .stream() + .filter(e -> e.getValue().predicate.test(action)) + // Filter out the failure store privileges since these are confusing w.r.t. authorization failure messages are a handled + // separately + .filter(e -> false == (e.getValue().getSelectorPrivilege() == IndexComponentSelectorPrivilege.FAILURES)) + .map(Map.Entry::getKey) + .toList(); } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java index b0bffec41d35d..ba3642dbf05a4 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java @@ -11,6 +11,7 @@ import org.elasticsearch.action.admin.cluster.repositories.get.GetRepositoriesAction; import org.elasticsearch.action.admin.indices.alias.TransportIndicesAliasesAction; import org.elasticsearch.action.admin.indices.rollover.RolloverAction; +import org.elasticsearch.cluster.metadata.DataStream; import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.util.set.Sets; @@ -78,7 +79,12 @@ public class ReservedRolesStore implements BiConsumer, ActionListene RoleDescriptor.IndicesPrivileges.builder().indices("*").privileges("all").allowRestrictedIndices(false).build(), RoleDescriptor.IndicesPrivileges.builder() .indices("*") - .privileges("monitor", "read", "view_index_metadata", "read_cross_cluster", "read_failure_store") + .privileges( + // TODO are there edge-cases where this is a bad idea? + DataStream.isFailureStoreFeatureFlagEnabled() + ? new String[] { "monitor", "read", "view_index_metadata", "read_cross_cluster", "read_failure_store" } + : new String[] { "monitor", "read", "view_index_metadata", "read_cross_cluster" } + ) .allowRestrictedIndices(true) .build() }, new RoleDescriptor.ApplicationResourcePrivileges[] { @@ -95,7 +101,7 @@ public class ReservedRolesStore implements BiConsumer, ActionListene new RoleDescriptor.RemoteIndicesPrivileges( RoleDescriptor.IndicesPrivileges.builder() .indices("*") - .privileges("monitor", "read", "view_index_metadata", "read_cross_cluster", "read_failure_store") + .privileges("monitor", "read", "view_index_metadata", "read_cross_cluster") .allowRestrictedIndices(true) .build(), "*" diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java index 630ff45136895..a4646c0d736c5 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java @@ -36,7 +36,6 @@ import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeTests; import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilegeResolver; -import org.elasticsearch.xpack.core.security.authz.privilege.IndexComponentSelectorPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.authz.restriction.Workflow; import org.elasticsearch.xpack.core.security.authz.restriction.WorkflowResolver; @@ -761,16 +760,9 @@ public void testHasPrivilegesForIndexPatterns() { ); } { - Role.Builder builder = Role.builder(EMPTY_RESTRICTED_INDICES, "a-role"); - fromRole = builder.add( - FieldPermissions.DEFAULT, - Collections.emptySet(), - IndexPrivilege.READ, - true, - IndexComponentSelectorPrivilege.DATA, - "ind-1*", - ".security" - ).build(); + fromRole = Role.builder(EMPTY_RESTRICTED_INDICES, "a-role") + .add(FieldPermissions.DEFAULT, Collections.emptySet(), IndexPrivilege.READ, true, "ind-1*", ".security") + .build(); verifyResourcesPrivileges( fromRole, diff --git a/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/search/56_search_application_search_with_apikey.yml b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/search/56_search_application_search_with_apikey.yml index 7a7fb19823860..b11b5fb4f3b6b 100644 --- a/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/search/56_search_application_search_with_apikey.yml +++ b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/search/56_search_application_search_with_apikey.yml @@ -362,7 +362,7 @@ teardown: name: test-search-application - match: { status: 403 } - match: { error.type: security_exception } - - match: { error.reason: "action [indices:data/read/xpack/application/search_application/search] is unauthorized for API key id [${api_key_id_1}] of user [entsearch-user] on indices [test-search-application], this action is granted by the index privileges [read,read_failure_store,all]" } + - match: { error.reason: "action [indices:data/read/xpack/application/search_application/search] is unauthorized for API key id [${api_key_id_1}] of user [entsearch-user] on indices [test-search-application], this action is granted by the index privileges [read,all]" } # Query Search Application 'test-search-application-1' with new API key (api_key_encoded_1) should be allowed: - do: diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityEsqlIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityEsqlIT.java index b88107f6b4bab..dcf993ea4ce7a 100644 --- a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityEsqlIT.java +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityEsqlIT.java @@ -702,7 +702,7 @@ public void testCrossClusterQueryWithOnlyRemotePrivs() throws Exception { error.getMessage(), containsString( "action [indices:data/read/esql] is unauthorized for user [remote_search_user] with effective roles [remote_search], " - + "this action is granted by the index privileges [read,read_failure_store,read_cross_cluster,all]" + + "this action is granted by the index privileges [read,read_cross_cluster,all]" ) ); @@ -718,7 +718,7 @@ public void testCrossClusterQueryWithOnlyRemotePrivs() throws Exception { error.getMessage(), containsString( "action [indices:data/read/esql] is unauthorized for user [remote_search_user] with effective roles " - + "[remote_search], this action is granted by the index privileges [read,read_failure_store,read_cross_cluster,all]" + + "[remote_search], this action is granted by the index privileges [read,read_cross_cluster,all]" ) ); @@ -733,7 +733,7 @@ public void testCrossClusterQueryWithOnlyRemotePrivs() throws Exception { error.getMessage(), containsString( "action [indices:data/read/esql] is unauthorized for user [remote_search_user] with effective roles " - + "[remote_search], this action is granted by the index privileges [read,read_failure_store,read_cross_cluster,all]" + + "[remote_search], this action is granted by the index privileges [read,read_cross_cluster,all]" ) ); } diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityFcActionAuthorizationIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityFcActionAuthorizationIT.java index 5477687d22cf6..e816ebe1995d2 100644 --- a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityFcActionAuthorizationIT.java +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityFcActionAuthorizationIT.java @@ -435,7 +435,7 @@ public void testUpdateCrossClusterApiKey() throws Exception { + "for user [foo] with assigned roles [role] authenticated by API key id [" + apiKeyId + "] of user [test_user] on indices [index], this action is granted by the index privileges " - + "[view_index_metadata,manage,manage_failure_store_internal,read,read_failure_store,all]" + + "[view_index_metadata,manage,read,all]" ) ); @@ -483,7 +483,7 @@ public void testUpdateCrossClusterApiKey() throws Exception { + "for user [foo] with assigned roles [role] authenticated by API key id [" + apiKeyId + "] of user [test_user] on indices [index], this action is granted by the index privileges " - + "[view_index_metadata,manage,manage_failure_store_internal,read,read_failure_store,all]" + + "[view_index_metadata,manage,all]" ) ); } diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyWorkflowsRestrictionRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyWorkflowsRestrictionRestIT.java index bebc4a1e89d99..e0b9d9ecd0ede 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyWorkflowsRestrictionRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyWorkflowsRestrictionRestIT.java @@ -187,7 +187,7 @@ public void testWorkflowsRestrictionAllowsAccess() throws IOException { + apiKeyId + "] of user [" + WORKFLOW_API_KEY_USER - + "] on indices [my-app-b], this action is granted by the index privileges [read,read_failure_store,all]" + + "] on indices [my-app-b], this action is granted by the index privileges [read,all]" ) ); assertThat(e.getMessage(), not(containsString("access restricted by workflow"))); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java index d8fd89bdd6061..979e59a0379e6 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java @@ -543,38 +543,28 @@ public static void buildRoleFromDescriptors( for (Map.Entry, MergeableIndicesPrivilege> entry : indicesPrivilegesMap.entrySet()) { MergeableIndicesPrivilege indicesPrivilege = entry.getValue(); - Map> split = IndexComponentSelectorPrivilege.groupBySelector( + Map> split = IndexComponentSelectorPrivilege.partitionBySelectorPrivilege( indicesPrivilege.privileges ); FieldPermissions fieldPermissions = fieldPermissionsCache.getFieldPermissions(indicesPrivilege.fieldPermissionsDefinition); String[] indices = indicesPrivilege.indices.toArray(Strings.EMPTY_ARRAY); for (Map.Entry> privilegesBySelector : split.entrySet()) { - builder.add( - fieldPermissions, - indicesPrivilege.query, - IndexPrivilege.get(privilegesBySelector.getValue()), - false, - privilegesBySelector.getKey(), - indices - ); + IndexPrivilege indexPrivilege = IndexPrivilege.get(privilegesBySelector.getValue()); + assert indexPrivilege.getSelectorPrivilege() == privilegesBySelector.getKey(); + builder.add(fieldPermissions, indicesPrivilege.query, indexPrivilege, false, indices); } } for (Map.Entry, MergeableIndicesPrivilege> entry : restrictedIndicesPrivilegesMap.entrySet()) { MergeableIndicesPrivilege indicesPrivilege = entry.getValue(); - Map> split = IndexComponentSelectorPrivilege.groupBySelector( + Map> split = IndexComponentSelectorPrivilege.partitionBySelectorPrivilege( indicesPrivilege.privileges ); FieldPermissions fieldPermissions = fieldPermissionsCache.getFieldPermissions(indicesPrivilege.fieldPermissionsDefinition); String[] indices = indicesPrivilege.indices.toArray(Strings.EMPTY_ARRAY); for (Map.Entry> privilegesBySelector : split.entrySet()) { - builder.add( - fieldPermissions, - indicesPrivilege.query, - IndexPrivilege.get(privilegesBySelector.getValue()), - true, - privilegesBySelector.getKey(), - indices - ); + IndexPrivilege indexPrivilege = IndexPrivilege.get(privilegesBySelector.getValue()); + assert indexPrivilege.getSelectorPrivilege() == privilegesBySelector.getKey(); + builder.add(fieldPermissions, indicesPrivilege.query, IndexPrivilege.get(privilegesBySelector.getValue()), true, indices); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java index 0f32b122dd9e5..4f0e15eec7708 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java @@ -987,10 +987,7 @@ public void testUnknownRoleCausesDenial() { ) ) ); - assertThat( - securityException, - throwableWithMessage(containsString("this action is granted by the index privileges [read,read_failure_store,all]")) - ); + assertThat(securityException, throwableWithMessage(containsString("this action is granted by the index privileges [read,all]"))); verify(auditTrail).accessDenied(eq(requestId), eq(authentication), eq(action), eq(request), authzInfoRoles(Role.EMPTY.names())); verifyNoMoreInteractions(auditTrail); @@ -1036,10 +1033,7 @@ public void testServiceAccountDenial() { throwableWithMessage(containsString("[" + action + "] is unauthorized for service account [" + serviceUser.principal() + "]")) ); verify(auditTrail).accessDenied(eq(requestId), eq(authentication), eq(action), eq(request), authzInfoRoles(role.names())); - assertThat( - securityException, - throwableWithMessage(containsString("this action is granted by the index privileges [read,read_failure_store,all]")) - ); + assertThat(securityException, throwableWithMessage(containsString("this action is granted by the index privileges [read,all]"))); verifyNoMoreInteractions(auditTrail); } @@ -1089,10 +1083,7 @@ public void testThatRoleWithNoIndicesIsDenied() { containsString("[" + action + "] is unauthorized" + " for user [test user]" + " with effective roles [no_indices]") ) ); - assertThat( - securityException, - throwableWithMessage(containsString("this action is granted by the index privileges [read,read_failure_store,all]")) - ); + assertThat(securityException, throwableWithMessage(containsString("this action is granted by the index privileges [read,all]"))); verify(auditTrail).accessDenied( eq(requestId), @@ -1545,10 +1536,7 @@ public void testDenialErrorMessagesForSearchAction() { assertThat(securityException, throwableWithMessage(containsString("other-4"))); assertThat(securityException, throwableWithMessage(not(containsString("all-1")))); assertThat(securityException, throwableWithMessage(not(containsString("read-2")))); - assertThat( - securityException, - throwableWithMessage(containsString(", this action is granted by the index privileges [read,read_failure_store,all]")) - ); + assertThat(securityException, throwableWithMessage(containsString(", this action is granted by the index privileges [read,all]"))); } public void testDenialErrorMessagesForBulkIngest() throws Exception { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java index 4bce2af8c727c..0efa6b20b70c3 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java @@ -30,7 +30,6 @@ import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions; 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.IndexComponentSelectorPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; import org.elasticsearch.xpack.core.security.test.TestRestrictedIndices; @@ -189,9 +188,7 @@ public void testSecurityIndicesAreRestrictedForDefaultRole() { public void testSecurityIndicesAreNotRemovedFromUnrestrictedRole() { Role.Builder builder = Role.builder(RESTRICTED_INDICES, randomAlphaOfLength(8)); - Role role = builder.add(FieldPermissions.DEFAULT, null, IndexPrivilege.ALL, true, IndexComponentSelectorPrivilege.DATA, "*") - .cluster(Set.of("all"), Set.of()) - .build(); + Role role = builder.add(FieldPermissions.DEFAULT, null, IndexPrivilege.ALL, true, "*").cluster(Set.of("all"), Set.of()).build(); Settings indexSettings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersion.current()).build(); final String internalSecurityIndex = randomFrom( TestRestrictedIndices.INTERNAL_SECURITY_MAIN_INDEX_6, diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java index a3861c41606ed..482715bb74c83 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java @@ -95,7 +95,6 @@ import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeTests; import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilegeResolver; import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivileges.ManageApplicationPrivileges; -import org.elasticsearch.xpack.core.security.authz.privilege.IndexComponentSelectorPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.Privilege; import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; @@ -781,15 +780,9 @@ public void testCheckRestrictedIndexPatternPermission() throws Exception { 0, randomIntBetween(2, XPackPlugin.ASYNC_RESULTS_INDEX.length() - 2) ); - Role.Builder builder1 = Role.builder(RESTRICTED_INDICES, "role"); - Role role = builder1.add( - FieldPermissions.DEFAULT, - null, - IndexPrivilege.INDEX, - false, - IndexComponentSelectorPrivilege.DATA, - patternPrefix + "*" - ).build(); + Role role = Role.builder(RESTRICTED_INDICES, "role") + .add(FieldPermissions.DEFAULT, null, IndexPrivilege.INDEX, false, patternPrefix + "*") + .build(); RBACAuthorizationInfo authzInfo = new RBACAuthorizationInfo(role, null); String prePatternPrefix = patternPrefix.substring(0, randomIntBetween(1, patternPrefix.length() - 1)) + "*"; @@ -898,15 +891,9 @@ public void testCheckRestrictedIndexPatternPermission() throws Exception { containsInAnyOrder(ResourcePrivileges.builder(restrictedIndexMatchingWildcard).addPrivileges(Map.of("index", false)).build()) ); - Role.Builder builder = Role.builder(RESTRICTED_INDICES, "role"); - role = builder.add( - FieldPermissions.DEFAULT, - null, - IndexPrivilege.INDEX, - true, - IndexComponentSelectorPrivilege.DATA, - patternPrefix + "*" - ).build(); + role = Role.builder(RESTRICTED_INDICES, "role") + .add(FieldPermissions.DEFAULT, null, IndexPrivilege.INDEX, true, patternPrefix + "*") + .build(); authzInfo = new RBACAuthorizationInfo(role, null); response = hasPrivileges( IndicesPrivileges.builder() @@ -929,23 +916,10 @@ public void testCheckRestrictedIndexPatternPermission() throws Exception { public void testCheckExplicitRestrictedIndexPermissions() throws Exception { final boolean restrictedIndexPermission = randomBoolean(); final boolean restrictedMonitorPermission = randomBoolean(); - Role.Builder builder = Role.builder(RESTRICTED_INDICES, "role"); - Role.Builder builder1 = builder.add( - FieldPermissions.DEFAULT, - null, - IndexPrivilege.INDEX, - restrictedIndexPermission, - IndexComponentSelectorPrivilege.DATA, - ".sec*" - ); - Role role = builder1.add( - FieldPermissions.DEFAULT, - null, - IndexPrivilege.MONITOR, - restrictedMonitorPermission, - IndexComponentSelectorPrivilege.DATA, - ".security*" - ).build(); + Role role = Role.builder(RESTRICTED_INDICES, "role") + .add(FieldPermissions.DEFAULT, null, IndexPrivilege.INDEX, restrictedIndexPermission, ".sec*") + .add(FieldPermissions.DEFAULT, null, IndexPrivilege.MONITOR, restrictedMonitorPermission, ".security*") + .build(); RBACAuthorizationInfo authzInfo = new RBACAuthorizationInfo(role, null); String explicitRestrictedIndex = randomFrom(TestRestrictedIndices.SAMPLE_RESTRICTED_NAMES); @@ -1000,23 +974,10 @@ public void testCheckExplicitRestrictedIndexPermissions() throws Exception { } public void testCheckRestrictedIndexWildcardPermissions() throws Exception { - Role.Builder builder2 = Role.builder(RESTRICTED_INDICES, "role"); - Role.Builder builder3 = builder2.add( - FieldPermissions.DEFAULT, - null, - IndexPrivilege.INDEX, - false, - IndexComponentSelectorPrivilege.DATA, - ".sec*" - ); - Role role = builder3.add( - FieldPermissions.DEFAULT, - null, - IndexPrivilege.MONITOR, - true, - IndexComponentSelectorPrivilege.DATA, - ".security*" - ).build(); + Role role = Role.builder(RESTRICTED_INDICES, "role") + .add(FieldPermissions.DEFAULT, null, IndexPrivilege.INDEX, false, ".sec*") + .add(FieldPermissions.DEFAULT, null, IndexPrivilege.MONITOR, true, ".security*") + .build(); RBACAuthorizationInfo authzInfo = new RBACAuthorizationInfo(role, null); PrivilegesCheckResult response = hasPrivileges( @@ -1051,23 +1012,10 @@ public void testCheckRestrictedIndexWildcardPermissions() throws Exception { ) ); - Role.Builder builder = Role.builder(RESTRICTED_INDICES, "role"); - Role.Builder builder1 = builder.add( - FieldPermissions.DEFAULT, - null, - IndexPrivilege.INDEX, - true, - IndexComponentSelectorPrivilege.DATA, - ".sec*" - ); - role = builder1.add( - FieldPermissions.DEFAULT, - null, - IndexPrivilege.MONITOR, - false, - IndexComponentSelectorPrivilege.DATA, - ".security*" - ).build(); + role = Role.builder(RESTRICTED_INDICES, "role") + .add(FieldPermissions.DEFAULT, null, IndexPrivilege.INDEX, true, ".sec*") + .add(FieldPermissions.DEFAULT, null, IndexPrivilege.MONITOR, false, ".security*") + .build(); authzInfo = new RBACAuthorizationInfo(role, null); response = hasPrivileges( @@ -1340,23 +1288,18 @@ public void testBuildUserPrivilegeResponse() { final ManageApplicationPrivileges manageApplicationPrivileges = new ManageApplicationPrivileges(Sets.newHashSet("app01", "app02")); final BytesArray query = new BytesArray(""" {"term":{"public":true}}"""); - Role.Builder builder = Role.builder(RESTRICTED_INDICES, "test", "role") - .cluster(newHashSet("monitor", "manage_watcher"), Collections.singleton(manageApplicationPrivileges)) - .add(IndexPrivilege.get(newHashSet("read", "write")), "index-1") - .add(IndexPrivilege.ALL, "index-2", "index-3"); - FieldPermissions fieldPermissions = new FieldPermissions( - new FieldPermissionsDefinition(new String[] { "public.*" }, new String[0]) - ); - boolean allowRestrictedIndices = randomBoolean(); - final Role role = builder.add( - fieldPermissions, - Collections.singleton(query), - IndexPrivilege.READ, - allowRestrictedIndices, - IndexComponentSelectorPrivilege.DATA, - "index-4", - "index-5" - ) + final Role role = Role.builder(RESTRICTED_INDICES, "test", "role") + .cluster(Sets.newHashSet("monitor", "manage_watcher"), Collections.singleton(manageApplicationPrivileges)) + .add(IndexPrivilege.get(Sets.newHashSet("read", "write")), "index-1") + .add(IndexPrivilege.ALL, "index-2", "index-3") + .add( + new FieldPermissions(new FieldPermissionsDefinition(new String[] { "public.*" }, new String[0])), + Collections.singleton(query), + IndexPrivilege.READ, + randomBoolean(), + "index-4", + "index-5" + ) .addApplicationPrivilege(ApplicationPrivilegeTests.createPrivilege("app01", "read", "data:read"), Collections.singleton("*")) .runAs(new Privilege(Sets.newHashSet("user01", "user02"), "user01", "user02")) .addRemoteIndicesGroup(Set.of("remote-1"), FieldPermissions.DEFAULT, null, IndexPrivilege.READ, false, "remote-index-1") @@ -1809,7 +1752,7 @@ public void testGetRoleDescriptorsForRemoteClusterForReservedRoles() { IndicesPrivileges.builder().indices("*").privileges("all").allowRestrictedIndices(false).build(), IndicesPrivileges.builder() .indices("*") - .privileges("monitor", "read", "read_cross_cluster", "read_failure_store", "view_index_metadata") + .privileges("monitor", "read", "read_cross_cluster", "view_index_metadata") .allowRestrictedIndices(true) .build() }, null, @@ -1962,18 +1905,16 @@ public void testChildSearchActionAuthorizationIsNotSkippedWhenRoleHasDLS() { final String[] indices = { "test-index" }; final BytesArray query = new BytesArray(""" {"term":{"foo":bar}}"""); - Role.Builder builder = Role.builder(RESTRICTED_INDICES, "test-role"); - FieldPermissions fieldPermissions = new FieldPermissions(new FieldPermissionsDefinition(new String[] { "foo" }, new String[0])); - boolean allowRestrictedIndices = randomBoolean(); final Role role = Mockito.spy( - builder.add( - fieldPermissions, - Set.of(query), - IndexPrivilege.READ, - allowRestrictedIndices, - IndexComponentSelectorPrivilege.DATA, - indices - ).build() + Role.builder(RESTRICTED_INDICES, "test-role") + .add( + new FieldPermissions(new FieldPermissionsDefinition(new String[] { "foo" }, new String[0])), + Set.of(query), + IndexPrivilege.READ, + randomBoolean(), + indices + ) + .build() ); final String action = randomFrom( diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java index 29e89ca4d635f..4488c28750dc0 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java @@ -35,7 +35,6 @@ import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsDefinition; import org.elasticsearch.xpack.core.security.authz.permission.IndicesPermission; import org.elasticsearch.xpack.core.security.authz.permission.Role; -import org.elasticsearch.xpack.core.security.authz.privilege.IndexComponentSelectorPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.support.StringMatcher; import org.elasticsearch.xpack.core.security.test.TestRestrictedIndices; @@ -70,17 +69,9 @@ public void testAuthorize() { // basics: Set query = Collections.singleton(new BytesArray("{}")); String[] fields = new String[] { "_field" }; - Role.Builder builder6 = Role.builder(RESTRICTED_INDICES, "_role"); - FieldPermissions fieldPermissions5 = new FieldPermissions(fieldPermissionDef(fields, null)); - boolean allowRestrictedIndices6 = randomBoolean(); - Role role = builder6.add( - fieldPermissions5, - query, - IndexPrivilege.ALL, - allowRestrictedIndices6, - IndexComponentSelectorPrivilege.ALL, - "_index" - ).build(); + Role role = Role.builder(RESTRICTED_INDICES, "_role") + .add(new FieldPermissions(fieldPermissionDef(fields, null)), query, IndexPrivilege.ALL, randomBoolean(), "_index") + .build(); IndicesAccessControl permissions = role.authorize( TransportSearchAction.TYPE.name(), Sets.newHashSet("_index"), @@ -95,17 +86,9 @@ public void testAuthorize() { assertThat(permissions.getIndexPermissions("_index").getDocumentPermissions().getSingleSetOfQueries(), equalTo(query)); // no document level security: - Role.Builder builder5 = Role.builder(RESTRICTED_INDICES, "_role"); - FieldPermissions fieldPermissions4 = new FieldPermissions(fieldPermissionDef(fields, null)); - boolean allowRestrictedIndices5 = randomBoolean(); - role = builder5.add( - fieldPermissions4, - null, - IndexPrivilege.ALL, - allowRestrictedIndices5, - IndexComponentSelectorPrivilege.ALL, - "_index" - ).build(); + role = Role.builder(RESTRICTED_INDICES, "_role") + .add(new FieldPermissions(fieldPermissionDef(fields, null)), null, IndexPrivilege.ALL, randomBoolean(), "_index") + .build(); permissions = role.authorize(TransportSearchAction.TYPE.name(), Sets.newHashSet("_index"), md, fieldPermissionsCache); assertThat(permissions.getIndexPermissions("_index"), notNullValue()); assertTrue(permissions.getIndexPermissions("_index").getFieldPermissions().grantsAccessTo("_field")); @@ -114,16 +97,9 @@ public void testAuthorize() { assertThat(permissions.getIndexPermissions("_index").getDocumentPermissions().getListOfQueries(), nullValue()); // no field level security: - Role.Builder builder4 = Role.builder(RESTRICTED_INDICES, "_role"); - boolean allowRestrictedIndices4 = randomBoolean(); - role = builder4.add( - FieldPermissions.DEFAULT, - query, - IndexPrivilege.ALL, - allowRestrictedIndices4, - IndexComponentSelectorPrivilege.ALL, - "_index" - ).build(); + role = Role.builder(RESTRICTED_INDICES, "_role") + .add(FieldPermissions.DEFAULT, query, IndexPrivilege.ALL, randomBoolean(), "_index") + .build(); permissions = role.authorize(TransportSearchAction.TYPE.name(), Sets.newHashSet("_index"), md, fieldPermissionsCache); assertThat(permissions.getIndexPermissions("_index"), notNullValue()); assertFalse(permissions.getIndexPermissions("_index").getFieldPermissions().hasFieldLevelSecurity()); @@ -132,17 +108,9 @@ public void testAuthorize() { assertThat(permissions.getIndexPermissions("_index").getDocumentPermissions().getSingleSetOfQueries(), equalTo(query)); // index group associated with an alias: - Role.Builder builder3 = Role.builder(RESTRICTED_INDICES, "_role"); - FieldPermissions fieldPermissions3 = new FieldPermissions(fieldPermissionDef(fields, null)); - boolean allowRestrictedIndices3 = randomBoolean(); - role = builder3.add( - fieldPermissions3, - query, - IndexPrivilege.ALL, - allowRestrictedIndices3, - IndexComponentSelectorPrivilege.ALL, - "_alias" - ).build(); + role = Role.builder(RESTRICTED_INDICES, "_role") + .add(new FieldPermissions(fieldPermissionDef(fields, null)), query, IndexPrivilege.ALL, randomBoolean(), "_alias") + .build(); permissions = role.authorize(TransportSearchAction.TYPE.name(), Sets.newHashSet("_alias"), md, fieldPermissionsCache); assertThat(permissions.getIndexPermissions("_index"), notNullValue()); assertTrue(permissions.getIndexPermissions("_index").getFieldPermissions().grantsAccessTo("_field")); @@ -164,17 +132,9 @@ public void testAuthorize() { new String[] { "foo", "*" }, new String[] { randomAlphaOfLengthBetween(1, 10), "*" } ); - Role.Builder builder2 = Role.builder(RESTRICTED_INDICES, "_role"); - FieldPermissions fieldPermissions2 = new FieldPermissions(fieldPermissionDef(allFields, null)); - boolean allowRestrictedIndices2 = randomBoolean(); - role = builder2.add( - fieldPermissions2, - query, - IndexPrivilege.ALL, - allowRestrictedIndices2, - IndexComponentSelectorPrivilege.ALL, - "_alias" - ).build(); + role = Role.builder(RESTRICTED_INDICES, "_role") + .add(new FieldPermissions(fieldPermissionDef(allFields, null)), query, IndexPrivilege.ALL, randomBoolean(), "_alias") + .build(); permissions = role.authorize(TransportSearchAction.TYPE.name(), Sets.newHashSet("_alias"), md, fieldPermissionsCache); assertThat(permissions.getIndexPermissions("_index"), notNullValue()); assertFalse(permissions.getIndexPermissions("_index").getFieldPermissions().hasFieldLevelSecurity()); @@ -196,27 +156,10 @@ public void testAuthorize() { // match all fields with more than one permission Set fooQuery = Collections.singleton(new BytesArray("{foo}")); allFields = randomFrom(new String[] { "*" }, new String[] { "foo", "*" }, new String[] { randomAlphaOfLengthBetween(1, 10), "*" }); - Role.Builder builder = Role.builder(RESTRICTED_INDICES, "_role"); - FieldPermissions fieldPermissions = new FieldPermissions(fieldPermissionDef(allFields, null)); - boolean allowRestrictedIndices = randomBoolean(); - Role.Builder builder1 = builder.add( - fieldPermissions, - fooQuery, - IndexPrivilege.ALL, - allowRestrictedIndices, - IndexComponentSelectorPrivilege.ALL, - "_alias" - ); - FieldPermissions fieldPermissions1 = new FieldPermissions(fieldPermissionDef(allFields, null)); - boolean allowRestrictedIndices1 = randomBoolean(); - role = builder1.add( - fieldPermissions1, - query, - IndexPrivilege.ALL, - allowRestrictedIndices1, - IndexComponentSelectorPrivilege.ALL, - "_alias" - ).build(); + role = Role.builder(RESTRICTED_INDICES, "_role") + .add(new FieldPermissions(fieldPermissionDef(allFields, null)), fooQuery, IndexPrivilege.ALL, randomBoolean(), "_alias") + .add(new FieldPermissions(fieldPermissionDef(allFields, null)), query, IndexPrivilege.ALL, randomBoolean(), "_alias") + .build(); permissions = role.authorize(TransportSearchAction.TYPE.name(), Sets.newHashSet("_alias"), md, fieldPermissionsCache); Set bothQueries = Sets.union(fooQuery, query); assertThat(permissions.getIndexPermissions("_index"), notNullValue()); @@ -248,27 +191,10 @@ public void testAuthorizeMultipleGroupsMixedDls() { Set query = Collections.singleton(new BytesArray("{}")); String[] fields = new String[] { "_field" }; - Role.Builder builder = Role.builder(RESTRICTED_INDICES, "_role"); - FieldPermissions fieldPermissions = new FieldPermissions(fieldPermissionDef(fields, null)); - boolean allowRestrictedIndices = randomBoolean(); - Role.Builder builder1 = builder.add( - fieldPermissions, - query, - IndexPrivilege.ALL, - allowRestrictedIndices, - IndexComponentSelectorPrivilege.ALL, - "_index" - ); - FieldPermissions fieldPermissions1 = new FieldPermissions(fieldPermissionDef(null, null)); - boolean allowRestrictedIndices1 = randomBoolean(); - Role role = builder1.add( - fieldPermissions1, - null, - IndexPrivilege.ALL, - allowRestrictedIndices1, - IndexComponentSelectorPrivilege.ALL, - "*" - ).build(); + Role role = Role.builder(RESTRICTED_INDICES, "_role") + .add(new FieldPermissions(fieldPermissionDef(fields, null)), query, IndexPrivilege.ALL, randomBoolean(), "_index") + .add(new FieldPermissions(fieldPermissionDef(null, null)), null, IndexPrivilege.ALL, randomBoolean(), "*") + .build(); IndicesAccessControl permissions = role.authorize( TransportSearchAction.TYPE.name(), Sets.newHashSet("_index"), @@ -330,7 +256,6 @@ public void testCorePermissionAuthorize() { FieldPermissions.DEFAULT, null, randomBoolean(), - IndexComponentSelectorPrivilege.ALL, "a1" ) .addGroup( @@ -338,7 +263,6 @@ public void testCorePermissionAuthorize() { new FieldPermissions(fieldPermissionDef(null, new String[] { "denied_field" })), null, randomBoolean(), - IndexComponentSelectorPrivilege.DATA, "a1" ) .build(); @@ -364,7 +288,6 @@ public void testCorePermissionAuthorize() { FieldPermissions.DEFAULT, null, randomBoolean(), - IndexComponentSelectorPrivilege.ALL, "a1" ) .addGroup( @@ -372,7 +295,6 @@ public void testCorePermissionAuthorize() { new FieldPermissions(fieldPermissionDef(null, new String[] { "denied_field" })), null, randomBoolean(), - IndexComponentSelectorPrivilege.ALL, "a1" ) .addGroup( @@ -380,7 +302,6 @@ public void testCorePermissionAuthorize() { new FieldPermissions(fieldPermissionDef(new String[] { "*_field" }, new String[] { "denied_field" })), null, randomBoolean(), - IndexComponentSelectorPrivilege.ALL, "a2" ) .addGroup( @@ -388,7 +309,6 @@ public void testCorePermissionAuthorize() { new FieldPermissions(fieldPermissionDef(new String[] { "*_field2" }, new String[] { "denied_field2" })), null, randomBoolean(), - IndexComponentSelectorPrivilege.ALL, "a2" ) .build(); @@ -450,7 +370,6 @@ public void testSecurityIndicesPermissions() { FieldPermissions.DEFAULT, null, false, - IndexComponentSelectorPrivilege.ALL, "*" ).build(); IndicesAccessControl iac = indicesPermission.authorize( @@ -471,7 +390,6 @@ public void testSecurityIndicesPermissions() { FieldPermissions.DEFAULT, null, true, - IndexComponentSelectorPrivilege.ALL, "*" ).build(); iac = indicesPermission.authorize( @@ -502,7 +420,6 @@ public void testAsyncSearchIndicesPermissions() { FieldPermissions.DEFAULT, null, false, - IndexComponentSelectorPrivilege.ALL, "*" ).build(); IndicesAccessControl iac = indicesPermission.authorize( @@ -521,7 +438,6 @@ public void testAsyncSearchIndicesPermissions() { FieldPermissions.DEFAULT, null, true, - IndexComponentSelectorPrivilege.ALL, "*" ).build(); iac = indicesPermission.authorize( @@ -559,7 +475,6 @@ public void testAuthorizationForBackingIndices() { FieldPermissions.DEFAULT, null, false, - IndexComponentSelectorPrivilege.DATA, dataStreamName ).build(); IndicesAccessControl iac = indicesPermission.authorize( @@ -580,7 +495,6 @@ public void testAuthorizationForBackingIndices() { FieldPermissions.DEFAULT, null, false, - IndexComponentSelectorPrivilege.DATA, dataStreamName ).build(); iac = indicesPermission.authorize( @@ -626,7 +540,6 @@ public void testAuthorizationForMappingUpdates() { FieldPermissions.DEFAULT, null, randomBoolean(), - IndexComponentSelectorPrivilege.DATA, "test*" ) .addGroup( @@ -634,7 +547,6 @@ public void testAuthorizationForMappingUpdates() { new FieldPermissions(fieldPermissionDef(null, new String[] { "denied_field" })), null, randomBoolean(), - IndexComponentSelectorPrivilege.DATA, "test_write*" ) .build(); @@ -733,7 +645,6 @@ public void testIndicesPermissionHasFieldOrDocumentLevelSecurity() { fieldPermissions, queries, randomBoolean(), - IndexComponentSelectorPrivilege.ALL, "*" ).build(); assertThat(indicesPermission1.hasFieldOrDocumentLevelSecurity(), is(true)); @@ -744,7 +655,6 @@ public void testIndicesPermissionHasFieldOrDocumentLevelSecurity() { FieldPermissions.DEFAULT, null, true, - IndexComponentSelectorPrivilege.ALL, "*" ).build(); assertThat(indicesPermission2.hasFieldOrDocumentLevelSecurity(), is(false)); @@ -755,9 +665,8 @@ public void testIndicesPermissionHasFieldOrDocumentLevelSecurity() { FieldPermissions.DEFAULT, null, true, - IndexComponentSelectorPrivilege.ALL, "*" - ).addGroup(IndexPrivilege.NONE, fieldPermissions, queries, randomBoolean(), IndexComponentSelectorPrivilege.DATA, "*").build(); + ).addGroup(IndexPrivilege.NONE, fieldPermissions, queries, randomBoolean(), "*").build(); assertThat(indicesPermission3.hasFieldOrDocumentLevelSecurity(), is(false)); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/QueryableBuiltInRolesUtilsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/QueryableBuiltInRolesUtilsTests.java index 933512d3426c4..009e073667f78 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/QueryableBuiltInRolesUtilsTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/QueryableBuiltInRolesUtilsTests.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.security.support; +import org.elasticsearch.cluster.metadata.DataStream; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.RoleDescriptorTestHelper; @@ -40,7 +41,11 @@ public static void setupReservedRolesStore() { public void testCalculateHash() { assertThat( QueryableBuiltInRolesUtils.calculateHash(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR), - equalTo("bWEFdFo4WX229wdhdecfiz5QHMYEssh3ex8hizRgg+Q=") + equalTo( + DataStream.isFailureStoreFeatureFlagEnabled() + ? "3na054vyhPlUqSeq8cim+JwuBc+81r7JViA27peTAGc=" + : "bWEFdFo4WX229wdhdecfiz5QHMYEssh3ex8hizRgg+Q=" + ) ); } From 0858cd37f8e71dcf9c1290ae362f41f0638860e0 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 17 Feb 2025 15:57:47 +0100 Subject: [PATCH 015/131] Remote indices too --- .../core/security/authz/permission/Role.java | 31 ++++++++++----- .../ConfigurableClusterPrivileges.java | 1 - .../IndexComponentSelectorPrivilege.java | 4 +- .../authz/store/ReservedRolesStore.java | 7 +++- .../authz/privilege/IndexPrivilegeTests.java | 16 +++----- .../authz/store/CompositeRolesStore.java | 38 ++++++++++++------- .../RestGetBuiltinPrivilegesAction.java | 14 ++++++- 7 files changed, 72 insertions(+), 39 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java index aeebb5608f31e..1c7ac1c896acb 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java @@ -426,17 +426,28 @@ static SimpleRole buildFromRoleDescriptor( final String[] clusterAliases = remoteIndicesPrivileges.remoteClusters(); assert Arrays.equals(new String[] { "*" }, clusterAliases) : "reserved role should not define remote indices privileges for specific clusters"; - final RoleDescriptor.IndicesPrivileges indicesPrivileges = remoteIndicesPrivileges.indicesPrivileges(); - builder.addRemoteIndicesGroup( - Set.of(clusterAliases), - fieldPermissionsCache.getFieldPermissions( - new FieldPermissionsDefinition(indicesPrivileges.getGrantedFields(), indicesPrivileges.getDeniedFields()) - ), - indicesPrivileges.getQuery() == null ? null : Collections.singleton(indicesPrivileges.getQuery()), - IndexPrivilege.get(Set.of(indicesPrivileges.getPrivileges())), - indicesPrivileges.allowRestrictedIndices(), - indicesPrivileges.getIndices() + final RoleDescriptor.IndicesPrivileges indexPrivilege = remoteIndicesPrivileges.indicesPrivileges(); + FieldPermissions fieldPermissions = fieldPermissionsCache.getFieldPermissions( + new FieldPermissionsDefinition(indexPrivilege.getGrantedFields(), indexPrivilege.getDeniedFields()) + ); + Set query = indexPrivilege.getQuery() == null ? null : Collections.singleton(indexPrivilege.getQuery()); + boolean allowRestrictedIndices = indexPrivilege.allowRestrictedIndices(); + Map> split = IndexComponentSelectorPrivilege.partitionBySelectorPrivilege( + indexPrivilege.getPrivileges() ); + for (var entry : split.entrySet()) { + IndexPrivilege privilege = IndexPrivilege.get(entry.getValue()); + assert privilege.getSelectorPrivilege() == entry.getKey() + : "expected selector privilege to match the partitioned privilege"; + builder.addRemoteIndicesGroup( + Set.of(clusterAliases), + fieldPermissions, + query, + privilege, + allowRestrictedIndices, + indexPrivilege.getIndices() + ); + } } RemoteClusterPermissions remoteClusterPermissions = roleDescriptor.getRemoteClusterPermissions(); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java index cf2ffbfba8692..148fdf21fd2df 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java @@ -419,7 +419,6 @@ public ManageRolesPrivilege(List manageRolesInd FieldPermissions.DEFAULT, null, false, - // TODO indexPatternPrivilege.indexPatterns() ); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java index ce4410bcfe808..e56f4d9df5d70 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java @@ -16,6 +16,8 @@ import java.util.function.Predicate; import java.util.stream.Collectors; +import static org.elasticsearch.common.util.set.Sets.newHashSet; + public record IndexComponentSelectorPrivilege(String name, Predicate predicate) { public static final IndexComponentSelectorPrivilege ALL = new IndexComponentSelectorPrivilege("all", Predicates.always()); public static final IndexComponentSelectorPrivilege DATA = new IndexComponentSelectorPrivilege( @@ -40,7 +42,7 @@ public static Set get(Set indexPrivileg } public static Map> partitionBySelectorPrivilege(String... indexPrivileges) { - return partitionBySelectorPrivilege(Set.of(indexPrivileges)); + return partitionBySelectorPrivilege(newHashSet(indexPrivileges)); } public static Map> partitionBySelectorPrivilege(Set indexPrivileges) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java index ba3642dbf05a4..cc5a6fc6ab4b3 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java @@ -101,7 +101,12 @@ public class ReservedRolesStore implements BiConsumer, ActionListene new RoleDescriptor.RemoteIndicesPrivileges( RoleDescriptor.IndicesPrivileges.builder() .indices("*") - .privileges("monitor", "read", "view_index_metadata", "read_cross_cluster") + .privileges( + // TODO are there edge-cases where this is a bad idea? + DataStream.isFailureStoreFeatureFlagEnabled() + ? new String[] { "monitor", "read", "view_index_metadata", "read_cross_cluster", "read_failure_store" } + : new String[] { "monitor", "read", "view_index_metadata", "read_cross_cluster" } + ) .allowRestrictedIndices(true) .build(), "*" diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java index 4d9678f571f76..fd901f1a3e2e9 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java @@ -58,32 +58,26 @@ public void testOrderingOfPrivilegeNames() throws Exception { } public void testFindPrivilegesThatGrant() { - assertThat(findPrivilegesThatGrant(TransportSearchAction.TYPE.name()), equalTo(List.of("read", "read_failure_store", "all"))); + assertThat(findPrivilegesThatGrant(TransportSearchAction.TYPE.name()), equalTo(List.of("read", "all"))); assertThat(findPrivilegesThatGrant(TransportIndexAction.NAME), equalTo(List.of("create_doc", "create", "index", "write", "all"))); assertThat(findPrivilegesThatGrant(TransportUpdateAction.NAME), equalTo(List.of("index", "write", "all"))); assertThat(findPrivilegesThatGrant(TransportDeleteAction.NAME), equalTo(List.of("delete", "write", "all"))); assertThat( findPrivilegesThatGrant(IndicesStatsAction.NAME), - equalTo(List.of("monitor", "manage", "manage_failure_store_internal", "cross_cluster_replication", "all")) - ); - assertThat( - findPrivilegesThatGrant(RefreshAction.NAME), - equalTo(List.of("maintenance", "manage", "manage_failure_store_internal", "all")) + equalTo(List.of("monitor", "manage", "cross_cluster_replication", "all")) ); + assertThat(findPrivilegesThatGrant(RefreshAction.NAME), equalTo(List.of("maintenance", "manage", "all"))); } public void testPrivilegesForRollupFieldCapsAction() { final Collection privileges = findPrivilegesThatGrant(GetRollupIndexCapsAction.NAME); - assertThat( - Set.copyOf(privileges), - equalTo(Set.of("manage", "all", "read_failure_store", "view_index_metadata", "read", "manage_failure_store_internal")) - ); + assertThat(Set.copyOf(privileges), equalTo(Set.of("manage", "all", "view_index_metadata", "read"))); } public void testPrivilegesForGetCheckPointAction() { assertThat( findPrivilegesThatGrant(GetCheckpointAction.NAME), - containsInAnyOrder("monitor", "view_index_metadata", "manage", "manage_failure_store_internal", "all") + containsInAnyOrder("monitor", "view_index_metadata", "manage", "all") ); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java index 979e59a0379e6..31f8b717a41a1 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java @@ -556,11 +556,11 @@ public static void buildRoleFromDescriptors( } for (Map.Entry, MergeableIndicesPrivilege> entry : restrictedIndicesPrivilegesMap.entrySet()) { MergeableIndicesPrivilege indicesPrivilege = entry.getValue(); + FieldPermissions fieldPermissions = fieldPermissionsCache.getFieldPermissions(indicesPrivilege.fieldPermissionsDefinition); + String[] indices = indicesPrivilege.indices.toArray(Strings.EMPTY_ARRAY); Map> split = IndexComponentSelectorPrivilege.partitionBySelectorPrivilege( indicesPrivilege.privileges ); - FieldPermissions fieldPermissions = fieldPermissionsCache.getFieldPermissions(indicesPrivilege.fieldPermissionsDefinition); - String[] indices = indicesPrivilege.indices.toArray(Strings.EMPTY_ARRAY); for (Map.Entry> privilegesBySelector : split.entrySet()) { IndexPrivilege indexPrivilege = IndexPrivilege.get(privilegesBySelector.getValue()); assert indexPrivilege.getSelectorPrivilege() == privilegesBySelector.getKey(); @@ -569,18 +569,28 @@ public static void buildRoleFromDescriptors( } remoteIndicesPrivilegesByCluster.forEach((clusterAliasKey, remoteIndicesPrivilegesForCluster) -> { - remoteIndicesPrivilegesForCluster.forEach( - (privilege) -> builder.addRemoteIndicesGroup( - clusterAliasKey, - fieldPermissionsCache.getFieldPermissions( - new FieldPermissionsDefinition(privilege.getGrantedFields(), privilege.getDeniedFields()) - ), - privilege.getQuery() == null ? null : newHashSet(privilege.getQuery()), - IndexPrivilege.get(newHashSet(Objects.requireNonNull(privilege.getPrivileges()))), - privilege.allowRestrictedIndices(), - newHashSet(Objects.requireNonNull(privilege.getIndices())).toArray(new String[0]) - ) - ); + remoteIndicesPrivilegesForCluster.forEach((privilege) -> { + FieldPermissions fieldPermissions = fieldPermissionsCache.getFieldPermissions( + new FieldPermissionsDefinition(privilege.getGrantedFields(), privilege.getDeniedFields()) + ); + Set query = privilege.getQuery() == null ? null : newHashSet(privilege.getQuery()); + String[] indices = newHashSet(Objects.requireNonNull(privilege.getIndices())).toArray(new String[0]); + Map> split = IndexComponentSelectorPrivilege.partitionBySelectorPrivilege( + Objects.requireNonNull(privilege.getPrivileges()) + ); + for (Map.Entry> privilegesBySelector : split.entrySet()) { + IndexPrivilege indexPrivilege = IndexPrivilege.get(privilegesBySelector.getValue()); + assert indexPrivilege.getSelectorPrivilege() == privilegesBySelector.getKey(); + builder.addRemoteIndicesGroup( + clusterAliasKey, + fieldPermissions, + query, + indexPrivilege, + privilege.allowRestrictedIndices(), + indices + ); + } + }); }); if (remoteClusterPermissions.hasAnyPrivileges()) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetBuiltinPrivilegesAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetBuiltinPrivilegesAction.java index e0ef46dc73a18..70c009a606c40 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetBuiltinPrivilegesAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetBuiltinPrivilegesAction.java @@ -24,10 +24,12 @@ import org.elasticsearch.xpack.core.security.action.privilege.GetBuiltinPrivilegesRequest; import org.elasticsearch.xpack.core.security.action.privilege.GetBuiltinPrivilegesResponse; import org.elasticsearch.xpack.core.security.action.privilege.GetBuiltinPrivilegesResponseTranslator; +import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.security.authz.store.NativeRolesStore; import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler; import java.io.IOException; +import java.util.Arrays; import java.util.List; import static org.elasticsearch.rest.RestRequest.Method.GET; @@ -71,7 +73,8 @@ public RestResponse buildResponse(GetBuiltinPrivilegesResponse response, XConten final var translatedResponse = responseTranslator.translate(response); builder.startObject(); builder.array("cluster", translatedResponse.getClusterPrivileges()); - builder.array("index", translatedResponse.getIndexPrivileges()); + // TODO remove the filter once we can update docs tests again + builder.array("index", filterOutFailureStorePrivileges(translatedResponse)); String[] remoteClusterPrivileges = translatedResponse.getRemoteClusterPrivileges(); if (remoteClusterPrivileges.length > 0) { // remote clusters are not supported in stateless mode, so hide entirely builder.array("remote_cluster", remoteClusterPrivileges); @@ -79,6 +82,15 @@ public RestResponse buildResponse(GetBuiltinPrivilegesResponse response, XConten builder.endObject(); return new RestResponse(RestStatus.OK, builder); } + + private static String[] filterOutFailureStorePrivileges(GetBuiltinPrivilegesResponse translatedResponse) { + return Arrays.stream(translatedResponse.getIndexPrivileges()) + .filter( + p -> false == (p.equals(IndexPrivilege.READ_FAILURE_STORE.getSingleName()) + || p.equals(IndexPrivilege.MANAGE_FAILURE_STORE_INTERNAL.getSingleName())) + ) + .toArray(String[]::new); + } } ); } From 8fcb15c9c84665bf49e11ec24fcd4fc3fc1ec851 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 17 Feb 2025 15:58:20 +0100 Subject: [PATCH 016/131] Undo docs test --- .../reference/rest-api/security/get-builtin-privileges.asciidoc | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/reference/rest-api/security/get-builtin-privileges.asciidoc b/docs/reference/rest-api/security/get-builtin-privileges.asciidoc index 33d052a7ba2d1..08a03a5b1e830 100644 --- a/docs/reference/rest-api/security/get-builtin-privileges.asciidoc +++ b/docs/reference/rest-api/security/get-builtin-privileges.asciidoc @@ -148,7 +148,6 @@ A successful call returns an object with "cluster", "index", and "remote_cluster "maintenance", "manage", "manage_data_stream_lifecycle", - "manage_failure_store_internal", "manage_follow_index", "manage_ilm", "manage_leader_index", @@ -156,7 +155,6 @@ A successful call returns an object with "cluster", "index", and "remote_cluster "none", "read", "read_cross_cluster", - "read_failure_store", "view_index_metadata", "write" ], From bf3d46b60defcf3408b6564e2429d5679cdd986f Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 17 Feb 2025 16:04:11 +0100 Subject: [PATCH 017/131] Simlify --- ...lusterSecurityFcActionAuthorizationIT.java | 2 +- .../authz/AuthorizedIndicesTests.java | 6 ++- .../authz/store/CompositeRolesStoreTests.java | 50 +++---------------- 3 files changed, 12 insertions(+), 46 deletions(-) diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityFcActionAuthorizationIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityFcActionAuthorizationIT.java index e816ebe1995d2..793313e238651 100644 --- a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityFcActionAuthorizationIT.java +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityFcActionAuthorizationIT.java @@ -483,7 +483,7 @@ public void testUpdateCrossClusterApiKey() throws Exception { + "for user [foo] with assigned roles [role] authenticated by API key id [" + apiKeyId + "] of user [test_user] on indices [index], this action is granted by the index privileges " - + "[view_index_metadata,manage,all]" + + "[view_index_metadata,manage,read,all]" ) ); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java index 0efa6b20b70c3..e9408fd34c3ed 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java @@ -187,8 +187,10 @@ public void testSecurityIndicesAreRestrictedForDefaultRole() { } public void testSecurityIndicesAreNotRemovedFromUnrestrictedRole() { - Role.Builder builder = Role.builder(RESTRICTED_INDICES, randomAlphaOfLength(8)); - Role role = builder.add(FieldPermissions.DEFAULT, null, IndexPrivilege.ALL, true, "*").cluster(Set.of("all"), Set.of()).build(); + Role role = Role.builder(RESTRICTED_INDICES, randomAlphaOfLength(8)) + .add(FieldPermissions.DEFAULT, null, IndexPrivilege.ALL, true, "*") + .cluster(Set.of("all"), Set.of()) + .build(); Settings indexSettings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersion.current()).build(); final String internalSecurityIndex = randomFrom( TestRestrictedIndices.INTERNAL_SECURITY_MAIN_INDEX_6, diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java index bc5a65bff4d46..d408c08b95b6f 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java @@ -86,7 +86,6 @@ import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeTests; import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilegeResolver; import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivilege; -import org.elasticsearch.xpack.core.security.authz.privilege.IndexComponentSelectorPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.authz.restriction.Workflow; import org.elasticsearch.xpack.core.security.authz.restriction.WorkflowResolver; @@ -1270,7 +1269,6 @@ public void testBuildRoleWithFlsAndDlsInRemoteIndicesDefinition() { false, "{\"match\":{\"field\":\"a\"}}", new FieldPermissionsDefinition.FieldGrantExcludeGroup(new String[] { "field" }, null), - IndexComponentSelectorPrivilege.DATA, "index-1" ) ); @@ -1306,7 +1304,6 @@ public void testBuildRoleWithFlsAndDlsInRemoteIndicesDefinition() { false, "{\"match\":{\"field\":\"a\"}}", new FieldPermissionsDefinition.FieldGrantExcludeGroup(new String[] { "field" }, null), - IndexComponentSelectorPrivilege.DATA, "index-1" ), indexGroup( @@ -1314,7 +1311,6 @@ public void testBuildRoleWithFlsAndDlsInRemoteIndicesDefinition() { false, "{\"match\":{\"field\":\"b\"}}", new FieldPermissionsDefinition.FieldGrantExcludeGroup(new String[] { "other" }, null), - IndexComponentSelectorPrivilege.DATA, "index-1" ) ); @@ -1542,10 +1538,7 @@ public void testBuildRoleWithReadFailureStorePrivilegeOnly() { new IndicesPrivileges[] { IndicesPrivileges.builder().indices(indexPattern).privileges("read_failure_store").build() } ) ); - assertHasIndexGroups( - role.indices(), - indexGroup(IndexPrivilege.READ_FAILURE_STORE, false, IndexComponentSelectorPrivilege.FAILURES, indexPattern) - ); + assertHasIndexGroups(role.indices(), indexGroup(IndexPrivilege.READ_FAILURE_STORE, false, indexPattern)); } public void testBuildRoleWithReadFailureStorePrivilegeDuplicatesMerged() { @@ -1558,10 +1551,7 @@ public void testBuildRoleWithReadFailureStorePrivilegeDuplicatesMerged() { IndicesPrivileges.builder().indices(indexPattern).privileges("read_failure_store").build() } ) ); - assertHasIndexGroups( - role.indices(), - indexGroup(IndexPrivilege.READ_FAILURE_STORE, false, IndexComponentSelectorPrivilege.FAILURES, indexPattern) - ); + assertHasIndexGroups(role.indices(), indexGroup(IndexPrivilege.READ_FAILURE_STORE, false, indexPattern)); } public void testBuildRoleWithReadFailureStoreAndReadPrivilegeSplit() { @@ -1575,8 +1565,8 @@ public void testBuildRoleWithReadFailureStoreAndReadPrivilegeSplit() { ); assertHasIndexGroups( role.indices(), - indexGroup(IndexPrivilege.READ_FAILURE_STORE, false, IndexComponentSelectorPrivilege.FAILURES, indexPattern), - indexGroup(IndexPrivilege.READ, false, IndexComponentSelectorPrivilege.DATA, indexPattern) + indexGroup(IndexPrivilege.READ_FAILURE_STORE, false, indexPattern), + indexGroup(IndexPrivilege.READ, false, indexPattern) ); } @@ -1592,8 +1582,8 @@ public void testBuildRoleWithMultipleReadFailureStoreAndReadPrivilegeSplit() { ); assertHasIndexGroups( role.indices(), - indexGroup(IndexPrivilege.READ_FAILURE_STORE, false, IndexComponentSelectorPrivilege.FAILURES, indexPattern), - indexGroup(IndexPrivilege.READ, false, IndexComponentSelectorPrivilege.DATA, indexPattern) + indexGroup(IndexPrivilege.READ_FAILURE_STORE, false, indexPattern), + indexGroup(IndexPrivilege.READ, false, indexPattern) ); } @@ -1609,12 +1599,7 @@ public void testBuildRoleWithAllPrivilegeIsNeverSplit() { ); assertHasIndexGroups( role.indices(), - indexGroup( - IndexPrivilege.get(Set.of("read", "read_failure_store", "all")), - false, - IndexComponentSelectorPrivilege.ALL, - indexPattern - ) + indexGroup(IndexPrivilege.get(Set.of("read", "read_failure_store", "all")), false, indexPattern) ); } @@ -3470,23 +3455,6 @@ private static Matcher indexGroup( allowRestrictedIndices, null, new FieldPermissionsDefinition.FieldGrantExcludeGroup(null, null), - IndexComponentSelectorPrivilege.DATA, - indices - ); - } - - private static Matcher indexGroup( - final IndexPrivilege privilege, - final boolean allowRestrictedIndices, - final IndexComponentSelectorPrivilege selectorPrivilege, - final String... indices - ) { - return indexGroup( - privilege, - allowRestrictedIndices, - null, - new FieldPermissionsDefinition.FieldGrantExcludeGroup(null, null), - selectorPrivilege, indices ); } @@ -3496,7 +3464,6 @@ private static Matcher indexGroup( final boolean allowRestrictedIndices, @Nullable final String query, final FieldPermissionsDefinition.FieldGrantExcludeGroup flsGroup, - IndexComponentSelectorPrivilege selectorPrivilege, final String... indices ) { return new BaseMatcher<>() { @@ -3510,7 +3477,6 @@ public boolean matches(Object o) { && equalTo(privilege).matches(group.privilege()) && equalTo(allowRestrictedIndices).matches(group.allowRestrictedIndices()) && equalTo(new FieldPermissions(new FieldPermissionsDefinition(Set.of(flsGroup)))).matches(group.getFieldPermissions()) - && equalTo(selectorPrivilege).matches(group.getSelectorPrivilege()) && arrayContaining(indices).matches(group.indices()); } @@ -3528,8 +3494,6 @@ public void describeTo(Description description) { + query + ", fieldGrantExcludeGroup=" + flsGroup - + ", selectorPrivilege=" - + selectorPrivilege + '}' ); } From 4d301c98e3461eb88418a4b64eaa9b801a7dab0e Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 17 Feb 2025 16:12:28 +0100 Subject: [PATCH 018/131] Cleaner selector handling --- .../xpack/core/security/authz/privilege/IndexPrivilege.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java index 53538cfe65343..991f3c3a2e245 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java @@ -31,6 +31,7 @@ import org.elasticsearch.action.datastreams.PromoteDataStreamAction; import org.elasticsearch.action.fieldcaps.TransportFieldCapabilitiesAction; import org.elasticsearch.action.search.TransportSearchShardsAction; +import org.elasticsearch.action.support.IndexComponentSelector; import org.elasticsearch.cluster.metadata.DataStream; import org.elasticsearch.common.Strings; import org.elasticsearch.core.Nullable; @@ -371,10 +372,9 @@ public static Set names() { public static Collection findPrivilegesThatGrant(String action) { return VALUES.entrySet() .stream() + // Only include privileges that grant data access; failures access is handled separately in authorization failure messages + .filter(e -> e.getValue().selectorPrivilege.grants(IndexComponentSelector.DATA)) .filter(e -> e.getValue().predicate.test(action)) - // Filter out the failure store privileges since these are confusing w.r.t. authorization failure messages are a handled - // separately - .filter(e -> false == (e.getValue().getSelectorPrivilege() == IndexComponentSelectorPrivilege.FAILURES)) .map(Map.Entry::getKey) .toList(); } From b03a593de924f7252ea21467033f4a80a14610e1 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 17 Feb 2025 16:28:58 +0100 Subject: [PATCH 019/131] Fix test --- .../security/authz/store/CompositeRolesStore.java | 1 - .../privilege/RestGetBuiltinPrivilegesAction.java | 12 +++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java index 31f8b717a41a1..2d89ffb69e094 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java @@ -747,7 +747,6 @@ private static void collatePrivilegesByIndices( final Set key = newHashSet(indicesPrivilege.getIndices()); indicesPrivilegesMap.compute(key, (k, value) -> { if (value == null) { - // TODO do we need to worry about FLS and DLS combining incorrectly for different selectors? return new MergeableIndicesPrivilege( indicesPrivilege.getIndices(), indicesPrivilege.getPrivileges(), diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetBuiltinPrivilegesAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetBuiltinPrivilegesAction.java index 70c009a606c40..b6cc14a33f46b 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetBuiltinPrivilegesAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetBuiltinPrivilegesAction.java @@ -31,6 +31,7 @@ import java.io.IOException; import java.util.Arrays; import java.util.List; +import java.util.Set; import static org.elasticsearch.rest.RestRequest.Method.GET; @@ -41,6 +42,11 @@ public class RestGetBuiltinPrivilegesAction extends SecurityBaseRestHandler { private static final Logger logger = LogManager.getLogger(RestGetBuiltinPrivilegesAction.class); + // TODO remove this once we can update docs tests again + private static final Set FAILURE_STORE_PRIVILEGES_TO_EXCLUDE = Set.of( + IndexPrivilege.READ_FAILURE_STORE.getSingleName(), + IndexPrivilege.MANAGE_FAILURE_STORE_INTERNAL.getSingleName() + ); private final GetBuiltinPrivilegesResponseTranslator responseTranslator; public RestGetBuiltinPrivilegesAction( @@ -73,7 +79,6 @@ public RestResponse buildResponse(GetBuiltinPrivilegesResponse response, XConten final var translatedResponse = responseTranslator.translate(response); builder.startObject(); builder.array("cluster", translatedResponse.getClusterPrivileges()); - // TODO remove the filter once we can update docs tests again builder.array("index", filterOutFailureStorePrivileges(translatedResponse)); String[] remoteClusterPrivileges = translatedResponse.getRemoteClusterPrivileges(); if (remoteClusterPrivileges.length > 0) { // remote clusters are not supported in stateless mode, so hide entirely @@ -85,10 +90,7 @@ public RestResponse buildResponse(GetBuiltinPrivilegesResponse response, XConten private static String[] filterOutFailureStorePrivileges(GetBuiltinPrivilegesResponse translatedResponse) { return Arrays.stream(translatedResponse.getIndexPrivileges()) - .filter( - p -> false == (p.equals(IndexPrivilege.READ_FAILURE_STORE.getSingleName()) - || p.equals(IndexPrivilege.MANAGE_FAILURE_STORE_INTERNAL.getSingleName())) - ) + .filter(p -> false == FAILURE_STORE_PRIVILEGES_TO_EXCLUDE.contains(p)) .toArray(String[]::new); } } From da43e54c104da38357aec494f3adc4819395cdb2 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 17 Feb 2025 17:46:07 +0100 Subject: [PATCH 020/131] Union --- .../ConfigurableClusterPrivileges.java | 1 + .../IndexComponentSelectorPrivilege.java | 14 +++++++- .../authz/privilege/IndexPrivilege.java | 34 +++++++++++-------- .../xpack/security/authz/RBACEngineTests.java | 5 +++ .../QueryableBuiltInRolesUtilsTests.java | 2 +- 5 files changed, 40 insertions(+), 16 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java index 148fdf21fd2df..d4365b72ee315 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java @@ -414,6 +414,7 @@ public ManageRolesPrivilege(List manageRolesInd this.requestPredicateSupplier = (restrictedIndices) -> { IndicesPermission.Builder indicesPermissionBuilder = new IndicesPermission.Builder(restrictedIndices); for (ManageRolesIndexPermissionGroup indexPatternPrivilege : manageRolesIndexPermissionGroups) { + // TODO handle selectors indicesPermissionBuilder.addGroup( IndexPrivilege.get(Set.of(indexPatternPrivilege.privileges())), FieldPermissions.DEFAULT, diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java index e56f4d9df5d70..82841fff1fd66 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java @@ -8,6 +8,7 @@ package org.elasticsearch.xpack.core.security.authz.privilege; import org.elasticsearch.action.support.IndexComponentSelector; +import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.core.Predicates; import java.util.HashSet; @@ -18,7 +19,11 @@ import static org.elasticsearch.common.util.set.Sets.newHashSet; -public record IndexComponentSelectorPrivilege(String name, Predicate predicate) { +public record IndexComponentSelectorPrivilege(Set names, Predicate predicate) { + IndexComponentSelectorPrivilege(String name, Predicate predicate) { + this(Set.of(name), predicate); + } + public static final IndexComponentSelectorPrivilege ALL = new IndexComponentSelectorPrivilege("all", Predicates.always()); public static final IndexComponentSelectorPrivilege DATA = new IndexComponentSelectorPrivilege( "data", @@ -37,6 +42,13 @@ public boolean isTotal() { return this == ALL; } + public IndexComponentSelectorPrivilege or(IndexComponentSelectorPrivilege other) { + if (this == ALL || other == ALL) { + return ALL; + } + return new IndexComponentSelectorPrivilege(Sets.union(names, other.names), predicate.or(other.predicate)); + } + public static Set get(Set indexPrivileges) { return indexPrivileges.stream().map(IndexComponentSelectorPrivilege::get).collect(Collectors.toSet()); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java index 991f3c3a2e245..e6c297f2fcf16 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java @@ -48,6 +48,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashSet; +import java.util.Iterator; import java.util.Locale; import java.util.Map; import java.util.Objects; @@ -185,11 +186,6 @@ public final class IndexPrivilege extends Privilege { READ_AUTOMATON, IndexComponentSelectorPrivilege.FAILURES ); - public static final IndexPrivilege MANAGE_FAILURE_STORE_INTERNAL = new IndexPrivilege( - "manage_failure_store_internal", - MANAGE_AUTOMATON, - IndexComponentSelectorPrivilege.FAILURES - ); public static final IndexPrivilege READ = new IndexPrivilege("read", READ_AUTOMATON); public static final IndexPrivilege READ_CROSS_CLUSTER = new IndexPrivilege("read_cross_cluster", READ_CROSS_CLUSTER_AUTOMATON); public static final IndexPrivilege CREATE = new IndexPrivilege("create", CREATE_AUTOMATON); @@ -199,6 +195,11 @@ public final class IndexPrivilege extends Privilege { public static final IndexPrivilege CREATE_DOC = new IndexPrivilege("create_doc", CREATE_DOC_AUTOMATON); public static final IndexPrivilege MONITOR = new IndexPrivilege("monitor", MONITOR_AUTOMATON); public static final IndexPrivilege MANAGE = new IndexPrivilege("manage", MANAGE_AUTOMATON); + public static final IndexPrivilege MANAGE_FAILURE_STORE_INTERNAL = new IndexPrivilege( + "manage_failure_store_internal", + MANAGE_AUTOMATON, + IndexComponentSelectorPrivilege.FAILURES + ); public static final IndexPrivilege DELETE_INDEX = new IndexPrivilege("delete_index", DELETE_INDEX_AUTOMATON); public static final IndexPrivilege CREATE_INDEX = new IndexPrivilege("create_index", CREATE_INDEX_AUTOMATON); public static final IndexPrivilege VIEW_METADATA = new IndexPrivilege("view_index_metadata", VIEW_METADATA_AUTOMATON); @@ -342,17 +343,22 @@ private static IndexPrivilege resolve(Set name) { selectorPrivileges.add(IndexComponentSelectorPrivilege.DATA); } - for (IndexComponentSelectorPrivilege selectorPrivilege : selectorPrivileges) { - if (selectorPrivilege == IndexComponentSelectorPrivilege.ALL) { - return new IndexPrivilege(name, unionAndMinimize(automata), IndexComponentSelectorPrivilege.ALL); - } + return new IndexPrivilege(name, unionAndMinimize(automata), union(selectorPrivileges)); + } + + private static IndexComponentSelectorPrivilege union(Set selectorPrivileges) { + assert selectorPrivileges.isEmpty() == false; + if (selectorPrivileges.contains(IndexComponentSelectorPrivilege.ALL)) { + return IndexComponentSelectorPrivilege.ALL; + } else if (selectorPrivileges.size() == 1) { + return selectorPrivileges.iterator().next(); } - if (selectorPrivileges.size() != 1) { - // TODO assertion and make this clearer - throw new IllegalArgumentException("Cannot mix different selector privileges in a single index privilege for [" + name + "]"); + Iterator iterator = selectorPrivileges.iterator(); + IndexComponentSelectorPrivilege result = iterator.next(); + while (iterator.hasNext()) { + result = result.or(iterator.next()); } - - return new IndexPrivilege(name, unionAndMinimize(automata), selectorPrivileges.iterator().next()); + return result; } static Map values() { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java index 482715bb74c83..fa9b871858fe8 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java @@ -1754,6 +1754,11 @@ public void testGetRoleDescriptorsForRemoteClusterForReservedRoles() { .indices("*") .privileges("monitor", "read", "read_cross_cluster", "view_index_metadata") .allowRestrictedIndices(true) + .build(), + IndicesPrivileges.builder() + .indices("*") + .privileges("read_failure_store") + .allowRestrictedIndices(true) .build() }, null, null, diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/QueryableBuiltInRolesUtilsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/QueryableBuiltInRolesUtilsTests.java index 009e073667f78..0f40e1eab4b68 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/QueryableBuiltInRolesUtilsTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/QueryableBuiltInRolesUtilsTests.java @@ -43,7 +43,7 @@ public void testCalculateHash() { QueryableBuiltInRolesUtils.calculateHash(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR), equalTo( DataStream.isFailureStoreFeatureFlagEnabled() - ? "3na054vyhPlUqSeq8cim+JwuBc+81r7JViA27peTAGc=" + ? "qgdWamvjudRKGezTGfjoSCr230sFDdh2t6xFUPYiW2Q=" : "bWEFdFo4WX229wdhdecfiz5QHMYEssh3ex8hizRgg+Q=" ) ); From 8582f80114bc6da104ab366fb58373b24ef2648d Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Tue, 18 Feb 2025 11:15:52 +0100 Subject: [PATCH 021/131] Rework IndexPrivilege#get --- .../role/RoleDescriptorRequestValidator.java | 4 +- .../authz/permission/IndicesPermission.java | 8 +- .../core/security/authz/permission/Role.java | 23 ++--- .../ConfigurableClusterPrivileges.java | 2 +- .../IndexComponentSelectorPrivilege.java | 69 ++------------ .../authz/privilege/IndexPrivilege.java | 89 +++++++++++++------ .../authz/permission/LimitedRoleTests.java | 3 +- .../authz/permission/SimpleRoleTests.java | 3 +- .../authz/privilege/IndexPrivilegeTests.java | 30 ++++--- .../authz/privilege/PrivilegeTests.java | 4 +- .../support/AutomatonPatternsTests.java | 8 +- .../security/CrossClusterShardTests.java | 2 +- .../authz/store/CompositeRolesStore.java | 29 ++---- .../DeprecationRoleDescriptorConsumer.java | 4 +- .../xpack/security/authz/RBACEngineTests.java | 5 +- .../authz/store/CompositeRolesStoreTests.java | 6 +- 16 files changed, 128 insertions(+), 161 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/RoleDescriptorRequestValidator.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/RoleDescriptorRequestValidator.java index ec8fcd1c421ef..1f912709b1442 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/RoleDescriptorRequestValidator.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/RoleDescriptorRequestValidator.java @@ -48,7 +48,7 @@ public static ActionRequestValidationException validate( if (roleDescriptor.getIndicesPrivileges() != null) { for (RoleDescriptor.IndicesPrivileges idp : roleDescriptor.getIndicesPrivileges()) { try { - IndexPrivilege.get(Set.of(idp.getPrivileges())); + IndexPrivilege.getSplitBySelector(Set.of(idp.getPrivileges())); } catch (IllegalArgumentException ile) { validationException = addValidationError(ile.getMessage(), validationException); } @@ -60,7 +60,7 @@ public static ActionRequestValidationException validate( validationException = addValidationError("remote index cluster alias cannot be an empty string", validationException); } try { - IndexPrivilege.get(Set.of(ridp.indicesPrivileges().getPrivileges())); + IndexPrivilege.getSplitBySelector(Set.of(ridp.indicesPrivileges().getPrivileges())); } catch (IllegalArgumentException ile) { validationException = addValidationError(ile.getMessage(), validationException); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 7aa249a956a56..1e33f95981627 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -302,7 +302,7 @@ public boolean checkResourcePrivileges( } } for (String privilege : checkForPrivileges) { - IndexPrivilege indexPrivilege = IndexPrivilege.get(Collections.singleton(privilege)); + IndexPrivilege indexPrivilege = IndexPrivilege.getSingle(Collections.singleton(privilege)); if (allowedIndexPrivilegesAutomaton != null && Automatons.subsetOf(indexPrivilege.getAutomaton(), allowedIndexPrivilegesAutomaton)) { if (resourcePrivilegesMapBuilder != null) { @@ -793,6 +793,7 @@ public static class Group { public static final Group[] EMPTY_ARRAY = new Group[0]; private final IndexPrivilege privilege; + private final IndexComponentSelectorPrivilege selectorPrivilege; private final Predicate actionMatcher; private final String[] indices; private final StringMatcher indexNameMatcher; @@ -816,6 +817,7 @@ public Group( assert indices.length != 0; this.privilege = privilege; this.actionMatcher = privilege.predicate(); + this.selectorPrivilege = privilege.getSelectorPrivilege(); this.indices = indices; this.allowRestrictedIndices = allowRestrictedIndices; ConcurrentHashMap indexNameAutomatonMemo = new ConcurrentHashMap<>(1); @@ -863,8 +865,8 @@ boolean hasQuery() { return query != null; } - public IndexComponentSelectorPrivilege getSelectorPrivilege() { - return privilege.getSelectorPrivilege(); + public boolean checkSelector(IndexComponentSelector selector) { + return selectorPrivilege.test(selector); } public boolean allowRestrictedIndices() { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java index 1c7ac1c896acb..aa9847c490290 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java @@ -26,7 +26,6 @@ import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilegeResolver; import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivilege; -import org.elasticsearch.xpack.core.security.authz.privilege.IndexComponentSelectorPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.Privilege; import org.elasticsearch.xpack.core.security.authz.restriction.WorkflowResolver; @@ -411,14 +410,9 @@ static SimpleRole buildFromRoleDescriptor( ); Set query = indexPrivilege.getQuery() == null ? null : Collections.singleton(indexPrivilege.getQuery()); boolean allowRestrictedIndices = indexPrivilege.allowRestrictedIndices(); - Map> split = IndexComponentSelectorPrivilege.partitionBySelectorPrivilege( - indexPrivilege.getPrivileges() - ); - for (var entry : split.entrySet()) { - IndexPrivilege privilege = IndexPrivilege.get(entry.getValue()); - assert privilege.getSelectorPrivilege() == entry.getKey() - : "expected selector privilege to match the partitioned privilege"; - builder.add(fieldPermissions, query, privilege, allowRestrictedIndices, indexPrivilege.getIndices()); + Set splitPrivileges = IndexPrivilege.getSplitBySelector(Set.of(indexPrivilege.getPrivileges())); + for (var entry : splitPrivileges) { + builder.add(fieldPermissions, query, entry, allowRestrictedIndices, indexPrivilege.getIndices()); } } @@ -432,18 +426,13 @@ static SimpleRole buildFromRoleDescriptor( ); Set query = indexPrivilege.getQuery() == null ? null : Collections.singleton(indexPrivilege.getQuery()); boolean allowRestrictedIndices = indexPrivilege.allowRestrictedIndices(); - Map> split = IndexComponentSelectorPrivilege.partitionBySelectorPrivilege( - indexPrivilege.getPrivileges() - ); - for (var entry : split.entrySet()) { - IndexPrivilege privilege = IndexPrivilege.get(entry.getValue()); - assert privilege.getSelectorPrivilege() == entry.getKey() - : "expected selector privilege to match the partitioned privilege"; + Set splitPrivileges = IndexPrivilege.getSplitBySelector(Set.of(indexPrivilege.getPrivileges())); + for (var entry : splitPrivileges) { builder.addRemoteIndicesGroup( Set.of(clusterAliases), fieldPermissions, query, - privilege, + entry, allowRestrictedIndices, indexPrivilege.getIndices() ); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java index d4365b72ee315..c07e0d13e0eae 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java @@ -416,7 +416,7 @@ public ManageRolesPrivilege(List manageRolesInd for (ManageRolesIndexPermissionGroup indexPatternPrivilege : manageRolesIndexPermissionGroups) { // TODO handle selectors indicesPermissionBuilder.addGroup( - IndexPrivilege.get(Set.of(indexPatternPrivilege.privileges())), + IndexPrivilege.getSingle(Set.of(indexPatternPrivilege.privileges())), FieldPermissions.DEFAULT, null, false, diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java index 82841fff1fd66..d701fe6a87060 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java @@ -8,18 +8,14 @@ package org.elasticsearch.xpack.core.security.authz.privilege; import org.elasticsearch.action.support.IndexComponentSelector; -import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.core.Predicates; -import java.util.HashSet; -import java.util.Map; import java.util.Set; import java.util.function.Predicate; -import java.util.stream.Collectors; -import static org.elasticsearch.common.util.set.Sets.newHashSet; - -public record IndexComponentSelectorPrivilege(Set names, Predicate predicate) { +public record IndexComponentSelectorPrivilege(Set names, Predicate predicate) + implements + Predicate { IndexComponentSelectorPrivilege(String name, Predicate predicate) { this(Set.of(name), predicate); } @@ -34,63 +30,8 @@ public record IndexComponentSelectorPrivilege(Set names, Predicate get(Set indexPrivileges) { - return indexPrivileges.stream().map(IndexComponentSelectorPrivilege::get).collect(Collectors.toSet()); - } - - public static Map> partitionBySelectorPrivilege(String... indexPrivileges) { - return partitionBySelectorPrivilege(newHashSet(indexPrivileges)); - } - - public static Map> partitionBySelectorPrivilege(Set indexPrivileges) { - final Set dataAccessPrivileges = new HashSet<>(); - final Set failuresAccessPrivileges = new HashSet<>(); - - for (String indexPrivilege : indexPrivileges) { - final IndexComponentSelectorPrivilege selectorPrivilege = get(indexPrivilege); - // If we ever hit `all`, the entire group can be treated as granting "all" access and we can return early - if (selectorPrivilege == ALL) { - return Map.of(ALL, indexPrivileges); - } - - if (selectorPrivilege == DATA) { - dataAccessPrivileges.add(indexPrivilege); - } else if (selectorPrivilege == FAILURES) { - failuresAccessPrivileges.add(indexPrivilege); - } else { - final var message = "index privilege [" + indexPrivilege + "] mapped to an unexpected selector [" + selectorPrivilege + "]"; - assert false : message; - throw new IllegalStateException(message); - } - } - - if (dataAccessPrivileges.isEmpty()) { - return Map.of(FAILURES, failuresAccessPrivileges); - } else if (failuresAccessPrivileges.isEmpty()) { - return Map.of(DATA, dataAccessPrivileges); - } else { - return Map.of(DATA, dataAccessPrivileges, FAILURES, failuresAccessPrivileges); - } - } - - private static IndexComponentSelectorPrivilege get(String indexPrivilegeName) { - final IndexPrivilege indexPrivilege = IndexPrivilege.getNamedOrNull(indexPrivilegeName); - // `null` means we got a raw action instead of a named privilege; all raw actions are treated as data access - return indexPrivilege == null ? DATA : indexPrivilege.getSelectorPrivilege(); - } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java index e6c297f2fcf16..d63cfbfa2835b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java @@ -48,7 +48,6 @@ import java.util.Collection; import java.util.Collections; import java.util.HashSet; -import java.util.Iterator; import java.util.Locale; import java.util.Map; import java.util.Objects; @@ -257,7 +256,7 @@ public final class IndexPrivilege extends Privilege { public static final Predicate ACTION_MATCHER = ALL.predicate(); public static final Predicate CREATE_INDEX_MATCHER = CREATE_INDEX.predicate(); - private static final ConcurrentHashMap, IndexPrivilege> CACHE = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap, Set> CACHE = new ConcurrentHashMap<>(); private final IndexComponentSelectorPrivilege selectorPrivilege; @@ -278,10 +277,18 @@ private IndexPrivilege(Set name, Automaton automaton, IndexComponentSele this.selectorPrivilege = selectorPrivilege; } - public static IndexPrivilege get(Set name) { + public static IndexPrivilege getSingle(Set name) { + var single = getSplitBySelector(name); + if (single.size() != 1) { + throw new IllegalArgumentException("expected singleton"); + } + return single.iterator().next(); + } + + public static Set getSplitBySelector(Set name) { return CACHE.computeIfAbsent(name, (theName) -> { if (theName.isEmpty()) { - return NONE; + return Set.of(NONE); } else { return resolve(theName); } @@ -304,15 +311,18 @@ public static IndexPrivilege getNamedOrNull(String name) { return VALUES.get(name.toLowerCase(Locale.ROOT)); } - private static IndexPrivilege resolve(Set name) { + private static Set resolve(Set name) { final int size = name.size(); if (size == 0) { throw new IllegalArgumentException("empty set should not be used"); } Set actions = new HashSet<>(); - Set automata = new HashSet<>(); - Set selectorPrivileges = new HashSet<>(); + Set allAccessPrivileges = new HashSet<>(); + Set dataAccessPrivileges = new HashSet<>(); + Set failureAccessPrivileges = new HashSet<>(); + + boolean containsAll = name.stream().anyMatch(n -> getNamedOrNull(n) == ALL); for (String part : name) { part = part.toLowerCase(Locale.ROOT); if (ACTION_MATCHER.test(part)) { @@ -320,10 +330,17 @@ private static IndexPrivilege resolve(Set name) { } else { IndexPrivilege indexPrivilege = part == null ? null : VALUES.get(part); if (indexPrivilege != null && size == 1) { - return indexPrivilege; + return Set.of(indexPrivilege); } else if (indexPrivilege != null) { - automata.add(indexPrivilege.automaton); - selectorPrivileges.add(indexPrivilege.getSelectorPrivilege()); + if (containsAll) { + allAccessPrivileges.add(indexPrivilege); + } else if (indexPrivilege.selectorPrivilege == IndexComponentSelectorPrivilege.DATA) { + dataAccessPrivileges.add(indexPrivilege); + } else if (indexPrivilege.selectorPrivilege == IndexComponentSelectorPrivilege.FAILURES) { + failureAccessPrivileges.add(indexPrivilege); + } else { + assert false : "unexpected selector [" + indexPrivilege.selectorPrivilege + "]"; + } } else { String errorMessage = "unknown index privilege [" + part @@ -338,27 +355,47 @@ private static IndexPrivilege resolve(Set name) { } } - if (actions.isEmpty() == false) { - automata.add(patterns(actions)); - selectorPrivileges.add(IndexComponentSelectorPrivilege.DATA); + if (false == allAccessPrivileges.isEmpty()) { + assert name.size() == actions.size() + allAccessPrivileges.size() + : "expected [" + + name.size() + + "] but was [" + + (actions.size() + allAccessPrivileges.size()) + + "] for " + + name + + " " + + allAccessPrivileges + + " " + + actions; + return Set.of(union(allAccessPrivileges, actions, IndexComponentSelectorPrivilege.ALL)); } - return new IndexPrivilege(name, unionAndMinimize(automata), union(selectorPrivileges)); + final Set result = new HashSet<>(); + if (false == failureAccessPrivileges.isEmpty()) { + result.add(union(failureAccessPrivileges, Set.of(), IndexComponentSelectorPrivilege.FAILURES)); + } + if (false == dataAccessPrivileges.isEmpty() || false == actions.isEmpty()) { + result.add(union(dataAccessPrivileges, actions, IndexComponentSelectorPrivilege.DATA)); + } + return result; } - private static IndexComponentSelectorPrivilege union(Set selectorPrivileges) { - assert selectorPrivileges.isEmpty() == false; - if (selectorPrivileges.contains(IndexComponentSelectorPrivilege.ALL)) { - return IndexComponentSelectorPrivilege.ALL; - } else if (selectorPrivileges.size() == 1) { - return selectorPrivileges.iterator().next(); + private static IndexPrivilege union( + Collection privileges, + Collection actions, + IndexComponentSelectorPrivilege selectorPrivilege + ) { + Set automata = HashSet.newHashSet(privileges.size() + actions.size()); + Set names = new HashSet<>(); + for (var privilege : privileges) { + automata.add(privilege.automaton); + names.add(privilege.getSingleName()); } - Iterator iterator = selectorPrivileges.iterator(); - IndexComponentSelectorPrivilege result = iterator.next(); - while (iterator.hasNext()) { - result = result.or(iterator.next()); + if (false == actions.isEmpty()) { + automata.add(patterns(actions)); + names.addAll(actions); } - return result; + return new IndexPrivilege(names, unionAndMinimize(automata), selectorPrivilege); } static Map values() { @@ -379,7 +416,7 @@ public static Collection findPrivilegesThatGrant(String action) { return VALUES.entrySet() .stream() // Only include privileges that grant data access; failures access is handled separately in authorization failure messages - .filter(e -> e.getValue().selectorPrivilege.grants(IndexComponentSelector.DATA)) + .filter(e -> e.getValue().selectorPrivilege.test(IndexComponentSelector.DATA)) .filter(e -> e.getValue().predicate.test(action)) .map(Map.Entry::getKey) .toList(); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java index a4646c0d736c5..3d62212cfc812 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java @@ -239,7 +239,8 @@ private static FieldPermissions randomFlsPermissions(String... grantedFields) { } private static IndexPrivilege randomIndexPrivilege() { - return IndexPrivilege.get(Set.of(randomFrom(IndexPrivilege.names()))); + // TODO handle failure store + return IndexPrivilege.getSingle(Set.of(randomFrom(IndexPrivilege.names()))); } public void testGetRoleDescriptorsIntersectionForRemoteClusterReturnsEmpty() { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/SimpleRoleTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/SimpleRoleTests.java index 5401be220fe8b..1a9878b479046 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/SimpleRoleTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/SimpleRoleTests.java @@ -176,7 +176,8 @@ public void testGetRoleDescriptorsIntersectionForRemoteCluster() { Set.of(randomAlphaOfLength(8)), new FieldPermissions(new FieldPermissionsDefinition(new String[] { randomAlphaOfLength(5) }, null)), null, - IndexPrivilege.get(Set.of(randomFrom(IndexPrivilege.names()))), + // TODO handle failure store + IndexPrivilege.getSingle(Set.of(randomFrom(IndexPrivilege.names()))), randomBoolean(), randomAlphaOfLength(9) ) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java index fd901f1a3e2e9..81aa59e40d195 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java @@ -84,36 +84,39 @@ public void testPrivilegesForGetCheckPointAction() { public void testRelationshipBetweenPrivileges() { assertThat( Automatons.subsetOf( - IndexPrivilege.get(Set.of("view_index_metadata")).automaton, - IndexPrivilege.get(Set.of("manage")).automaton + IndexPrivilege.getSingle(Set.of("view_index_metadata")).automaton, + IndexPrivilege.getSingle(Set.of("manage")).automaton ), is(true) ); assertThat( - Automatons.subsetOf(IndexPrivilege.get(Set.of("monitor")).automaton, IndexPrivilege.get(Set.of("manage")).automaton), + Automatons.subsetOf( + IndexPrivilege.getSingle(Set.of("monitor")).automaton, + IndexPrivilege.getSingle(Set.of("manage")).automaton + ), is(true) ); assertThat( Automatons.subsetOf( - IndexPrivilege.get(Set.of("create", "create_doc", "index", "delete")).automaton, - IndexPrivilege.get(Set.of("write")).automaton + IndexPrivilege.getSingle(Set.of("create", "create_doc", "index", "delete")).automaton, + IndexPrivilege.getSingle(Set.of("write")).automaton ), is(true) ); assertThat( Automatons.subsetOf( - IndexPrivilege.get(Set.of("create_index", "delete_index")).automaton, - IndexPrivilege.get(Set.of("manage")).automaton + IndexPrivilege.getSingle(Set.of("create_index", "delete_index")).automaton, + IndexPrivilege.getSingle(Set.of("manage")).automaton ), is(true) ); } public void testCrossClusterReplicationPrivileges() { - final IndexPrivilege crossClusterReplication = IndexPrivilege.get(Set.of("cross_cluster_replication")); + final IndexPrivilege crossClusterReplication = IndexPrivilege.getSingle(Set.of("cross_cluster_replication")); List.of( "indices:data/read/xpack/ccr/shard_changes", "indices:monitor/stats", @@ -122,11 +125,11 @@ public void testCrossClusterReplicationPrivileges() { "indices:admin/seq_no/renew_retention_lease" ).forEach(action -> assertThat(crossClusterReplication.predicate.test(action + randomAlphaOfLengthBetween(0, 8)), is(true))); assertThat( - Automatons.subsetOf(crossClusterReplication.automaton, IndexPrivilege.get(Set.of("manage", "read", "monitor")).automaton), + Automatons.subsetOf(crossClusterReplication.automaton, IndexPrivilege.getSingle(Set.of("manage", "read", "monitor")).automaton), is(true) ); - final IndexPrivilege crossClusterReplicationInternal = IndexPrivilege.get(Set.of("cross_cluster_replication_internal")); + final IndexPrivilege crossClusterReplicationInternal = IndexPrivilege.getSingle(Set.of("cross_cluster_replication_internal")); List.of( "indices:internal/admin/ccr/restore/session/clear", "indices:internal/admin/ccr/restore/file_chunk/get", @@ -139,10 +142,13 @@ public void testCrossClusterReplicationPrivileges() { ); assertThat( - Automatons.subsetOf(crossClusterReplicationInternal.automaton, IndexPrivilege.get(Set.of("manage")).automaton), + Automatons.subsetOf(crossClusterReplicationInternal.automaton, IndexPrivilege.getSingle(Set.of("manage")).automaton), is(false) ); - assertThat(Automatons.subsetOf(crossClusterReplicationInternal.automaton, IndexPrivilege.get(Set.of("all")).automaton), is(true)); + assertThat( + Automatons.subsetOf(crossClusterReplicationInternal.automaton, IndexPrivilege.getSingle(Set.of("all")).automaton), + is(true) + ); } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/PrivilegeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/PrivilegeTests.java index a58acf82ea44e..5b9a2bc463f2e 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/PrivilegeTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/PrivilegeTests.java @@ -203,7 +203,7 @@ public void testClusterAction() throws Exception { public void testIndexAction() throws Exception { Set actionName = Sets.newHashSet("indices:admin/mapping/delete"); - IndexPrivilege index = IndexPrivilege.get(actionName); + IndexPrivilege index = IndexPrivilege.getSingle(actionName); assertThat(index, notNullValue()); assertThat(index.predicate().test("indices:admin/mapping/delete"), is(true)); assertThat(index.predicate().test("indices:admin/mapping/dele"), is(false)); @@ -216,7 +216,7 @@ public void testIndexCollapse() throws Exception { IndexPrivilege second = values[randomIntBetween(0, values.length - 1)]; Set name = Sets.newHashSet(first.name().iterator().next(), second.name().iterator().next()); - IndexPrivilege index = IndexPrivilege.get(name); + IndexPrivilege index = IndexPrivilege.getSingle(name); if (Automatons.subsetOf(second.getAutomaton(), first.getAutomaton())) { assertTrue(AutomatonTestUtil.sameLanguage(index.getAutomaton(), first.getAutomaton())); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/support/AutomatonPatternsTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/support/AutomatonPatternsTests.java index 1539651b1aed6..9a254289bd761 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/support/AutomatonPatternsTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/support/AutomatonPatternsTests.java @@ -37,7 +37,7 @@ public void testRemoteClusterPrivsDoNotOverlap() { // check that the action patterns for remote CCS are not allowed by remote CCR privileges Arrays.stream(CCS_INDICES_PRIVILEGE_NAMES).forEach(ccsPrivilege -> { - Automaton ccsAutomaton = IndexPrivilege.get(Set.of(ccsPrivilege)).getAutomaton(); + Automaton ccsAutomaton = IndexPrivilege.getSingle(Set.of(ccsPrivilege)).getAutomaton(); Automatons.getPatterns(ccsAutomaton).forEach(ccsPattern -> { // emulate an action name that could be allowed by a CCS privilege String actionName = ccsPattern.replaceAll("\\*", randomAlphaOfLengthBetween(1, 8)); @@ -49,14 +49,14 @@ public void testRemoteClusterPrivsDoNotOverlap() { ccrPrivileges, ccsPattern ); - assertFalse(errorMessage, IndexPrivilege.get(Set.of(ccrPrivileges)).predicate().test(actionName)); + assertFalse(errorMessage, IndexPrivilege.getSingle(Set.of(ccrPrivileges)).predicate().test(actionName)); }); }); }); // check that the action patterns for remote CCR are not allowed by remote CCS privileges Arrays.stream(CCR_INDICES_PRIVILEGE_NAMES).forEach(ccrPrivilege -> { - Automaton ccrAutomaton = IndexPrivilege.get(Set.of(ccrPrivilege)).getAutomaton(); + Automaton ccrAutomaton = IndexPrivilege.getSingle(Set.of(ccrPrivilege)).getAutomaton(); Automatons.getPatterns(ccrAutomaton).forEach(ccrPattern -> { // emulate an action name that could be allowed by a CCR privilege String actionName = ccrPattern.replaceAll("\\*", randomAlphaOfLengthBetween(1, 8)); @@ -72,7 +72,7 @@ public void testRemoteClusterPrivsDoNotOverlap() { ccsPrivileges, ccrPattern ); - assertFalse(errorMessage, IndexPrivilege.get(Set.of(ccsPrivileges)).predicate().test(actionName)); + assertFalse(errorMessage, IndexPrivilege.getSingle(Set.of(ccsPrivileges)).predicate().test(actionName)); } }); }); diff --git a/x-pack/plugin/security/qa/consistency-checks/src/test/java/org/elasticsearch/xpack/security/CrossClusterShardTests.java b/x-pack/plugin/security/qa/consistency-checks/src/test/java/org/elasticsearch/xpack/security/CrossClusterShardTests.java index 057ebdece5c61..d81adb40d8163 100644 --- a/x-pack/plugin/security/qa/consistency-checks/src/test/java/org/elasticsearch/xpack/security/CrossClusterShardTests.java +++ b/x-pack/plugin/security/qa/consistency-checks/src/test/java/org/elasticsearch/xpack/security/CrossClusterShardTests.java @@ -112,7 +112,7 @@ public void testCheckForNewShardLevelTransportActions() throws Exception { List shardActions = transportActionBindings.stream() .map(binding -> binding.getProvider().get()) - .filter(action -> IndexPrivilege.get(crossClusterPrivilegeNames).predicate().test(action.actionName)) + .filter(action -> IndexPrivilege.getSingle(crossClusterPrivilegeNames).predicate().test(action.actionName)) .filter(this::actionIsLikelyShardAction) .map(action -> action.actionName) .toList(); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java index 2d89ffb69e094..0330ae8028ac9 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java @@ -40,7 +40,6 @@ import org.elasticsearch.xpack.core.security.authz.permission.Role; import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivilege; -import org.elasticsearch.xpack.core.security.authz.privilege.IndexComponentSelectorPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.Privilege; import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; @@ -543,28 +542,20 @@ public static void buildRoleFromDescriptors( for (Map.Entry, MergeableIndicesPrivilege> entry : indicesPrivilegesMap.entrySet()) { MergeableIndicesPrivilege indicesPrivilege = entry.getValue(); - Map> split = IndexComponentSelectorPrivilege.partitionBySelectorPrivilege( - indicesPrivilege.privileges - ); FieldPermissions fieldPermissions = fieldPermissionsCache.getFieldPermissions(indicesPrivilege.fieldPermissionsDefinition); String[] indices = indicesPrivilege.indices.toArray(Strings.EMPTY_ARRAY); - for (Map.Entry> privilegesBySelector : split.entrySet()) { - IndexPrivilege indexPrivilege = IndexPrivilege.get(privilegesBySelector.getValue()); - assert indexPrivilege.getSelectorPrivilege() == privilegesBySelector.getKey(); - builder.add(fieldPermissions, indicesPrivilege.query, indexPrivilege, false, indices); + Set splitPrivileges = IndexPrivilege.getSplitBySelector(indicesPrivilege.privileges); + for (var splitPrivilege : splitPrivileges) { + builder.add(fieldPermissions, indicesPrivilege.query, splitPrivilege, false, indices); } } for (Map.Entry, MergeableIndicesPrivilege> entry : restrictedIndicesPrivilegesMap.entrySet()) { MergeableIndicesPrivilege indicesPrivilege = entry.getValue(); FieldPermissions fieldPermissions = fieldPermissionsCache.getFieldPermissions(indicesPrivilege.fieldPermissionsDefinition); String[] indices = indicesPrivilege.indices.toArray(Strings.EMPTY_ARRAY); - Map> split = IndexComponentSelectorPrivilege.partitionBySelectorPrivilege( - indicesPrivilege.privileges - ); - for (Map.Entry> privilegesBySelector : split.entrySet()) { - IndexPrivilege indexPrivilege = IndexPrivilege.get(privilegesBySelector.getValue()); - assert indexPrivilege.getSelectorPrivilege() == privilegesBySelector.getKey(); - builder.add(fieldPermissions, indicesPrivilege.query, IndexPrivilege.get(privilegesBySelector.getValue()), true, indices); + Set splitPrivileges = IndexPrivilege.getSplitBySelector(indicesPrivilege.privileges); + for (var splitPrivilege : splitPrivileges) { + builder.add(fieldPermissions, indicesPrivilege.query, splitPrivilege, true, indices); } } @@ -575,12 +566,10 @@ public static void buildRoleFromDescriptors( ); Set query = privilege.getQuery() == null ? null : newHashSet(privilege.getQuery()); String[] indices = newHashSet(Objects.requireNonNull(privilege.getIndices())).toArray(new String[0]); - Map> split = IndexComponentSelectorPrivilege.partitionBySelectorPrivilege( - Objects.requireNonNull(privilege.getPrivileges()) + Set splitPrivileges = IndexPrivilege.getSplitBySelector( + Set.of(Objects.requireNonNull(privilege.getPrivileges())) ); - for (Map.Entry> privilegesBySelector : split.entrySet()) { - IndexPrivilege indexPrivilege = IndexPrivilege.get(privilegesBySelector.getValue()); - assert indexPrivilege.getSelectorPrivilege() == privilegesBySelector.getKey(); + for (var indexPrivilege : splitPrivileges) { builder.addRemoteIndicesGroup( clusterAliasKey, fieldPermissions, diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/DeprecationRoleDescriptorConsumer.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/DeprecationRoleDescriptorConsumer.java index 8ff535f3f6231..17d1c86646eb4 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/DeprecationRoleDescriptorConsumer.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/DeprecationRoleDescriptorConsumer.java @@ -183,7 +183,7 @@ private void logDeprecatedPermission(RoleDescriptor roleDescriptor) { for (Map.Entry> privilegesByAlias : privilegesByAliasMap.entrySet()) { final String aliasName = privilegesByAlias.getKey(); final Set aliasPrivilegeNames = privilegesByAlias.getValue(); - final Automaton aliasPrivilegeAutomaton = IndexPrivilege.get(aliasPrivilegeNames).getAutomaton(); + final Automaton aliasPrivilegeAutomaton = IndexPrivilege.getSingle(aliasPrivilegeNames).getAutomaton(); final SortedSet inferiorIndexNames = new TreeSet<>(); // check if the alias grants superiors privileges than the indices it points to for (Index index : aliasOrIndexMap.get(aliasName).getIndices()) { @@ -193,7 +193,7 @@ private void logDeprecatedPermission(RoleDescriptor roleDescriptor) { // compute automaton once per index no matter how many times it is pointed to final Automaton indexPrivilegeAutomaton = indexAutomatonMap.computeIfAbsent( index.getName(), - i -> IndexPrivilege.get(indexPrivileges).getAutomaton() + i -> IndexPrivilege.getSingle(indexPrivileges).getAutomaton() ); if (false == Automatons.subsetOf(indexPrivilegeAutomaton, aliasPrivilegeAutomaton)) { inferiorIndexNames.add(index.getName()); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java index fa9b871858fe8..5ac570697b396 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java @@ -1290,7 +1290,7 @@ public void testBuildUserPrivilegeResponse() { {"term":{"public":true}}"""); final Role role = Role.builder(RESTRICTED_INDICES, "test", "role") .cluster(Sets.newHashSet("monitor", "manage_watcher"), Collections.singleton(manageApplicationPrivileges)) - .add(IndexPrivilege.get(Sets.newHashSet("read", "write")), "index-1") + .add(IndexPrivilege.getSingle(Sets.newHashSet("read", "write")), "index-1") .add(IndexPrivilege.ALL, "index-2", "index-3") .add( new FieldPermissions(new FieldPermissionsDefinition(new String[] { "public.*" }, new String[0])), @@ -1617,7 +1617,8 @@ public void testGetRoleDescriptorsIntersectionForRemoteClusterHasDeterministicOr for (int i = 0; i < numGroups; i++) { remoteIndicesBuilder.addGroup( Set.copyOf(randomNonEmptySubsetOf(List.of(concreteClusterAlias, "*"))), - IndexPrivilege.get(Set.copyOf(randomSubsetOf(randomIntBetween(1, 4), IndexPrivilege.names()))), + // TODO handle failure store + IndexPrivilege.getSingle(Set.copyOf(randomSubsetOf(randomIntBetween(1, 4), IndexPrivilege.names()))), new FieldPermissions( new FieldPermissionsDefinition( Set.of( diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java index d408c08b95b6f..09f2fa3b8ef66 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java @@ -1492,8 +1492,8 @@ public void testBuildRoleWithMultipleRemoteMergedAcrossPrivilegesAndDescriptors( assertHasRemoteIndexGroupsForClusters( role.remoteIndices(), Set.of("remote-1"), - indexGroup(IndexPrivilege.get(Set.of("read")), false, "index-1"), - indexGroup(IndexPrivilege.get(Set.of("none")), false, "index-1") + indexGroup(IndexPrivilege.getSingle(Set.of("read")), false, "index-1"), + indexGroup(IndexPrivilege.getSingle(Set.of("none")), false, "index-1") ); } @@ -1599,7 +1599,7 @@ public void testBuildRoleWithAllPrivilegeIsNeverSplit() { ); assertHasIndexGroups( role.indices(), - indexGroup(IndexPrivilege.get(Set.of("read", "read_failure_store", "all")), false, indexPattern) + indexGroup(IndexPrivilege.getSingle(Set.of("read", "read_failure_store", "all")), false, indexPattern) ); } From 9732f8d5b9a6f2a5874d622288689c9ed8f47167 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Tue, 18 Feb 2025 11:47:27 +0100 Subject: [PATCH 022/131] Assert on names --- .../authz/privilege/IndexPrivilege.java | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java index d63cfbfa2835b..8098820eee0f5 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java @@ -326,7 +326,7 @@ private static Set resolve(Set name) { for (String part : name) { part = part.toLowerCase(Locale.ROOT); if (ACTION_MATCHER.test(part)) { - actions.add(actionToPattern(part)); + actions.add(part); } else { IndexPrivilege indexPrivilege = part == null ? null : VALUES.get(part); if (indexPrivilege != null && size == 1) { @@ -356,18 +356,9 @@ private static Set resolve(Set name) { } if (false == allAccessPrivileges.isEmpty()) { - assert name.size() == actions.size() + allAccessPrivileges.size() - : "expected [" - + name.size() - + "] but was [" - + (actions.size() + allAccessPrivileges.size()) - + "] for " - + name - + " " - + allAccessPrivileges - + " " - + actions; - return Set.of(union(allAccessPrivileges, actions, IndexComponentSelectorPrivilege.ALL)); + Set result = Set.of(union(allAccessPrivileges, actions, IndexComponentSelectorPrivilege.ALL)); + assertNamesMatch(result, name); + return result; } final Set result = new HashSet<>(); @@ -377,9 +368,15 @@ private static Set resolve(Set name) { if (false == dataAccessPrivileges.isEmpty() || false == actions.isEmpty()) { result.add(union(dataAccessPrivileges, actions, IndexComponentSelectorPrivilege.DATA)); } + assertNamesMatch(result, name); return result; } + private static void assertNamesMatch(Set privileges, Set names) { + assert names.equals(privileges.stream().map(Privilege::name).flatMap(Set::stream).collect(Collectors.toSet())) + : "mismatch between names [" + names + "] and names on split privileges [" + privileges + "]"; + } + private static IndexPrivilege union( Collection privileges, Collection actions, @@ -388,12 +385,14 @@ private static IndexPrivilege union( Set automata = HashSet.newHashSet(privileges.size() + actions.size()); Set names = new HashSet<>(); for (var privilege : privileges) { - automata.add(privilege.automaton); names.add(privilege.getSingleName()); + automata.add(privilege.automaton); } + if (false == actions.isEmpty()) { - automata.add(patterns(actions)); names.addAll(actions); + // TODO for-loop or optimize? + automata.add(patterns(actions.stream().map(Privilege::actionToPattern).toArray(String[]::new))); } return new IndexPrivilege(names, unionAndMinimize(automata), selectorPrivilege); } From 0c848e4cbb8f25b939194ea84cf70a6f80e0ee84 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Tue, 18 Feb 2025 11:58:38 +0100 Subject: [PATCH 023/131] More clean up and TODOs --- .../authz/privilege/IndexPrivilege.java | 50 ++++++++++++------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java index 8098820eee0f5..a193d51aa309e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java @@ -277,6 +277,7 @@ private IndexPrivilege(Set name, Automaton automaton, IndexComponentSele this.selectorPrivilege = selectorPrivilege; } + // TODO javadoc public static IndexPrivilege getSingle(Set name) { var single = getSplitBySelector(name); if (single.size() != 1) { @@ -285,6 +286,7 @@ public static IndexPrivilege getSingle(Set name) { return single.iterator().next(); } + // TODO javadoc public static Set getSplitBySelector(Set name) { return CACHE.computeIfAbsent(name, (theName) -> { if (theName.isEmpty()) { @@ -295,17 +297,6 @@ public static Set getSplitBySelector(Set name) { }); } - public IndexComponentSelectorPrivilege getSelectorPrivilege() { - return selectorPrivilege; - } - - public String getSingleName() { - if (name().size() != 1) { - throw new IllegalStateException("Expected a single name, but got: " + name()); - } - return name().iterator().next(); - } - @Nullable public static IndexPrivilege getNamedOrNull(String name) { return VALUES.get(name.toLowerCase(Locale.ROOT)); @@ -355,10 +346,24 @@ private static Set resolve(Set name) { } } + final Set result = combineIntoResult(allAccessPrivileges, failureAccessPrivileges, dataAccessPrivileges, actions); + assertNamesMatch(result, name); + return result; + } + + private static Set combineIntoResult( + Set allAccessPrivileges, + Set failureAccessPrivileges, + Set dataAccessPrivileges, + Set actions + ) { + assert false == allAccessPrivileges.isEmpty() + || false == failureAccessPrivileges.isEmpty() + || false == dataAccessPrivileges.isEmpty() + || false == actions.isEmpty() : "at least one of the privilege sets or actions must be non-empty"; + if (false == allAccessPrivileges.isEmpty()) { - Set result = Set.of(union(allAccessPrivileges, actions, IndexComponentSelectorPrivilege.ALL)); - assertNamesMatch(result, name); - return result; + return Set.of(union(allAccessPrivileges, actions, IndexComponentSelectorPrivilege.ALL)); } final Set result = new HashSet<>(); @@ -368,7 +373,6 @@ private static Set resolve(Set name) { if (false == dataAccessPrivileges.isEmpty() || false == actions.isEmpty()) { result.add(union(dataAccessPrivileges, actions, IndexComponentSelectorPrivilege.DATA)); } - assertNamesMatch(result, name); return result; } @@ -383,15 +387,14 @@ private static IndexPrivilege union( IndexComponentSelectorPrivilege selectorPrivilege ) { Set automata = HashSet.newHashSet(privileges.size() + actions.size()); - Set names = new HashSet<>(); - for (var privilege : privileges) { + Set names = HashSet.newHashSet(privileges.size() + actions.size()); + for (IndexPrivilege privilege : privileges) { names.add(privilege.getSingleName()); automata.add(privilege.automaton); } if (false == actions.isEmpty()) { names.addAll(actions); - // TODO for-loop or optimize? automata.add(patterns(actions.stream().map(Privilege::actionToPattern).toArray(String[]::new))); } return new IndexPrivilege(names, unionAndMinimize(automata), selectorPrivilege); @@ -420,4 +423,15 @@ public static Collection findPrivilegesThatGrant(String action) { .map(Map.Entry::getKey) .toList(); } + + public IndexComponentSelectorPrivilege getSelectorPrivilege() { + return selectorPrivilege; + } + + public String getSingleName() { + if (name().size() != 1) { + throw new IllegalStateException("Expected a single name, but got: " + name()); + } + return name().iterator().next(); + } } From fc016641297a9628879a5f253ce939c64cbbbff9 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Tue, 18 Feb 2025 13:07:54 +0100 Subject: [PATCH 024/131] Fix and simplify --- .../core/security/authz/permission/Role.java | 67 ++++++++++++------- .../authz/privilege/IndexPrivilege.java | 18 +++-- .../authz/store/ReservedRolesStore.java | 17 ++--- .../authz/store/CompositeRolesStore.java | 54 +++++++-------- .../RestGetBuiltinPrivilegesAction.java | 5 +- .../QueryableBuiltInRolesUtilsTests.java | 7 +- 6 files changed, 82 insertions(+), 86 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java index aa9847c490290..15a74f26f5f07 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java @@ -255,6 +255,19 @@ public Builder add(IndexPrivilege privilege, String... indices) { return add(FieldPermissions.DEFAULT, null, privilege, false, indices); } + public Builder add( + FieldPermissions fieldPermissions, + Set query, + Set privilegesSplitBySelector, + boolean allowRestrictedIndices, + String... indices + ) { + for (var indexPrivilege : privilegesSplitBySelector) { + add(fieldPermissions, query, indexPrivilege, allowRestrictedIndices, indices); + } + return this; + } + public Builder add( FieldPermissions fieldPermissions, Set query, @@ -266,6 +279,20 @@ public Builder add( return this; } + public Builder addRemoteIndicesGroup( + final Set remoteClusterAliases, + final FieldPermissions fieldPermissions, + final Set query, + final Set privilegesSplitBySelector, + final boolean allowRestrictedIndices, + final String... indices + ) { + for (var indexPrivilege : privilegesSplitBySelector) { + addRemoteIndicesGroup(remoteClusterAliases, fieldPermissions, query, indexPrivilege, allowRestrictedIndices, indices); + } + return this; + } + public Builder addRemoteIndicesGroup( final Set remoteClusterAliases, final FieldPermissions fieldPermissions, @@ -405,15 +432,15 @@ static SimpleRole buildFromRoleDescriptor( ); for (RoleDescriptor.IndicesPrivileges indexPrivilege : roleDescriptor.getIndicesPrivileges()) { - FieldPermissions fieldPermissions = fieldPermissionsCache.getFieldPermissions( - new FieldPermissionsDefinition(indexPrivilege.getGrantedFields(), indexPrivilege.getDeniedFields()) + builder.add( + fieldPermissionsCache.getFieldPermissions( + new FieldPermissionsDefinition(indexPrivilege.getGrantedFields(), indexPrivilege.getDeniedFields()) + ), + indexPrivilege.getQuery() == null ? null : Collections.singleton(indexPrivilege.getQuery()), + IndexPrivilege.getSplitBySelector(Set.of(indexPrivilege.getPrivileges())), + indexPrivilege.allowRestrictedIndices(), + indexPrivilege.getIndices() ); - Set query = indexPrivilege.getQuery() == null ? null : Collections.singleton(indexPrivilege.getQuery()); - boolean allowRestrictedIndices = indexPrivilege.allowRestrictedIndices(); - Set splitPrivileges = IndexPrivilege.getSplitBySelector(Set.of(indexPrivilege.getPrivileges())); - for (var entry : splitPrivileges) { - builder.add(fieldPermissions, query, entry, allowRestrictedIndices, indexPrivilege.getIndices()); - } } for (RoleDescriptor.RemoteIndicesPrivileges remoteIndicesPrivileges : roleDescriptor.getRemoteIndicesPrivileges()) { @@ -421,22 +448,16 @@ static SimpleRole buildFromRoleDescriptor( assert Arrays.equals(new String[] { "*" }, clusterAliases) : "reserved role should not define remote indices privileges for specific clusters"; final RoleDescriptor.IndicesPrivileges indexPrivilege = remoteIndicesPrivileges.indicesPrivileges(); - FieldPermissions fieldPermissions = fieldPermissionsCache.getFieldPermissions( - new FieldPermissionsDefinition(indexPrivilege.getGrantedFields(), indexPrivilege.getDeniedFields()) + builder.addRemoteIndicesGroup( + Set.of(clusterAliases), + fieldPermissionsCache.getFieldPermissions( + new FieldPermissionsDefinition(indexPrivilege.getGrantedFields(), indexPrivilege.getDeniedFields()) + ), + indexPrivilege.getQuery() == null ? null : Collections.singleton(indexPrivilege.getQuery()), + IndexPrivilege.getSplitBySelector(Set.of(indexPrivilege.getPrivileges())), + indexPrivilege.allowRestrictedIndices(), + indexPrivilege.getIndices() ); - Set query = indexPrivilege.getQuery() == null ? null : Collections.singleton(indexPrivilege.getQuery()); - boolean allowRestrictedIndices = indexPrivilege.allowRestrictedIndices(); - Set splitPrivileges = IndexPrivilege.getSplitBySelector(Set.of(indexPrivilege.getPrivileges())); - for (var entry : splitPrivileges) { - builder.addRemoteIndicesGroup( - Set.of(clusterAliases), - fieldPermissions, - query, - entry, - allowRestrictedIndices, - indexPrivilege.getIndices() - ); - } } RemoteClusterPermissions remoteClusterPermissions = roleDescriptor.getRemoteClusterPermissions(); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java index a193d51aa309e..8810d6d0abf51 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java @@ -194,11 +194,6 @@ public final class IndexPrivilege extends Privilege { public static final IndexPrivilege CREATE_DOC = new IndexPrivilege("create_doc", CREATE_DOC_AUTOMATON); public static final IndexPrivilege MONITOR = new IndexPrivilege("monitor", MONITOR_AUTOMATON); public static final IndexPrivilege MANAGE = new IndexPrivilege("manage", MANAGE_AUTOMATON); - public static final IndexPrivilege MANAGE_FAILURE_STORE_INTERNAL = new IndexPrivilege( - "manage_failure_store_internal", - MANAGE_AUTOMATON, - IndexComponentSelectorPrivilege.FAILURES - ); public static final IndexPrivilege DELETE_INDEX = new IndexPrivilege("delete_index", DELETE_INDEX_AUTOMATON); public static final IndexPrivilege CREATE_INDEX = new IndexPrivilege("create_index", CREATE_INDEX_AUTOMATON); public static final IndexPrivilege VIEW_METADATA = new IndexPrivilege("view_index_metadata", VIEW_METADATA_AUTOMATON); @@ -248,8 +243,7 @@ public final class IndexPrivilege extends Privilege { entry("auto_configure", AUTO_CONFIGURE), entry("cross_cluster_replication", CROSS_CLUSTER_REPLICATION), entry("cross_cluster_replication_internal", CROSS_CLUSTER_REPLICATION_INTERNAL), - DataStream.isFailureStoreFeatureFlagEnabled() ? entry("read_failure_store", READ_FAILURE_STORE) : null, - DataStream.isFailureStoreFeatureFlagEnabled() ? entry("manage_failure_store_internal", MANAGE_FAILURE_STORE_INTERNAL) : null + DataStream.isFailureStoreFeatureFlagEnabled() ? entry("read_failure_store", READ_FAILURE_STORE) : null ).filter(Objects::nonNull).collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)) ); @@ -347,7 +341,7 @@ private static Set resolve(Set name) { } final Set result = combineIntoResult(allAccessPrivileges, failureAccessPrivileges, dataAccessPrivileges, actions); - assertNamesMatch(result, name); + assertNamesMatch(name, result); return result; } @@ -376,8 +370,12 @@ private static Set combineIntoResult( return result; } - private static void assertNamesMatch(Set privileges, Set names) { - assert names.equals(privileges.stream().map(Privilege::name).flatMap(Set::stream).collect(Collectors.toSet())) + private static void assertNamesMatch(Set names, Set privileges) { + // TODO clean up + assert names.stream() + .map(n -> n.toLowerCase(Locale.ROOT)) + .collect(Collectors.toSet()) + .equals(privileges.stream().map(Privilege::name).flatMap(Set::stream).collect(Collectors.toSet())) : "mismatch between names [" + names + "] and names on split privileges [" + privileges + "]"; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java index cc5a6fc6ab4b3..1ec1d5912db68 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java @@ -11,7 +11,6 @@ import org.elasticsearch.action.admin.cluster.repositories.get.GetRepositoriesAction; import org.elasticsearch.action.admin.indices.alias.TransportIndicesAliasesAction; import org.elasticsearch.action.admin.indices.rollover.RolloverAction; -import org.elasticsearch.cluster.metadata.DataStream; import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.util.set.Sets; @@ -79,12 +78,8 @@ public class ReservedRolesStore implements BiConsumer, ActionListene RoleDescriptor.IndicesPrivileges.builder().indices("*").privileges("all").allowRestrictedIndices(false).build(), RoleDescriptor.IndicesPrivileges.builder() .indices("*") - .privileges( - // TODO are there edge-cases where this is a bad idea? - DataStream.isFailureStoreFeatureFlagEnabled() - ? new String[] { "monitor", "read", "view_index_metadata", "read_cross_cluster", "read_failure_store" } - : new String[] { "monitor", "read", "view_index_metadata", "read_cross_cluster" } - ) + // TODO add read_failure_store when failures authorization is implemented + .privileges("monitor", "read", "view_index_metadata", "read_cross_cluster") .allowRestrictedIndices(true) .build() }, new RoleDescriptor.ApplicationResourcePrivileges[] { @@ -101,12 +96,8 @@ public class ReservedRolesStore implements BiConsumer, ActionListene new RoleDescriptor.RemoteIndicesPrivileges( RoleDescriptor.IndicesPrivileges.builder() .indices("*") - .privileges( - // TODO are there edge-cases where this is a bad idea? - DataStream.isFailureStoreFeatureFlagEnabled() - ? new String[] { "monitor", "read", "view_index_metadata", "read_cross_cluster", "read_failure_store" } - : new String[] { "monitor", "read", "view_index_metadata", "read_cross_cluster" } - ) + // TODO add read_failure_store when failures authorization is implemented + .privileges("monitor", "read", "view_index_metadata", "read_cross_cluster") .allowRestrictedIndices(true) .build(), "*" diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java index 0330ae8028ac9..ad75da117f0ea 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java @@ -31,7 +31,6 @@ import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.IndicesPrivileges; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.RemoteIndicesPrivileges; import org.elasticsearch.xpack.core.security.authz.accesscontrol.DocumentSubsetBitsetCache; -import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsCache; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsDefinition; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsDefinition.FieldGrantExcludeGroup; @@ -542,43 +541,38 @@ public static void buildRoleFromDescriptors( for (Map.Entry, MergeableIndicesPrivilege> entry : indicesPrivilegesMap.entrySet()) { MergeableIndicesPrivilege indicesPrivilege = entry.getValue(); - FieldPermissions fieldPermissions = fieldPermissionsCache.getFieldPermissions(indicesPrivilege.fieldPermissionsDefinition); - String[] indices = indicesPrivilege.indices.toArray(Strings.EMPTY_ARRAY); - Set splitPrivileges = IndexPrivilege.getSplitBySelector(indicesPrivilege.privileges); - for (var splitPrivilege : splitPrivileges) { - builder.add(fieldPermissions, indicesPrivilege.query, splitPrivilege, false, indices); - } + builder.add( + fieldPermissionsCache.getFieldPermissions(indicesPrivilege.fieldPermissionsDefinition), + indicesPrivilege.query, + IndexPrivilege.getSplitBySelector(indicesPrivilege.privileges), + false, + indicesPrivilege.indices.toArray(Strings.EMPTY_ARRAY) + ); + } for (Map.Entry, MergeableIndicesPrivilege> entry : restrictedIndicesPrivilegesMap.entrySet()) { MergeableIndicesPrivilege indicesPrivilege = entry.getValue(); - FieldPermissions fieldPermissions = fieldPermissionsCache.getFieldPermissions(indicesPrivilege.fieldPermissionsDefinition); - String[] indices = indicesPrivilege.indices.toArray(Strings.EMPTY_ARRAY); - Set splitPrivileges = IndexPrivilege.getSplitBySelector(indicesPrivilege.privileges); - for (var splitPrivilege : splitPrivileges) { - builder.add(fieldPermissions, indicesPrivilege.query, splitPrivilege, true, indices); - } + builder.add( + fieldPermissionsCache.getFieldPermissions(indicesPrivilege.fieldPermissionsDefinition), + indicesPrivilege.query, + IndexPrivilege.getSplitBySelector(indicesPrivilege.privileges), + true, + indicesPrivilege.indices.toArray(Strings.EMPTY_ARRAY) + ); } remoteIndicesPrivilegesByCluster.forEach((clusterAliasKey, remoteIndicesPrivilegesForCluster) -> { remoteIndicesPrivilegesForCluster.forEach((privilege) -> { - FieldPermissions fieldPermissions = fieldPermissionsCache.getFieldPermissions( - new FieldPermissionsDefinition(privilege.getGrantedFields(), privilege.getDeniedFields()) - ); - Set query = privilege.getQuery() == null ? null : newHashSet(privilege.getQuery()); - String[] indices = newHashSet(Objects.requireNonNull(privilege.getIndices())).toArray(new String[0]); - Set splitPrivileges = IndexPrivilege.getSplitBySelector( - Set.of(Objects.requireNonNull(privilege.getPrivileges())) + builder.addRemoteIndicesGroup( + clusterAliasKey, + fieldPermissionsCache.getFieldPermissions( + new FieldPermissionsDefinition(privilege.getGrantedFields(), privilege.getDeniedFields()) + ), + privilege.getQuery() == null ? null : newHashSet(privilege.getQuery()), + IndexPrivilege.getSplitBySelector(Set.of(Objects.requireNonNull(privilege.getPrivileges()))), + privilege.allowRestrictedIndices(), + newHashSet(Objects.requireNonNull(privilege.getIndices())).toArray(new String[0]) ); - for (var indexPrivilege : splitPrivileges) { - builder.addRemoteIndicesGroup( - clusterAliasKey, - fieldPermissions, - query, - indexPrivilege, - privilege.allowRestrictedIndices(), - indices - ); - } }); }); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetBuiltinPrivilegesAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetBuiltinPrivilegesAction.java index b6cc14a33f46b..95716ec4d8cc2 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetBuiltinPrivilegesAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetBuiltinPrivilegesAction.java @@ -43,10 +43,7 @@ public class RestGetBuiltinPrivilegesAction extends SecurityBaseRestHandler { private static final Logger logger = LogManager.getLogger(RestGetBuiltinPrivilegesAction.class); // TODO remove this once we can update docs tests again - private static final Set FAILURE_STORE_PRIVILEGES_TO_EXCLUDE = Set.of( - IndexPrivilege.READ_FAILURE_STORE.getSingleName(), - IndexPrivilege.MANAGE_FAILURE_STORE_INTERNAL.getSingleName() - ); + private static final Set FAILURE_STORE_PRIVILEGES_TO_EXCLUDE = Set.of(IndexPrivilege.READ_FAILURE_STORE.getSingleName()); private final GetBuiltinPrivilegesResponseTranslator responseTranslator; public RestGetBuiltinPrivilegesAction( diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/QueryableBuiltInRolesUtilsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/QueryableBuiltInRolesUtilsTests.java index 0f40e1eab4b68..933512d3426c4 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/QueryableBuiltInRolesUtilsTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/QueryableBuiltInRolesUtilsTests.java @@ -7,7 +7,6 @@ package org.elasticsearch.xpack.security.support; -import org.elasticsearch.cluster.metadata.DataStream; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.RoleDescriptorTestHelper; @@ -41,11 +40,7 @@ public static void setupReservedRolesStore() { public void testCalculateHash() { assertThat( QueryableBuiltInRolesUtils.calculateHash(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR), - equalTo( - DataStream.isFailureStoreFeatureFlagEnabled() - ? "qgdWamvjudRKGezTGfjoSCr230sFDdh2t6xFUPYiW2Q=" - : "bWEFdFo4WX229wdhdecfiz5QHMYEssh3ex8hizRgg+Q=" - ) + equalTo("bWEFdFo4WX229wdhdecfiz5QHMYEssh3ex8hizRgg+Q=") ); } From 79433cb56fc3f62b46ed434cf15b43d8f67e8607 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Tue, 18 Feb 2025 13:30:55 +0100 Subject: [PATCH 025/131] Test fixes --- .../core/security/authz/permission/Role.java | 12 ++--- .../authz/privilege/IndexPrivilegeTests.java | 2 +- .../authz/store/CompositeRolesStore.java | 45 ++++++++-------- .../xpack/security/authz/RBACEngineTests.java | 52 +++++++++++-------- 4 files changed, 58 insertions(+), 53 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java index 15a74f26f5f07..f5836fe1e400e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java @@ -447,16 +447,16 @@ static SimpleRole buildFromRoleDescriptor( final String[] clusterAliases = remoteIndicesPrivileges.remoteClusters(); assert Arrays.equals(new String[] { "*" }, clusterAliases) : "reserved role should not define remote indices privileges for specific clusters"; - final RoleDescriptor.IndicesPrivileges indexPrivilege = remoteIndicesPrivileges.indicesPrivileges(); + final RoleDescriptor.IndicesPrivileges indicesPrivileges = remoteIndicesPrivileges.indicesPrivileges(); builder.addRemoteIndicesGroup( Set.of(clusterAliases), fieldPermissionsCache.getFieldPermissions( - new FieldPermissionsDefinition(indexPrivilege.getGrantedFields(), indexPrivilege.getDeniedFields()) + new FieldPermissionsDefinition(indicesPrivileges.getGrantedFields(), indicesPrivileges.getDeniedFields()) ), - indexPrivilege.getQuery() == null ? null : Collections.singleton(indexPrivilege.getQuery()), - IndexPrivilege.getSplitBySelector(Set.of(indexPrivilege.getPrivileges())), - indexPrivilege.allowRestrictedIndices(), - indexPrivilege.getIndices() + indicesPrivileges.getQuery() == null ? null : Collections.singleton(indicesPrivileges.getQuery()), + IndexPrivilege.getSplitBySelector(Set.of(indicesPrivileges.getPrivileges())), + indicesPrivileges.allowRestrictedIndices(), + indicesPrivileges.getIndices() ); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java index 81aa59e40d195..f3be5a10a42b9 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java @@ -64,7 +64,7 @@ public void testFindPrivilegesThatGrant() { assertThat(findPrivilegesThatGrant(TransportDeleteAction.NAME), equalTo(List.of("delete", "write", "all"))); assertThat( findPrivilegesThatGrant(IndicesStatsAction.NAME), - equalTo(List.of("monitor", "manage", "cross_cluster_replication", "all")) + equalTo(List.of("monitor", "cross_cluster_replication", "manage", "all")) ); assertThat(findPrivilegesThatGrant(RefreshAction.NAME), equalTo(List.of("maintenance", "manage", "all"))); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java index ad75da117f0ea..5d55fc31f6180 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java @@ -539,41 +539,38 @@ public static void buildRoleFromDescriptors( .cluster(clusterPrivileges, configurableClusterPrivileges) .runAs(runAsPrivilege); - for (Map.Entry, MergeableIndicesPrivilege> entry : indicesPrivilegesMap.entrySet()) { - MergeableIndicesPrivilege indicesPrivilege = entry.getValue(); - builder.add( - fieldPermissionsCache.getFieldPermissions(indicesPrivilege.fieldPermissionsDefinition), - indicesPrivilege.query, - IndexPrivilege.getSplitBySelector(indicesPrivilege.privileges), + indicesPrivilegesMap.forEach( + (key, privilege) -> builder.add( + fieldPermissionsCache.getFieldPermissions(privilege.fieldPermissionsDefinition), + privilege.query, + IndexPrivilege.getSplitBySelector(privilege.privileges), false, - indicesPrivilege.indices.toArray(Strings.EMPTY_ARRAY) - ); - - } - for (Map.Entry, MergeableIndicesPrivilege> entry : restrictedIndicesPrivilegesMap.entrySet()) { - MergeableIndicesPrivilege indicesPrivilege = entry.getValue(); - builder.add( - fieldPermissionsCache.getFieldPermissions(indicesPrivilege.fieldPermissionsDefinition), - indicesPrivilege.query, - IndexPrivilege.getSplitBySelector(indicesPrivilege.privileges), + privilege.indices.toArray(Strings.EMPTY_ARRAY) + ) + ); + restrictedIndicesPrivilegesMap.forEach( + (key, privilege) -> builder.add( + fieldPermissionsCache.getFieldPermissions(privilege.fieldPermissionsDefinition), + privilege.query, + IndexPrivilege.getSplitBySelector(privilege.privileges), true, - indicesPrivilege.indices.toArray(Strings.EMPTY_ARRAY) - ); - } + privilege.indices.toArray(Strings.EMPTY_ARRAY) + ) + ); remoteIndicesPrivilegesByCluster.forEach((clusterAliasKey, remoteIndicesPrivilegesForCluster) -> { - remoteIndicesPrivilegesForCluster.forEach((privilege) -> { - builder.addRemoteIndicesGroup( + remoteIndicesPrivilegesForCluster.forEach( + (privilege) -> builder.addRemoteIndicesGroup( clusterAliasKey, fieldPermissionsCache.getFieldPermissions( new FieldPermissionsDefinition(privilege.getGrantedFields(), privilege.getDeniedFields()) ), privilege.getQuery() == null ? null : newHashSet(privilege.getQuery()), - IndexPrivilege.getSplitBySelector(Set.of(Objects.requireNonNull(privilege.getPrivileges()))), + IndexPrivilege.getSplitBySelector(newHashSet(Objects.requireNonNull(privilege.getPrivileges()))), privilege.allowRestrictedIndices(), newHashSet(Objects.requireNonNull(privilege.getIndices())).toArray(new String[0]) - ); - }); + ) + ); }); if (remoteClusterPermissions.hasAnyPrivileges()) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java index 5ac570697b396..7667f7ca87e20 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java @@ -1614,24 +1614,34 @@ public void testGetRoleDescriptorsIntersectionForRemoteClusterHasDeterministicOr final RemoteIndicesPermission.Builder remoteIndicesBuilder = RemoteIndicesPermission.builder(); final String concreteClusterAlias = randomAlphaOfLength(10); final int numGroups = randomIntBetween(2, 5); + int extraGroups = 0; for (int i = 0; i < numGroups; i++) { - remoteIndicesBuilder.addGroup( - Set.copyOf(randomNonEmptySubsetOf(List.of(concreteClusterAlias, "*"))), - // TODO handle failure store - IndexPrivilege.getSingle(Set.copyOf(randomSubsetOf(randomIntBetween(1, 4), IndexPrivilege.names()))), - new FieldPermissions( - new FieldPermissionsDefinition( - Set.of( - randomBoolean() - ? randomFieldGrantExcludeGroup() - : new FieldPermissionsDefinition.FieldGrantExcludeGroup(null, null) - ) - ) - ), - randomBoolean() ? Set.of(randomDlsQuery()) : null, - randomBoolean(), - generateRandomStringArray(3, 10, false, false) + Set splitBySelector = IndexPrivilege.getSplitBySelector( + Set.copyOf(randomSubsetOf(randomIntBetween(1, 4), IndexPrivilege.names())) ); + // If we end up with failure and data access, we will split and end up with extra groups. Need to account for this for the + // final assertion + if (splitBySelector.size() == 2) { + extraGroups++; + } + for (var privilege : splitBySelector) { + remoteIndicesBuilder.addGroup( + Set.copyOf(randomNonEmptySubsetOf(List.of(concreteClusterAlias, "*"))), + privilege, + new FieldPermissions( + new FieldPermissionsDefinition( + Set.of( + randomBoolean() + ? randomFieldGrantExcludeGroup() + : new FieldPermissionsDefinition.FieldGrantExcludeGroup(null, null) + ) + ) + ), + randomBoolean() ? Set.of(randomDlsQuery()) : null, + randomBoolean(), + generateRandomStringArray(3, 10, false, false) + ); + } } final RemoteIndicesPermission permissions = remoteIndicesBuilder.build(); List remoteIndicesGroups = permissions.remoteIndicesGroups(); @@ -1673,7 +1683,10 @@ public void testGetRoleDescriptorsIntersectionForRemoteClusterHasDeterministicOr final RoleDescriptorsIntersection actual2 = future2.get(); assertThat(actual1, equalTo(actual2)); - assertThat(actual1.roleDescriptorsList().iterator().next().iterator().next().getIndicesPrivileges().length, equalTo(numGroups)); + assertThat( + actual1.roleDescriptorsList().iterator().next().iterator().next().getIndicesPrivileges().length, + equalTo(numGroups + extraGroups) + ); } public void testGetRoleDescriptorsIntersectionForRemoteClusterWithoutMatchingGroups() throws ExecutionException, InterruptedException { @@ -1755,11 +1768,6 @@ public void testGetRoleDescriptorsForRemoteClusterForReservedRoles() { .indices("*") .privileges("monitor", "read", "read_cross_cluster", "view_index_metadata") .allowRestrictedIndices(true) - .build(), - IndicesPrivileges.builder() - .indices("*") - .privileges("read_failure_store") - .allowRestrictedIndices(true) .build() }, null, null, From 6ae9f842defeb87aaa7a8b09733005276d06c338 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Tue, 18 Feb 2025 14:10:54 +0100 Subject: [PATCH 026/131] Javadoc and test fixes --- .../authz/permission/IndicesPermission.java | 2 +- .../ConfigurableClusterPrivileges.java | 2 +- .../authz/privilege/IndexPrivilege.java | 16 +++++++-- .../authz/permission/LimitedRoleTests.java | 2 +- .../authz/permission/SimpleRoleTests.java | 2 +- .../authz/privilege/IndexPrivilegeTests.java | 31 +++++++++-------- .../authz/privilege/PrivilegeTests.java | 33 ++++++++++++++----- .../support/AutomatonPatternsTests.java | 8 ++--- .../security/CrossClusterShardTests.java | 2 +- .../DeprecationRoleDescriptorConsumer.java | 4 +-- .../xpack/security/authz/RBACEngineTests.java | 2 +- .../authz/store/CompositeRolesStoreTests.java | 6 ++-- 12 files changed, 71 insertions(+), 39 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 1e33f95981627..f14b5a0620a39 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -302,7 +302,7 @@ public boolean checkResourcePrivileges( } } for (String privilege : checkForPrivileges) { - IndexPrivilege indexPrivilege = IndexPrivilege.getSingle(Collections.singleton(privilege)); + IndexPrivilege indexPrivilege = IndexPrivilege.getSingleSelector(Collections.singleton(privilege)); if (allowedIndexPrivilegesAutomaton != null && Automatons.subsetOf(indexPrivilege.getAutomaton(), allowedIndexPrivilegesAutomaton)) { if (resourcePrivilegesMapBuilder != null) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java index c07e0d13e0eae..e6b6d2a5ed9da 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java @@ -416,7 +416,7 @@ public ManageRolesPrivilege(List manageRolesInd for (ManageRolesIndexPermissionGroup indexPatternPrivilege : manageRolesIndexPermissionGroups) { // TODO handle selectors indicesPermissionBuilder.addGroup( - IndexPrivilege.getSingle(Set.of(indexPatternPrivilege.privileges())), + IndexPrivilege.getSingleSelector(Set.of(indexPatternPrivilege.privileges())), FieldPermissions.DEFAULT, null, false, diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java index 8810d6d0abf51..148bb6553f4c8 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java @@ -271,8 +271,12 @@ private IndexPrivilege(Set name, Automaton automaton, IndexComponentSele this.selectorPrivilege = selectorPrivilege; } - // TODO javadoc - public static IndexPrivilege getSingle(Set name) { + /** + * TODO more detail + * Returns an index privilege for a single selector. Delegates to {@link #getSplitBySelector(Set)} but throws if the result has more + * than one selector. The caller must ensure that the names only map to privileges with the same selector. + */ + public static IndexPrivilege getSingleSelector(Set name) { var single = getSplitBySelector(name); if (single.size() != 1) { throw new IllegalArgumentException("expected singleton"); @@ -280,7 +284,13 @@ public static IndexPrivilege getSingle(Set name) { return single.iterator().next(); } - // TODO javadoc + /** + * TODO more detail + * Returns a set of index privileges, each privilege responsible for a separate selector. + * For instance, `getSplitBySelector(Set.of("view_index_metadata", "write", "read_failure_store"))` will return two index privileges + * one covering "view_index_metadata" and "write" for a {@link IndexComponentSelectorPrivilege#DATA}, the other covering + * "read_failure_store" for a {@link IndexComponentSelectorPrivilege#FAILURES} selector. + */ public static Set getSplitBySelector(Set name) { return CACHE.computeIfAbsent(name, (theName) -> { if (theName.isEmpty()) { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java index 3d62212cfc812..6c5a38600b3df 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java @@ -240,7 +240,7 @@ private static FieldPermissions randomFlsPermissions(String... grantedFields) { private static IndexPrivilege randomIndexPrivilege() { // TODO handle failure store - return IndexPrivilege.getSingle(Set.of(randomFrom(IndexPrivilege.names()))); + return IndexPrivilege.getSingleSelector(Set.of(randomFrom(IndexPrivilege.names()))); } public void testGetRoleDescriptorsIntersectionForRemoteClusterReturnsEmpty() { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/SimpleRoleTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/SimpleRoleTests.java index 1a9878b479046..a9c2e346562b1 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/SimpleRoleTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/SimpleRoleTests.java @@ -177,7 +177,7 @@ public void testGetRoleDescriptorsIntersectionForRemoteCluster() { new FieldPermissions(new FieldPermissionsDefinition(new String[] { randomAlphaOfLength(5) }, null)), null, // TODO handle failure store - IndexPrivilege.getSingle(Set.of(randomFrom(IndexPrivilege.names()))), + IndexPrivilege.getSingleSelector(Set.of(randomFrom(IndexPrivilege.names()))), randomBoolean(), randomAlphaOfLength(9) ) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java index f3be5a10a42b9..799728cf13c4f 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java @@ -84,39 +84,39 @@ public void testPrivilegesForGetCheckPointAction() { public void testRelationshipBetweenPrivileges() { assertThat( Automatons.subsetOf( - IndexPrivilege.getSingle(Set.of("view_index_metadata")).automaton, - IndexPrivilege.getSingle(Set.of("manage")).automaton + IndexPrivilege.getSingleSelector(Set.of("view_index_metadata")).automaton, + IndexPrivilege.getSingleSelector(Set.of("manage")).automaton ), is(true) ); assertThat( Automatons.subsetOf( - IndexPrivilege.getSingle(Set.of("monitor")).automaton, - IndexPrivilege.getSingle(Set.of("manage")).automaton + IndexPrivilege.getSingleSelector(Set.of("monitor")).automaton, + IndexPrivilege.getSingleSelector(Set.of("manage")).automaton ), is(true) ); assertThat( Automatons.subsetOf( - IndexPrivilege.getSingle(Set.of("create", "create_doc", "index", "delete")).automaton, - IndexPrivilege.getSingle(Set.of("write")).automaton + IndexPrivilege.getSingleSelector(Set.of("create", "create_doc", "index", "delete")).automaton, + IndexPrivilege.getSingleSelector(Set.of("write")).automaton ), is(true) ); assertThat( Automatons.subsetOf( - IndexPrivilege.getSingle(Set.of("create_index", "delete_index")).automaton, - IndexPrivilege.getSingle(Set.of("manage")).automaton + IndexPrivilege.getSingleSelector(Set.of("create_index", "delete_index")).automaton, + IndexPrivilege.getSingleSelector(Set.of("manage")).automaton ), is(true) ); } public void testCrossClusterReplicationPrivileges() { - final IndexPrivilege crossClusterReplication = IndexPrivilege.getSingle(Set.of("cross_cluster_replication")); + final IndexPrivilege crossClusterReplication = IndexPrivilege.getSingleSelector(Set.of("cross_cluster_replication")); List.of( "indices:data/read/xpack/ccr/shard_changes", "indices:monitor/stats", @@ -125,11 +125,16 @@ public void testCrossClusterReplicationPrivileges() { "indices:admin/seq_no/renew_retention_lease" ).forEach(action -> assertThat(crossClusterReplication.predicate.test(action + randomAlphaOfLengthBetween(0, 8)), is(true))); assertThat( - Automatons.subsetOf(crossClusterReplication.automaton, IndexPrivilege.getSingle(Set.of("manage", "read", "monitor")).automaton), + Automatons.subsetOf( + crossClusterReplication.automaton, + IndexPrivilege.getSingleSelector(Set.of("manage", "read", "monitor")).automaton + ), is(true) ); - final IndexPrivilege crossClusterReplicationInternal = IndexPrivilege.getSingle(Set.of("cross_cluster_replication_internal")); + final IndexPrivilege crossClusterReplicationInternal = IndexPrivilege.getSingleSelector( + Set.of("cross_cluster_replication_internal") + ); List.of( "indices:internal/admin/ccr/restore/session/clear", "indices:internal/admin/ccr/restore/file_chunk/get", @@ -142,11 +147,11 @@ public void testCrossClusterReplicationPrivileges() { ); assertThat( - Automatons.subsetOf(crossClusterReplicationInternal.automaton, IndexPrivilege.getSingle(Set.of("manage")).automaton), + Automatons.subsetOf(crossClusterReplicationInternal.automaton, IndexPrivilege.getSingleSelector(Set.of("manage")).automaton), is(false) ); assertThat( - Automatons.subsetOf(crossClusterReplicationInternal.automaton, IndexPrivilege.getSingle(Set.of("all")).automaton), + Automatons.subsetOf(crossClusterReplicationInternal.automaton, IndexPrivilege.getSingleSelector(Set.of("all")).automaton), is(true) ); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/PrivilegeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/PrivilegeTests.java index 5b9a2bc463f2e..2425d3d4fa71f 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/PrivilegeTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/PrivilegeTests.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.core.security.authz.privilege; import org.apache.lucene.tests.util.automaton.AutomatonTestUtil; +import org.apache.lucene.util.automaton.Automaton; import org.elasticsearch.action.admin.cluster.health.TransportClusterHealthAction; import org.elasticsearch.action.admin.cluster.node.tasks.cancel.TransportCancelTasksAction; import org.elasticsearch.action.admin.cluster.reroute.TransportClusterRerouteAction; @@ -66,6 +67,7 @@ import org.junit.Rule; import org.junit.rules.ExpectedException; +import java.util.Iterator; import java.util.Set; import java.util.function.Predicate; @@ -203,29 +205,44 @@ public void testClusterAction() throws Exception { public void testIndexAction() throws Exception { Set actionName = Sets.newHashSet("indices:admin/mapping/delete"); - IndexPrivilege index = IndexPrivilege.getSingle(actionName); + IndexPrivilege index = IndexPrivilege.getSingleSelector(actionName); assertThat(index, notNullValue()); assertThat(index.predicate().test("indices:admin/mapping/delete"), is(true)); assertThat(index.predicate().test("indices:admin/mapping/dele"), is(false)); assertThat(IndexPrivilege.READ_CROSS_CLUSTER.predicate().test("internal:transport/proxy/indices:data/read/query"), is(true)); } - public void testIndexCollapse() throws Exception { - IndexPrivilege[] values = IndexPrivilege.values().values().toArray(new IndexPrivilege[IndexPrivilege.values().size()]); + public void testIndexCollapse() { + IndexPrivilege[] values = IndexPrivilege.values().values().toArray(new IndexPrivilege[0]); IndexPrivilege first = values[randomIntBetween(0, values.length - 1)]; IndexPrivilege second = values[randomIntBetween(0, values.length - 1)]; Set name = Sets.newHashSet(first.name().iterator().next(), second.name().iterator().next()); - IndexPrivilege index = IndexPrivilege.getSingle(name); + Set indices = IndexPrivilege.getSplitBySelector(name); + + Automaton automaton = null; + if (indices.size() == 1) { + IndexPrivilege index = indices.iterator().next(); + automaton = index.getAutomaton(); + } else if (indices.size() == 2) { + Iterator it = indices.iterator(); + IndexPrivilege index1 = it.next(); + IndexPrivilege index2 = it.next(); + automaton = Automatons.unionAndMinimize(Set.of(index1.getAutomaton(), index2.getAutomaton())); + } else { + fail("indices.size() must be 1 or 2"); + assert false; + } if (Automatons.subsetOf(second.getAutomaton(), first.getAutomaton())) { - assertTrue(AutomatonTestUtil.sameLanguage(index.getAutomaton(), first.getAutomaton())); + assertTrue(AutomatonTestUtil.sameLanguage(automaton, first.getAutomaton())); } else if (Automatons.subsetOf(first.getAutomaton(), second.getAutomaton())) { - assertTrue(AutomatonTestUtil.sameLanguage(index.getAutomaton(), second.getAutomaton())); + assertTrue(AutomatonTestUtil.sameLanguage(automaton, second.getAutomaton())); } else { - assertFalse(AutomatonTestUtil.sameLanguage(index.getAutomaton(), first.getAutomaton())); - assertFalse(AutomatonTestUtil.sameLanguage(index.getAutomaton(), second.getAutomaton())); + assertFalse(AutomatonTestUtil.sameLanguage(automaton, first.getAutomaton())); + assertFalse(AutomatonTestUtil.sameLanguage(automaton, second.getAutomaton())); } + } public void testSystem() throws Exception { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/support/AutomatonPatternsTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/support/AutomatonPatternsTests.java index 9a254289bd761..9060111a06346 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/support/AutomatonPatternsTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/support/AutomatonPatternsTests.java @@ -37,7 +37,7 @@ public void testRemoteClusterPrivsDoNotOverlap() { // check that the action patterns for remote CCS are not allowed by remote CCR privileges Arrays.stream(CCS_INDICES_PRIVILEGE_NAMES).forEach(ccsPrivilege -> { - Automaton ccsAutomaton = IndexPrivilege.getSingle(Set.of(ccsPrivilege)).getAutomaton(); + Automaton ccsAutomaton = IndexPrivilege.getSingleSelector(Set.of(ccsPrivilege)).getAutomaton(); Automatons.getPatterns(ccsAutomaton).forEach(ccsPattern -> { // emulate an action name that could be allowed by a CCS privilege String actionName = ccsPattern.replaceAll("\\*", randomAlphaOfLengthBetween(1, 8)); @@ -49,14 +49,14 @@ public void testRemoteClusterPrivsDoNotOverlap() { ccrPrivileges, ccsPattern ); - assertFalse(errorMessage, IndexPrivilege.getSingle(Set.of(ccrPrivileges)).predicate().test(actionName)); + assertFalse(errorMessage, IndexPrivilege.getSingleSelector(Set.of(ccrPrivileges)).predicate().test(actionName)); }); }); }); // check that the action patterns for remote CCR are not allowed by remote CCS privileges Arrays.stream(CCR_INDICES_PRIVILEGE_NAMES).forEach(ccrPrivilege -> { - Automaton ccrAutomaton = IndexPrivilege.getSingle(Set.of(ccrPrivilege)).getAutomaton(); + Automaton ccrAutomaton = IndexPrivilege.getSingleSelector(Set.of(ccrPrivilege)).getAutomaton(); Automatons.getPatterns(ccrAutomaton).forEach(ccrPattern -> { // emulate an action name that could be allowed by a CCR privilege String actionName = ccrPattern.replaceAll("\\*", randomAlphaOfLengthBetween(1, 8)); @@ -72,7 +72,7 @@ public void testRemoteClusterPrivsDoNotOverlap() { ccsPrivileges, ccrPattern ); - assertFalse(errorMessage, IndexPrivilege.getSingle(Set.of(ccsPrivileges)).predicate().test(actionName)); + assertFalse(errorMessage, IndexPrivilege.getSingleSelector(Set.of(ccsPrivileges)).predicate().test(actionName)); } }); }); diff --git a/x-pack/plugin/security/qa/consistency-checks/src/test/java/org/elasticsearch/xpack/security/CrossClusterShardTests.java b/x-pack/plugin/security/qa/consistency-checks/src/test/java/org/elasticsearch/xpack/security/CrossClusterShardTests.java index d81adb40d8163..ee2297d39dfc1 100644 --- a/x-pack/plugin/security/qa/consistency-checks/src/test/java/org/elasticsearch/xpack/security/CrossClusterShardTests.java +++ b/x-pack/plugin/security/qa/consistency-checks/src/test/java/org/elasticsearch/xpack/security/CrossClusterShardTests.java @@ -112,7 +112,7 @@ public void testCheckForNewShardLevelTransportActions() throws Exception { List shardActions = transportActionBindings.stream() .map(binding -> binding.getProvider().get()) - .filter(action -> IndexPrivilege.getSingle(crossClusterPrivilegeNames).predicate().test(action.actionName)) + .filter(action -> IndexPrivilege.getSingleSelector(crossClusterPrivilegeNames).predicate().test(action.actionName)) .filter(this::actionIsLikelyShardAction) .map(action -> action.actionName) .toList(); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/DeprecationRoleDescriptorConsumer.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/DeprecationRoleDescriptorConsumer.java index 17d1c86646eb4..898614285d433 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/DeprecationRoleDescriptorConsumer.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/DeprecationRoleDescriptorConsumer.java @@ -183,7 +183,7 @@ private void logDeprecatedPermission(RoleDescriptor roleDescriptor) { for (Map.Entry> privilegesByAlias : privilegesByAliasMap.entrySet()) { final String aliasName = privilegesByAlias.getKey(); final Set aliasPrivilegeNames = privilegesByAlias.getValue(); - final Automaton aliasPrivilegeAutomaton = IndexPrivilege.getSingle(aliasPrivilegeNames).getAutomaton(); + final Automaton aliasPrivilegeAutomaton = IndexPrivilege.getSingleSelector(aliasPrivilegeNames).getAutomaton(); final SortedSet inferiorIndexNames = new TreeSet<>(); // check if the alias grants superiors privileges than the indices it points to for (Index index : aliasOrIndexMap.get(aliasName).getIndices()) { @@ -193,7 +193,7 @@ private void logDeprecatedPermission(RoleDescriptor roleDescriptor) { // compute automaton once per index no matter how many times it is pointed to final Automaton indexPrivilegeAutomaton = indexAutomatonMap.computeIfAbsent( index.getName(), - i -> IndexPrivilege.getSingle(indexPrivileges).getAutomaton() + i -> IndexPrivilege.getSingleSelector(indexPrivileges).getAutomaton() ); if (false == Automatons.subsetOf(indexPrivilegeAutomaton, aliasPrivilegeAutomaton)) { inferiorIndexNames.add(index.getName()); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java index 7667f7ca87e20..17cbe9c6aad12 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java @@ -1290,7 +1290,7 @@ public void testBuildUserPrivilegeResponse() { {"term":{"public":true}}"""); final Role role = Role.builder(RESTRICTED_INDICES, "test", "role") .cluster(Sets.newHashSet("monitor", "manage_watcher"), Collections.singleton(manageApplicationPrivileges)) - .add(IndexPrivilege.getSingle(Sets.newHashSet("read", "write")), "index-1") + .add(IndexPrivilege.getSingleSelector(Sets.newHashSet("read", "write")), "index-1") .add(IndexPrivilege.ALL, "index-2", "index-3") .add( new FieldPermissions(new FieldPermissionsDefinition(new String[] { "public.*" }, new String[0])), diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java index 09f2fa3b8ef66..24be0c6c2df21 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java @@ -1492,8 +1492,8 @@ public void testBuildRoleWithMultipleRemoteMergedAcrossPrivilegesAndDescriptors( assertHasRemoteIndexGroupsForClusters( role.remoteIndices(), Set.of("remote-1"), - indexGroup(IndexPrivilege.getSingle(Set.of("read")), false, "index-1"), - indexGroup(IndexPrivilege.getSingle(Set.of("none")), false, "index-1") + indexGroup(IndexPrivilege.getSingleSelector(Set.of("read")), false, "index-1"), + indexGroup(IndexPrivilege.getSingleSelector(Set.of("none")), false, "index-1") ); } @@ -1599,7 +1599,7 @@ public void testBuildRoleWithAllPrivilegeIsNeverSplit() { ); assertHasIndexGroups( role.indices(), - indexGroup(IndexPrivilege.getSingle(Set.of("read", "read_failure_store", "all")), false, indexPattern) + indexGroup(IndexPrivilege.getSingleSelector(Set.of("read", "read_failure_store", "all")), false, indexPattern) ); } From 66ccd339b5fc3d2848af2a390b2b3c15c16a4b43 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Tue, 18 Feb 2025 14:30:00 +0100 Subject: [PATCH 027/131] More test fixes --- .../security/authz/privilege/IndexPrivilege.java | 15 ++++++++------- .../authz/permission/LimitedRoleTests.java | 1 - .../authz/permission/SimpleRoleTests.java | 3 +-- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java index 148bb6553f4c8..564db109302d1 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java @@ -277,11 +277,11 @@ private IndexPrivilege(Set name, Automaton automaton, IndexComponentSele * than one selector. The caller must ensure that the names only map to privileges with the same selector. */ public static IndexPrivilege getSingleSelector(Set name) { - var single = getSplitBySelector(name); - if (single.size() != 1) { - throw new IllegalArgumentException("expected singleton"); + final Set splitBySelector = getSplitBySelector(name); + if (splitBySelector.size() != 1) { + throw new IllegalArgumentException("index privilege patterns " + name + " did not map to a single selector " + splitBySelector); } - return single.iterator().next(); + return splitBySelector.iterator().next(); } /** @@ -437,9 +437,10 @@ public IndexComponentSelectorPrivilege getSelectorPrivilege() { } public String getSingleName() { - if (name().size() != 1) { - throw new IllegalStateException("Expected a single name, but got: " + name()); + final Set names = name(); + if (names.size() != 1) { + throw new IllegalStateException("expected single name for privilege but got " + names); } - return name().iterator().next(); + return names.iterator().next(); } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java index 6c5a38600b3df..1c4ea7a99d22f 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java @@ -239,7 +239,6 @@ private static FieldPermissions randomFlsPermissions(String... grantedFields) { } private static IndexPrivilege randomIndexPrivilege() { - // TODO handle failure store return IndexPrivilege.getSingleSelector(Set.of(randomFrom(IndexPrivilege.names()))); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/SimpleRoleTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/SimpleRoleTests.java index a9c2e346562b1..649e2da324733 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/SimpleRoleTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/SimpleRoleTests.java @@ -176,8 +176,7 @@ public void testGetRoleDescriptorsIntersectionForRemoteCluster() { Set.of(randomAlphaOfLength(8)), new FieldPermissions(new FieldPermissionsDefinition(new String[] { randomAlphaOfLength(5) }, null)), null, - // TODO handle failure store - IndexPrivilege.getSingleSelector(Set.of(randomFrom(IndexPrivilege.names()))), + IndexPrivilege.getSplitBySelector(Set.of(randomFrom(IndexPrivilege.names()))), randomBoolean(), randomAlphaOfLength(9) ) From 7e1e2c0906d253de26df405aa19a6129ff17f341 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Tue, 18 Feb 2025 14:51:54 +0100 Subject: [PATCH 028/131] Moar --- .../ConfigurableClusterPrivileges.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java index e6b6d2a5ed9da..c145a318c4398 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java @@ -414,14 +414,16 @@ public ManageRolesPrivilege(List manageRolesInd this.requestPredicateSupplier = (restrictedIndices) -> { IndicesPermission.Builder indicesPermissionBuilder = new IndicesPermission.Builder(restrictedIndices); for (ManageRolesIndexPermissionGroup indexPatternPrivilege : manageRolesIndexPermissionGroups) { - // TODO handle selectors - indicesPermissionBuilder.addGroup( - IndexPrivilege.getSingleSelector(Set.of(indexPatternPrivilege.privileges())), - FieldPermissions.DEFAULT, - null, - false, - indexPatternPrivilege.indexPatterns() - ); + Set splitBySelector = IndexPrivilege.getSplitBySelector(Set.of(indexPatternPrivilege.privileges())); + for (IndexPrivilege indexPrivilege : splitBySelector) { + indicesPermissionBuilder.addGroup( + indexPrivilege, + FieldPermissions.DEFAULT, + null, + false, + indexPatternPrivilege.indexPatterns() + ); + } } final IndicesPermission indicesPermission = indicesPermissionBuilder.build(); From 37dd755c8554d2ba3fef172b316bf8d14954a056 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Tue, 18 Feb 2025 16:16:15 +0100 Subject: [PATCH 029/131] Fix assertion --- .../action/role/RoleDescriptorRequestValidator.java | 9 ++++++++- .../RemoteClusterSecurityFcActionAuthorizationIT.java | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/RoleDescriptorRequestValidator.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/RoleDescriptorRequestValidator.java index 1f912709b1442..2edbb7525f076 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/RoleDescriptorRequestValidator.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/RoleDescriptorRequestValidator.java @@ -11,6 +11,7 @@ import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilegeResolver; +import org.elasticsearch.xpack.core.security.authz.privilege.IndexComponentSelectorPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.authz.restriction.WorkflowResolver; import org.elasticsearch.xpack.core.security.support.MetadataUtils; @@ -60,7 +61,13 @@ public static ActionRequestValidationException validate( validationException = addValidationError("remote index cluster alias cannot be an empty string", validationException); } try { - IndexPrivilege.getSplitBySelector(Set.of(ridp.indicesPrivileges().getPrivileges())); + var privileges = IndexPrivilege.getSplitBySelector(Set.of(ridp.indicesPrivileges().getPrivileges())); + if (privileges.stream().anyMatch(p -> p.getSelectorPrivilege() == IndexComponentSelectorPrivilege.FAILURES)) { + validationException = addValidationError( + "remote index privileges cannot contain privileges that grant access to the failure store", + validationException + ); + } } catch (IllegalArgumentException ile) { validationException = addValidationError(ile.getMessage(), validationException); } diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityFcActionAuthorizationIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityFcActionAuthorizationIT.java index 793313e238651..9a619c8b78ea4 100644 --- a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityFcActionAuthorizationIT.java +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityFcActionAuthorizationIT.java @@ -435,7 +435,7 @@ public void testUpdateCrossClusterApiKey() throws Exception { + "for user [foo] with assigned roles [role] authenticated by API key id [" + apiKeyId + "] of user [test_user] on indices [index], this action is granted by the index privileges " - + "[view_index_metadata,manage,read,all]" + + "[read,view_index_metadata,manage,all]" ) ); @@ -483,7 +483,7 @@ public void testUpdateCrossClusterApiKey() throws Exception { + "for user [foo] with assigned roles [role] authenticated by API key id [" + apiKeyId + "] of user [test_user] on indices [index], this action is granted by the index privileges " - + "[view_index_metadata,manage,read,all]" + + "[read,view_index_metadata,manage,all]" ) ); } From aae20bd5370b9b4d4ca1ed43077a8a2a606138f8 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Tue, 18 Feb 2025 18:04:02 +0100 Subject: [PATCH 030/131] Tests --- .../action/role/PutRoleRequestTests.java | 17 ++++++++++ .../authz/store/CompositeRolesStoreTests.java | 33 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequestTests.java index 239d48ca9c2e1..e7d93b45a576d 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequestTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequestTests.java @@ -8,6 +8,7 @@ import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.support.WriteRequest; +import org.elasticsearch.cluster.metadata.DataStream; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.ApplicationResourcePrivileges; import org.elasticsearch.xpack.core.security.authz.permission.RemoteClusterPermissionGroup; @@ -48,6 +49,22 @@ public void testValidationErrorWithUnknownClusterPrivilegeName() { assertValidationError("unknown cluster privilege [" + unknownClusterPrivilegeName.toLowerCase(Locale.ROOT) + "]", request); } + public void testValidationErrorWithFailureStorePrivilegeInRemoteIndices() { + assumeTrue("requires failure store feature", DataStream.isFailureStoreFeatureFlagEnabled()); + final PutRoleRequest request = new PutRoleRequest(); + request.name(randomAlphaOfLengthBetween(4, 9)); + request.addRemoteIndex( + new String[] { "*" }, + new String[] { "index" }, + new String[] { "read_failure_store", "read", "indices:data/read" }, + null, + null, + null, + randomBoolean() + ); + assertValidationError("remote index privileges cannot contain privileges that grant access to the failure store", request); + } + public void testValidationErrorWithTooLongRoleName() { final PutRoleRequest request = new PutRoleRequest(); request.name( diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java index 24be0c6c2df21..ebd300a40cc32 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java @@ -23,6 +23,7 @@ import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.client.internal.Client; import org.elasticsearch.cluster.health.ClusterHealthStatus; +import org.elasticsearch.cluster.metadata.DataStream; import org.elasticsearch.cluster.metadata.IndexAbstraction; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.metadata.Metadata; @@ -86,6 +87,7 @@ import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeTests; import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilegeResolver; import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.IndexComponentSelectorPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.authz.restriction.Workflow; import org.elasticsearch.xpack.core.security.authz.restriction.WorkflowResolver; @@ -1531,6 +1533,7 @@ public void testBuildRoleWithMultipleRemoteClusterMerged() { } public void testBuildRoleWithReadFailureStorePrivilegeOnly() { + assumeTrue("requires failure store feature", DataStream.isFailureStoreFeatureFlagEnabled()); String indexPattern = randomAlphanumericOfLength(10); final Role role = buildRole( roleDescriptorWithIndicesPrivileges( @@ -1542,6 +1545,7 @@ public void testBuildRoleWithReadFailureStorePrivilegeOnly() { } public void testBuildRoleWithReadFailureStorePrivilegeDuplicatesMerged() { + assumeTrue("requires failure store feature", DataStream.isFailureStoreFeatureFlagEnabled()); String indexPattern = randomAlphanumericOfLength(10); final Role role = buildRole( roleDescriptorWithIndicesPrivileges( @@ -1555,6 +1559,7 @@ public void testBuildRoleWithReadFailureStorePrivilegeDuplicatesMerged() { } public void testBuildRoleWithReadFailureStoreAndReadPrivilegeSplit() { + assumeTrue("requires failure store feature", DataStream.isFailureStoreFeatureFlagEnabled()); String indexPattern = randomAlphanumericOfLength(10); final Role role = buildRole( roleDescriptorWithIndicesPrivileges( @@ -1571,6 +1576,7 @@ public void testBuildRoleWithReadFailureStoreAndReadPrivilegeSplit() { } public void testBuildRoleWithMultipleReadFailureStoreAndReadPrivilegeSplit() { + assumeTrue("requires failure store feature", DataStream.isFailureStoreFeatureFlagEnabled()); String indexPattern = randomAlphanumericOfLength(10); final Role role = buildRole( roleDescriptorWithIndicesPrivileges( @@ -1588,6 +1594,7 @@ public void testBuildRoleWithMultipleReadFailureStoreAndReadPrivilegeSplit() { } public void testBuildRoleWithAllPrivilegeIsNeverSplit() { + assumeTrue("requires failure store feature", DataStream.isFailureStoreFeatureFlagEnabled()); String indexPattern = randomAlphanumericOfLength(10); final Role role = buildRole( roleDescriptorWithIndicesPrivileges( @@ -1603,6 +1610,32 @@ public void testBuildRoleWithAllPrivilegeIsNeverSplit() { ); } + public void testBuildRoleNeverSplitsWithoutFailureStoreRelatedPrivileges() { + String indexPattern = randomAlphanumericOfLength(10); + List nonFailurePrivileges = IndexPrivilege.names() + .stream() + .filter(p -> IndexPrivilege.getNamedOrNull(p).getSelectorPrivilege() != IndexComponentSelectorPrivilege.FAILURES) + .toList(); + Set usedPrivileges = new HashSet<>(); + + int n = randomIntBetween(1, 5); + IndicesPrivileges[] indicesPrivileges = new IndicesPrivileges[n]; + for (int i = 0; i < n; i++) { + IndicesPrivileges.Builder builder = IndicesPrivileges.builder(); + // TODO this is due to an unrelated bug in index collation logic + List privileges = randomValueOtherThanMany( + p -> p.get(0).equals("none"), + () -> randomNonEmptySubsetOf(nonFailurePrivileges) + ); + usedPrivileges.addAll(privileges); + indicesPrivileges[i] = builder.indices(indexPattern).privileges(privileges).build(); + } + + final Role role = buildRole(roleDescriptorWithIndicesPrivileges("r1", indicesPrivileges)); + final IndicesPermission actual = role.indices(); + assertHasIndexGroups(actual, indexGroup(IndexPrivilege.getSingleSelector(usedPrivileges), false, indexPattern)); + } + public void testCustomRolesProviderFailures() throws Exception { final FileRolesStore fileRolesStore = mock(FileRolesStore.class); doCallRealMethod().when(fileRolesStore).accept(anySet(), anyActionListener()); From 253f6ab95f3daedce35f7823f58f6d11a40726eb Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 19 Feb 2025 09:26:16 +0100 Subject: [PATCH 031/131] Test get privileges --- .../authz/privilege/IndexPrivilegeTests.java | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java index 799728cf13c4f..cd79269e84cc1 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java @@ -13,6 +13,7 @@ import org.elasticsearch.action.index.TransportIndexAction; import org.elasticsearch.action.search.TransportSearchAction; import org.elasticsearch.action.update.TransportUpdateAction; +import org.elasticsearch.cluster.metadata.DataStream; import org.elasticsearch.common.util.iterable.Iterables; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.core.rollup.action.GetRollupIndexCapsAction; @@ -69,6 +70,74 @@ public void testFindPrivilegesThatGrant() { assertThat(findPrivilegesThatGrant(RefreshAction.NAME), equalTo(List.of("maintenance", "manage", "all"))); } + public void testGetSingleSelector() { + { + IndexPrivilege actual = IndexPrivilege.getSingleSelector(Set.of("all")); + assertThat(actual, equalTo(IndexPrivilege.ALL)); + assertThat(actual.getSelectorPrivilege(), equalTo(IndexComponentSelectorPrivilege.ALL)); + } + { + IndexPrivilege actual = IndexPrivilege.getSingleSelector(Set.of("read")); + assertThat(actual, equalTo(IndexPrivilege.READ)); + assertThat(actual.getSelectorPrivilege(), equalTo(IndexComponentSelectorPrivilege.DATA)); + } + { + IndexPrivilege actual = IndexPrivilege.getSingleSelector(Set.of("none")); + assertThat(actual, equalTo(IndexPrivilege.NONE)); + assertThat(actual.getSelectorPrivilege(), equalTo(IndexComponentSelectorPrivilege.DATA)); + } + { + IndexPrivilege actual = IndexPrivilege.getSingleSelector(Set.of()); + assertThat(actual, equalTo(IndexPrivilege.NONE)); + assertThat(actual.getSelectorPrivilege(), equalTo(IndexComponentSelectorPrivilege.DATA)); + } + { + IndexPrivilege actual = IndexPrivilege.getSingleSelector(Set.of("indices:data/read/search")); + assertThat(actual.getSingleName(), equalTo("indices:data/read/search")); + assertThat(actual.predicate.test("indices:data/read/search"), is(true)); + assertThat(actual.getSelectorPrivilege(), equalTo(IndexComponentSelectorPrivilege.DATA)); + } + { + IndexPrivilege actual = IndexPrivilege.getSingleSelector(Set.of("all", "read", "indices:data/read/search")); + assertThat(actual.name, equalTo(Set.of("all", "read", "indices:data/read/search"))); + assertThat(Automatons.subsetOf(IndexPrivilege.ALL.automaton, actual.automaton), is(true)); + assertThat(actual.getSelectorPrivilege(), equalTo(IndexComponentSelectorPrivilege.ALL)); + } + } + + public void testGetSingleSelectorWithFailuresSelector() { + assumeTrue("This test requires the failure store to be enabled", DataStream.isFailureStoreFeatureFlagEnabled()); + { + IndexPrivilege actual = IndexPrivilege.getSingleSelector(Set.of("read_failure_store")); + assertThat(actual, equalTo(IndexPrivilege.READ_FAILURE_STORE)); + assertThat(actual.getSelectorPrivilege(), equalTo(IndexComponentSelectorPrivilege.FAILURES)); + } + { + IndexPrivilege actual = IndexPrivilege.getSingleSelector(Set.of("all", "read_failure_store")); + assertThat(actual.name(), equalTo(Set.of("all", "read_failure_store"))); + assertThat(actual.getSelectorPrivilege(), equalTo(IndexComponentSelectorPrivilege.ALL)); + assertThat(Automatons.subsetOf(IndexPrivilege.ALL.automaton, actual.automaton), is(true)); + } + { + IndexPrivilege actual = IndexPrivilege.getSingleSelector(Set.of("all", "indices:data/read/search", "read_failure_store")); + assertThat(actual.name(), equalTo(Set.of("all", "indices:data/read/search", "read_failure_store"))); + assertThat(actual.getSelectorPrivilege(), equalTo(IndexComponentSelectorPrivilege.ALL)); + assertThat(Automatons.subsetOf(IndexPrivilege.ALL.automaton, actual.automaton), is(true)); + } + { + IndexPrivilege actual = IndexPrivilege.getSingleSelector(Set.of("all", "read", "read_failure_store")); + assertThat(actual.name(), equalTo(Set.of("all", "read", "read_failure_store"))); + assertThat(actual.getSelectorPrivilege(), equalTo(IndexComponentSelectorPrivilege.ALL)); + assertThat(Automatons.subsetOf(IndexPrivilege.ALL.automaton, actual.automaton), is(true)); + } + expectThrows(IllegalArgumentException.class, () -> IndexPrivilege.getSingleSelector(Set.of("read", "read_failure_store"))); + expectThrows( + IllegalArgumentException.class, + () -> IndexPrivilege.getSingleSelector(Set.of("indices:data/read/search", "read_failure_store")) + ); + expectThrows(IllegalArgumentException.class, () -> IndexPrivilege.getSingleSelector(Set.of("none", "read_failure_store"))); + } + public void testPrivilegesForRollupFieldCapsAction() { final Collection privileges = findPrivilegesThatGrant(GetRollupIndexCapsAction.NAME); assertThat(Set.copyOf(privileges), equalTo(Set.of("manage", "all", "view_index_metadata", "read"))); From 5922f1faab70026f282adc23fc1ce017be6812cd Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 19 Feb 2025 10:26:02 +0100 Subject: [PATCH 032/131] Javadoc and renames --- .../role/RoleDescriptorRequestValidator.java | 8 +- .../authz/permission/IndicesPermission.java | 10 +- .../core/security/authz/permission/Role.java | 4 +- .../ConfigurableClusterPrivileges.java | 4 +- ...a => IndexComponentSelectorPredicate.java} | 15 +- .../authz/privilege/IndexPrivilege.java | 132 ++++++++++-------- .../authz/permission/LimitedRoleTests.java | 2 +- .../authz/permission/SimpleRoleTests.java | 2 +- .../authz/privilege/IndexPrivilegeTests.java | 119 +++++++++++----- .../authz/privilege/PrivilegeTests.java | 4 +- .../support/AutomatonPatternsTests.java | 11 +- .../security/CrossClusterShardTests.java | 2 +- .../authz/store/CompositeRolesStore.java | 6 +- .../DeprecationRoleDescriptorConsumer.java | 4 +- .../xpack/security/authz/RBACEngineTests.java | 4 +- .../authz/store/CompositeRolesStoreTests.java | 12 +- 16 files changed, 207 insertions(+), 132 deletions(-) rename x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/{IndexComponentSelectorPrivilege.java => IndexComponentSelectorPredicate.java} (53%) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/RoleDescriptorRequestValidator.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/RoleDescriptorRequestValidator.java index 2edbb7525f076..8329c1170ecdd 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/RoleDescriptorRequestValidator.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/RoleDescriptorRequestValidator.java @@ -11,7 +11,7 @@ import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilegeResolver; -import org.elasticsearch.xpack.core.security.authz.privilege.IndexComponentSelectorPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.IndexComponentSelectorPredicate; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.authz.restriction.WorkflowResolver; import org.elasticsearch.xpack.core.security.support.MetadataUtils; @@ -49,7 +49,7 @@ public static ActionRequestValidationException validate( if (roleDescriptor.getIndicesPrivileges() != null) { for (RoleDescriptor.IndicesPrivileges idp : roleDescriptor.getIndicesPrivileges()) { try { - IndexPrivilege.getSplitBySelector(Set.of(idp.getPrivileges())); + IndexPrivilege.getSplitBySelectorAccess(Set.of(idp.getPrivileges())); } catch (IllegalArgumentException ile) { validationException = addValidationError(ile.getMessage(), validationException); } @@ -61,8 +61,8 @@ public static ActionRequestValidationException validate( validationException = addValidationError("remote index cluster alias cannot be an empty string", validationException); } try { - var privileges = IndexPrivilege.getSplitBySelector(Set.of(ridp.indicesPrivileges().getPrivileges())); - if (privileges.stream().anyMatch(p -> p.getSelectorPrivilege() == IndexComponentSelectorPrivilege.FAILURES)) { + var privileges = IndexPrivilege.getSplitBySelectorAccess(Set.of(ridp.indicesPrivileges().getPrivileges())); + if (privileges.stream().anyMatch(p -> p.getSelectorPredicate() == IndexComponentSelectorPredicate.FAILURES)) { validationException = addValidationError( "remote index privileges cannot contain privileges that grant access to the failure store", validationException diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index f14b5a0620a39..33a78e01d77d4 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -27,7 +27,7 @@ import org.elasticsearch.index.Index; import org.elasticsearch.xpack.core.security.authz.RestrictedIndices; import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; -import org.elasticsearch.xpack.core.security.authz.privilege.IndexComponentSelectorPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.IndexComponentSelectorPredicate; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.support.Automatons; import org.elasticsearch.xpack.core.security.support.StringMatcher; @@ -302,7 +302,7 @@ public boolean checkResourcePrivileges( } } for (String privilege : checkForPrivileges) { - IndexPrivilege indexPrivilege = IndexPrivilege.getSingleSelector(Collections.singleton(privilege)); + IndexPrivilege indexPrivilege = IndexPrivilege.getSingleSelectorOrThrow(Collections.singleton(privilege)); if (allowedIndexPrivilegesAutomaton != null && Automatons.subsetOf(indexPrivilege.getAutomaton(), allowedIndexPrivilegesAutomaton)) { if (resourcePrivilegesMapBuilder != null) { @@ -793,7 +793,7 @@ public static class Group { public static final Group[] EMPTY_ARRAY = new Group[0]; private final IndexPrivilege privilege; - private final IndexComponentSelectorPrivilege selectorPrivilege; + private final IndexComponentSelectorPredicate selectorPredicate; private final Predicate actionMatcher; private final String[] indices; private final StringMatcher indexNameMatcher; @@ -817,7 +817,7 @@ public Group( assert indices.length != 0; this.privilege = privilege; this.actionMatcher = privilege.predicate(); - this.selectorPrivilege = privilege.getSelectorPrivilege(); + this.selectorPredicate = privilege.getSelectorPredicate(); this.indices = indices; this.allowRestrictedIndices = allowRestrictedIndices; ConcurrentHashMap indexNameAutomatonMemo = new ConcurrentHashMap<>(1); @@ -866,7 +866,7 @@ boolean hasQuery() { } public boolean checkSelector(IndexComponentSelector selector) { - return selectorPrivilege.test(selector); + return selectorPredicate.test(selector); } public boolean allowRestrictedIndices() { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java index f5836fe1e400e..63229e11c6ef7 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java @@ -437,7 +437,7 @@ static SimpleRole buildFromRoleDescriptor( new FieldPermissionsDefinition(indexPrivilege.getGrantedFields(), indexPrivilege.getDeniedFields()) ), indexPrivilege.getQuery() == null ? null : Collections.singleton(indexPrivilege.getQuery()), - IndexPrivilege.getSplitBySelector(Set.of(indexPrivilege.getPrivileges())), + IndexPrivilege.getSplitBySelectorAccess(Set.of(indexPrivilege.getPrivileges())), indexPrivilege.allowRestrictedIndices(), indexPrivilege.getIndices() ); @@ -454,7 +454,7 @@ static SimpleRole buildFromRoleDescriptor( new FieldPermissionsDefinition(indicesPrivileges.getGrantedFields(), indicesPrivileges.getDeniedFields()) ), indicesPrivileges.getQuery() == null ? null : Collections.singleton(indicesPrivileges.getQuery()), - IndexPrivilege.getSplitBySelector(Set.of(indicesPrivileges.getPrivileges())), + IndexPrivilege.getSplitBySelectorAccess(Set.of(indicesPrivileges.getPrivileges())), indicesPrivileges.allowRestrictedIndices(), indicesPrivileges.getIndices() ); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java index c145a318c4398..206fdf113935d 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java @@ -414,7 +414,9 @@ public ManageRolesPrivilege(List manageRolesInd this.requestPredicateSupplier = (restrictedIndices) -> { IndicesPermission.Builder indicesPermissionBuilder = new IndicesPermission.Builder(restrictedIndices); for (ManageRolesIndexPermissionGroup indexPatternPrivilege : manageRolesIndexPermissionGroups) { - Set splitBySelector = IndexPrivilege.getSplitBySelector(Set.of(indexPatternPrivilege.privileges())); + Set splitBySelector = IndexPrivilege.getSplitBySelectorAccess( + Set.of(indexPatternPrivilege.privileges()) + ); for (IndexPrivilege indexPrivilege : splitBySelector) { indicesPermissionBuilder.addGroup( indexPrivilege, diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPredicate.java similarity index 53% rename from x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java rename to x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPredicate.java index d701fe6a87060..6d90e76e664a8 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPredicate.java @@ -13,19 +13,24 @@ import java.util.Set; import java.util.function.Predicate; -public record IndexComponentSelectorPrivilege(Set names, Predicate predicate) +/** + * A predicate to capture role access by {@link IndexComponentSelector}. + * This is assigned to each {@link org.elasticsearch.xpack.core.security.authz.permission.IndicesPermission.Group} during role building. + * See also {@link org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege#getSplitBySelectorAccess(Set)}. + */ +public record IndexComponentSelectorPredicate(Set names, Predicate predicate) implements Predicate { - IndexComponentSelectorPrivilege(String name, Predicate predicate) { + IndexComponentSelectorPredicate(String name, Predicate predicate) { this(Set.of(name), predicate); } - public static final IndexComponentSelectorPrivilege ALL = new IndexComponentSelectorPrivilege("all", Predicates.always()); - public static final IndexComponentSelectorPrivilege DATA = new IndexComponentSelectorPrivilege( + public static final IndexComponentSelectorPredicate ALL = new IndexComponentSelectorPredicate("all", Predicates.always()); + public static final IndexComponentSelectorPredicate DATA = new IndexComponentSelectorPredicate( "data", IndexComponentSelector.DATA::equals ); - public static final IndexComponentSelectorPrivilege FAILURES = new IndexComponentSelectorPrivilege( + public static final IndexComponentSelectorPredicate FAILURES = new IndexComponentSelectorPredicate( "failures", IndexComponentSelector.FAILURES::equals ); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java index 564db109302d1..a2ee11a771afb 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java @@ -179,11 +179,11 @@ public final class IndexPrivilege extends Privilege { ); public static final IndexPrivilege NONE = new IndexPrivilege("none", Automatons.EMPTY); - public static final IndexPrivilege ALL = new IndexPrivilege("all", ALL_AUTOMATON, IndexComponentSelectorPrivilege.ALL); + public static final IndexPrivilege ALL = new IndexPrivilege("all", ALL_AUTOMATON, IndexComponentSelectorPredicate.ALL); public static final IndexPrivilege READ_FAILURE_STORE = new IndexPrivilege( "read_failure_store", READ_AUTOMATON, - IndexComponentSelectorPrivilege.FAILURES + IndexComponentSelectorPredicate.FAILURES ); public static final IndexPrivilege READ = new IndexPrivilege("read", READ_AUTOMATON); public static final IndexPrivilege READ_CROSS_CLUSTER = new IndexPrivilege("read_cross_cluster", READ_CROSS_CLUSTER_AUTOMATON); @@ -252,47 +252,59 @@ public final class IndexPrivilege extends Privilege { private static final ConcurrentHashMap, Set> CACHE = new ConcurrentHashMap<>(); - private final IndexComponentSelectorPrivilege selectorPrivilege; + private final IndexComponentSelectorPredicate selectorPredicate; private IndexPrivilege(String name, Automaton automaton) { this(Collections.singleton(name), automaton); } - private IndexPrivilege(String name, Automaton automaton, IndexComponentSelectorPrivilege selectorPrivilege) { - this(Collections.singleton(name), automaton, selectorPrivilege); + private IndexPrivilege(String name, Automaton automaton, IndexComponentSelectorPredicate selectorPredicate) { + this(Collections.singleton(name), automaton, selectorPredicate); } private IndexPrivilege(Set name, Automaton automaton) { - this(name, automaton, IndexComponentSelectorPrivilege.DATA); + this(name, automaton, IndexComponentSelectorPredicate.DATA); } - private IndexPrivilege(Set name, Automaton automaton, IndexComponentSelectorPrivilege selectorPrivilege) { + private IndexPrivilege(Set name, Automaton automaton, IndexComponentSelectorPredicate selectorPredicate) { super(name, automaton); - this.selectorPrivilege = selectorPrivilege; + this.selectorPredicate = selectorPredicate; } /** - * TODO more detail - * Returns an index privilege for a single selector. Delegates to {@link #getSplitBySelector(Set)} but throws if the result has more - * than one selector. The caller must ensure that the names only map to privileges with the same selector. + * Delegates to {@link #getSplitBySelectorAccess(Set)} but throws if the result is not a singleton, i.e., covers more than one selector. + * Use this method if you know that the input name set corresponds to privileges covering the same selector, for instance if you have a + * single input name, or multiple names that all grant access to one selector e.g., {@link IndexComponentSelector#DATA}. */ - public static IndexPrivilege getSingleSelector(Set name) { - final Set splitBySelector = getSplitBySelector(name); + public static IndexPrivilege getSingleSelectorOrThrow(Set names) { + final Set splitBySelector = getSplitBySelectorAccess(names); if (splitBySelector.size() != 1) { - throw new IllegalArgumentException("index privilege patterns " + name + " did not map to a single selector " + splitBySelector); + throw new IllegalArgumentException( + "index privilege patterns " + names + " did not map to a single selector " + splitBySelector + ); } return splitBySelector.iterator().next(); } /** - * TODO more detail - * Returns a set of index privileges, each privilege responsible for a separate selector. - * For instance, `getSplitBySelector(Set.of("view_index_metadata", "write", "read_failure_store"))` will return two index privileges - * one covering "view_index_metadata" and "write" for a {@link IndexComponentSelectorPrivilege#DATA}, the other covering - * "read_failure_store" for a {@link IndexComponentSelectorPrivilege#FAILURES} selector. + * Returns a set {@link IndexPrivilege} that captures the access granted by the privileges and actions specified in the input name set. + * This method returns a set of index privileges, instead of a single index privilege to capture that different index privileges grant + * access to different {@link IndexComponentSelector}s. Most privileges grant access to the + * (implicit) {@link IndexComponentSelector#DATA} selector. The {@link IndexPrivilege#READ_FAILURE_STORE} grants access to + * {@link IndexComponentSelector#FAILURES}. + * The implementation for authorization for access by selector requires that index privileges are (generally) not combined across + * selector boundaries since their underlying automata would be combined, granting more access than is valid. + * This method conceptually splits the input names into ones that correspond to different selector access, and return an index privilege + * for each partition. + * For instance, `getSplitBySelectorAccess(Set.of("view_index_metadata", "write", "read_failure_store"))` will return two index + * privileges one covering `view_index_metadata` and `write` for a {@link IndexComponentSelectorPredicate#DATA}, the other covering + * `read_failure_store` for a {@link IndexComponentSelectorPredicate#FAILURES} selector. + * A notable exception is the {@link IndexPrivilege#ALL} privilege. If this privilege is included in the input name set, this method + * returns a single index privilege that grants access to all selectors. + * All raw actions are treated as granting access to the {@link IndexComponentSelector#DATA} selector. */ - public static Set getSplitBySelector(Set name) { - return CACHE.computeIfAbsent(name, (theName) -> { + public static Set getSplitBySelectorAccess(Set names) { + return CACHE.computeIfAbsent(names, (theName) -> { if (theName.isEmpty()) { return Set.of(NONE); } else { @@ -312,12 +324,12 @@ private static Set resolve(Set name) { throw new IllegalArgumentException("empty set should not be used"); } - Set actions = new HashSet<>(); - Set allAccessPrivileges = new HashSet<>(); - Set dataAccessPrivileges = new HashSet<>(); - Set failureAccessPrivileges = new HashSet<>(); + final Set actions = new HashSet<>(); + final Set allSelectorAccessPrivileges = new HashSet<>(); + final Set dataSelectorAccessPrivileges = new HashSet<>(); + final Set failuresSelectorAccessPrivileges = new HashSet<>(); - boolean containsAll = name.stream().anyMatch(n -> getNamedOrNull(n) == ALL); + boolean containsAllAccessPrivilege = name.stream().anyMatch(n -> getNamedOrNull(n) == ALL); for (String part : name) { part = part.toLowerCase(Locale.ROOT); if (ACTION_MATCHER.test(part)) { @@ -327,14 +339,18 @@ private static Set resolve(Set name) { if (indexPrivilege != null && size == 1) { return Set.of(indexPrivilege); } else if (indexPrivilege != null) { - if (containsAll) { - allAccessPrivileges.add(indexPrivilege); - } else if (indexPrivilege.selectorPrivilege == IndexComponentSelectorPrivilege.DATA) { - dataAccessPrivileges.add(indexPrivilege); - } else if (indexPrivilege.selectorPrivilege == IndexComponentSelectorPrivilege.FAILURES) { - failureAccessPrivileges.add(indexPrivilege); + // if we have an all access privilege, we don't need to partition anymore since it grants access to all selectors and + // any other name in the group has its selector-access superseded. + if (containsAllAccessPrivilege) { + allSelectorAccessPrivileges.add(indexPrivilege); + } else if (indexPrivilege.selectorPredicate == IndexComponentSelectorPredicate.DATA) { + dataSelectorAccessPrivileges.add(indexPrivilege); + } else if (indexPrivilege.selectorPredicate == IndexComponentSelectorPredicate.FAILURES) { + failuresSelectorAccessPrivileges.add(indexPrivilege); } else { - assert false : "unexpected selector [" + indexPrivilege.selectorPrivilege + "]"; + String errorMessage = "unexpected selector [" + indexPrivilege.selectorPredicate + "]"; + assert false : errorMessage; + throw new IllegalStateException(errorMessage); } } else { String errorMessage = "unknown index privilege [" @@ -350,38 +366,44 @@ private static Set resolve(Set name) { } } - final Set result = combineIntoResult(allAccessPrivileges, failureAccessPrivileges, dataAccessPrivileges, actions); + final Set result = combineIndexPrivileges( + allSelectorAccessPrivileges, + dataSelectorAccessPrivileges, + failuresSelectorAccessPrivileges, + actions + ); assertNamesMatch(name, result); return result; } - private static Set combineIntoResult( - Set allAccessPrivileges, - Set failureAccessPrivileges, - Set dataAccessPrivileges, + private static Set combineIndexPrivileges( + Set allSelectorAccessPrivileges, + Set dataSelectorAccessPrivileges, + Set failuresSelectorAccessPrivileges, Set actions ) { - assert false == allAccessPrivileges.isEmpty() - || false == failureAccessPrivileges.isEmpty() - || false == dataAccessPrivileges.isEmpty() + assert false == allSelectorAccessPrivileges.isEmpty() + || false == dataSelectorAccessPrivileges.isEmpty() + || false == failuresSelectorAccessPrivileges.isEmpty() || false == actions.isEmpty() : "at least one of the privilege sets or actions must be non-empty"; - if (false == allAccessPrivileges.isEmpty()) { - return Set.of(union(allAccessPrivileges, actions, IndexComponentSelectorPrivilege.ALL)); + if (false == allSelectorAccessPrivileges.isEmpty()) { + assert failuresSelectorAccessPrivileges.isEmpty() && dataSelectorAccessPrivileges.isEmpty() + : "data and failure access must be empty when all access is present"; + return Set.of(union(allSelectorAccessPrivileges, actions, IndexComponentSelectorPredicate.ALL)); } final Set result = new HashSet<>(); - if (false == failureAccessPrivileges.isEmpty()) { - result.add(union(failureAccessPrivileges, Set.of(), IndexComponentSelectorPrivilege.FAILURES)); + if (false == failuresSelectorAccessPrivileges.isEmpty()) { + result.add(union(failuresSelectorAccessPrivileges, Set.of(), IndexComponentSelectorPredicate.FAILURES)); } - if (false == dataAccessPrivileges.isEmpty() || false == actions.isEmpty()) { - result.add(union(dataAccessPrivileges, actions, IndexComponentSelectorPrivilege.DATA)); + if (false == dataSelectorAccessPrivileges.isEmpty() || false == actions.isEmpty()) { + result.add(union(dataSelectorAccessPrivileges, actions, IndexComponentSelectorPredicate.DATA)); } return result; } private static void assertNamesMatch(Set names, Set privileges) { - // TODO clean up assert names.stream() .map(n -> n.toLowerCase(Locale.ROOT)) .collect(Collectors.toSet()) @@ -392,10 +414,10 @@ private static void assertNamesMatch(Set names, Set priv private static IndexPrivilege union( Collection privileges, Collection actions, - IndexComponentSelectorPrivilege selectorPrivilege + IndexComponentSelectorPredicate selectorPredicate ) { - Set automata = HashSet.newHashSet(privileges.size() + actions.size()); - Set names = HashSet.newHashSet(privileges.size() + actions.size()); + final Set automata = HashSet.newHashSet(privileges.size() + actions.size()); + final Set names = HashSet.newHashSet(privileges.size() + actions.size()); for (IndexPrivilege privilege : privileges) { names.add(privilege.getSingleName()); automata.add(privilege.automaton); @@ -405,7 +427,7 @@ private static IndexPrivilege union( names.addAll(actions); automata.add(patterns(actions.stream().map(Privilege::actionToPattern).toArray(String[]::new))); } - return new IndexPrivilege(names, unionAndMinimize(automata), selectorPrivilege); + return new IndexPrivilege(names, unionAndMinimize(automata), selectorPredicate); } static Map values() { @@ -426,14 +448,14 @@ public static Collection findPrivilegesThatGrant(String action) { return VALUES.entrySet() .stream() // Only include privileges that grant data access; failures access is handled separately in authorization failure messages - .filter(e -> e.getValue().selectorPrivilege.test(IndexComponentSelector.DATA)) + .filter(e -> e.getValue().selectorPredicate.test(IndexComponentSelector.DATA)) .filter(e -> e.getValue().predicate.test(action)) .map(Map.Entry::getKey) .toList(); } - public IndexComponentSelectorPrivilege getSelectorPrivilege() { - return selectorPrivilege; + public IndexComponentSelectorPredicate getSelectorPredicate() { + return selectorPredicate; } public String getSingleName() { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java index 1c4ea7a99d22f..8331c39b505cf 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java @@ -239,7 +239,7 @@ private static FieldPermissions randomFlsPermissions(String... grantedFields) { } private static IndexPrivilege randomIndexPrivilege() { - return IndexPrivilege.getSingleSelector(Set.of(randomFrom(IndexPrivilege.names()))); + return IndexPrivilege.getSingleSelectorOrThrow(Set.of(randomFrom(IndexPrivilege.names()))); } public void testGetRoleDescriptorsIntersectionForRemoteClusterReturnsEmpty() { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/SimpleRoleTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/SimpleRoleTests.java index 649e2da324733..a0cb6e4fe56a8 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/SimpleRoleTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/SimpleRoleTests.java @@ -176,7 +176,7 @@ public void testGetRoleDescriptorsIntersectionForRemoteCluster() { Set.of(randomAlphaOfLength(8)), new FieldPermissions(new FieldPermissionsDefinition(new String[] { randomAlphaOfLength(5) }, null)), null, - IndexPrivilege.getSplitBySelector(Set.of(randomFrom(IndexPrivilege.names()))), + IndexPrivilege.getSplitBySelectorAccess(Set.of(randomFrom(IndexPrivilege.names()))), randomBoolean(), randomAlphaOfLength(9) ) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java index cd79269e84cc1..55d44681daddb 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java @@ -70,72 +70,109 @@ public void testFindPrivilegesThatGrant() { assertThat(findPrivilegesThatGrant(RefreshAction.NAME), equalTo(List.of("maintenance", "manage", "all"))); } - public void testGetSingleSelector() { + public void testGetSingleSelectorOrThrow() { { - IndexPrivilege actual = IndexPrivilege.getSingleSelector(Set.of("all")); + IndexPrivilege actual = IndexPrivilege.getSingleSelectorOrThrow(Set.of("all")); assertThat(actual, equalTo(IndexPrivilege.ALL)); - assertThat(actual.getSelectorPrivilege(), equalTo(IndexComponentSelectorPrivilege.ALL)); + assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.ALL)); } { - IndexPrivilege actual = IndexPrivilege.getSingleSelector(Set.of("read")); + IndexPrivilege actual = IndexPrivilege.getSingleSelectorOrThrow(Set.of("read")); assertThat(actual, equalTo(IndexPrivilege.READ)); - assertThat(actual.getSelectorPrivilege(), equalTo(IndexComponentSelectorPrivilege.DATA)); + assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.DATA)); } { - IndexPrivilege actual = IndexPrivilege.getSingleSelector(Set.of("none")); + IndexPrivilege actual = IndexPrivilege.getSingleSelectorOrThrow(Set.of("none")); assertThat(actual, equalTo(IndexPrivilege.NONE)); - assertThat(actual.getSelectorPrivilege(), equalTo(IndexComponentSelectorPrivilege.DATA)); + assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.DATA)); } { - IndexPrivilege actual = IndexPrivilege.getSingleSelector(Set.of()); + IndexPrivilege actual = IndexPrivilege.getSingleSelectorOrThrow(Set.of()); assertThat(actual, equalTo(IndexPrivilege.NONE)); - assertThat(actual.getSelectorPrivilege(), equalTo(IndexComponentSelectorPrivilege.DATA)); + assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.DATA)); } { - IndexPrivilege actual = IndexPrivilege.getSingleSelector(Set.of("indices:data/read/search")); + IndexPrivilege actual = IndexPrivilege.getSingleSelectorOrThrow(Set.of("indices:data/read/search")); assertThat(actual.getSingleName(), equalTo("indices:data/read/search")); assertThat(actual.predicate.test("indices:data/read/search"), is(true)); - assertThat(actual.getSelectorPrivilege(), equalTo(IndexComponentSelectorPrivilege.DATA)); + assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.DATA)); } { - IndexPrivilege actual = IndexPrivilege.getSingleSelector(Set.of("all", "read", "indices:data/read/search")); + IndexPrivilege actual = IndexPrivilege.getSingleSelectorOrThrow(Set.of("all", "read", "indices:data/read/search")); assertThat(actual.name, equalTo(Set.of("all", "read", "indices:data/read/search"))); assertThat(Automatons.subsetOf(IndexPrivilege.ALL.automaton, actual.automaton), is(true)); - assertThat(actual.getSelectorPrivilege(), equalTo(IndexComponentSelectorPrivilege.ALL)); + assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.ALL)); } } - public void testGetSingleSelectorWithFailuresSelector() { + public void testGetSingleSelectorWithFailuresSelectorOrThrow() { assumeTrue("This test requires the failure store to be enabled", DataStream.isFailureStoreFeatureFlagEnabled()); { - IndexPrivilege actual = IndexPrivilege.getSingleSelector(Set.of("read_failure_store")); + IndexPrivilege actual = IndexPrivilege.getSingleSelectorOrThrow(Set.of("read_failure_store")); assertThat(actual, equalTo(IndexPrivilege.READ_FAILURE_STORE)); - assertThat(actual.getSelectorPrivilege(), equalTo(IndexComponentSelectorPrivilege.FAILURES)); + assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.FAILURES)); } { - IndexPrivilege actual = IndexPrivilege.getSingleSelector(Set.of("all", "read_failure_store")); + IndexPrivilege actual = IndexPrivilege.getSingleSelectorOrThrow(Set.of("all", "read_failure_store")); assertThat(actual.name(), equalTo(Set.of("all", "read_failure_store"))); - assertThat(actual.getSelectorPrivilege(), equalTo(IndexComponentSelectorPrivilege.ALL)); + assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.ALL)); assertThat(Automatons.subsetOf(IndexPrivilege.ALL.automaton, actual.automaton), is(true)); } { - IndexPrivilege actual = IndexPrivilege.getSingleSelector(Set.of("all", "indices:data/read/search", "read_failure_store")); + IndexPrivilege actual = IndexPrivilege.getSingleSelectorOrThrow( + Set.of("all", "indices:data/read/search", "read_failure_store") + ); assertThat(actual.name(), equalTo(Set.of("all", "indices:data/read/search", "read_failure_store"))); - assertThat(actual.getSelectorPrivilege(), equalTo(IndexComponentSelectorPrivilege.ALL)); + assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.ALL)); assertThat(Automatons.subsetOf(IndexPrivilege.ALL.automaton, actual.automaton), is(true)); } { - IndexPrivilege actual = IndexPrivilege.getSingleSelector(Set.of("all", "read", "read_failure_store")); + IndexPrivilege actual = IndexPrivilege.getSingleSelectorOrThrow(Set.of("all", "read", "read_failure_store")); assertThat(actual.name(), equalTo(Set.of("all", "read", "read_failure_store"))); - assertThat(actual.getSelectorPrivilege(), equalTo(IndexComponentSelectorPrivilege.ALL)); + assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.ALL)); assertThat(Automatons.subsetOf(IndexPrivilege.ALL.automaton, actual.automaton), is(true)); } - expectThrows(IllegalArgumentException.class, () -> IndexPrivilege.getSingleSelector(Set.of("read", "read_failure_store"))); + expectThrows(IllegalArgumentException.class, () -> IndexPrivilege.getSingleSelectorOrThrow(Set.of("read", "read_failure_store"))); expectThrows( IllegalArgumentException.class, - () -> IndexPrivilege.getSingleSelector(Set.of("indices:data/read/search", "read_failure_store")) + () -> IndexPrivilege.getSingleSelectorOrThrow(Set.of("indices:data/read/search", "read_failure_store")) ); - expectThrows(IllegalArgumentException.class, () -> IndexPrivilege.getSingleSelector(Set.of("none", "read_failure_store"))); + expectThrows(IllegalArgumentException.class, () -> IndexPrivilege.getSingleSelectorOrThrow(Set.of("none", "read_failure_store"))); + } + + public void testGetSingleSelectorWithFailuresSelectorOrThrow() { + assumeTrue("This test requires the failure store to be enabled", DataStream.isFailureStoreFeatureFlagEnabled()); + { + IndexPrivilege actual = IndexPrivilege.getSingleSelectorOrThrow(Set.of("read_failure_store")); + assertThat(actual, equalTo(IndexPrivilege.READ_FAILURE_STORE)); + assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.FAILURES)); + } + { + IndexPrivilege actual = IndexPrivilege.getSingleSelectorOrThrow(Set.of("all", "read_failure_store")); + assertThat(actual.name(), equalTo(Set.of("all", "read_failure_store"))); + assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.ALL)); + assertThat(Automatons.subsetOf(IndexPrivilege.ALL.automaton, actual.automaton), is(true)); + } + { + IndexPrivilege actual = IndexPrivilege.getSingleSelectorOrThrow( + Set.of("all", "indices:data/read/search", "read_failure_store") + ); + assertThat(actual.name(), equalTo(Set.of("all", "indices:data/read/search", "read_failure_store"))); + assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.ALL)); + assertThat(Automatons.subsetOf(IndexPrivilege.ALL.automaton, actual.automaton), is(true)); + } + { + IndexPrivilege actual = IndexPrivilege.getSingleSelectorOrThrow(Set.of("all", "read", "read_failure_store")); + assertThat(actual.name(), equalTo(Set.of("all", "read", "read_failure_store"))); + assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.ALL)); + assertThat(Automatons.subsetOf(IndexPrivilege.ALL.automaton, actual.automaton), is(true)); + } + expectThrows(IllegalArgumentException.class, () -> IndexPrivilege.getSingleSelectorOrThrow(Set.of("read", "read_failure_store"))); + expectThrows( + IllegalArgumentException.class, + () -> IndexPrivilege.getSingleSelectorOrThrow(Set.of("indices:data/read/search", "read_failure_store")) + ); + expectThrows(IllegalArgumentException.class, () -> IndexPrivilege.getSingleSelectorOrThrow(Set.of("none", "read_failure_store"))); } public void testPrivilegesForRollupFieldCapsAction() { @@ -153,39 +190,39 @@ public void testPrivilegesForGetCheckPointAction() { public void testRelationshipBetweenPrivileges() { assertThat( Automatons.subsetOf( - IndexPrivilege.getSingleSelector(Set.of("view_index_metadata")).automaton, - IndexPrivilege.getSingleSelector(Set.of("manage")).automaton + IndexPrivilege.getSingleSelectorOrThrow(Set.of("view_index_metadata")).automaton, + IndexPrivilege.getSingleSelectorOrThrow(Set.of("manage")).automaton ), is(true) ); assertThat( Automatons.subsetOf( - IndexPrivilege.getSingleSelector(Set.of("monitor")).automaton, - IndexPrivilege.getSingleSelector(Set.of("manage")).automaton + IndexPrivilege.getSingleSelectorOrThrow(Set.of("monitor")).automaton, + IndexPrivilege.getSingleSelectorOrThrow(Set.of("manage")).automaton ), is(true) ); assertThat( Automatons.subsetOf( - IndexPrivilege.getSingleSelector(Set.of("create", "create_doc", "index", "delete")).automaton, - IndexPrivilege.getSingleSelector(Set.of("write")).automaton + IndexPrivilege.getSingleSelectorOrThrow(Set.of("create", "create_doc", "index", "delete")).automaton, + IndexPrivilege.getSingleSelectorOrThrow(Set.of("write")).automaton ), is(true) ); assertThat( Automatons.subsetOf( - IndexPrivilege.getSingleSelector(Set.of("create_index", "delete_index")).automaton, - IndexPrivilege.getSingleSelector(Set.of("manage")).automaton + IndexPrivilege.getSingleSelectorOrThrow(Set.of("create_index", "delete_index")).automaton, + IndexPrivilege.getSingleSelectorOrThrow(Set.of("manage")).automaton ), is(true) ); } public void testCrossClusterReplicationPrivileges() { - final IndexPrivilege crossClusterReplication = IndexPrivilege.getSingleSelector(Set.of("cross_cluster_replication")); + final IndexPrivilege crossClusterReplication = IndexPrivilege.getSingleSelectorOrThrow(Set.of("cross_cluster_replication")); List.of( "indices:data/read/xpack/ccr/shard_changes", "indices:monitor/stats", @@ -196,12 +233,12 @@ public void testCrossClusterReplicationPrivileges() { assertThat( Automatons.subsetOf( crossClusterReplication.automaton, - IndexPrivilege.getSingleSelector(Set.of("manage", "read", "monitor")).automaton + IndexPrivilege.getSingleSelectorOrThrow(Set.of("manage", "read", "monitor")).automaton ), is(true) ); - final IndexPrivilege crossClusterReplicationInternal = IndexPrivilege.getSingleSelector( + final IndexPrivilege crossClusterReplicationInternal = IndexPrivilege.getSingleSelectorOrThrow( Set.of("cross_cluster_replication_internal") ); List.of( @@ -216,11 +253,17 @@ public void testCrossClusterReplicationPrivileges() { ); assertThat( - Automatons.subsetOf(crossClusterReplicationInternal.automaton, IndexPrivilege.getSingleSelector(Set.of("manage")).automaton), + Automatons.subsetOf( + crossClusterReplicationInternal.automaton, + IndexPrivilege.getSingleSelectorOrThrow(Set.of("manage")).automaton + ), is(false) ); assertThat( - Automatons.subsetOf(crossClusterReplicationInternal.automaton, IndexPrivilege.getSingleSelector(Set.of("all")).automaton), + Automatons.subsetOf( + crossClusterReplicationInternal.automaton, + IndexPrivilege.getSingleSelectorOrThrow(Set.of("all")).automaton + ), is(true) ); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/PrivilegeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/PrivilegeTests.java index 2425d3d4fa71f..8540af2ddc339 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/PrivilegeTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/PrivilegeTests.java @@ -205,7 +205,7 @@ public void testClusterAction() throws Exception { public void testIndexAction() throws Exception { Set actionName = Sets.newHashSet("indices:admin/mapping/delete"); - IndexPrivilege index = IndexPrivilege.getSingleSelector(actionName); + IndexPrivilege index = IndexPrivilege.getSingleSelectorOrThrow(actionName); assertThat(index, notNullValue()); assertThat(index.predicate().test("indices:admin/mapping/delete"), is(true)); assertThat(index.predicate().test("indices:admin/mapping/dele"), is(false)); @@ -218,7 +218,7 @@ public void testIndexCollapse() { IndexPrivilege second = values[randomIntBetween(0, values.length - 1)]; Set name = Sets.newHashSet(first.name().iterator().next(), second.name().iterator().next()); - Set indices = IndexPrivilege.getSplitBySelector(name); + Set indices = IndexPrivilege.getSplitBySelectorAccess(name); Automaton automaton = null; if (indices.size() == 1) { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/support/AutomatonPatternsTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/support/AutomatonPatternsTests.java index 9060111a06346..926cd4260ec78 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/support/AutomatonPatternsTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/support/AutomatonPatternsTests.java @@ -37,7 +37,7 @@ public void testRemoteClusterPrivsDoNotOverlap() { // check that the action patterns for remote CCS are not allowed by remote CCR privileges Arrays.stream(CCS_INDICES_PRIVILEGE_NAMES).forEach(ccsPrivilege -> { - Automaton ccsAutomaton = IndexPrivilege.getSingleSelector(Set.of(ccsPrivilege)).getAutomaton(); + Automaton ccsAutomaton = IndexPrivilege.getSingleSelectorOrThrow(Set.of(ccsPrivilege)).getAutomaton(); Automatons.getPatterns(ccsAutomaton).forEach(ccsPattern -> { // emulate an action name that could be allowed by a CCS privilege String actionName = ccsPattern.replaceAll("\\*", randomAlphaOfLengthBetween(1, 8)); @@ -49,14 +49,14 @@ public void testRemoteClusterPrivsDoNotOverlap() { ccrPrivileges, ccsPattern ); - assertFalse(errorMessage, IndexPrivilege.getSingleSelector(Set.of(ccrPrivileges)).predicate().test(actionName)); + assertFalse(errorMessage, IndexPrivilege.getSingleSelectorOrThrow(Set.of(ccrPrivileges)).predicate().test(actionName)); }); }); }); // check that the action patterns for remote CCR are not allowed by remote CCS privileges Arrays.stream(CCR_INDICES_PRIVILEGE_NAMES).forEach(ccrPrivilege -> { - Automaton ccrAutomaton = IndexPrivilege.getSingleSelector(Set.of(ccrPrivilege)).getAutomaton(); + Automaton ccrAutomaton = IndexPrivilege.getSingleSelectorOrThrow(Set.of(ccrPrivilege)).getAutomaton(); Automatons.getPatterns(ccrAutomaton).forEach(ccrPattern -> { // emulate an action name that could be allowed by a CCR privilege String actionName = ccrPattern.replaceAll("\\*", randomAlphaOfLengthBetween(1, 8)); @@ -72,7 +72,10 @@ public void testRemoteClusterPrivsDoNotOverlap() { ccsPrivileges, ccrPattern ); - assertFalse(errorMessage, IndexPrivilege.getSingleSelector(Set.of(ccsPrivileges)).predicate().test(actionName)); + assertFalse( + errorMessage, + IndexPrivilege.getSingleSelectorOrThrow(Set.of(ccsPrivileges)).predicate().test(actionName) + ); } }); }); diff --git a/x-pack/plugin/security/qa/consistency-checks/src/test/java/org/elasticsearch/xpack/security/CrossClusterShardTests.java b/x-pack/plugin/security/qa/consistency-checks/src/test/java/org/elasticsearch/xpack/security/CrossClusterShardTests.java index ee2297d39dfc1..2cb2783173b64 100644 --- a/x-pack/plugin/security/qa/consistency-checks/src/test/java/org/elasticsearch/xpack/security/CrossClusterShardTests.java +++ b/x-pack/plugin/security/qa/consistency-checks/src/test/java/org/elasticsearch/xpack/security/CrossClusterShardTests.java @@ -112,7 +112,7 @@ public void testCheckForNewShardLevelTransportActions() throws Exception { List shardActions = transportActionBindings.stream() .map(binding -> binding.getProvider().get()) - .filter(action -> IndexPrivilege.getSingleSelector(crossClusterPrivilegeNames).predicate().test(action.actionName)) + .filter(action -> IndexPrivilege.getSingleSelectorOrThrow(crossClusterPrivilegeNames).predicate().test(action.actionName)) .filter(this::actionIsLikelyShardAction) .map(action -> action.actionName) .toList(); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java index 5d55fc31f6180..8c86ea729e933 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java @@ -543,7 +543,7 @@ public static void buildRoleFromDescriptors( (key, privilege) -> builder.add( fieldPermissionsCache.getFieldPermissions(privilege.fieldPermissionsDefinition), privilege.query, - IndexPrivilege.getSplitBySelector(privilege.privileges), + IndexPrivilege.getSplitBySelectorAccess(privilege.privileges), false, privilege.indices.toArray(Strings.EMPTY_ARRAY) ) @@ -552,7 +552,7 @@ public static void buildRoleFromDescriptors( (key, privilege) -> builder.add( fieldPermissionsCache.getFieldPermissions(privilege.fieldPermissionsDefinition), privilege.query, - IndexPrivilege.getSplitBySelector(privilege.privileges), + IndexPrivilege.getSplitBySelectorAccess(privilege.privileges), true, privilege.indices.toArray(Strings.EMPTY_ARRAY) ) @@ -566,7 +566,7 @@ public static void buildRoleFromDescriptors( new FieldPermissionsDefinition(privilege.getGrantedFields(), privilege.getDeniedFields()) ), privilege.getQuery() == null ? null : newHashSet(privilege.getQuery()), - IndexPrivilege.getSplitBySelector(newHashSet(Objects.requireNonNull(privilege.getPrivileges()))), + IndexPrivilege.getSplitBySelectorAccess(newHashSet(Objects.requireNonNull(privilege.getPrivileges()))), privilege.allowRestrictedIndices(), newHashSet(Objects.requireNonNull(privilege.getIndices())).toArray(new String[0]) ) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/DeprecationRoleDescriptorConsumer.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/DeprecationRoleDescriptorConsumer.java index 898614285d433..ce4ded387c284 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/DeprecationRoleDescriptorConsumer.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/DeprecationRoleDescriptorConsumer.java @@ -183,7 +183,7 @@ private void logDeprecatedPermission(RoleDescriptor roleDescriptor) { for (Map.Entry> privilegesByAlias : privilegesByAliasMap.entrySet()) { final String aliasName = privilegesByAlias.getKey(); final Set aliasPrivilegeNames = privilegesByAlias.getValue(); - final Automaton aliasPrivilegeAutomaton = IndexPrivilege.getSingleSelector(aliasPrivilegeNames).getAutomaton(); + final Automaton aliasPrivilegeAutomaton = IndexPrivilege.getSingleSelectorOrThrow(aliasPrivilegeNames).getAutomaton(); final SortedSet inferiorIndexNames = new TreeSet<>(); // check if the alias grants superiors privileges than the indices it points to for (Index index : aliasOrIndexMap.get(aliasName).getIndices()) { @@ -193,7 +193,7 @@ private void logDeprecatedPermission(RoleDescriptor roleDescriptor) { // compute automaton once per index no matter how many times it is pointed to final Automaton indexPrivilegeAutomaton = indexAutomatonMap.computeIfAbsent( index.getName(), - i -> IndexPrivilege.getSingleSelector(indexPrivileges).getAutomaton() + i -> IndexPrivilege.getSingleSelectorOrThrow(indexPrivileges).getAutomaton() ); if (false == Automatons.subsetOf(indexPrivilegeAutomaton, aliasPrivilegeAutomaton)) { inferiorIndexNames.add(index.getName()); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java index 17cbe9c6aad12..b106f565c2b1c 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java @@ -1290,7 +1290,7 @@ public void testBuildUserPrivilegeResponse() { {"term":{"public":true}}"""); final Role role = Role.builder(RESTRICTED_INDICES, "test", "role") .cluster(Sets.newHashSet("monitor", "manage_watcher"), Collections.singleton(manageApplicationPrivileges)) - .add(IndexPrivilege.getSingleSelector(Sets.newHashSet("read", "write")), "index-1") + .add(IndexPrivilege.getSingleSelectorOrThrow(Sets.newHashSet("read", "write")), "index-1") .add(IndexPrivilege.ALL, "index-2", "index-3") .add( new FieldPermissions(new FieldPermissionsDefinition(new String[] { "public.*" }, new String[0])), @@ -1616,7 +1616,7 @@ public void testGetRoleDescriptorsIntersectionForRemoteClusterHasDeterministicOr final int numGroups = randomIntBetween(2, 5); int extraGroups = 0; for (int i = 0; i < numGroups; i++) { - Set splitBySelector = IndexPrivilege.getSplitBySelector( + Set splitBySelector = IndexPrivilege.getSplitBySelectorAccess( Set.copyOf(randomSubsetOf(randomIntBetween(1, 4), IndexPrivilege.names())) ); // If we end up with failure and data access, we will split and end up with extra groups. Need to account for this for the diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java index ebd300a40cc32..d79265ff09cf8 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java @@ -87,7 +87,7 @@ import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeTests; import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilegeResolver; import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivilege; -import org.elasticsearch.xpack.core.security.authz.privilege.IndexComponentSelectorPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.IndexComponentSelectorPredicate; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.authz.restriction.Workflow; import org.elasticsearch.xpack.core.security.authz.restriction.WorkflowResolver; @@ -1494,8 +1494,8 @@ public void testBuildRoleWithMultipleRemoteMergedAcrossPrivilegesAndDescriptors( assertHasRemoteIndexGroupsForClusters( role.remoteIndices(), Set.of("remote-1"), - indexGroup(IndexPrivilege.getSingleSelector(Set.of("read")), false, "index-1"), - indexGroup(IndexPrivilege.getSingleSelector(Set.of("none")), false, "index-1") + indexGroup(IndexPrivilege.getSingleSelectorOrThrow(Set.of("read")), false, "index-1"), + indexGroup(IndexPrivilege.getSingleSelectorOrThrow(Set.of("none")), false, "index-1") ); } @@ -1606,7 +1606,7 @@ public void testBuildRoleWithAllPrivilegeIsNeverSplit() { ); assertHasIndexGroups( role.indices(), - indexGroup(IndexPrivilege.getSingleSelector(Set.of("read", "read_failure_store", "all")), false, indexPattern) + indexGroup(IndexPrivilege.getSingleSelectorOrThrow(Set.of("read", "read_failure_store", "all")), false, indexPattern) ); } @@ -1614,7 +1614,7 @@ public void testBuildRoleNeverSplitsWithoutFailureStoreRelatedPrivileges() { String indexPattern = randomAlphanumericOfLength(10); List nonFailurePrivileges = IndexPrivilege.names() .stream() - .filter(p -> IndexPrivilege.getNamedOrNull(p).getSelectorPrivilege() != IndexComponentSelectorPrivilege.FAILURES) + .filter(p -> IndexPrivilege.getNamedOrNull(p).getSelectorPredicate() != IndexComponentSelectorPredicate.FAILURES) .toList(); Set usedPrivileges = new HashSet<>(); @@ -1633,7 +1633,7 @@ public void testBuildRoleNeverSplitsWithoutFailureStoreRelatedPrivileges() { final Role role = buildRole(roleDescriptorWithIndicesPrivileges("r1", indicesPrivileges)); final IndicesPermission actual = role.indices(); - assertHasIndexGroups(actual, indexGroup(IndexPrivilege.getSingleSelector(usedPrivileges), false, indexPattern)); + assertHasIndexGroups(actual, indexGroup(IndexPrivilege.getSingleSelectorOrThrow(usedPrivileges), false, indexPattern)); } public void testCustomRolesProviderFailures() throws Exception { From 8f2eddc85105c38ef74f2947d9eee8e013ed4f6a Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 19 Feb 2025 10:28:52 +0100 Subject: [PATCH 033/131] Fix refactor wonkiness --- .../authz/privilege/IndexPrivilegeTests.java | 35 ------------------- 1 file changed, 35 deletions(-) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java index 55d44681daddb..a0d9ddb21bbd8 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java @@ -140,41 +140,6 @@ public void testGetSingleSelectorWithFailuresSelectorOrThrow() { expectThrows(IllegalArgumentException.class, () -> IndexPrivilege.getSingleSelectorOrThrow(Set.of("none", "read_failure_store"))); } - public void testGetSingleSelectorWithFailuresSelectorOrThrow() { - assumeTrue("This test requires the failure store to be enabled", DataStream.isFailureStoreFeatureFlagEnabled()); - { - IndexPrivilege actual = IndexPrivilege.getSingleSelectorOrThrow(Set.of("read_failure_store")); - assertThat(actual, equalTo(IndexPrivilege.READ_FAILURE_STORE)); - assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.FAILURES)); - } - { - IndexPrivilege actual = IndexPrivilege.getSingleSelectorOrThrow(Set.of("all", "read_failure_store")); - assertThat(actual.name(), equalTo(Set.of("all", "read_failure_store"))); - assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.ALL)); - assertThat(Automatons.subsetOf(IndexPrivilege.ALL.automaton, actual.automaton), is(true)); - } - { - IndexPrivilege actual = IndexPrivilege.getSingleSelectorOrThrow( - Set.of("all", "indices:data/read/search", "read_failure_store") - ); - assertThat(actual.name(), equalTo(Set.of("all", "indices:data/read/search", "read_failure_store"))); - assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.ALL)); - assertThat(Automatons.subsetOf(IndexPrivilege.ALL.automaton, actual.automaton), is(true)); - } - { - IndexPrivilege actual = IndexPrivilege.getSingleSelectorOrThrow(Set.of("all", "read", "read_failure_store")); - assertThat(actual.name(), equalTo(Set.of("all", "read", "read_failure_store"))); - assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.ALL)); - assertThat(Automatons.subsetOf(IndexPrivilege.ALL.automaton, actual.automaton), is(true)); - } - expectThrows(IllegalArgumentException.class, () -> IndexPrivilege.getSingleSelectorOrThrow(Set.of("read", "read_failure_store"))); - expectThrows( - IllegalArgumentException.class, - () -> IndexPrivilege.getSingleSelectorOrThrow(Set.of("indices:data/read/search", "read_failure_store")) - ); - expectThrows(IllegalArgumentException.class, () -> IndexPrivilege.getSingleSelectorOrThrow(Set.of("none", "read_failure_store"))); - } - public void testPrivilegesForRollupFieldCapsAction() { final Collection privileges = findPrivilegesThatGrant(GetRollupIndexCapsAction.NAME); assertThat(Set.copyOf(privileges), equalTo(Set.of("manage", "all", "view_index_metadata", "read"))); From e7fcf9395581d0367405da8b9948f008229107f8 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 19 Feb 2025 10:41:33 +0100 Subject: [PATCH 034/131] Naming so hard --- .../authz/permission/IndicesPermission.java | 2 +- .../authz/privilege/IndexPrivilege.java | 4 +- .../authz/permission/LimitedRoleTests.java | 2 +- .../authz/privilege/IndexPrivilegeTests.java | 60 ++++++++++--------- .../authz/privilege/PrivilegeTests.java | 2 +- .../support/AutomatonPatternsTests.java | 11 ++-- .../security/CrossClusterShardTests.java | 2 +- .../DeprecationRoleDescriptorConsumer.java | 4 +- .../xpack/security/authz/RBACEngineTests.java | 2 +- .../authz/store/CompositeRolesStoreTests.java | 8 +-- 10 files changed, 54 insertions(+), 43 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 33a78e01d77d4..5daea461b973f 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -302,7 +302,7 @@ public boolean checkResourcePrivileges( } } for (String privilege : checkForPrivileges) { - IndexPrivilege indexPrivilege = IndexPrivilege.getSingleSelectorOrThrow(Collections.singleton(privilege)); + IndexPrivilege indexPrivilege = IndexPrivilege.getWithSingleSelectorAccess(Collections.singleton(privilege)); if (allowedIndexPrivilegesAutomaton != null && Automatons.subsetOf(indexPrivilege.getAutomaton(), allowedIndexPrivilegesAutomaton)) { if (resourcePrivilegesMapBuilder != null) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java index a2ee11a771afb..777f797a26a44 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java @@ -73,6 +73,8 @@ * * Also note that the `internal:transport/proxy/` prefix is automatically added and stripped for actions that go * through a CCR/CCS proxy. No action should be explicitly named like that. + * + * Each named privilege is associated with an {@link IndexComponentSelector} it grants access to. */ public final class IndexPrivilege extends Privilege { private static final Logger logger = LogManager.getLogger(IndexPrivilege.class); @@ -276,7 +278,7 @@ private IndexPrivilege(Set name, Automaton automaton, IndexComponentSele * Use this method if you know that the input name set corresponds to privileges covering the same selector, for instance if you have a * single input name, or multiple names that all grant access to one selector e.g., {@link IndexComponentSelector#DATA}. */ - public static IndexPrivilege getSingleSelectorOrThrow(Set names) { + public static IndexPrivilege getWithSingleSelectorAccess(Set names) { final Set splitBySelector = getSplitBySelectorAccess(names); if (splitBySelector.size() != 1) { throw new IllegalArgumentException( diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java index 8331c39b505cf..1ff7b4584c267 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java @@ -239,7 +239,7 @@ private static FieldPermissions randomFlsPermissions(String... grantedFields) { } private static IndexPrivilege randomIndexPrivilege() { - return IndexPrivilege.getSingleSelectorOrThrow(Set.of(randomFrom(IndexPrivilege.names()))); + return IndexPrivilege.getWithSingleSelectorAccess(Set.of(randomFrom(IndexPrivilege.names()))); } public void testGetRoleDescriptorsIntersectionForRemoteClusterReturnsEmpty() { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java index a0d9ddb21bbd8..a8f54d6e5835f 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java @@ -70,35 +70,35 @@ public void testFindPrivilegesThatGrant() { assertThat(findPrivilegesThatGrant(RefreshAction.NAME), equalTo(List.of("maintenance", "manage", "all"))); } - public void testGetSingleSelectorOrThrow() { + public void testGetWithSingleSelectorAccess() { { - IndexPrivilege actual = IndexPrivilege.getSingleSelectorOrThrow(Set.of("all")); + IndexPrivilege actual = IndexPrivilege.getWithSingleSelectorAccess(Set.of("all")); assertThat(actual, equalTo(IndexPrivilege.ALL)); assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.ALL)); } { - IndexPrivilege actual = IndexPrivilege.getSingleSelectorOrThrow(Set.of("read")); + IndexPrivilege actual = IndexPrivilege.getWithSingleSelectorAccess(Set.of("read")); assertThat(actual, equalTo(IndexPrivilege.READ)); assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.DATA)); } { - IndexPrivilege actual = IndexPrivilege.getSingleSelectorOrThrow(Set.of("none")); + IndexPrivilege actual = IndexPrivilege.getWithSingleSelectorAccess(Set.of("none")); assertThat(actual, equalTo(IndexPrivilege.NONE)); assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.DATA)); } { - IndexPrivilege actual = IndexPrivilege.getSingleSelectorOrThrow(Set.of()); + IndexPrivilege actual = IndexPrivilege.getWithSingleSelectorAccess(Set.of()); assertThat(actual, equalTo(IndexPrivilege.NONE)); assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.DATA)); } { - IndexPrivilege actual = IndexPrivilege.getSingleSelectorOrThrow(Set.of("indices:data/read/search")); + IndexPrivilege actual = IndexPrivilege.getWithSingleSelectorAccess(Set.of("indices:data/read/search")); assertThat(actual.getSingleName(), equalTo("indices:data/read/search")); assertThat(actual.predicate.test("indices:data/read/search"), is(true)); assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.DATA)); } { - IndexPrivilege actual = IndexPrivilege.getSingleSelectorOrThrow(Set.of("all", "read", "indices:data/read/search")); + IndexPrivilege actual = IndexPrivilege.getWithSingleSelectorAccess(Set.of("all", "read", "indices:data/read/search")); assertThat(actual.name, equalTo(Set.of("all", "read", "indices:data/read/search"))); assertThat(Automatons.subsetOf(IndexPrivilege.ALL.automaton, actual.automaton), is(true)); assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.ALL)); @@ -108,18 +108,18 @@ public void testGetSingleSelectorOrThrow() { public void testGetSingleSelectorWithFailuresSelectorOrThrow() { assumeTrue("This test requires the failure store to be enabled", DataStream.isFailureStoreFeatureFlagEnabled()); { - IndexPrivilege actual = IndexPrivilege.getSingleSelectorOrThrow(Set.of("read_failure_store")); + IndexPrivilege actual = IndexPrivilege.getWithSingleSelectorAccess(Set.of("read_failure_store")); assertThat(actual, equalTo(IndexPrivilege.READ_FAILURE_STORE)); assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.FAILURES)); } { - IndexPrivilege actual = IndexPrivilege.getSingleSelectorOrThrow(Set.of("all", "read_failure_store")); + IndexPrivilege actual = IndexPrivilege.getWithSingleSelectorAccess(Set.of("all", "read_failure_store")); assertThat(actual.name(), equalTo(Set.of("all", "read_failure_store"))); assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.ALL)); assertThat(Automatons.subsetOf(IndexPrivilege.ALL.automaton, actual.automaton), is(true)); } { - IndexPrivilege actual = IndexPrivilege.getSingleSelectorOrThrow( + IndexPrivilege actual = IndexPrivilege.getWithSingleSelectorAccess( Set.of("all", "indices:data/read/search", "read_failure_store") ); assertThat(actual.name(), equalTo(Set.of("all", "indices:data/read/search", "read_failure_store"))); @@ -127,17 +127,23 @@ public void testGetSingleSelectorWithFailuresSelectorOrThrow() { assertThat(Automatons.subsetOf(IndexPrivilege.ALL.automaton, actual.automaton), is(true)); } { - IndexPrivilege actual = IndexPrivilege.getSingleSelectorOrThrow(Set.of("all", "read", "read_failure_store")); + IndexPrivilege actual = IndexPrivilege.getWithSingleSelectorAccess(Set.of("all", "read", "read_failure_store")); assertThat(actual.name(), equalTo(Set.of("all", "read", "read_failure_store"))); assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.ALL)); assertThat(Automatons.subsetOf(IndexPrivilege.ALL.automaton, actual.automaton), is(true)); } - expectThrows(IllegalArgumentException.class, () -> IndexPrivilege.getSingleSelectorOrThrow(Set.of("read", "read_failure_store"))); expectThrows( IllegalArgumentException.class, - () -> IndexPrivilege.getSingleSelectorOrThrow(Set.of("indices:data/read/search", "read_failure_store")) + () -> IndexPrivilege.getWithSingleSelectorAccess(Set.of("read", "read_failure_store")) + ); + expectThrows( + IllegalArgumentException.class, + () -> IndexPrivilege.getWithSingleSelectorAccess(Set.of("indices:data/read/search", "read_failure_store")) + ); + expectThrows( + IllegalArgumentException.class, + () -> IndexPrivilege.getWithSingleSelectorAccess(Set.of("none", "read_failure_store")) ); - expectThrows(IllegalArgumentException.class, () -> IndexPrivilege.getSingleSelectorOrThrow(Set.of("none", "read_failure_store"))); } public void testPrivilegesForRollupFieldCapsAction() { @@ -155,39 +161,39 @@ public void testPrivilegesForGetCheckPointAction() { public void testRelationshipBetweenPrivileges() { assertThat( Automatons.subsetOf( - IndexPrivilege.getSingleSelectorOrThrow(Set.of("view_index_metadata")).automaton, - IndexPrivilege.getSingleSelectorOrThrow(Set.of("manage")).automaton + IndexPrivilege.getWithSingleSelectorAccess(Set.of("view_index_metadata")).automaton, + IndexPrivilege.getWithSingleSelectorAccess(Set.of("manage")).automaton ), is(true) ); assertThat( Automatons.subsetOf( - IndexPrivilege.getSingleSelectorOrThrow(Set.of("monitor")).automaton, - IndexPrivilege.getSingleSelectorOrThrow(Set.of("manage")).automaton + IndexPrivilege.getWithSingleSelectorAccess(Set.of("monitor")).automaton, + IndexPrivilege.getWithSingleSelectorAccess(Set.of("manage")).automaton ), is(true) ); assertThat( Automatons.subsetOf( - IndexPrivilege.getSingleSelectorOrThrow(Set.of("create", "create_doc", "index", "delete")).automaton, - IndexPrivilege.getSingleSelectorOrThrow(Set.of("write")).automaton + IndexPrivilege.getWithSingleSelectorAccess(Set.of("create", "create_doc", "index", "delete")).automaton, + IndexPrivilege.getWithSingleSelectorAccess(Set.of("write")).automaton ), is(true) ); assertThat( Automatons.subsetOf( - IndexPrivilege.getSingleSelectorOrThrow(Set.of("create_index", "delete_index")).automaton, - IndexPrivilege.getSingleSelectorOrThrow(Set.of("manage")).automaton + IndexPrivilege.getWithSingleSelectorAccess(Set.of("create_index", "delete_index")).automaton, + IndexPrivilege.getWithSingleSelectorAccess(Set.of("manage")).automaton ), is(true) ); } public void testCrossClusterReplicationPrivileges() { - final IndexPrivilege crossClusterReplication = IndexPrivilege.getSingleSelectorOrThrow(Set.of("cross_cluster_replication")); + final IndexPrivilege crossClusterReplication = IndexPrivilege.getWithSingleSelectorAccess(Set.of("cross_cluster_replication")); List.of( "indices:data/read/xpack/ccr/shard_changes", "indices:monitor/stats", @@ -198,12 +204,12 @@ public void testCrossClusterReplicationPrivileges() { assertThat( Automatons.subsetOf( crossClusterReplication.automaton, - IndexPrivilege.getSingleSelectorOrThrow(Set.of("manage", "read", "monitor")).automaton + IndexPrivilege.getWithSingleSelectorAccess(Set.of("manage", "read", "monitor")).automaton ), is(true) ); - final IndexPrivilege crossClusterReplicationInternal = IndexPrivilege.getSingleSelectorOrThrow( + final IndexPrivilege crossClusterReplicationInternal = IndexPrivilege.getWithSingleSelectorAccess( Set.of("cross_cluster_replication_internal") ); List.of( @@ -220,14 +226,14 @@ public void testCrossClusterReplicationPrivileges() { assertThat( Automatons.subsetOf( crossClusterReplicationInternal.automaton, - IndexPrivilege.getSingleSelectorOrThrow(Set.of("manage")).automaton + IndexPrivilege.getWithSingleSelectorAccess(Set.of("manage")).automaton ), is(false) ); assertThat( Automatons.subsetOf( crossClusterReplicationInternal.automaton, - IndexPrivilege.getSingleSelectorOrThrow(Set.of("all")).automaton + IndexPrivilege.getWithSingleSelectorAccess(Set.of("all")).automaton ), is(true) ); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/PrivilegeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/PrivilegeTests.java index 8540af2ddc339..f5cf0b5e11100 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/PrivilegeTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/PrivilegeTests.java @@ -205,7 +205,7 @@ public void testClusterAction() throws Exception { public void testIndexAction() throws Exception { Set actionName = Sets.newHashSet("indices:admin/mapping/delete"); - IndexPrivilege index = IndexPrivilege.getSingleSelectorOrThrow(actionName); + IndexPrivilege index = IndexPrivilege.getWithSingleSelectorAccess(actionName); assertThat(index, notNullValue()); assertThat(index.predicate().test("indices:admin/mapping/delete"), is(true)); assertThat(index.predicate().test("indices:admin/mapping/dele"), is(false)); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/support/AutomatonPatternsTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/support/AutomatonPatternsTests.java index 926cd4260ec78..237c5921c55cf 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/support/AutomatonPatternsTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/support/AutomatonPatternsTests.java @@ -37,7 +37,7 @@ public void testRemoteClusterPrivsDoNotOverlap() { // check that the action patterns for remote CCS are not allowed by remote CCR privileges Arrays.stream(CCS_INDICES_PRIVILEGE_NAMES).forEach(ccsPrivilege -> { - Automaton ccsAutomaton = IndexPrivilege.getSingleSelectorOrThrow(Set.of(ccsPrivilege)).getAutomaton(); + Automaton ccsAutomaton = IndexPrivilege.getWithSingleSelectorAccess(Set.of(ccsPrivilege)).getAutomaton(); Automatons.getPatterns(ccsAutomaton).forEach(ccsPattern -> { // emulate an action name that could be allowed by a CCS privilege String actionName = ccsPattern.replaceAll("\\*", randomAlphaOfLengthBetween(1, 8)); @@ -49,14 +49,17 @@ public void testRemoteClusterPrivsDoNotOverlap() { ccrPrivileges, ccsPattern ); - assertFalse(errorMessage, IndexPrivilege.getSingleSelectorOrThrow(Set.of(ccrPrivileges)).predicate().test(actionName)); + assertFalse( + errorMessage, + IndexPrivilege.getWithSingleSelectorAccess(Set.of(ccrPrivileges)).predicate().test(actionName) + ); }); }); }); // check that the action patterns for remote CCR are not allowed by remote CCS privileges Arrays.stream(CCR_INDICES_PRIVILEGE_NAMES).forEach(ccrPrivilege -> { - Automaton ccrAutomaton = IndexPrivilege.getSingleSelectorOrThrow(Set.of(ccrPrivilege)).getAutomaton(); + Automaton ccrAutomaton = IndexPrivilege.getWithSingleSelectorAccess(Set.of(ccrPrivilege)).getAutomaton(); Automatons.getPatterns(ccrAutomaton).forEach(ccrPattern -> { // emulate an action name that could be allowed by a CCR privilege String actionName = ccrPattern.replaceAll("\\*", randomAlphaOfLengthBetween(1, 8)); @@ -74,7 +77,7 @@ public void testRemoteClusterPrivsDoNotOverlap() { ); assertFalse( errorMessage, - IndexPrivilege.getSingleSelectorOrThrow(Set.of(ccsPrivileges)).predicate().test(actionName) + IndexPrivilege.getWithSingleSelectorAccess(Set.of(ccsPrivileges)).predicate().test(actionName) ); } }); diff --git a/x-pack/plugin/security/qa/consistency-checks/src/test/java/org/elasticsearch/xpack/security/CrossClusterShardTests.java b/x-pack/plugin/security/qa/consistency-checks/src/test/java/org/elasticsearch/xpack/security/CrossClusterShardTests.java index 2cb2783173b64..cca137261247a 100644 --- a/x-pack/plugin/security/qa/consistency-checks/src/test/java/org/elasticsearch/xpack/security/CrossClusterShardTests.java +++ b/x-pack/plugin/security/qa/consistency-checks/src/test/java/org/elasticsearch/xpack/security/CrossClusterShardTests.java @@ -112,7 +112,7 @@ public void testCheckForNewShardLevelTransportActions() throws Exception { List shardActions = transportActionBindings.stream() .map(binding -> binding.getProvider().get()) - .filter(action -> IndexPrivilege.getSingleSelectorOrThrow(crossClusterPrivilegeNames).predicate().test(action.actionName)) + .filter(action -> IndexPrivilege.getWithSingleSelectorAccess(crossClusterPrivilegeNames).predicate().test(action.actionName)) .filter(this::actionIsLikelyShardAction) .map(action -> action.actionName) .toList(); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/DeprecationRoleDescriptorConsumer.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/DeprecationRoleDescriptorConsumer.java index ce4ded387c284..3315b82e500e3 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/DeprecationRoleDescriptorConsumer.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/DeprecationRoleDescriptorConsumer.java @@ -183,7 +183,7 @@ private void logDeprecatedPermission(RoleDescriptor roleDescriptor) { for (Map.Entry> privilegesByAlias : privilegesByAliasMap.entrySet()) { final String aliasName = privilegesByAlias.getKey(); final Set aliasPrivilegeNames = privilegesByAlias.getValue(); - final Automaton aliasPrivilegeAutomaton = IndexPrivilege.getSingleSelectorOrThrow(aliasPrivilegeNames).getAutomaton(); + final Automaton aliasPrivilegeAutomaton = IndexPrivilege.getWithSingleSelectorAccess(aliasPrivilegeNames).getAutomaton(); final SortedSet inferiorIndexNames = new TreeSet<>(); // check if the alias grants superiors privileges than the indices it points to for (Index index : aliasOrIndexMap.get(aliasName).getIndices()) { @@ -193,7 +193,7 @@ private void logDeprecatedPermission(RoleDescriptor roleDescriptor) { // compute automaton once per index no matter how many times it is pointed to final Automaton indexPrivilegeAutomaton = indexAutomatonMap.computeIfAbsent( index.getName(), - i -> IndexPrivilege.getSingleSelectorOrThrow(indexPrivileges).getAutomaton() + i -> IndexPrivilege.getWithSingleSelectorAccess(indexPrivileges).getAutomaton() ); if (false == Automatons.subsetOf(indexPrivilegeAutomaton, aliasPrivilegeAutomaton)) { inferiorIndexNames.add(index.getName()); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java index b106f565c2b1c..6bc230e7d4ac0 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java @@ -1290,7 +1290,7 @@ public void testBuildUserPrivilegeResponse() { {"term":{"public":true}}"""); final Role role = Role.builder(RESTRICTED_INDICES, "test", "role") .cluster(Sets.newHashSet("monitor", "manage_watcher"), Collections.singleton(manageApplicationPrivileges)) - .add(IndexPrivilege.getSingleSelectorOrThrow(Sets.newHashSet("read", "write")), "index-1") + .add(IndexPrivilege.getWithSingleSelectorAccess(Sets.newHashSet("read", "write")), "index-1") .add(IndexPrivilege.ALL, "index-2", "index-3") .add( new FieldPermissions(new FieldPermissionsDefinition(new String[] { "public.*" }, new String[0])), diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java index d79265ff09cf8..406b2357f4df6 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java @@ -1494,8 +1494,8 @@ public void testBuildRoleWithMultipleRemoteMergedAcrossPrivilegesAndDescriptors( assertHasRemoteIndexGroupsForClusters( role.remoteIndices(), Set.of("remote-1"), - indexGroup(IndexPrivilege.getSingleSelectorOrThrow(Set.of("read")), false, "index-1"), - indexGroup(IndexPrivilege.getSingleSelectorOrThrow(Set.of("none")), false, "index-1") + indexGroup(IndexPrivilege.getWithSingleSelectorAccess(Set.of("read")), false, "index-1"), + indexGroup(IndexPrivilege.getWithSingleSelectorAccess(Set.of("none")), false, "index-1") ); } @@ -1606,7 +1606,7 @@ public void testBuildRoleWithAllPrivilegeIsNeverSplit() { ); assertHasIndexGroups( role.indices(), - indexGroup(IndexPrivilege.getSingleSelectorOrThrow(Set.of("read", "read_failure_store", "all")), false, indexPattern) + indexGroup(IndexPrivilege.getWithSingleSelectorAccess(Set.of("read", "read_failure_store", "all")), false, indexPattern) ); } @@ -1633,7 +1633,7 @@ public void testBuildRoleNeverSplitsWithoutFailureStoreRelatedPrivileges() { final Role role = buildRole(roleDescriptorWithIndicesPrivileges("r1", indicesPrivileges)); final IndicesPermission actual = role.indices(); - assertHasIndexGroups(actual, indexGroup(IndexPrivilege.getSingleSelectorOrThrow(usedPrivileges), false, indexPattern)); + assertHasIndexGroups(actual, indexGroup(IndexPrivilege.getWithSingleSelectorAccess(usedPrivileges), false, indexPattern)); } public void testCustomRolesProviderFailures() throws Exception { From 17855abe57f75bbaa62b649e392adf06e12c52fa Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 19 Feb 2025 10:43:36 +0100 Subject: [PATCH 035/131] Javadoc --- .../xpack/core/security/authz/privilege/IndexPrivilege.java | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java index 777f797a26a44..ec518df3fa711 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java @@ -277,6 +277,7 @@ private IndexPrivilege(Set name, Automaton automaton, IndexComponentSele * Delegates to {@link #getSplitBySelectorAccess(Set)} but throws if the result is not a singleton, i.e., covers more than one selector. * Use this method if you know that the input name set corresponds to privileges covering the same selector, for instance if you have a * single input name, or multiple names that all grant access to one selector e.g., {@link IndexComponentSelector#DATA}. + * @throws IllegalArgumentException if privileges and actions for input names cover access to more than one selector */ public static IndexPrivilege getWithSingleSelectorAccess(Set names) { final Set splitBySelector = getSplitBySelectorAccess(names); From 4a44748641852cf4066f44a20dde93065a76f1ec Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 19 Feb 2025 11:54:28 +0100 Subject: [PATCH 036/131] More split tests --- .../authz/privilege/IndexPrivilege.java | 16 ++-- .../authz/privilege/IndexPrivilegeTests.java | 76 ++++++++++++++++++- 2 files changed, 83 insertions(+), 9 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java index ec518df3fa711..2e2ca20abacc3 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java @@ -369,14 +369,14 @@ private static Set resolve(Set name) { } } - final Set result = combineIndexPrivileges( + final Set combined = combineIndexPrivileges( allSelectorAccessPrivileges, dataSelectorAccessPrivileges, failuresSelectorAccessPrivileges, actions ); - assertNamesMatch(name, result); - return result; + assertNamesMatch(name, combined); + return combined; } private static Set combineIndexPrivileges( @@ -396,14 +396,14 @@ private static Set combineIndexPrivileges( return Set.of(union(allSelectorAccessPrivileges, actions, IndexComponentSelectorPredicate.ALL)); } - final Set result = new HashSet<>(); + final Set combined = new HashSet<>(); if (false == failuresSelectorAccessPrivileges.isEmpty()) { - result.add(union(failuresSelectorAccessPrivileges, Set.of(), IndexComponentSelectorPredicate.FAILURES)); + combined.add(union(failuresSelectorAccessPrivileges, Set.of(), IndexComponentSelectorPredicate.FAILURES)); } if (false == dataSelectorAccessPrivileges.isEmpty() || false == actions.isEmpty()) { - result.add(union(dataSelectorAccessPrivileges, actions, IndexComponentSelectorPredicate.DATA)); + combined.add(union(dataSelectorAccessPrivileges, actions, IndexComponentSelectorPredicate.DATA)); } - return result; + return combined; } private static void assertNamesMatch(Set names, Set privileges) { @@ -428,7 +428,7 @@ private static IndexPrivilege union( if (false == actions.isEmpty()) { names.addAll(actions); - automata.add(patterns(actions.stream().map(Privilege::actionToPattern).toArray(String[]::new))); + automata.add(patterns(actions.stream().map(Privilege::actionToPattern).toList())); } return new IndexPrivilege(names, unionAndMinimize(automata), selectorPredicate); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java index a8f54d6e5835f..b95a7e6ca5124 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java @@ -105,7 +105,7 @@ public void testGetWithSingleSelectorAccess() { } } - public void testGetSingleSelectorWithFailuresSelectorOrThrow() { + public void testGetWithSingleSelectorAccessFailuresSelector() { assumeTrue("This test requires the failure store to be enabled", DataStream.isFailureStoreFeatureFlagEnabled()); { IndexPrivilege actual = IndexPrivilege.getWithSingleSelectorAccess(Set.of("read_failure_store")); @@ -146,6 +146,80 @@ public void testGetSingleSelectorWithFailuresSelectorOrThrow() { ); } + public void testGetSplitBySelectorAccess() { + assumeTrue("This test requires the failure store to be enabled", DataStream.isFailureStoreFeatureFlagEnabled()); + { + Set actual = IndexPrivilege.getSplitBySelectorAccess(Set.of("read_failure_store")); + assertThat(actual, containsInAnyOrder(IndexPrivilege.READ_FAILURE_STORE)); + assertThat(actual.iterator().next().getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.FAILURES)); + } + { + Set actual = IndexPrivilege.getSplitBySelectorAccess(Set.of("read_failure_store", "READ_FAILURE_STORE")); + assertThat(actual, containsInAnyOrder(IndexPrivilege.READ_FAILURE_STORE)); + assertThat(actual.iterator().next().getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.FAILURES)); + } + { + Set actual = IndexPrivilege.getSplitBySelectorAccess( + Set.of("read_failure_store", "read", "READ_FAILURE_STORE") + ); + assertThat(actual, containsInAnyOrder(IndexPrivilege.READ_FAILURE_STORE, IndexPrivilege.READ)); + List actualPredicates = actual.stream().map(IndexPrivilege::getSelectorPredicate).toList(); + assertThat( + actualPredicates, + containsInAnyOrder(IndexComponentSelectorPredicate.DATA, IndexComponentSelectorPredicate.FAILURES) + ); + } + { + Set actual = IndexPrivilege.getSplitBySelectorAccess( + Set.of("read_failure_store", "read", "view_index_metadata") + ); + assertThat( + actual, + containsInAnyOrder( + IndexPrivilege.READ_FAILURE_STORE, + IndexPrivilege.getWithSingleSelectorAccess(Set.of("read", "view_index_metadata")) + ) + ); + List actualPredicates = actual.stream().map(IndexPrivilege::getSelectorPredicate).toList(); + assertThat( + actualPredicates, + containsInAnyOrder(IndexComponentSelectorPredicate.DATA, IndexComponentSelectorPredicate.FAILURES) + ); + } + { + Set actual = IndexPrivilege.getSplitBySelectorAccess( + Set.of("read_failure_store", "read", "indices:data/read/search", "view_index_metadata") + ); + assertThat( + actual, + containsInAnyOrder( + IndexPrivilege.READ_FAILURE_STORE, + IndexPrivilege.getWithSingleSelectorAccess(Set.of("read", "indices:data/read/search", "view_index_metadata")) + ) + ); + List actualPredicates = actual.stream().map(IndexPrivilege::getSelectorPredicate).toList(); + assertThat( + actualPredicates, + containsInAnyOrder(IndexComponentSelectorPredicate.DATA, IndexComponentSelectorPredicate.FAILURES) + ); + } + { + Set actual = IndexPrivilege.getSplitBySelectorAccess( + Set.of("read_failure_store", "all", "read", "indices:data/read/search", "view_index_metadata") + ); + assertThat( + actual, + containsInAnyOrder( + IndexPrivilege.getWithSingleSelectorAccess( + Set.of("read_failure_store", "all", "read", "indices:data/read/search", "view_index_metadata") + ) + ) + ); + List actualPredicates = actual.stream().map(IndexPrivilege::getSelectorPredicate).toList(); + assertThat(actualPredicates, containsInAnyOrder(IndexComponentSelectorPredicate.ALL)); + } + } + public void testPrivilegesForRollupFieldCapsAction() { final Collection privileges = findPrivilegesThatGrant(GetRollupIndexCapsAction.NAME); assertThat(Set.copyOf(privileges), equalTo(Set.of("manage", "all", "view_index_metadata", "read"))); From 481da2a90af4e1ce8060bc71cee12a7df0d3e637 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 19 Feb 2025 12:19:56 +0100 Subject: [PATCH 037/131] Tests --- .../authz/privilege/IndexPrivilegeTests.java | 4 +- .../authz/store/CompositeRolesStoreTests.java | 72 +++++++++++++++---- 2 files changed, 59 insertions(+), 17 deletions(-) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java index b95a7e6ca5124..76cec8339ef99 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java @@ -106,7 +106,7 @@ public void testGetWithSingleSelectorAccess() { } public void testGetWithSingleSelectorAccessFailuresSelector() { - assumeTrue("This test requires the failure store to be enabled", DataStream.isFailureStoreFeatureFlagEnabled()); + assumeTrue("requires failure store feature", DataStream.isFailureStoreFeatureFlagEnabled()); { IndexPrivilege actual = IndexPrivilege.getWithSingleSelectorAccess(Set.of("read_failure_store")); assertThat(actual, equalTo(IndexPrivilege.READ_FAILURE_STORE)); @@ -147,7 +147,7 @@ public void testGetWithSingleSelectorAccessFailuresSelector() { } public void testGetSplitBySelectorAccess() { - assumeTrue("This test requires the failure store to be enabled", DataStream.isFailureStoreFeatureFlagEnabled()); + assumeTrue("requires failure store feature", DataStream.isFailureStoreFeatureFlagEnabled()); { Set actual = IndexPrivilege.getSplitBySelectorAccess(Set.of("read_failure_store")); assertThat(actual, containsInAnyOrder(IndexPrivilege.READ_FAILURE_STORE)); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java index 406b2357f4df6..80f431ee2a309 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java @@ -1535,78 +1535,120 @@ public void testBuildRoleWithMultipleRemoteClusterMerged() { public void testBuildRoleWithReadFailureStorePrivilegeOnly() { assumeTrue("requires failure store feature", DataStream.isFailureStoreFeatureFlagEnabled()); String indexPattern = randomAlphanumericOfLength(10); + boolean allowRestrictedIndices = randomBoolean(); final Role role = buildRole( roleDescriptorWithIndicesPrivileges( "r1", - new IndicesPrivileges[] { IndicesPrivileges.builder().indices(indexPattern).privileges("read_failure_store").build() } + new IndicesPrivileges[] { + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read_failure_store") + .allowRestrictedIndices(allowRestrictedIndices) + .build() } ) ); - assertHasIndexGroups(role.indices(), indexGroup(IndexPrivilege.READ_FAILURE_STORE, false, indexPattern)); + assertHasIndexGroups(role.indices(), indexGroup(IndexPrivilege.READ_FAILURE_STORE, allowRestrictedIndices, indexPattern)); } public void testBuildRoleWithReadFailureStorePrivilegeDuplicatesMerged() { assumeTrue("requires failure store feature", DataStream.isFailureStoreFeatureFlagEnabled()); String indexPattern = randomAlphanumericOfLength(10); + boolean allowRestrictedIndices = randomBoolean(); final Role role = buildRole( roleDescriptorWithIndicesPrivileges( "r1", new IndicesPrivileges[] { - IndicesPrivileges.builder().indices(indexPattern).privileges("read_failure_store").build(), - IndicesPrivileges.builder().indices(indexPattern).privileges("read_failure_store").build() } + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read_failure_store") + .allowRestrictedIndices(allowRestrictedIndices) + .build(), + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read_failure_store") + .allowRestrictedIndices(allowRestrictedIndices) + .build() } ) ); - assertHasIndexGroups(role.indices(), indexGroup(IndexPrivilege.READ_FAILURE_STORE, false, indexPattern)); + assertHasIndexGroups(role.indices(), indexGroup(IndexPrivilege.READ_FAILURE_STORE, allowRestrictedIndices, indexPattern)); } public void testBuildRoleWithReadFailureStoreAndReadPrivilegeSplit() { assumeTrue("requires failure store feature", DataStream.isFailureStoreFeatureFlagEnabled()); String indexPattern = randomAlphanumericOfLength(10); + boolean allowRestrictedIndices = randomBoolean(); final Role role = buildRole( roleDescriptorWithIndicesPrivileges( "r1", new IndicesPrivileges[] { - IndicesPrivileges.builder().indices(indexPattern).privileges("read", "read_failure_store").build() } + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read", "read_failure_store") + .allowRestrictedIndices(allowRestrictedIndices) + .build() } ) ); assertHasIndexGroups( role.indices(), - indexGroup(IndexPrivilege.READ_FAILURE_STORE, false, indexPattern), - indexGroup(IndexPrivilege.READ, false, indexPattern) + indexGroup(IndexPrivilege.READ_FAILURE_STORE, allowRestrictedIndices, indexPattern), + indexGroup(IndexPrivilege.READ, allowRestrictedIndices, indexPattern) ); } public void testBuildRoleWithMultipleReadFailureStoreAndReadPrivilegeSplit() { assumeTrue("requires failure store feature", DataStream.isFailureStoreFeatureFlagEnabled()); String indexPattern = randomAlphanumericOfLength(10); + boolean allowRestrictedIndices = randomBoolean(); final Role role = buildRole( roleDescriptorWithIndicesPrivileges( "r1", new IndicesPrivileges[] { - IndicesPrivileges.builder().indices(indexPattern).privileges("read").build(), - IndicesPrivileges.builder().indices(indexPattern).privileges("read_failure_store").build() } + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read") + .allowRestrictedIndices(allowRestrictedIndices) + .build(), + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read_failure_store") + .allowRestrictedIndices(allowRestrictedIndices) + .build() } ) ); assertHasIndexGroups( role.indices(), - indexGroup(IndexPrivilege.READ_FAILURE_STORE, false, indexPattern), - indexGroup(IndexPrivilege.READ, false, indexPattern) + indexGroup(IndexPrivilege.READ_FAILURE_STORE, allowRestrictedIndices, indexPattern), + indexGroup(IndexPrivilege.READ, allowRestrictedIndices, indexPattern) ); } public void testBuildRoleWithAllPrivilegeIsNeverSplit() { assumeTrue("requires failure store feature", DataStream.isFailureStoreFeatureFlagEnabled()); String indexPattern = randomAlphanumericOfLength(10); + boolean allowRestrictedIndices = randomBoolean(); final Role role = buildRole( roleDescriptorWithIndicesPrivileges( "r1", new IndicesPrivileges[] { - IndicesPrivileges.builder().indices(indexPattern).privileges("read", "read_failure_store", "all").build(), - IndicesPrivileges.builder().indices(indexPattern).privileges("read_failure_store").build() } + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read", "read_failure_store", "all") + .allowRestrictedIndices(allowRestrictedIndices) + .build(), + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read_failure_store") + .allowRestrictedIndices(allowRestrictedIndices) + .build() } ) ); assertHasIndexGroups( role.indices(), - indexGroup(IndexPrivilege.getWithSingleSelectorAccess(Set.of("read", "read_failure_store", "all")), false, indexPattern) + indexGroup( + IndexPrivilege.getWithSingleSelectorAccess(Set.of("read", "read_failure_store", "all")), + allowRestrictedIndices, + indexPattern + ) ); } From 8036ecf775ce1467b2642b97c05c2b64753cbcc9 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 19 Feb 2025 12:47:57 +0100 Subject: [PATCH 038/131] FLS DLS tests --- .../authz/store/CompositeRolesStoreTests.java | 67 ++++++++++++++++++- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java index 80f431ee2a309..6c9793f1715b9 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java @@ -1652,6 +1652,57 @@ public void testBuildRoleWithAllPrivilegeIsNeverSplit() { ); } + public void testBuildRoleWithFailureStorePrivilegeCollatesToRemoveDlsFlsFromAnotherGroup() { + assumeTrue("requires failure store feature", DataStream.isFailureStoreFeatureFlagEnabled()); + String indexPattern = randomAlphanumericOfLength(10); + boolean allowRestrictedIndices = randomBoolean(); + final Role role = buildRole( + roleDescriptorWithIndicesPrivileges( + "r1", + new IndicesPrivileges[] { + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read_failure_store") + .allowRestrictedIndices(allowRestrictedIndices) + .build(), + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read", "view_index_metadata") + .query("{\"match\":{\"field\":\"a\"}}") + .grantedFields("field") + .allowRestrictedIndices(allowRestrictedIndices) + .build() } + ) + ); + assertHasIndexGroups( + role.indices(), + indexGroup( + IndexPrivilege.getWithSingleSelectorAccess(Set.of("read_failure_store")), + allowRestrictedIndices, + null, + new FieldPermissionsDefinition( + Set.of( + new FieldPermissionsDefinition.FieldGrantExcludeGroup(null, null), + new FieldPermissionsDefinition.FieldGrantExcludeGroup(new String[] { "field" }, null) + ) + ), + indexPattern + ), + indexGroup( + IndexPrivilege.getWithSingleSelectorAccess(Set.of("read", "view_index_metadata")), + allowRestrictedIndices, + null, + new FieldPermissionsDefinition( + Set.of( + new FieldPermissionsDefinition.FieldGrantExcludeGroup(null, null), + new FieldPermissionsDefinition.FieldGrantExcludeGroup(new String[] { "field" }, null) + ) + ), + indexPattern + ) + ); + } + public void testBuildRoleNeverSplitsWithoutFailureStoreRelatedPrivileges() { String indexPattern = randomAlphanumericOfLength(10); List nonFailurePrivileges = IndexPrivilege.names() @@ -3540,6 +3591,16 @@ private static Matcher indexGroup( @Nullable final String query, final FieldPermissionsDefinition.FieldGrantExcludeGroup flsGroup, final String... indices + ) { + return indexGroup(privilege, allowRestrictedIndices, query, new FieldPermissionsDefinition(Set.of(flsGroup)), indices); + } + + private static Matcher indexGroup( + final IndexPrivilege privilege, + final boolean allowRestrictedIndices, + @Nullable final String query, + final FieldPermissionsDefinition fieldPermissionsDefinition, + final String... indices ) { return new BaseMatcher<>() { @Override @@ -3551,7 +3612,7 @@ public boolean matches(Object o) { return equalTo(query == null ? null : Set.of(new BytesArray(query))).matches(group.getQuery()) && equalTo(privilege).matches(group.privilege()) && equalTo(allowRestrictedIndices).matches(group.allowRestrictedIndices()) - && equalTo(new FieldPermissions(new FieldPermissionsDefinition(Set.of(flsGroup)))).matches(group.getFieldPermissions()) + && equalTo(new FieldPermissions(fieldPermissionsDefinition)).matches(group.getFieldPermissions()) && arrayContaining(indices).matches(group.indices()); } @@ -3567,8 +3628,8 @@ public void describeTo(Description description) { + Strings.arrayToCommaDelimitedString(indices) + ", query=" + query - + ", fieldGrantExcludeGroup=" - + flsGroup + + ", fieldPermissionsDefinition=" + + fieldPermissionsDefinition + '}' ); } From 16668b0961fe9bc6b44b23bae249bd16a1038839 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 19 Feb 2025 14:33:36 +0100 Subject: [PATCH 039/131] Automaton and fls dls test --- .../role/RoleDescriptorRequestValidator.java | 4 +- .../core/security/authz/permission/Role.java | 4 +- .../ConfigurableClusterPrivileges.java | 4 +- .../IndexComponentSelectorPredicate.java | 2 +- .../authz/privilege/IndexPrivilege.java | 11 +++-- .../authz/permission/SimpleRoleTests.java | 2 +- .../authz/privilege/IndexPrivilegeTests.java | 18 +++---- .../authz/privilege/PrivilegeTests.java | 2 +- .../authz/store/CompositeRolesStore.java | 6 +-- .../xpack/security/authz/RBACEngineTests.java | 2 +- .../authz/store/CompositeRolesStoreTests.java | 47 +++++++++++++++++++ 11 files changed, 72 insertions(+), 30 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/RoleDescriptorRequestValidator.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/RoleDescriptorRequestValidator.java index 8329c1170ecdd..c295cbd58ce35 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/RoleDescriptorRequestValidator.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/RoleDescriptorRequestValidator.java @@ -49,7 +49,7 @@ public static ActionRequestValidationException validate( if (roleDescriptor.getIndicesPrivileges() != null) { for (RoleDescriptor.IndicesPrivileges idp : roleDescriptor.getIndicesPrivileges()) { try { - IndexPrivilege.getSplitBySelectorAccess(Set.of(idp.getPrivileges())); + IndexPrivilege.splitBySelectorAccess(Set.of(idp.getPrivileges())); } catch (IllegalArgumentException ile) { validationException = addValidationError(ile.getMessage(), validationException); } @@ -61,7 +61,7 @@ public static ActionRequestValidationException validate( validationException = addValidationError("remote index cluster alias cannot be an empty string", validationException); } try { - var privileges = IndexPrivilege.getSplitBySelectorAccess(Set.of(ridp.indicesPrivileges().getPrivileges())); + var privileges = IndexPrivilege.splitBySelectorAccess(Set.of(ridp.indicesPrivileges().getPrivileges())); if (privileges.stream().anyMatch(p -> p.getSelectorPredicate() == IndexComponentSelectorPredicate.FAILURES)) { validationException = addValidationError( "remote index privileges cannot contain privileges that grant access to the failure store", diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java index 63229e11c6ef7..e5ff20c2e08ab 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java @@ -437,7 +437,7 @@ static SimpleRole buildFromRoleDescriptor( new FieldPermissionsDefinition(indexPrivilege.getGrantedFields(), indexPrivilege.getDeniedFields()) ), indexPrivilege.getQuery() == null ? null : Collections.singleton(indexPrivilege.getQuery()), - IndexPrivilege.getSplitBySelectorAccess(Set.of(indexPrivilege.getPrivileges())), + IndexPrivilege.splitBySelectorAccess(Set.of(indexPrivilege.getPrivileges())), indexPrivilege.allowRestrictedIndices(), indexPrivilege.getIndices() ); @@ -454,7 +454,7 @@ static SimpleRole buildFromRoleDescriptor( new FieldPermissionsDefinition(indicesPrivileges.getGrantedFields(), indicesPrivileges.getDeniedFields()) ), indicesPrivileges.getQuery() == null ? null : Collections.singleton(indicesPrivileges.getQuery()), - IndexPrivilege.getSplitBySelectorAccess(Set.of(indicesPrivileges.getPrivileges())), + IndexPrivilege.splitBySelectorAccess(Set.of(indicesPrivileges.getPrivileges())), indicesPrivileges.allowRestrictedIndices(), indicesPrivileges.getIndices() ); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java index 206fdf113935d..acf4e3ee33a0a 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java @@ -414,9 +414,7 @@ public ManageRolesPrivilege(List manageRolesInd this.requestPredicateSupplier = (restrictedIndices) -> { IndicesPermission.Builder indicesPermissionBuilder = new IndicesPermission.Builder(restrictedIndices); for (ManageRolesIndexPermissionGroup indexPatternPrivilege : manageRolesIndexPermissionGroups) { - Set splitBySelector = IndexPrivilege.getSplitBySelectorAccess( - Set.of(indexPatternPrivilege.privileges()) - ); + Set splitBySelector = IndexPrivilege.splitBySelectorAccess(Set.of(indexPatternPrivilege.privileges())); for (IndexPrivilege indexPrivilege : splitBySelector) { indicesPermissionBuilder.addGroup( indexPrivilege, diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPredicate.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPredicate.java index 6d90e76e664a8..33d639d144ab7 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPredicate.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPredicate.java @@ -16,7 +16,7 @@ /** * A predicate to capture role access by {@link IndexComponentSelector}. * This is assigned to each {@link org.elasticsearch.xpack.core.security.authz.permission.IndicesPermission.Group} during role building. - * See also {@link org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege#getSplitBySelectorAccess(Set)}. + * See also {@link org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege#splitBySelectorAccess(Set)}. */ public record IndexComponentSelectorPredicate(Set names, Predicate predicate) implements diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java index 2e2ca20abacc3..46b8c780b2b71 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java @@ -184,7 +184,8 @@ public final class IndexPrivilege extends Privilege { public static final IndexPrivilege ALL = new IndexPrivilege("all", ALL_AUTOMATON, IndexComponentSelectorPredicate.ALL); public static final IndexPrivilege READ_FAILURE_STORE = new IndexPrivilege( "read_failure_store", - READ_AUTOMATON, + // TODO use READ_AUTOMATON here in authorization follow-up + Automatons.EMPTY, IndexComponentSelectorPredicate.FAILURES ); public static final IndexPrivilege READ = new IndexPrivilege("read", READ_AUTOMATON); @@ -274,13 +275,13 @@ private IndexPrivilege(Set name, Automaton automaton, IndexComponentSele } /** - * Delegates to {@link #getSplitBySelectorAccess(Set)} but throws if the result is not a singleton, i.e., covers more than one selector. + * Delegates to {@link #splitBySelectorAccess(Set)} but throws if the result is not a singleton, i.e., covers more than one selector. * Use this method if you know that the input name set corresponds to privileges covering the same selector, for instance if you have a * single input name, or multiple names that all grant access to one selector e.g., {@link IndexComponentSelector#DATA}. * @throws IllegalArgumentException if privileges and actions for input names cover access to more than one selector */ public static IndexPrivilege getWithSingleSelectorAccess(Set names) { - final Set splitBySelector = getSplitBySelectorAccess(names); + final Set splitBySelector = splitBySelectorAccess(names); if (splitBySelector.size() != 1) { throw new IllegalArgumentException( "index privilege patterns " + names + " did not map to a single selector " + splitBySelector @@ -299,14 +300,14 @@ public static IndexPrivilege getWithSingleSelectorAccess(Set names) { * selector boundaries since their underlying automata would be combined, granting more access than is valid. * This method conceptually splits the input names into ones that correspond to different selector access, and return an index privilege * for each partition. - * For instance, `getSplitBySelectorAccess(Set.of("view_index_metadata", "write", "read_failure_store"))` will return two index + * For instance, `splitBySelectorAccess(Set.of("view_index_metadata", "write", "read_failure_store"))` will return two index * privileges one covering `view_index_metadata` and `write` for a {@link IndexComponentSelectorPredicate#DATA}, the other covering * `read_failure_store` for a {@link IndexComponentSelectorPredicate#FAILURES} selector. * A notable exception is the {@link IndexPrivilege#ALL} privilege. If this privilege is included in the input name set, this method * returns a single index privilege that grants access to all selectors. * All raw actions are treated as granting access to the {@link IndexComponentSelector#DATA} selector. */ - public static Set getSplitBySelectorAccess(Set names) { + public static Set splitBySelectorAccess(Set names) { return CACHE.computeIfAbsent(names, (theName) -> { if (theName.isEmpty()) { return Set.of(NONE); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/SimpleRoleTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/SimpleRoleTests.java index a0cb6e4fe56a8..e42e3e4156c15 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/SimpleRoleTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/SimpleRoleTests.java @@ -176,7 +176,7 @@ public void testGetRoleDescriptorsIntersectionForRemoteCluster() { Set.of(randomAlphaOfLength(8)), new FieldPermissions(new FieldPermissionsDefinition(new String[] { randomAlphaOfLength(5) }, null)), null, - IndexPrivilege.getSplitBySelectorAccess(Set.of(randomFrom(IndexPrivilege.names()))), + IndexPrivilege.splitBySelectorAccess(Set.of(randomFrom(IndexPrivilege.names()))), randomBoolean(), randomAlphaOfLength(9) ) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java index 76cec8339ef99..e3bde5dbdd664 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java @@ -146,22 +146,20 @@ public void testGetWithSingleSelectorAccessFailuresSelector() { ); } - public void testGetSplitBySelectorAccess() { + public void testSplitBySelectorAccess() { assumeTrue("requires failure store feature", DataStream.isFailureStoreFeatureFlagEnabled()); { - Set actual = IndexPrivilege.getSplitBySelectorAccess(Set.of("read_failure_store")); + Set actual = IndexPrivilege.splitBySelectorAccess(Set.of("read_failure_store")); assertThat(actual, containsInAnyOrder(IndexPrivilege.READ_FAILURE_STORE)); assertThat(actual.iterator().next().getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.FAILURES)); } { - Set actual = IndexPrivilege.getSplitBySelectorAccess(Set.of("read_failure_store", "READ_FAILURE_STORE")); + Set actual = IndexPrivilege.splitBySelectorAccess(Set.of("read_failure_store", "READ_FAILURE_STORE")); assertThat(actual, containsInAnyOrder(IndexPrivilege.READ_FAILURE_STORE)); assertThat(actual.iterator().next().getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.FAILURES)); } { - Set actual = IndexPrivilege.getSplitBySelectorAccess( - Set.of("read_failure_store", "read", "READ_FAILURE_STORE") - ); + Set actual = IndexPrivilege.splitBySelectorAccess(Set.of("read_failure_store", "read", "READ_FAILURE_STORE")); assertThat(actual, containsInAnyOrder(IndexPrivilege.READ_FAILURE_STORE, IndexPrivilege.READ)); List actualPredicates = actual.stream().map(IndexPrivilege::getSelectorPredicate).toList(); assertThat( @@ -170,9 +168,7 @@ public void testGetSplitBySelectorAccess() { ); } { - Set actual = IndexPrivilege.getSplitBySelectorAccess( - Set.of("read_failure_store", "read", "view_index_metadata") - ); + Set actual = IndexPrivilege.splitBySelectorAccess(Set.of("read_failure_store", "read", "view_index_metadata")); assertThat( actual, containsInAnyOrder( @@ -187,7 +183,7 @@ public void testGetSplitBySelectorAccess() { ); } { - Set actual = IndexPrivilege.getSplitBySelectorAccess( + Set actual = IndexPrivilege.splitBySelectorAccess( Set.of("read_failure_store", "read", "indices:data/read/search", "view_index_metadata") ); assertThat( @@ -204,7 +200,7 @@ public void testGetSplitBySelectorAccess() { ); } { - Set actual = IndexPrivilege.getSplitBySelectorAccess( + Set actual = IndexPrivilege.splitBySelectorAccess( Set.of("read_failure_store", "all", "read", "indices:data/read/search", "view_index_metadata") ); assertThat( diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/PrivilegeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/PrivilegeTests.java index f5cf0b5e11100..6b2177f2414cf 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/PrivilegeTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/PrivilegeTests.java @@ -218,7 +218,7 @@ public void testIndexCollapse() { IndexPrivilege second = values[randomIntBetween(0, values.length - 1)]; Set name = Sets.newHashSet(first.name().iterator().next(), second.name().iterator().next()); - Set indices = IndexPrivilege.getSplitBySelectorAccess(name); + Set indices = IndexPrivilege.splitBySelectorAccess(name); Automaton automaton = null; if (indices.size() == 1) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java index 8c86ea729e933..b7a3516eda7db 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java @@ -543,7 +543,7 @@ public static void buildRoleFromDescriptors( (key, privilege) -> builder.add( fieldPermissionsCache.getFieldPermissions(privilege.fieldPermissionsDefinition), privilege.query, - IndexPrivilege.getSplitBySelectorAccess(privilege.privileges), + IndexPrivilege.splitBySelectorAccess(privilege.privileges), false, privilege.indices.toArray(Strings.EMPTY_ARRAY) ) @@ -552,7 +552,7 @@ public static void buildRoleFromDescriptors( (key, privilege) -> builder.add( fieldPermissionsCache.getFieldPermissions(privilege.fieldPermissionsDefinition), privilege.query, - IndexPrivilege.getSplitBySelectorAccess(privilege.privileges), + IndexPrivilege.splitBySelectorAccess(privilege.privileges), true, privilege.indices.toArray(Strings.EMPTY_ARRAY) ) @@ -566,7 +566,7 @@ public static void buildRoleFromDescriptors( new FieldPermissionsDefinition(privilege.getGrantedFields(), privilege.getDeniedFields()) ), privilege.getQuery() == null ? null : newHashSet(privilege.getQuery()), - IndexPrivilege.getSplitBySelectorAccess(newHashSet(Objects.requireNonNull(privilege.getPrivileges()))), + IndexPrivilege.splitBySelectorAccess(newHashSet(Objects.requireNonNull(privilege.getPrivileges()))), privilege.allowRestrictedIndices(), newHashSet(Objects.requireNonNull(privilege.getIndices())).toArray(new String[0]) ) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java index 6bc230e7d4ac0..a63a414294917 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java @@ -1616,7 +1616,7 @@ public void testGetRoleDescriptorsIntersectionForRemoteClusterHasDeterministicOr final int numGroups = randomIntBetween(2, 5); int extraGroups = 0; for (int i = 0; i < numGroups; i++) { - Set splitBySelector = IndexPrivilege.getSplitBySelectorAccess( + Set splitBySelector = IndexPrivilege.splitBySelectorAccess( Set.copyOf(randomSubsetOf(randomIntBetween(1, 4), IndexPrivilege.names())) ); // If we end up with failure and data access, we will split and end up with extra groups. Need to account for this for the diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java index 6c9793f1715b9..80b1f6e042343 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java @@ -1703,6 +1703,53 @@ public void testBuildRoleWithFailureStorePrivilegeCollatesToRemoveDlsFlsFromAnot ); } + public void testBuildRoleWithFailureStorePrivilegeCollatesToKeepDlsFlsFromAnotherGroup() { + assumeTrue("requires failure store feature", DataStream.isFailureStoreFeatureFlagEnabled()); + String indexPattern = randomAlphanumericOfLength(10); + boolean allowRestrictedIndices = randomBoolean(); + final Role role = buildRole( + roleDescriptorWithIndicesPrivileges( + "r1", + new IndicesPrivileges[] { + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read_failure_store") + .query("{\"match\":{\"field\":\"a\"}}") + .grantedFields("field") + .allowRestrictedIndices(allowRestrictedIndices) + .build(), + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read", "view_index_metadata") + .query("{\"match\":{\"field\":\"a\"}}") + .grantedFields("field") + .allowRestrictedIndices(allowRestrictedIndices) + .build() } + ) + ); + assertHasIndexGroups( + role.indices(), + indexGroup( + IndexPrivilege.getWithSingleSelectorAccess(Set.of("read_failure_store")), + allowRestrictedIndices, + "{\"match\":{\"field\":\"a\"}}", + new FieldPermissionsDefinition( + Set.of(new FieldPermissionsDefinition.FieldGrantExcludeGroup(new String[] { "field" }, null)) + ), + indexPattern + ), + indexGroup( + IndexPrivilege.getWithSingleSelectorAccess(Set.of("read", "view_index_metadata")), + allowRestrictedIndices, + "{\"match\":{\"field\":\"a\"}}", + new FieldPermissionsDefinition( + Set.of(new FieldPermissionsDefinition.FieldGrantExcludeGroup(new String[] { "field" }, null)) + ), + indexPattern + ) + ); + } + public void testBuildRoleNeverSplitsWithoutFailureStoreRelatedPrivileges() { String indexPattern = randomAlphanumericOfLength(10); List nonFailurePrivileges = IndexPrivilege.names() From 879b2b6840b57008f553170b991632d671b06153 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 19 Feb 2025 15:01:54 +0100 Subject: [PATCH 040/131] Manage roles --- .../authz/privilege/ConfigurableClusterPrivileges.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java index acf4e3ee33a0a..9cc8d109c9a01 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java @@ -558,6 +558,15 @@ public static ManageRolesPrivilege parse(XContentParser parser) throws IOExcepti if (indexPrivilege.privileges == null || indexPrivilege.privileges.length == 0) { throw new IllegalArgumentException("Indices privileges must define at least one privilege"); } + for (String privilege : indexPrivilege.privileges) { + IndexPrivilege namedPrivilege = IndexPrivilege.getNamedOrNull(privilege); + if (namedPrivilege != null && namedPrivilege.getSelectorPredicate() == IndexComponentSelectorPredicate.FAILURES) { + throw new IllegalArgumentException( + "Failure store related privileges and not supported as targets of manage roles but found [" + privilege + "]" + ); + } + } + } return new ManageRolesPrivilege(indexPrivileges); } From f6b6cfb6ab32367654cc68bcfcaeb68624d7764c Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 19 Feb 2025 20:01:56 +0100 Subject: [PATCH 041/131] More --- .../RemoteClusterSecurityFcActionAuthorizationIT.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityFcActionAuthorizationIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityFcActionAuthorizationIT.java index 9a619c8b78ea4..793313e238651 100644 --- a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityFcActionAuthorizationIT.java +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityFcActionAuthorizationIT.java @@ -435,7 +435,7 @@ public void testUpdateCrossClusterApiKey() throws Exception { + "for user [foo] with assigned roles [role] authenticated by API key id [" + apiKeyId + "] of user [test_user] on indices [index], this action is granted by the index privileges " - + "[read,view_index_metadata,manage,all]" + + "[view_index_metadata,manage,read,all]" ) ); @@ -483,7 +483,7 @@ public void testUpdateCrossClusterApiKey() throws Exception { + "for user [foo] with assigned roles [role] authenticated by API key id [" + apiKeyId + "] of user [test_user] on indices [index], this action is granted by the index privileges " - + "[read,view_index_metadata,manage,all]" + + "[view_index_metadata,manage,read,all]" ) ); } From c2baa5b092496391d8c7261f254ad7fb1413f80d Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 19 Feb 2025 21:10:37 +0100 Subject: [PATCH 042/131] Fix test --- .../privilege/ConfigurableClusterPrivileges.java | 2 +- .../security/authz/RoleDescriptorTestHelper.java | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java index 9cc8d109c9a01..8c66e5004f329 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java @@ -562,7 +562,7 @@ public static ManageRolesPrivilege parse(XContentParser parser) throws IOExcepti IndexPrivilege namedPrivilege = IndexPrivilege.getNamedOrNull(privilege); if (namedPrivilege != null && namedPrivilege.getSelectorPredicate() == IndexComponentSelectorPredicate.FAILURES) { throw new IllegalArgumentException( - "Failure store related privileges and not supported as targets of manage roles but found [" + privilege + "]" + "Failure store related privileges are not supported as targets of manage roles but found [" + privilege + "]" ); } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptorTestHelper.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptorTestHelper.java index 77a37cec45b25..82f71675b1fbe 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptorTestHelper.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptorTestHelper.java @@ -14,6 +14,7 @@ import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilegeResolver; import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivileges; +import org.elasticsearch.xpack.core.security.authz.privilege.IndexComponentSelectorPredicate; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.support.MetadataUtils; @@ -23,6 +24,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import static org.elasticsearch.test.ESTestCase.generateRandomStringArray; import static org.elasticsearch.test.ESTestCase.randomAlphaOfLengthBetween; @@ -128,10 +130,14 @@ public static ConfigurableClusterPrivilege[] randomManageRolesPrivileges() { () -> { String[] indexPatterns = randomArray(1, 5, String[]::new, () -> randomAlphaOfLengthBetween(5, 100)); - int startIndex = randomIntBetween(0, IndexPrivilege.names().size() - 2); - int endIndex = randomIntBetween(startIndex + 1, IndexPrivilege.names().size()); + Set validNames = IndexPrivilege.names().stream().filter(p -> { + IndexPrivilege named = IndexPrivilege.getNamedOrNull(p); + return named != null && named.getSelectorPredicate() != IndexComponentSelectorPredicate.FAILURES; + }).collect(Collectors.toSet()); + int startIndex = randomIntBetween(0, validNames.size() - 2); + int endIndex = randomIntBetween(startIndex + 1, validNames.size()); - String[] indexPrivileges = IndexPrivilege.names().stream().toList().subList(startIndex, endIndex).toArray(String[]::new); + String[] indexPrivileges = validNames.stream().toList().subList(startIndex, endIndex).toArray(String[]::new); return new ConfigurableClusterPrivileges.ManageRolesPrivilege.ManageRolesIndexPermissionGroup( indexPatterns, indexPrivileges From baf63ed786d00d6812c03e4028e5a861e6cbe3be Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 19 Feb 2025 23:28:35 +0100 Subject: [PATCH 043/131] Fix --- .../privilege/FailureStoreSecurityRestIT.java | 101 ++++++++++++++++++ .../DeprecationRoleDescriptorConsumer.java | 37 ++++--- 2 files changed, 123 insertions(+), 15 deletions(-) create mode 100644 x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/privilege/FailureStoreSecurityRestIT.java diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/privilege/FailureStoreSecurityRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/privilege/FailureStoreSecurityRestIT.java new file mode 100644 index 0000000000000..be3251683148b --- /dev/null +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/privilege/FailureStoreSecurityRestIT.java @@ -0,0 +1,101 @@ +/* + * 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.privilege; + +import org.elasticsearch.client.Request; +import org.elasticsearch.client.Response; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.test.cluster.ElasticsearchCluster; +import org.elasticsearch.test.cluster.FeatureFlag; +import org.elasticsearch.test.rest.ESRestTestCase; +import org.elasticsearch.xcontent.json.JsonXContent; +import org.elasticsearch.xpack.security.SecurityOnTrialLicenseRestTestCase; +import org.junit.ClassRule; + +import java.io.IOException; +import java.util.Map; + +import static org.hamcrest.Matchers.equalTo; + +public class FailureStoreSecurityRestIT extends ESRestTestCase { + + @ClassRule + public static ElasticsearchCluster cluster = ElasticsearchCluster.local() + .apply(SecurityOnTrialLicenseRestTestCase.commonTrialSecurityClusterConfig) + .feature(FeatureFlag.FAILURE_STORE_ENABLED) + .build(); + + @Override + protected String getTestRestCluster() { + return cluster.getHttpAddresses(); + } + + @Override + protected Settings restAdminSettings() { + String token = basicAuthHeaderValue("admin_user", new SecureString("admin-password".toCharArray())); + return Settings.builder().put(ThreadContext.PREFIX + ".Authorization", token).build(); + } + + public void testGetUserPrivileges() throws IOException { + Request roleRequest = new Request("PUT", "/_security/role/role"); + roleRequest.setJsonEntity(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["*"], + "privileges": ["read", "read_failure_store"] + } + ] + } + """); + assertOK(adminClient().performRequest(roleRequest)); + + Request userRequest = new Request("PUT", "/_security/user/user"); + userRequest.setJsonEntity(""" + { + "password": "x-pack-test-password", + "roles": ["role"] + } + """); + assertOK(adminClient().performRequest(userRequest)); + + Request request = new Request("GET", "/_security/user/_privileges"); + request.setOptions( + request.getOptions() + .toBuilder() + .addHeader("Authorization", basicAuthHeaderValue("user", new SecureString("x-pack-test-password".toCharArray()))) + ); + Response response = client().performRequest(request); + assertOK(response); + assertThat(responseAsMap(response), equalTo(mapFromJson(""" + { + "cluster": ["all"], + "global": [], + "indices": [{ + "names": ["*"], + "privileges": ["read"], + "allow_restricted_indices": false + }, + { + "names": ["*"], + "privileges": ["read_failure_store"], + "allow_restricted_indices": false + }], + "applications": [], + "run_as": [] + }"""))); + } + + private static Map mapFromJson(String json) { + return XContentHelper.convertToMap(JsonXContent.jsonXContent, json, false); + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/DeprecationRoleDescriptorConsumer.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/DeprecationRoleDescriptorConsumer.java index 3315b82e500e3..88e0afe042b44 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/DeprecationRoleDescriptorConsumer.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/DeprecationRoleDescriptorConsumer.java @@ -179,27 +179,34 @@ private void logDeprecatedPermission(RoleDescriptor roleDescriptor) { } } // compute privileges Automaton for each alias and for each of the indices it points to - final Map indexAutomatonMap = new HashMap<>(); + final Map> indexPrivilegeMap = new HashMap<>(); for (Map.Entry> privilegesByAlias : privilegesByAliasMap.entrySet()) { final String aliasName = privilegesByAlias.getKey(); final Set aliasPrivilegeNames = privilegesByAlias.getValue(); - final Automaton aliasPrivilegeAutomaton = IndexPrivilege.getWithSingleSelectorAccess(aliasPrivilegeNames).getAutomaton(); + final Set aliasPrivileges = IndexPrivilege.splitBySelectorAccess(aliasPrivilegeNames); final SortedSet inferiorIndexNames = new TreeSet<>(); - // check if the alias grants superiors privileges than the indices it points to - for (Index index : aliasOrIndexMap.get(aliasName).getIndices()) { - final Set indexPrivileges = privilegesByIndexMap.get(index.getName()); - // null iff the index does not have *any* privilege - if (indexPrivileges != null) { - // compute automaton once per index no matter how many times it is pointed to - final Automaton indexPrivilegeAutomaton = indexAutomatonMap.computeIfAbsent( - index.getName(), - i -> IndexPrivilege.getWithSingleSelectorAccess(indexPrivileges).getAutomaton() - ); - if (false == Automatons.subsetOf(indexPrivilegeAutomaton, aliasPrivilegeAutomaton)) { + for (var aliasPrivilege : aliasPrivileges) { + final Automaton aliasPrivilegeAutomaton = aliasPrivilege.getAutomaton(); + // check if the alias grants superiors privileges than the indices it points to + for (Index index : aliasOrIndexMap.get(aliasName).getIndices()) { + final Set indexPrivileges = privilegesByIndexMap.get(index.getName()); + // null iff the index does not have *any* privilege + if (indexPrivileges != null) { + // compute automaton once per index no matter how many times it is pointed to + final Set indexPrivilegeSet = indexPrivilegeMap.computeIfAbsent( + index.getName(), + i -> IndexPrivilege.splitBySelectorAccess(indexPrivileges) + ); + for (var indexPrivilege : indexPrivilegeSet) { + // TODO still not quite right + if (indexPrivilege.getSelectorPredicate() == aliasPrivilege.getSelectorPredicate() + && false == Automatons.subsetOf(indexPrivilege.getAutomaton(), aliasPrivilegeAutomaton)) { + inferiorIndexNames.add(index.getName()); + } + } + } else { inferiorIndexNames.add(index.getName()); } - } else { - inferiorIndexNames.add(index.getName()); } } // log inferior indices for this role, for this alias From 22b861f792e899eba024c2aac75f11ff7f508fd9 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Thu, 20 Feb 2025 08:41:17 +0100 Subject: [PATCH 044/131] Fix and more tests --- .../FailureStoreSecurityRestIT.java | 105 ++++++++++++++---- .../DeprecationRoleDescriptorConsumer.java | 15 ++- 2 files changed, 92 insertions(+), 28 deletions(-) rename x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/{privilege => failurestore}/FailureStoreSecurityRestIT.java (66%) diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/privilege/FailureStoreSecurityRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java similarity index 66% rename from x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/privilege/FailureStoreSecurityRestIT.java rename to x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java index be3251683148b..1ce2acac908ee 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/privilege/FailureStoreSecurityRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java @@ -5,7 +5,7 @@ * 2.0. */ -package org.elasticsearch.xpack.security.privilege; +package org.elasticsearch.xpack.security.failurestore; import org.elasticsearch.client.Request; import org.elasticsearch.client.Response; @@ -45,8 +45,16 @@ protected Settings restAdminSettings() { } public void testGetUserPrivileges() throws IOException { - Request roleRequest = new Request("PUT", "/_security/role/role"); - roleRequest.setJsonEntity(""" + Request userRequest = new Request("PUT", "/_security/user/user"); + userRequest.setJsonEntity(""" + { + "password": "x-pack-test-password", + "roles": ["role"] + } + """); + assertOK(adminClient().performRequest(userRequest)); + + putRole(""" { "cluster": ["all"], "indices": [ @@ -57,26 +65,7 @@ public void testGetUserPrivileges() throws IOException { ] } """); - assertOK(adminClient().performRequest(roleRequest)); - - Request userRequest = new Request("PUT", "/_security/user/user"); - userRequest.setJsonEntity(""" - { - "password": "x-pack-test-password", - "roles": ["role"] - } - """); - assertOK(adminClient().performRequest(userRequest)); - - Request request = new Request("GET", "/_security/user/_privileges"); - request.setOptions( - request.getOptions() - .toBuilder() - .addHeader("Authorization", basicAuthHeaderValue("user", new SecureString("x-pack-test-password".toCharArray()))) - ); - Response response = client().performRequest(request); - assertOK(response); - assertThat(responseAsMap(response), equalTo(mapFromJson(""" + expectUserPrivilegesResponse(""" { "cluster": ["all"], "global": [], @@ -92,7 +81,75 @@ public void testGetUserPrivileges() throws IOException { }], "applications": [], "run_as": [] - }"""))); + }"""); + + putRole(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["*"], + "privileges": ["read_failure_store"] + } + ] + } + """); + expectUserPrivilegesResponse(""" + { + "cluster": ["all"], + "global": [], + "indices": [ + { + "names": ["*"], + "privileges": ["read_failure_store"], + "allow_restricted_indices": false + }], + "applications": [], + "run_as": [] + }"""); + + putRole(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["*"], + "privileges": ["all", "read_failure_store"] + } + ] + } + """); + expectUserPrivilegesResponse(""" + { + "cluster": ["all"], + "global": [], + "indices": [ + { + "names": ["*"], + "privileges": ["all", "read_failure_store"], + "allow_restricted_indices": false + }], + "applications": [], + "run_as": [] + }"""); + } + + private static void expectUserPrivilegesResponse(String userPrivilegesResponse) throws IOException { + Request request = new Request("GET", "/_security/user/_privileges"); + request.setOptions( + request.getOptions() + .toBuilder() + .addHeader("Authorization", basicAuthHeaderValue("user", new SecureString("x-pack-test-password".toCharArray()))) + ); + Response response = client().performRequest(request); + assertOK(response); + assertThat(responseAsMap(response), equalTo(mapFromJson(userPrivilegesResponse))); + } + + private static void putRole(String rolePayload) throws IOException { + Request roleRequest = new Request("PUT", "/_security/role/role"); + roleRequest.setJsonEntity(rolePayload); + assertOK(adminClient().performRequest(roleRequest)); } private static Map mapFromJson(String json) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/DeprecationRoleDescriptorConsumer.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/DeprecationRoleDescriptorConsumer.java index 88e0afe042b44..279758a138339 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/DeprecationRoleDescriptorConsumer.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/DeprecationRoleDescriptorConsumer.java @@ -19,6 +19,7 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.IndicesPrivileges; +import org.elasticsearch.xpack.core.security.authz.privilege.IndexComponentSelectorPredicate; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.support.Automatons; import org.elasticsearch.xpack.core.security.support.StringMatcher; @@ -186,21 +187,27 @@ private void logDeprecatedPermission(RoleDescriptor roleDescriptor) { final Set aliasPrivileges = IndexPrivilege.splitBySelectorAccess(aliasPrivilegeNames); final SortedSet inferiorIndexNames = new TreeSet<>(); for (var aliasPrivilege : aliasPrivileges) { + // TODO implement failures handling in a follow-up + if (aliasPrivilege.getSelectorPredicate() == IndexComponentSelectorPredicate.FAILURES) { + continue; + } final Automaton aliasPrivilegeAutomaton = aliasPrivilege.getAutomaton(); // check if the alias grants superiors privileges than the indices it points to for (Index index : aliasOrIndexMap.get(aliasName).getIndices()) { final Set indexPrivileges = privilegesByIndexMap.get(index.getName()); // null iff the index does not have *any* privilege if (indexPrivileges != null) { - // compute automaton once per index no matter how many times it is pointed to + // compute privilege set once per index no matter how many times it is pointed to final Set indexPrivilegeSet = indexPrivilegeMap.computeIfAbsent( index.getName(), i -> IndexPrivilege.splitBySelectorAccess(indexPrivileges) ); for (var indexPrivilege : indexPrivilegeSet) { - // TODO still not quite right - if (indexPrivilege.getSelectorPredicate() == aliasPrivilege.getSelectorPredicate() - && false == Automatons.subsetOf(indexPrivilege.getAutomaton(), aliasPrivilegeAutomaton)) { + // TODO implement failures handling in a follow-up + if (indexPrivilege.getSelectorPredicate() == IndexComponentSelectorPredicate.FAILURES) { + continue; + } + if (false == Automatons.subsetOf(indexPrivilege.getAutomaton(), aliasPrivilegeAutomaton)) { inferiorIndexNames.add(index.getName()); } } From 18258c187305389c83f8ad7984cdab42179c54e7 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Thu, 20 Feb 2025 09:03:38 +0100 Subject: [PATCH 045/131] Beef up coverage --- .../authz/store/CompositeRolesStoreTests.java | 350 +++++++++++++----- 1 file changed, 253 insertions(+), 97 deletions(-) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java index 80b1f6e042343..b224785123bdf 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java @@ -1554,23 +1554,48 @@ public void testBuildRoleWithReadFailureStorePrivilegeDuplicatesMerged() { assumeTrue("requires failure store feature", DataStream.isFailureStoreFeatureFlagEnabled()); String indexPattern = randomAlphanumericOfLength(10); boolean allowRestrictedIndices = randomBoolean(); - final Role role = buildRole( - roleDescriptorWithIndicesPrivileges( - "r1", - new IndicesPrivileges[] { - IndicesPrivileges.builder() - .indices(indexPattern) - .privileges("read_failure_store") - .allowRestrictedIndices(allowRestrictedIndices) - .build(), - IndicesPrivileges.builder() - .indices(indexPattern) - .privileges("read_failure_store") - .allowRestrictedIndices(allowRestrictedIndices) - .build() } + final List roles = List.of( + buildRole( + roleDescriptorWithIndicesPrivileges( + "r1", + new IndicesPrivileges[] { + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read_failure_store") + .allowRestrictedIndices(allowRestrictedIndices) + .build(), + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read_failure_store") + .allowRestrictedIndices(allowRestrictedIndices) + .build() } + ) + ), + buildRole( + roleDescriptorWithIndicesPrivileges( + "r1", + new IndicesPrivileges[] { + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read_failure_store") + .allowRestrictedIndices(allowRestrictedIndices) + .build() } + ), + roleDescriptorWithIndicesPrivileges( + "r2", + new IndicesPrivileges[] { + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read_failure_store") + .allowRestrictedIndices(allowRestrictedIndices) + .build() } + ) ) ); - assertHasIndexGroups(role.indices(), indexGroup(IndexPrivilege.READ_FAILURE_STORE, allowRestrictedIndices, indexPattern)); + // the roles are different "format" but the same so should produce the same index groups + for (var role : roles) { + assertHasIndexGroups(role.indices(), indexGroup(IndexPrivilege.READ_FAILURE_STORE, allowRestrictedIndices, indexPattern)); + } } public void testBuildRoleWithReadFailureStoreAndReadPrivilegeSplit() { @@ -1595,112 +1620,243 @@ public void testBuildRoleWithReadFailureStoreAndReadPrivilegeSplit() { ); } - public void testBuildRoleWithMultipleReadFailureStoreAndReadPrivilegeSplit() { + public void testBuildRoleWithReadFailureStoreAndReadPrivilegeAndMultipleIndexPatternsSplit() { assumeTrue("requires failure store feature", DataStream.isFailureStoreFeatureFlagEnabled()); String indexPattern = randomAlphanumericOfLength(10); + String otherIndexPattern = randomValueOtherThan(indexPattern, () -> randomAlphanumericOfLength(10)); boolean allowRestrictedIndices = randomBoolean(); - final Role role = buildRole( - roleDescriptorWithIndicesPrivileges( - "r1", - new IndicesPrivileges[] { - IndicesPrivileges.builder() - .indices(indexPattern) - .privileges("read") - .allowRestrictedIndices(allowRestrictedIndices) - .build(), - IndicesPrivileges.builder() - .indices(indexPattern) - .privileges("read_failure_store") - .allowRestrictedIndices(allowRestrictedIndices) - .build() } + List roles = List.of( + buildRole( + roleDescriptorWithIndicesPrivileges( + "r1", + new IndicesPrivileges[] { + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read", "write") + .allowRestrictedIndices(allowRestrictedIndices) + .build(), + IndicesPrivileges.builder() + .indices(otherIndexPattern) + .privileges("read", "read_failure_store") + .allowRestrictedIndices(allowRestrictedIndices) + .build() } + ) + ), + buildRole( + roleDescriptorWithIndicesPrivileges( + "r1", + new IndicesPrivileges[] { + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read", "write") + .allowRestrictedIndices(allowRestrictedIndices) + .build() } + ), + roleDescriptorWithIndicesPrivileges( + "r2", + new IndicesPrivileges[] { + IndicesPrivileges.builder() + .indices(otherIndexPattern) + .privileges("read", "read_failure_store") + .allowRestrictedIndices(allowRestrictedIndices) + .build() } + ) ) ); - assertHasIndexGroups( - role.indices(), - indexGroup(IndexPrivilege.READ_FAILURE_STORE, allowRestrictedIndices, indexPattern), - indexGroup(IndexPrivilege.READ, allowRestrictedIndices, indexPattern) - ); + // the roles are different "format" but the same so should produce the same index groups + for (var role : roles) { + assertHasIndexGroups( + role.indices(), + indexGroup(IndexPrivilege.getWithSingleSelectorAccess(Set.of("read", "write")), allowRestrictedIndices, indexPattern), + indexGroup(IndexPrivilege.READ, allowRestrictedIndices, otherIndexPattern), + indexGroup(IndexPrivilege.READ_FAILURE_STORE, allowRestrictedIndices, otherIndexPattern) + ); + } } - public void testBuildRoleWithAllPrivilegeIsNeverSplit() { + public void testBuildRoleWithMultipleReadFailureStoreAndReadPrivilegeSplit() { assumeTrue("requires failure store feature", DataStream.isFailureStoreFeatureFlagEnabled()); String indexPattern = randomAlphanumericOfLength(10); boolean allowRestrictedIndices = randomBoolean(); - final Role role = buildRole( - roleDescriptorWithIndicesPrivileges( - "r1", - new IndicesPrivileges[] { - IndicesPrivileges.builder() - .indices(indexPattern) - .privileges("read", "read_failure_store", "all") - .allowRestrictedIndices(allowRestrictedIndices) - .build(), - IndicesPrivileges.builder() - .indices(indexPattern) - .privileges("read_failure_store") - .allowRestrictedIndices(allowRestrictedIndices) - .build() } + final List roles = List.of( + buildRole( + roleDescriptorWithIndicesPrivileges( + "r1", + new IndicesPrivileges[] { + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read") + .allowRestrictedIndices(allowRestrictedIndices) + .build(), + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read_failure_store") + .allowRestrictedIndices(allowRestrictedIndices) + .build() } + ) + ), + buildRole( + roleDescriptorWithIndicesPrivileges( + "r1", + new IndicesPrivileges[] { + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read") + .allowRestrictedIndices(allowRestrictedIndices) + .build() } + ), + roleDescriptorWithIndicesPrivileges( + "r2", + new IndicesPrivileges[] { + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read_failure_store") + .allowRestrictedIndices(allowRestrictedIndices) + .build() } + ) ) ); - assertHasIndexGroups( - role.indices(), - indexGroup( - IndexPrivilege.getWithSingleSelectorAccess(Set.of("read", "read_failure_store", "all")), - allowRestrictedIndices, - indexPattern + // the roles are different "format" but the same so should produce the same index groups + for (var role : roles) { + assertHasIndexGroups( + role.indices(), + indexGroup(IndexPrivilege.READ_FAILURE_STORE, allowRestrictedIndices, indexPattern), + indexGroup(IndexPrivilege.READ, allowRestrictedIndices, indexPattern) + ); + } + } + + public void testBuildRoleWithAllPrivilegeIsNeverSplit() { + assumeTrue("requires failure store feature", DataStream.isFailureStoreFeatureFlagEnabled()); + String indexPattern = randomAlphanumericOfLength(10); + boolean allowRestrictedIndices = randomBoolean(); + final List roles = List.of( + buildRole( + roleDescriptorWithIndicesPrivileges( + "r1", + new IndicesPrivileges[] { + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read", "read_failure_store", "all") + .allowRestrictedIndices(allowRestrictedIndices) + .build(), + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read_failure_store") + .allowRestrictedIndices(allowRestrictedIndices) + .build() } + ) + ), + buildRole( + roleDescriptorWithIndicesPrivileges( + "r1", + new IndicesPrivileges[] { + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read", "read_failure_store", "all") + .allowRestrictedIndices(allowRestrictedIndices) + .build() } + ), + roleDescriptorWithIndicesPrivileges( + "r2", + new IndicesPrivileges[] { + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read_failure_store") + .allowRestrictedIndices(allowRestrictedIndices) + .build() } + ) ) ); + // the roles are different "format" but the same so should produce the same index groups + for (var role : roles) { + assertHasIndexGroups( + role.indices(), + indexGroup( + IndexPrivilege.getWithSingleSelectorAccess(Set.of("read", "read_failure_store", "all")), + allowRestrictedIndices, + indexPattern + ) + ); + } } public void testBuildRoleWithFailureStorePrivilegeCollatesToRemoveDlsFlsFromAnotherGroup() { assumeTrue("requires failure store feature", DataStream.isFailureStoreFeatureFlagEnabled()); String indexPattern = randomAlphanumericOfLength(10); boolean allowRestrictedIndices = randomBoolean(); - final Role role = buildRole( - roleDescriptorWithIndicesPrivileges( - "r1", - new IndicesPrivileges[] { - IndicesPrivileges.builder() - .indices(indexPattern) - .privileges("read_failure_store") - .allowRestrictedIndices(allowRestrictedIndices) - .build(), - IndicesPrivileges.builder() - .indices(indexPattern) - .privileges("read", "view_index_metadata") - .query("{\"match\":{\"field\":\"a\"}}") - .grantedFields("field") - .allowRestrictedIndices(allowRestrictedIndices) - .build() } - ) - ); - assertHasIndexGroups( - role.indices(), - indexGroup( - IndexPrivilege.getWithSingleSelectorAccess(Set.of("read_failure_store")), - allowRestrictedIndices, - null, - new FieldPermissionsDefinition( - Set.of( - new FieldPermissionsDefinition.FieldGrantExcludeGroup(null, null), - new FieldPermissionsDefinition.FieldGrantExcludeGroup(new String[] { "field" }, null) - ) - ), - indexPattern + final List roles = List.of( + buildRole( + roleDescriptorWithIndicesPrivileges( + "r1", + new IndicesPrivileges[] { + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read_failure_store") + .allowRestrictedIndices(allowRestrictedIndices) + .build(), + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read", "view_index_metadata") + .query("{\"match\":{\"field\":\"a\"}}") + .grantedFields("field") + .allowRestrictedIndices(allowRestrictedIndices) + .build() } + ) ), - indexGroup( - IndexPrivilege.getWithSingleSelectorAccess(Set.of("read", "view_index_metadata")), - allowRestrictedIndices, - null, - new FieldPermissionsDefinition( - Set.of( - new FieldPermissionsDefinition.FieldGrantExcludeGroup(null, null), - new FieldPermissionsDefinition.FieldGrantExcludeGroup(new String[] { "field" }, null) - ) + buildRole( + roleDescriptorWithIndicesPrivileges( + "r1", + new IndicesPrivileges[] { + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read_failure_store") + .allowRestrictedIndices(allowRestrictedIndices) + .build() } ), - indexPattern + roleDescriptorWithIndicesPrivileges( + "r2", + new IndicesPrivileges[] { + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read", "view_index_metadata") + .query("{\"match\":{\"field\":\"a\"}}") + .grantedFields("field") + .allowRestrictedIndices(allowRestrictedIndices) + .build() } + ) ) ); + for (var role : roles) { + assertHasIndexGroups( + role.indices(), + indexGroup( + IndexPrivilege.getWithSingleSelectorAccess(Set.of("read_failure_store")), + allowRestrictedIndices, + null, + new FieldPermissionsDefinition( + Set.of( + new FieldPermissionsDefinition.FieldGrantExcludeGroup(null, null), + new FieldPermissionsDefinition.FieldGrantExcludeGroup(new String[] { "field" }, null) + ) + ), + indexPattern + ), + indexGroup( + IndexPrivilege.getWithSingleSelectorAccess(Set.of("read", "view_index_metadata")), + allowRestrictedIndices, + null, + new FieldPermissionsDefinition( + Set.of( + new FieldPermissionsDefinition.FieldGrantExcludeGroup(null, null), + new FieldPermissionsDefinition.FieldGrantExcludeGroup(new String[] { "field" }, null) + ) + ), + indexPattern + ) + ); + + } } public void testBuildRoleWithFailureStorePrivilegeCollatesToKeepDlsFlsFromAnotherGroup() { From 6422450deac1b84d6a4f14979716dc2242782b7f Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 3 Mar 2025 12:43:31 +0100 Subject: [PATCH 046/131] WIP review feedback --- .../role/RoleDescriptorRequestValidator.java | 4 +- .../authz/permission/IndicesPermission.java | 3 +- .../core/security/authz/permission/Role.java | 4 +- .../ConfigurableClusterPrivileges.java | 4 +- .../IndexComponentSelectorPredicate.java | 2 +- .../authz/privilege/IndexPrivilege.java | 36 ++++--- .../authz/permission/LimitedRoleTests.java | 2 +- .../authz/permission/SimpleRoleTests.java | 2 +- .../authz/privilege/IndexPrivilegeTests.java | 100 ++++++++---------- .../authz/privilege/PrivilegeTests.java | 2 +- .../authz/store/CompositeRolesStore.java | 6 +- .../DeprecationRoleDescriptorConsumer.java | 4 +- .../RestGetBuiltinPrivilegesAction.java | 3 +- .../xpack/security/authz/RBACEngineTests.java | 2 +- 14 files changed, 85 insertions(+), 89 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/RoleDescriptorRequestValidator.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/RoleDescriptorRequestValidator.java index c295cbd58ce35..761c2258d3d3f 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/RoleDescriptorRequestValidator.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/RoleDescriptorRequestValidator.java @@ -49,7 +49,7 @@ public static ActionRequestValidationException validate( if (roleDescriptor.getIndicesPrivileges() != null) { for (RoleDescriptor.IndicesPrivileges idp : roleDescriptor.getIndicesPrivileges()) { try { - IndexPrivilege.splitBySelectorAccess(Set.of(idp.getPrivileges())); + IndexPrivilege.resolveBySelectorAccess(Set.of(idp.getPrivileges())); } catch (IllegalArgumentException ile) { validationException = addValidationError(ile.getMessage(), validationException); } @@ -61,7 +61,7 @@ public static ActionRequestValidationException validate( validationException = addValidationError("remote index cluster alias cannot be an empty string", validationException); } try { - var privileges = IndexPrivilege.splitBySelectorAccess(Set.of(ridp.indicesPrivileges().getPrivileges())); + Set privileges = IndexPrivilege.resolveBySelectorAccess(Set.of(ridp.indicesPrivileges().getPrivileges())); if (privileges.stream().anyMatch(p -> p.getSelectorPredicate() == IndexComponentSelectorPredicate.FAILURES)) { validationException = addValidationError( "remote index privileges cannot contain privileges that grant access to the failure store", diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 213c5c4467f21..7d159a8f0b15c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -35,7 +35,6 @@ 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; @@ -302,7 +301,7 @@ public boolean checkResourcePrivileges( } } for (String privilege : checkForPrivileges) { - IndexPrivilege indexPrivilege = IndexPrivilege.getWithSingleSelectorAccess(Collections.singleton(privilege)); + IndexPrivilege indexPrivilege = IndexPrivilege.get(privilege); if (allowedIndexPrivilegesAutomaton != null && Automatons.subsetOf(indexPrivilege.getAutomaton(), allowedIndexPrivilegesAutomaton)) { if (resourcePrivilegesMapBuilder != null) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java index d5f98b076bcf5..14a121d1c091b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java @@ -437,7 +437,7 @@ static SimpleRole buildFromRoleDescriptor( new FieldPermissionsDefinition(indexPrivilege.getGrantedFields(), indexPrivilege.getDeniedFields()) ), indexPrivilege.getQuery() == null ? null : Collections.singleton(indexPrivilege.getQuery()), - IndexPrivilege.splitBySelectorAccess(Set.of(indexPrivilege.getPrivileges())), + IndexPrivilege.resolveBySelectorAccess(Set.of(indexPrivilege.getPrivileges())), indexPrivilege.allowRestrictedIndices(), indexPrivilege.getIndices() ); @@ -454,7 +454,7 @@ static SimpleRole buildFromRoleDescriptor( new FieldPermissionsDefinition(indicesPrivileges.getGrantedFields(), indicesPrivileges.getDeniedFields()) ), indicesPrivileges.getQuery() == null ? null : Collections.singleton(indicesPrivileges.getQuery()), - IndexPrivilege.splitBySelectorAccess(Set.of(indicesPrivileges.getPrivileges())), + IndexPrivilege.resolveBySelectorAccess(Set.of(indicesPrivileges.getPrivileges())), indicesPrivileges.allowRestrictedIndices(), indicesPrivileges.getIndices() ); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java index 8c66e5004f329..c9c6d1fa96a2a 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java @@ -414,7 +414,9 @@ public ManageRolesPrivilege(List manageRolesInd this.requestPredicateSupplier = (restrictedIndices) -> { IndicesPermission.Builder indicesPermissionBuilder = new IndicesPermission.Builder(restrictedIndices); for (ManageRolesIndexPermissionGroup indexPatternPrivilege : manageRolesIndexPermissionGroups) { - Set splitBySelector = IndexPrivilege.splitBySelectorAccess(Set.of(indexPatternPrivilege.privileges())); + Set splitBySelector = IndexPrivilege.resolveBySelectorAccess( + Set.of(indexPatternPrivilege.privileges()) + ); for (IndexPrivilege indexPrivilege : splitBySelector) { indicesPermissionBuilder.addGroup( indexPrivilege, diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPredicate.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPredicate.java index 33d639d144ab7..0083de155032b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPredicate.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexComponentSelectorPredicate.java @@ -16,7 +16,7 @@ /** * A predicate to capture role access by {@link IndexComponentSelector}. * This is assigned to each {@link org.elasticsearch.xpack.core.security.authz.permission.IndicesPermission.Group} during role building. - * See also {@link org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege#splitBySelectorAccess(Set)}. + * See also {@link org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege#resolveBySelectorAccess(Set)}. */ public record IndexComponentSelectorPredicate(Set names, Predicate predicate) implements diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java index 46b8c780b2b71..23c8063d6d115 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java @@ -275,13 +275,13 @@ private IndexPrivilege(Set name, Automaton automaton, IndexComponentSele } /** - * Delegates to {@link #splitBySelectorAccess(Set)} but throws if the result is not a singleton, i.e., covers more than one selector. + * Delegates to {@link #resolveBySelectorAccess(Set)} but throws if the result is not a singleton, i.e., covers more than one selector. * Use this method if you know that the input name set corresponds to privileges covering the same selector, for instance if you have a * single input name, or multiple names that all grant access to one selector e.g., {@link IndexComponentSelector#DATA}. * @throws IllegalArgumentException if privileges and actions for input names cover access to more than one selector */ public static IndexPrivilege getWithSingleSelectorAccess(Set names) { - final Set splitBySelector = splitBySelectorAccess(names); + final Set splitBySelector = resolveBySelectorAccess(names); if (splitBySelector.size() != 1) { throw new IllegalArgumentException( "index privilege patterns " + names + " did not map to a single selector " + splitBySelector @@ -290,6 +290,22 @@ public static IndexPrivilege getWithSingleSelectorAccess(Set names) { return splitBySelector.iterator().next(); } + /** + * Returns a {@link IndexPrivilege} that corresponds to the given raw action pattern or privilege name. + */ + public static IndexPrivilege get(String actionOrPrivilege) { + final Set privilegeSingleton = resolveBySelectorAccess(Set.of(actionOrPrivilege)); + if (privilegeSingleton.size() != 1) { + throw new IllegalArgumentException( + "index privilege name or action " + + actionOrPrivilege + + " must map to exactly one privilege but mapped to " + + privilegeSingleton + ); + } + return privilegeSingleton.iterator().next(); + } + /** * Returns a set {@link IndexPrivilege} that captures the access granted by the privileges and actions specified in the input name set. * This method returns a set of index privileges, instead of a single index privilege to capture that different index privileges grant @@ -300,14 +316,14 @@ public static IndexPrivilege getWithSingleSelectorAccess(Set names) { * selector boundaries since their underlying automata would be combined, granting more access than is valid. * This method conceptually splits the input names into ones that correspond to different selector access, and return an index privilege * for each partition. - * For instance, `splitBySelectorAccess(Set.of("view_index_metadata", "write", "read_failure_store"))` will return two index + * For instance, `resolveBySelectorAccess(Set.of("view_index_metadata", "write", "read_failure_store"))` will return two index * privileges one covering `view_index_metadata` and `write` for a {@link IndexComponentSelectorPredicate#DATA}, the other covering * `read_failure_store` for a {@link IndexComponentSelectorPredicate#FAILURES} selector. * A notable exception is the {@link IndexPrivilege#ALL} privilege. If this privilege is included in the input name set, this method * returns a single index privilege that grants access to all selectors. * All raw actions are treated as granting access to the {@link IndexComponentSelector#DATA} selector. */ - public static Set splitBySelectorAccess(Set names) { + public static Set resolveBySelectorAccess(Set names) { return CACHE.computeIfAbsent(names, (theName) -> { if (theName.isEmpty()) { return Set.of(NONE); @@ -377,7 +393,7 @@ private static Set resolve(Set name) { actions ); assertNamesMatch(name, combined); - return combined; + return Set.copyOf(combined); } private static Set combineIndexPrivileges( @@ -423,7 +439,7 @@ private static IndexPrivilege union( final Set automata = HashSet.newHashSet(privileges.size() + actions.size()); final Set names = HashSet.newHashSet(privileges.size() + actions.size()); for (IndexPrivilege privilege : privileges) { - names.add(privilege.getSingleName()); + names.addAll(privilege.name()); automata.add(privilege.automaton); } @@ -461,12 +477,4 @@ public static Collection findPrivilegesThatGrant(String action) { public IndexComponentSelectorPredicate getSelectorPredicate() { return selectorPredicate; } - - public String getSingleName() { - final Set names = name(); - if (names.size() != 1) { - throw new IllegalStateException("expected single name for privilege but got " + names); - } - return names.iterator().next(); - } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java index 257dc1edbff1e..dcc0bdec79118 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java @@ -239,7 +239,7 @@ private static FieldPermissions randomFlsPermissions(String... grantedFields) { } private static IndexPrivilege randomIndexPrivilege() { - return IndexPrivilege.getWithSingleSelectorAccess(Set.of(randomFrom(IndexPrivilege.names()))); + return IndexPrivilege.get(randomFrom(IndexPrivilege.names())); } public void testGetRoleDescriptorsIntersectionForRemoteClusterReturnsEmpty() { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/SimpleRoleTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/SimpleRoleTests.java index e42e3e4156c15..0d0c30f974565 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/SimpleRoleTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/SimpleRoleTests.java @@ -176,7 +176,7 @@ public void testGetRoleDescriptorsIntersectionForRemoteCluster() { Set.of(randomAlphaOfLength(8)), new FieldPermissions(new FieldPermissionsDefinition(new String[] { randomAlphaOfLength(5) }, null)), null, - IndexPrivilege.splitBySelectorAccess(Set.of(randomFrom(IndexPrivilege.names()))), + IndexPrivilege.resolveBySelectorAccess(Set.of(randomFrom(IndexPrivilege.names()))), randomBoolean(), randomAlphaOfLength(9) ) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java index e3bde5dbdd664..2d41a96ef0e33 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java @@ -70,96 +70,82 @@ public void testFindPrivilegesThatGrant() { assertThat(findPrivilegesThatGrant(RefreshAction.NAME), equalTo(List.of("maintenance", "manage", "all"))); } - public void testGetWithSingleSelectorAccess() { + public void testGet() { { - IndexPrivilege actual = IndexPrivilege.getWithSingleSelectorAccess(Set.of("all")); + IndexPrivilege actual = IndexPrivilege.get("all"); assertThat(actual, equalTo(IndexPrivilege.ALL)); assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.ALL)); } { - IndexPrivilege actual = IndexPrivilege.getWithSingleSelectorAccess(Set.of("read")); + IndexPrivilege actual = IndexPrivilege.get("read"); assertThat(actual, equalTo(IndexPrivilege.READ)); assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.DATA)); } { - IndexPrivilege actual = IndexPrivilege.getWithSingleSelectorAccess(Set.of("none")); + IndexPrivilege actual = IndexPrivilege.get("none"); assertThat(actual, equalTo(IndexPrivilege.NONE)); assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.DATA)); } { - IndexPrivilege actual = IndexPrivilege.getWithSingleSelectorAccess(Set.of()); + IndexPrivilege actual = resolvePrivilegeAndAssertSingleton(Set.of()); assertThat(actual, equalTo(IndexPrivilege.NONE)); assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.DATA)); } { - IndexPrivilege actual = IndexPrivilege.getWithSingleSelectorAccess(Set.of("indices:data/read/search")); - assertThat(actual.getSingleName(), equalTo("indices:data/read/search")); + IndexPrivilege actual = IndexPrivilege.get("indices:data/read/search"); + assertThat(actual.name, containsInAnyOrder("indices:data/read/search")); assertThat(actual.predicate.test("indices:data/read/search"), is(true)); assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.DATA)); } { - IndexPrivilege actual = IndexPrivilege.getWithSingleSelectorAccess(Set.of("all", "read", "indices:data/read/search")); + IndexPrivilege actual = resolvePrivilegeAndAssertSingleton(Set.of("all", "read", "indices:data/read/search")); assertThat(actual.name, equalTo(Set.of("all", "read", "indices:data/read/search"))); assertThat(Automatons.subsetOf(IndexPrivilege.ALL.automaton, actual.automaton), is(true)); assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.ALL)); } } - public void testGetWithSingleSelectorAccessFailuresSelector() { + public void testResolveSameSelectorPrivileges() { assumeTrue("requires failure store feature", DataStream.isFailureStoreFeatureFlagEnabled()); { - IndexPrivilege actual = IndexPrivilege.getWithSingleSelectorAccess(Set.of("read_failure_store")); + IndexPrivilege actual = resolvePrivilegeAndAssertSingleton(Set.of("read_failure_store")); assertThat(actual, equalTo(IndexPrivilege.READ_FAILURE_STORE)); assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.FAILURES)); } { - IndexPrivilege actual = IndexPrivilege.getWithSingleSelectorAccess(Set.of("all", "read_failure_store")); + IndexPrivilege actual = resolvePrivilegeAndAssertSingleton(Set.of("all", "read_failure_store")); assertThat(actual.name(), equalTo(Set.of("all", "read_failure_store"))); assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.ALL)); assertThat(Automatons.subsetOf(IndexPrivilege.ALL.automaton, actual.automaton), is(true)); } { - IndexPrivilege actual = IndexPrivilege.getWithSingleSelectorAccess( - Set.of("all", "indices:data/read/search", "read_failure_store") - ); + IndexPrivilege actual = resolvePrivilegeAndAssertSingleton(Set.of("all", "indices:data/read/search", "read_failure_store")); assertThat(actual.name(), equalTo(Set.of("all", "indices:data/read/search", "read_failure_store"))); assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.ALL)); assertThat(Automatons.subsetOf(IndexPrivilege.ALL.automaton, actual.automaton), is(true)); } { - IndexPrivilege actual = IndexPrivilege.getWithSingleSelectorAccess(Set.of("all", "read", "read_failure_store")); + IndexPrivilege actual = resolvePrivilegeAndAssertSingleton(Set.of("all", "read", "read_failure_store")); assertThat(actual.name(), equalTo(Set.of("all", "read", "read_failure_store"))); assertThat(actual.getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.ALL)); assertThat(Automatons.subsetOf(IndexPrivilege.ALL.automaton, actual.automaton), is(true)); } - expectThrows( - IllegalArgumentException.class, - () -> IndexPrivilege.getWithSingleSelectorAccess(Set.of("read", "read_failure_store")) - ); - expectThrows( - IllegalArgumentException.class, - () -> IndexPrivilege.getWithSingleSelectorAccess(Set.of("indices:data/read/search", "read_failure_store")) - ); - expectThrows( - IllegalArgumentException.class, - () -> IndexPrivilege.getWithSingleSelectorAccess(Set.of("none", "read_failure_store")) - ); } - public void testSplitBySelectorAccess() { + public void testResolveBySelectorAccess() { assumeTrue("requires failure store feature", DataStream.isFailureStoreFeatureFlagEnabled()); { - Set actual = IndexPrivilege.splitBySelectorAccess(Set.of("read_failure_store")); + Set actual = IndexPrivilege.resolveBySelectorAccess(Set.of("read_failure_store")); assertThat(actual, containsInAnyOrder(IndexPrivilege.READ_FAILURE_STORE)); assertThat(actual.iterator().next().getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.FAILURES)); } { - Set actual = IndexPrivilege.splitBySelectorAccess(Set.of("read_failure_store", "READ_FAILURE_STORE")); + Set actual = IndexPrivilege.resolveBySelectorAccess(Set.of("read_failure_store", "READ_FAILURE_STORE")); assertThat(actual, containsInAnyOrder(IndexPrivilege.READ_FAILURE_STORE)); assertThat(actual.iterator().next().getSelectorPredicate(), equalTo(IndexComponentSelectorPredicate.FAILURES)); } { - Set actual = IndexPrivilege.splitBySelectorAccess(Set.of("read_failure_store", "read", "READ_FAILURE_STORE")); + Set actual = IndexPrivilege.resolveBySelectorAccess(Set.of("read_failure_store", "read", "READ_FAILURE_STORE")); assertThat(actual, containsInAnyOrder(IndexPrivilege.READ_FAILURE_STORE, IndexPrivilege.READ)); List actualPredicates = actual.stream().map(IndexPrivilege::getSelectorPredicate).toList(); assertThat( @@ -168,12 +154,14 @@ public void testSplitBySelectorAccess() { ); } { - Set actual = IndexPrivilege.splitBySelectorAccess(Set.of("read_failure_store", "read", "view_index_metadata")); + Set actual = IndexPrivilege.resolveBySelectorAccess( + Set.of("read_failure_store", "read", "view_index_metadata") + ); assertThat( actual, containsInAnyOrder( IndexPrivilege.READ_FAILURE_STORE, - IndexPrivilege.getWithSingleSelectorAccess(Set.of("read", "view_index_metadata")) + resolvePrivilegeAndAssertSingleton(Set.of("read", "view_index_metadata")) ) ); List actualPredicates = actual.stream().map(IndexPrivilege::getSelectorPredicate).toList(); @@ -183,14 +171,14 @@ public void testSplitBySelectorAccess() { ); } { - Set actual = IndexPrivilege.splitBySelectorAccess( + Set actual = IndexPrivilege.resolveBySelectorAccess( Set.of("read_failure_store", "read", "indices:data/read/search", "view_index_metadata") ); assertThat( actual, containsInAnyOrder( IndexPrivilege.READ_FAILURE_STORE, - IndexPrivilege.getWithSingleSelectorAccess(Set.of("read", "indices:data/read/search", "view_index_metadata")) + resolvePrivilegeAndAssertSingleton(Set.of("read", "indices:data/read/search", "view_index_metadata")) ) ); List actualPredicates = actual.stream().map(IndexPrivilege::getSelectorPredicate).toList(); @@ -200,13 +188,13 @@ public void testSplitBySelectorAccess() { ); } { - Set actual = IndexPrivilege.splitBySelectorAccess( + Set actual = IndexPrivilege.resolveBySelectorAccess( Set.of("read_failure_store", "all", "read", "indices:data/read/search", "view_index_metadata") ); assertThat( actual, containsInAnyOrder( - IndexPrivilege.getWithSingleSelectorAccess( + resolvePrivilegeAndAssertSingleton( Set.of("read_failure_store", "all", "read", "indices:data/read/search", "view_index_metadata") ) ) @@ -231,39 +219,39 @@ public void testPrivilegesForGetCheckPointAction() { public void testRelationshipBetweenPrivileges() { assertThat( Automatons.subsetOf( - IndexPrivilege.getWithSingleSelectorAccess(Set.of("view_index_metadata")).automaton, - IndexPrivilege.getWithSingleSelectorAccess(Set.of("manage")).automaton + resolvePrivilegeAndAssertSingleton(Set.of("view_index_metadata")).automaton, + resolvePrivilegeAndAssertSingleton(Set.of("manage")).automaton ), is(true) ); assertThat( Automatons.subsetOf( - IndexPrivilege.getWithSingleSelectorAccess(Set.of("monitor")).automaton, - IndexPrivilege.getWithSingleSelectorAccess(Set.of("manage")).automaton + resolvePrivilegeAndAssertSingleton(Set.of("monitor")).automaton, + resolvePrivilegeAndAssertSingleton(Set.of("manage")).automaton ), is(true) ); assertThat( Automatons.subsetOf( - IndexPrivilege.getWithSingleSelectorAccess(Set.of("create", "create_doc", "index", "delete")).automaton, - IndexPrivilege.getWithSingleSelectorAccess(Set.of("write")).automaton + resolvePrivilegeAndAssertSingleton(Set.of("create", "create_doc", "index", "delete")).automaton, + resolvePrivilegeAndAssertSingleton(Set.of("write")).automaton ), is(true) ); assertThat( Automatons.subsetOf( - IndexPrivilege.getWithSingleSelectorAccess(Set.of("create_index", "delete_index")).automaton, - IndexPrivilege.getWithSingleSelectorAccess(Set.of("manage")).automaton + resolvePrivilegeAndAssertSingleton(Set.of("create_index", "delete_index")).automaton, + resolvePrivilegeAndAssertSingleton(Set.of("manage")).automaton ), is(true) ); } public void testCrossClusterReplicationPrivileges() { - final IndexPrivilege crossClusterReplication = IndexPrivilege.getWithSingleSelectorAccess(Set.of("cross_cluster_replication")); + final IndexPrivilege crossClusterReplication = resolvePrivilegeAndAssertSingleton(Set.of("cross_cluster_replication")); List.of( "indices:data/read/xpack/ccr/shard_changes", "indices:monitor/stats", @@ -274,12 +262,12 @@ public void testCrossClusterReplicationPrivileges() { assertThat( Automatons.subsetOf( crossClusterReplication.automaton, - IndexPrivilege.getWithSingleSelectorAccess(Set.of("manage", "read", "monitor")).automaton + resolvePrivilegeAndAssertSingleton(Set.of("manage", "read", "monitor")).automaton ), is(true) ); - final IndexPrivilege crossClusterReplicationInternal = IndexPrivilege.getWithSingleSelectorAccess( + final IndexPrivilege crossClusterReplicationInternal = resolvePrivilegeAndAssertSingleton( Set.of("cross_cluster_replication_internal") ); List.of( @@ -294,19 +282,19 @@ public void testCrossClusterReplicationPrivileges() { ); assertThat( - Automatons.subsetOf( - crossClusterReplicationInternal.automaton, - IndexPrivilege.getWithSingleSelectorAccess(Set.of("manage")).automaton - ), + Automatons.subsetOf(crossClusterReplicationInternal.automaton, resolvePrivilegeAndAssertSingleton(Set.of("manage")).automaton), is(false) ); assertThat( - Automatons.subsetOf( - crossClusterReplicationInternal.automaton, - IndexPrivilege.getWithSingleSelectorAccess(Set.of("all")).automaton - ), + Automatons.subsetOf(crossClusterReplicationInternal.automaton, resolvePrivilegeAndAssertSingleton(Set.of("all")).automaton), is(true) ); } + public static IndexPrivilege resolvePrivilegeAndAssertSingleton(Set names) { + final Set splitBySelector = IndexPrivilege.resolveBySelectorAccess(names); + assertThat("expected singleton privilege set but got " + splitBySelector, splitBySelector.size(), equalTo(1)); + return splitBySelector.iterator().next(); + } + } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/PrivilegeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/PrivilegeTests.java index 6b2177f2414cf..9ccea6811a5dd 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/PrivilegeTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/PrivilegeTests.java @@ -218,7 +218,7 @@ public void testIndexCollapse() { IndexPrivilege second = values[randomIntBetween(0, values.length - 1)]; Set name = Sets.newHashSet(first.name().iterator().next(), second.name().iterator().next()); - Set indices = IndexPrivilege.splitBySelectorAccess(name); + Set indices = IndexPrivilege.resolveBySelectorAccess(name); Automaton automaton = null; if (indices.size() == 1) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java index f12ec0513e9fc..8e7c35e55ce3c 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java @@ -557,7 +557,7 @@ public static void buildRoleFromDescriptors( (key, privilege) -> builder.add( fieldPermissionsCache.getFieldPermissions(privilege.fieldPermissionsDefinition), privilege.query, - IndexPrivilege.splitBySelectorAccess(privilege.privileges), + IndexPrivilege.resolveBySelectorAccess(privilege.privileges), false, privilege.indices.toArray(Strings.EMPTY_ARRAY) ) @@ -566,7 +566,7 @@ public static void buildRoleFromDescriptors( (key, privilege) -> builder.add( fieldPermissionsCache.getFieldPermissions(privilege.fieldPermissionsDefinition), privilege.query, - IndexPrivilege.splitBySelectorAccess(privilege.privileges), + IndexPrivilege.resolveBySelectorAccess(privilege.privileges), true, privilege.indices.toArray(Strings.EMPTY_ARRAY) ) @@ -580,7 +580,7 @@ public static void buildRoleFromDescriptors( new FieldPermissionsDefinition(privilege.getGrantedFields(), privilege.getDeniedFields()) ), privilege.getQuery() == null ? null : newHashSet(privilege.getQuery()), - IndexPrivilege.splitBySelectorAccess(newHashSet(Objects.requireNonNull(privilege.getPrivileges()))), + IndexPrivilege.resolveBySelectorAccess(newHashSet(Objects.requireNonNull(privilege.getPrivileges()))), privilege.allowRestrictedIndices(), newHashSet(Objects.requireNonNull(privilege.getIndices())).toArray(new String[0]) ) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/DeprecationRoleDescriptorConsumer.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/DeprecationRoleDescriptorConsumer.java index 1c24029b89323..f64f412bfba9d 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/DeprecationRoleDescriptorConsumer.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/DeprecationRoleDescriptorConsumer.java @@ -194,7 +194,7 @@ private void logDeprecatedPermission(RoleDescriptor roleDescriptor) { for (Map.Entry> privilegesByAlias : privilegesByAliasMap.entrySet()) { final String aliasName = privilegesByAlias.getKey(); final Set aliasPrivilegeNames = privilegesByAlias.getValue(); - final Set aliasPrivileges = IndexPrivilege.splitBySelectorAccess(aliasPrivilegeNames); + final Set aliasPrivileges = IndexPrivilege.resolveBySelectorAccess(aliasPrivilegeNames); final SortedSet inferiorIndexNames = new TreeSet<>(); for (var aliasPrivilege : aliasPrivileges) { // TODO implement failures handling in a follow-up @@ -210,7 +210,7 @@ private void logDeprecatedPermission(RoleDescriptor roleDescriptor) { // compute privilege set once per index no matter how many times it is pointed to final Set indexPrivilegeSet = indexPrivilegeMap.computeIfAbsent( index.getName(), - i -> IndexPrivilege.splitBySelectorAccess(indexPrivileges) + i -> IndexPrivilege.resolveBySelectorAccess(indexPrivileges) ); for (var indexPrivilege : indexPrivilegeSet) { // TODO implement failures handling in a follow-up diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetBuiltinPrivilegesAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetBuiltinPrivilegesAction.java index 95716ec4d8cc2..6f9cca010a823 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetBuiltinPrivilegesAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetBuiltinPrivilegesAction.java @@ -24,7 +24,6 @@ import org.elasticsearch.xpack.core.security.action.privilege.GetBuiltinPrivilegesRequest; import org.elasticsearch.xpack.core.security.action.privilege.GetBuiltinPrivilegesResponse; import org.elasticsearch.xpack.core.security.action.privilege.GetBuiltinPrivilegesResponseTranslator; -import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.security.authz.store.NativeRolesStore; import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler; @@ -43,7 +42,7 @@ public class RestGetBuiltinPrivilegesAction extends SecurityBaseRestHandler { private static final Logger logger = LogManager.getLogger(RestGetBuiltinPrivilegesAction.class); // TODO remove this once we can update docs tests again - private static final Set FAILURE_STORE_PRIVILEGES_TO_EXCLUDE = Set.of(IndexPrivilege.READ_FAILURE_STORE.getSingleName()); + private static final Set FAILURE_STORE_PRIVILEGES_TO_EXCLUDE = Set.of("read_failure_store"); private final GetBuiltinPrivilegesResponseTranslator responseTranslator; public RestGetBuiltinPrivilegesAction( diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java index d683e4af1f39d..6d9ae6277fc6f 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java @@ -1616,7 +1616,7 @@ public void testGetRoleDescriptorsIntersectionForRemoteClusterHasDeterministicOr final int numGroups = randomIntBetween(2, 5); int extraGroups = 0; for (int i = 0; i < numGroups; i++) { - Set splitBySelector = IndexPrivilege.splitBySelectorAccess( + Set splitBySelector = IndexPrivilege.resolveBySelectorAccess( Set.copyOf(randomSubsetOf(randomIntBetween(1, 4), IndexPrivilege.names())) ); // If we end up with failure and data access, we will split and end up with extra groups. Need to account for this for the From 79a7fd5edb02116697328a6af1f17baf47d0e76b Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 3 Mar 2025 12:49:35 +0100 Subject: [PATCH 047/131] More --- .../authz/privilege/IndexPrivilege.java | 16 ------------ .../authz/privilege/PrivilegeTests.java | 2 +- .../support/AutomatonPatternsTests.java | 15 +++-------- .../security/CrossClusterShardTests.java | 8 ++++-- .../xpack/security/authz/RBACEngineTests.java | 3 ++- .../authz/store/CompositeRolesStoreTests.java | 26 ++++++++++++------- 6 files changed, 30 insertions(+), 40 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java index 23c8063d6d115..9cc5f8a4c9836 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java @@ -274,22 +274,6 @@ private IndexPrivilege(Set name, Automaton automaton, IndexComponentSele this.selectorPredicate = selectorPredicate; } - /** - * Delegates to {@link #resolveBySelectorAccess(Set)} but throws if the result is not a singleton, i.e., covers more than one selector. - * Use this method if you know that the input name set corresponds to privileges covering the same selector, for instance if you have a - * single input name, or multiple names that all grant access to one selector e.g., {@link IndexComponentSelector#DATA}. - * @throws IllegalArgumentException if privileges and actions for input names cover access to more than one selector - */ - public static IndexPrivilege getWithSingleSelectorAccess(Set names) { - final Set splitBySelector = resolveBySelectorAccess(names); - if (splitBySelector.size() != 1) { - throw new IllegalArgumentException( - "index privilege patterns " + names + " did not map to a single selector " + splitBySelector - ); - } - return splitBySelector.iterator().next(); - } - /** * Returns a {@link IndexPrivilege} that corresponds to the given raw action pattern or privilege name. */ diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/PrivilegeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/PrivilegeTests.java index 9ccea6811a5dd..b89c0111d445d 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/PrivilegeTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/PrivilegeTests.java @@ -205,7 +205,7 @@ public void testClusterAction() throws Exception { public void testIndexAction() throws Exception { Set actionName = Sets.newHashSet("indices:admin/mapping/delete"); - IndexPrivilege index = IndexPrivilege.getWithSingleSelectorAccess(actionName); + IndexPrivilege index = IndexPrivilegeTests.resolvePrivilegeAndAssertSingleton(actionName); assertThat(index, notNullValue()); assertThat(index.predicate().test("indices:admin/mapping/delete"), is(true)); assertThat(index.predicate().test("indices:admin/mapping/dele"), is(false)); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/support/AutomatonPatternsTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/support/AutomatonPatternsTests.java index 237c5921c55cf..dbd181f0b5a38 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/support/AutomatonPatternsTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/support/AutomatonPatternsTests.java @@ -13,7 +13,6 @@ import java.util.Arrays; import java.util.Locale; -import java.util.Set; import static org.elasticsearch.xpack.core.security.action.apikey.CrossClusterApiKeyRoleDescriptorBuilder.CCR_INDICES_PRIVILEGE_NAMES; import static org.elasticsearch.xpack.core.security.action.apikey.CrossClusterApiKeyRoleDescriptorBuilder.CCS_INDICES_PRIVILEGE_NAMES; @@ -37,7 +36,7 @@ public void testRemoteClusterPrivsDoNotOverlap() { // check that the action patterns for remote CCS are not allowed by remote CCR privileges Arrays.stream(CCS_INDICES_PRIVILEGE_NAMES).forEach(ccsPrivilege -> { - Automaton ccsAutomaton = IndexPrivilege.getWithSingleSelectorAccess(Set.of(ccsPrivilege)).getAutomaton(); + Automaton ccsAutomaton = IndexPrivilege.get(ccsPrivilege).getAutomaton(); Automatons.getPatterns(ccsAutomaton).forEach(ccsPattern -> { // emulate an action name that could be allowed by a CCS privilege String actionName = ccsPattern.replaceAll("\\*", randomAlphaOfLengthBetween(1, 8)); @@ -49,17 +48,14 @@ public void testRemoteClusterPrivsDoNotOverlap() { ccrPrivileges, ccsPattern ); - assertFalse( - errorMessage, - IndexPrivilege.getWithSingleSelectorAccess(Set.of(ccrPrivileges)).predicate().test(actionName) - ); + assertFalse(errorMessage, IndexPrivilege.get(ccrPrivileges).predicate().test(actionName)); }); }); }); // check that the action patterns for remote CCR are not allowed by remote CCS privileges Arrays.stream(CCR_INDICES_PRIVILEGE_NAMES).forEach(ccrPrivilege -> { - Automaton ccrAutomaton = IndexPrivilege.getWithSingleSelectorAccess(Set.of(ccrPrivilege)).getAutomaton(); + Automaton ccrAutomaton = IndexPrivilege.get(ccrPrivilege).getAutomaton(); Automatons.getPatterns(ccrAutomaton).forEach(ccrPattern -> { // emulate an action name that could be allowed by a CCR privilege String actionName = ccrPattern.replaceAll("\\*", randomAlphaOfLengthBetween(1, 8)); @@ -75,10 +71,7 @@ public void testRemoteClusterPrivsDoNotOverlap() { ccsPrivileges, ccrPattern ); - assertFalse( - errorMessage, - IndexPrivilege.getWithSingleSelectorAccess(Set.of(ccsPrivileges)).predicate().test(actionName) - ); + assertFalse(errorMessage, IndexPrivilege.get(ccsPrivileges).predicate().test(actionName)); } }); }); diff --git a/x-pack/plugin/security/qa/consistency-checks/src/test/java/org/elasticsearch/xpack/security/CrossClusterShardTests.java b/x-pack/plugin/security/qa/consistency-checks/src/test/java/org/elasticsearch/xpack/security/CrossClusterShardTests.java index cca137261247a..8454733122a5b 100644 --- a/x-pack/plugin/security/qa/consistency-checks/src/test/java/org/elasticsearch/xpack/security/CrossClusterShardTests.java +++ b/x-pack/plugin/security/qa/consistency-checks/src/test/java/org/elasticsearch/xpack/security/CrossClusterShardTests.java @@ -27,7 +27,7 @@ import org.elasticsearch.xpack.ccr.Ccr; import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin; import org.elasticsearch.xpack.core.security.action.apikey.CrossClusterApiKeyRoleDescriptorBuilder; -import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilegeTests; import org.elasticsearch.xpack.downsample.Downsample; import org.elasticsearch.xpack.downsample.DownsampleShardPersistentTaskExecutor; import org.elasticsearch.xpack.eql.plugin.EqlPlugin; @@ -112,7 +112,11 @@ public void testCheckForNewShardLevelTransportActions() throws Exception { List shardActions = transportActionBindings.stream() .map(binding -> binding.getProvider().get()) - .filter(action -> IndexPrivilege.getWithSingleSelectorAccess(crossClusterPrivilegeNames).predicate().test(action.actionName)) + .filter( + action -> IndexPrivilegeTests.resolvePrivilegeAndAssertSingleton(crossClusterPrivilegeNames) + .predicate() + .test(action.actionName) + ) .filter(this::actionIsLikelyShardAction) .map(action -> action.actionName) .toList(); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java index 6d9ae6277fc6f..4ce74f942a228 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java @@ -96,6 +96,7 @@ import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilegeResolver; import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivileges.ManageApplicationPrivileges; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilegeTests; import org.elasticsearch.xpack.core.security.authz.privilege.Privilege; import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; import org.elasticsearch.xpack.core.security.support.Automatons; @@ -1290,7 +1291,7 @@ public void testBuildUserPrivilegeResponse() { {"term":{"public":true}}"""); final Role role = Role.builder(RESTRICTED_INDICES, "test", "role") .cluster(Sets.newHashSet("monitor", "manage_watcher"), Collections.singleton(manageApplicationPrivileges)) - .add(IndexPrivilege.getWithSingleSelectorAccess(Sets.newHashSet("read", "write")), "index-1") + .add(IndexPrivilegeTests.resolvePrivilegeAndAssertSingleton(Sets.newHashSet("read", "write")), "index-1") .add(IndexPrivilege.ALL, "index-2", "index-3") .add( new FieldPermissions(new FieldPermissionsDefinition(new String[] { "public.*" }, new String[0])), diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java index 043948aae4a58..d816fb0b8c514 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java @@ -93,6 +93,7 @@ import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.IndexComponentSelectorPredicate; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilegeTests; import org.elasticsearch.xpack.core.security.authz.privilege.NamedClusterPrivilege; import org.elasticsearch.xpack.core.security.authz.restriction.Workflow; import org.elasticsearch.xpack.core.security.authz.restriction.WorkflowResolver; @@ -1508,8 +1509,8 @@ public void testBuildRoleWithMultipleRemoteMergedAcrossPrivilegesAndDescriptors( assertHasRemoteIndexGroupsForClusters( role.remoteIndices(), Set.of("remote-1"), - indexGroup(IndexPrivilege.getWithSingleSelectorAccess(Set.of("read")), false, "index-1"), - indexGroup(IndexPrivilege.getWithSingleSelectorAccess(Set.of("none")), false, "index-1") + indexGroup(IndexPrivilege.get("read"), false, "index-1"), + indexGroup(IndexPrivilege.get("none"), false, "index-1") ); } @@ -1681,7 +1682,11 @@ public void testBuildRoleWithReadFailureStoreAndReadPrivilegeAndMultipleIndexPat for (var role : roles) { assertHasIndexGroups( role.indices(), - indexGroup(IndexPrivilege.getWithSingleSelectorAccess(Set.of("read", "write")), allowRestrictedIndices, indexPattern), + indexGroup( + IndexPrivilegeTests.resolvePrivilegeAndAssertSingleton(Set.of("read", "write")), + allowRestrictedIndices, + indexPattern + ), indexGroup(IndexPrivilege.READ, allowRestrictedIndices, otherIndexPattern), indexGroup(IndexPrivilege.READ_FAILURE_STORE, allowRestrictedIndices, otherIndexPattern) ); @@ -1787,7 +1792,7 @@ public void testBuildRoleWithAllPrivilegeIsNeverSplit() { assertHasIndexGroups( role.indices(), indexGroup( - IndexPrivilege.getWithSingleSelectorAccess(Set.of("read", "read_failure_store", "all")), + IndexPrivilegeTests.resolvePrivilegeAndAssertSingleton(Set.of("read", "read_failure_store", "all")), allowRestrictedIndices, indexPattern ) @@ -1845,7 +1850,7 @@ public void testBuildRoleWithFailureStorePrivilegeCollatesToRemoveDlsFlsFromAnot assertHasIndexGroups( role.indices(), indexGroup( - IndexPrivilege.getWithSingleSelectorAccess(Set.of("read_failure_store")), + IndexPrivilege.get("read_failure_store"), allowRestrictedIndices, null, new FieldPermissionsDefinition( @@ -1857,7 +1862,7 @@ public void testBuildRoleWithFailureStorePrivilegeCollatesToRemoveDlsFlsFromAnot indexPattern ), indexGroup( - IndexPrivilege.getWithSingleSelectorAccess(Set.of("read", "view_index_metadata")), + IndexPrivilegeTests.resolvePrivilegeAndAssertSingleton(Set.of("read", "view_index_metadata")), allowRestrictedIndices, null, new FieldPermissionsDefinition( @@ -1900,7 +1905,7 @@ public void testBuildRoleWithFailureStorePrivilegeCollatesToKeepDlsFlsFromAnothe assertHasIndexGroups( role.indices(), indexGroup( - IndexPrivilege.getWithSingleSelectorAccess(Set.of("read_failure_store")), + IndexPrivilege.get("read_failure_store"), allowRestrictedIndices, "{\"match\":{\"field\":\"a\"}}", new FieldPermissionsDefinition( @@ -1909,7 +1914,7 @@ public void testBuildRoleWithFailureStorePrivilegeCollatesToKeepDlsFlsFromAnothe indexPattern ), indexGroup( - IndexPrivilege.getWithSingleSelectorAccess(Set.of("read", "view_index_metadata")), + IndexPrivilegeTests.resolvePrivilegeAndAssertSingleton(Set.of("read", "view_index_metadata")), allowRestrictedIndices, "{\"match\":{\"field\":\"a\"}}", new FieldPermissionsDefinition( @@ -1943,7 +1948,10 @@ public void testBuildRoleNeverSplitsWithoutFailureStoreRelatedPrivileges() { final Role role = buildRole(roleDescriptorWithIndicesPrivileges("r1", indicesPrivileges)); final IndicesPermission actual = role.indices(); - assertHasIndexGroups(actual, indexGroup(IndexPrivilege.getWithSingleSelectorAccess(usedPrivileges), false, indexPattern)); + assertHasIndexGroups( + actual, + indexGroup(IndexPrivilegeTests.resolvePrivilegeAndAssertSingleton(usedPrivileges), false, indexPattern) + ); } public void testCustomRolesProviderFailures() throws Exception { From b05f82b860147008e995941b4eddf7a3e538ad4d Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 3 Mar 2025 13:10:35 +0100 Subject: [PATCH 048/131] Assert in config cluser privs --- .../authz/privilege/ConfigurableClusterPrivileges.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java index c9c6d1fa96a2a..d5e350b20521a 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ConfigurableClusterPrivileges.java @@ -414,10 +414,10 @@ public ManageRolesPrivilege(List manageRolesInd this.requestPredicateSupplier = (restrictedIndices) -> { IndicesPermission.Builder indicesPermissionBuilder = new IndicesPermission.Builder(restrictedIndices); for (ManageRolesIndexPermissionGroup indexPatternPrivilege : manageRolesIndexPermissionGroups) { - Set splitBySelector = IndexPrivilege.resolveBySelectorAccess( - Set.of(indexPatternPrivilege.privileges()) - ); - for (IndexPrivilege indexPrivilege : splitBySelector) { + Set privileges = IndexPrivilege.resolveBySelectorAccess(Set.of(indexPatternPrivilege.privileges())); + assert privileges.stream().allMatch(p -> p.getSelectorPredicate() != IndexComponentSelectorPredicate.FAILURES) + : "not support for failures store access yet"; + for (IndexPrivilege indexPrivilege : privileges) { indicesPermissionBuilder.addGroup( indexPrivilege, FieldPermissions.DEFAULT, From 172141f249405eac5975ec11acf7d412cad0c213 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 3 Mar 2025 13:17:01 +0100 Subject: [PATCH 049/131] Tests --- .../authz/privilege/IndexPrivilegeTests.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java index 2d41a96ef0e33..a6342b9f3e19d 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java @@ -170,6 +170,33 @@ public void testResolveBySelectorAccess() { containsInAnyOrder(IndexComponentSelectorPredicate.DATA, IndexComponentSelectorPredicate.FAILURES) ); } + { + Set actual = IndexPrivilege.resolveBySelectorAccess(Set.of("read_failure_store", "indices:data/read/*")); + assertThat( + actual, + containsInAnyOrder(IndexPrivilege.READ_FAILURE_STORE, resolvePrivilegeAndAssertSingleton(Set.of("indices:data/read/*"))) + ); + List actualPredicates = actual.stream().map(IndexPrivilege::getSelectorPredicate).toList(); + assertThat( + actualPredicates, + containsInAnyOrder(IndexComponentSelectorPredicate.DATA, IndexComponentSelectorPredicate.FAILURES) + ); + } + { + Set actual = IndexPrivilege.resolveBySelectorAccess(Set.of("read_failure_store", "indices:data/read/search")); + assertThat( + actual, + containsInAnyOrder( + IndexPrivilege.READ_FAILURE_STORE, + resolvePrivilegeAndAssertSingleton(Set.of("indices:data/read/search")) + ) + ); + List actualPredicates = actual.stream().map(IndexPrivilege::getSelectorPredicate).toList(); + assertThat( + actualPredicates, + containsInAnyOrder(IndexComponentSelectorPredicate.DATA, IndexComponentSelectorPredicate.FAILURES) + ); + } { Set actual = IndexPrivilege.resolveBySelectorAccess( Set.of("read_failure_store", "read", "indices:data/read/search", "view_index_metadata") From 20f4c5cdec40d0d775a3dcede213e97fe5d98037 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 3 Mar 2025 14:00:32 +0100 Subject: [PATCH 050/131] More --- .../authz/store/CompositeRolesStoreTests.java | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java index d816fb0b8c514..9092565275439 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java @@ -1693,6 +1693,104 @@ public void testBuildRoleWithReadFailureStoreAndReadPrivilegeAndMultipleIndexPat } } + public void testBuildRoleWithReadOnRestrictedAndNonRestrictedIndices() { + assumeTrue("requires failure store feature", DataStream.isFailureStoreFeatureFlagEnabled()); + String indexPattern = randomAlphanumericOfLength(10); + List roles = List.of( + buildRole( + roleDescriptorWithIndicesPrivileges( + "r1", + new IndicesPrivileges[] { + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read", "read_failure_store") + .allowRestrictedIndices(true) + .build(), + IndicesPrivileges.builder().indices(indexPattern).privileges("read").allowRestrictedIndices(false).build() } + ) + ), + buildRole( + roleDescriptorWithIndicesPrivileges( + "r1", + new IndicesPrivileges[] { + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read", "read_failure_store") + .allowRestrictedIndices(true) + .build() } + ), + roleDescriptorWithIndicesPrivileges( + "r2", + new IndicesPrivileges[] { + IndicesPrivileges.builder().indices(indexPattern).privileges("read").allowRestrictedIndices(false).build() } + ) + ) + ); + // the roles are different "format" but the same so should produce the same index groups + for (var role : roles) { + IndicesPermission indices = role.indices(); + assertHasIndexGroups( + indices, + indexGroup(IndexPrivilege.get("read"), false, indexPattern), + indexGroup(IndexPrivilege.get("read"), true, indexPattern), + indexGroup(IndexPrivilege.get("read_failure_store"), true, indexPattern) + ); + } + } + + public void testBuildRoleWithReadFailureStoreOnRestrictedAndNonRestrictedIndices() { + assumeTrue("requires failure store feature", DataStream.isFailureStoreFeatureFlagEnabled()); + String indexPattern = randomAlphanumericOfLength(10); + List roles = List.of( + buildRole( + roleDescriptorWithIndicesPrivileges( + "r1", + new IndicesPrivileges[] { + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read", "read_failure_store") + .allowRestrictedIndices(true) + .build(), + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read_failure_store") + .allowRestrictedIndices(false) + .build() } + ) + ), + buildRole( + roleDescriptorWithIndicesPrivileges( + "r1", + new IndicesPrivileges[] { + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read", "read_failure_store") + .allowRestrictedIndices(true) + .build() } + ), + roleDescriptorWithIndicesPrivileges( + "r2", + new IndicesPrivileges[] { + IndicesPrivileges.builder() + .indices(indexPattern) + .privileges("read_failure_store") + .allowRestrictedIndices(false) + .build() } + ) + ) + ); + // the roles are different "format" but the same so should produce the same index groups + for (var role : roles) { + IndicesPermission indices = role.indices(); + assertHasIndexGroups( + indices, + indexGroup(IndexPrivilege.get("read_failure_store"), false, indexPattern), + indexGroup(IndexPrivilege.get("read"), true, indexPattern), + indexGroup(IndexPrivilege.get("read_failure_store"), true, indexPattern) + ); + } + } + public void testBuildRoleWithMultipleReadFailureStoreAndReadPrivilegeSplit() { assumeTrue("requires failure store feature", DataStream.isFailureStoreFeatureFlagEnabled()); String indexPattern = randomAlphanumericOfLength(10); From 183cebc3515f80aa59382bcd452e7a71d4b63ab7 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 3 Mar 2025 16:20:38 +0100 Subject: [PATCH 051/131] Deterministic order --- .../security/authz/privilege/IndexPrivilege.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java index 9cc5f8a4c9836..0af2c276beff2 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java @@ -48,6 +48,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Locale; import java.util.Map; import java.util.Objects; @@ -377,7 +378,7 @@ private static Set resolve(Set name) { actions ); assertNamesMatch(name, combined); - return Set.copyOf(combined); + return Collections.unmodifiableSet(combined); } private static Set combineIndexPrivileges( @@ -397,13 +398,14 @@ private static Set combineIndexPrivileges( return Set.of(union(allSelectorAccessPrivileges, actions, IndexComponentSelectorPredicate.ALL)); } - final Set combined = new HashSet<>(); - if (false == failuresSelectorAccessPrivileges.isEmpty()) { - combined.add(union(failuresSelectorAccessPrivileges, Set.of(), IndexComponentSelectorPredicate.FAILURES)); - } + // linked hash set to preserve order across selectors + final Set combined = new LinkedHashSet<>(); if (false == dataSelectorAccessPrivileges.isEmpty() || false == actions.isEmpty()) { combined.add(union(dataSelectorAccessPrivileges, actions, IndexComponentSelectorPredicate.DATA)); } + if (false == failuresSelectorAccessPrivileges.isEmpty()) { + combined.add(union(failuresSelectorAccessPrivileges, Set.of(), IndexComponentSelectorPredicate.FAILURES)); + } return combined; } From efd11b5c48fec7d9878f2f4a018d8ca1223150ed Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 3 Mar 2025 19:54:02 +0100 Subject: [PATCH 052/131] WIP --- .../cluster/metadata/IndexAbstraction.java | 12 + .../authz/permission/IndicesPermission.java | 13 +- .../authz/privilege/IndexPrivilege.java | 3 +- .../FailureStoreSecurityRestIT.java | 323 ++++++++++++++++++ 4 files changed, 345 insertions(+), 6 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstraction.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstraction.java index cf6431266f16b..87ff1b199d880 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstraction.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstraction.java @@ -100,6 +100,13 @@ default boolean isDataStreamRelated() { return false; } + /** + * @return whether this index abstraction is a failure index of a data stream + */ + default boolean isFailureIndexOfDataStream() { + return false; + } + /** * An index abstraction type. */ @@ -183,6 +190,11 @@ public DataStream getParentDataStream() { return dataStream; } + @Override + public boolean isFailureIndexOfDataStream() { + return getParentDataStream() != null && getParentDataStream().isFailureStoreIndex(getName()); + } + @Override public boolean isHidden() { return isHidden; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 7d159a8f0b15c..b83d8fa7e49f9 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -412,10 +412,15 @@ public boolean checkIndex(Group group) { final DataStream ds = indexAbstraction == null ? null : indexAbstraction.getParentDataStream(); if (ds != null) { if (group.checkIndex(ds.getName())) { - return true; + final IndexComponentSelector selectorToCheck = indexAbstraction.isFailureIndexOfDataStream() + ? IndexComponentSelector.FAILURES + : selector; + if (group.checkSelector(selectorToCheck)) { + return true; + } } } - return group.checkIndex(name); + return group.checkIndex(name) && group.checkSelector(selector); } /** @@ -864,8 +869,8 @@ boolean hasQuery() { return query != null; } - public boolean checkSelector(IndexComponentSelector selector) { - return selectorPredicate.test(selector); + public boolean checkSelector(@Nullable IndexComponentSelector selector) { + return selectorPredicate.test(selector == null ? IndexComponentSelector.DATA : selector); } public boolean allowRestrictedIndices() { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java index 0af2c276beff2..73e8922dc388b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java @@ -185,8 +185,7 @@ public final class IndexPrivilege extends Privilege { public static final IndexPrivilege ALL = new IndexPrivilege("all", ALL_AUTOMATON, IndexComponentSelectorPredicate.ALL); public static final IndexPrivilege READ_FAILURE_STORE = new IndexPrivilege( "read_failure_store", - // TODO use READ_AUTOMATON here in authorization follow-up - Automatons.EMPTY, + READ_AUTOMATON, IndexComponentSelectorPredicate.FAILURES ); public static final IndexPrivilege READ = new IndexPrivilege("read", READ_AUTOMATON); diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java index 1ce2acac908ee..cd88803216fbc 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java @@ -7,26 +7,45 @@ package org.elasticsearch.xpack.security.failurestore; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.client.Request; +import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.Response; +import org.elasticsearch.client.ResponseException; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.support.XContentMapValues; +import org.elasticsearch.search.SearchHit; +import org.elasticsearch.search.SearchResponseUtils; +import org.elasticsearch.test.TestSecurityClient; import org.elasticsearch.test.cluster.ElasticsearchCluster; import org.elasticsearch.test.cluster.FeatureFlag; import org.elasticsearch.test.rest.ESRestTestCase; import org.elasticsearch.xcontent.json.JsonXContent; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.SecurityOnTrialLicenseRestTestCase; import org.junit.ClassRule; import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Map; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasItem; public class FailureStoreSecurityRestIT extends ESRestTestCase { + private TestSecurityClient securityClient; + @ClassRule public static ElasticsearchCluster cluster = ElasticsearchCluster.local() .apply(SecurityOnTrialLicenseRestTestCase.commonTrialSecurityClusterConfig) @@ -134,6 +153,279 @@ public void testGetUserPrivileges() throws IOException { }"""); } + private static final String DATA_ACCESS_USER = "data_access_user"; + private static final String STAR_READ_ONLY_USER = "star_read_only_user"; + private static final String FAILURE_STORE_ACCESS_USER = "failure_store_access_user"; + private static final String BOTH_ACCESS_USER = "both_access_user"; + private static final String WRITE_ACCESS_USER = "write_access_user"; + private static final SecureString PASSWORD = new SecureString("elastic-password"); + + @SuppressWarnings("unchecked") + public void testFailureStoreAccess() throws IOException { + String dataAccessRole = "data_access"; + String starReadOnlyRole = "star_read_only_access"; + String failureStoreAccessRole = "failure_store_access"; + String bothAccessRole = "both_access"; + String writeAccessRole = "write_access"; + + createUser(DATA_ACCESS_USER, PASSWORD, List.of(dataAccessRole)); + createUser(STAR_READ_ONLY_USER, PASSWORD, List.of(starReadOnlyRole)); + createUser(FAILURE_STORE_ACCESS_USER, PASSWORD, List.of(failureStoreAccessRole)); + createUser(BOTH_ACCESS_USER, PASSWORD, randomBoolean() ? List.of(bothAccessRole) : List.of(dataAccessRole, failureStoreAccessRole)); + createUser(WRITE_ACCESS_USER, PASSWORD, List.of(writeAccessRole)); + + upsertRole(Strings.format(""" + { + "description": "Role with data access", + "cluster": ["all"], + "indices": [{"names": ["test*"], "privileges": ["read"]}] + }"""), dataAccessRole); + upsertRole(Strings.format(""" + { + "description": "Role with data access", + "cluster": ["all"], + "indices": [{"names": ["*"], "privileges": ["read"]}] + }"""), starReadOnlyRole); + upsertRole(Strings.format(""" + { + "description": "Role with failure store access", + "cluster": ["all"], + "indices": [{"names": ["test*"], "privileges": ["read_failure_store"]}] + }"""), failureStoreAccessRole); + upsertRole(Strings.format(""" + { + "description": "Role with both data and failure store access", + "cluster": ["all"], + "indices": [{"names": ["test*"], "privileges": ["read", "read_failure_store"]}] + }"""), bothAccessRole); + upsertRole(Strings.format(""" + { + "description": "Role with regular write access without failure store access", + "cluster": ["all"], + "indices": [{"names": ["test*"], "privileges": ["write", "auto_configure"]}] + }"""), writeAccessRole); + + createTemplates(); + List docIds = populateDataStreamWithBulkRequest(); + assertThat(docIds.size(), equalTo(2)); + assertThat(docIds, hasItem("1")); + String successDocId = "1"; + String failedDocId = docIds.stream().filter(id -> false == id.equals(successDocId)).findFirst().get(); + + Request dataStream = new Request("GET", "/_data_stream/test1"); + Response response = adminClient().performRequest(dataStream); + Map dataStreams = entityAsMap(response); + assertEquals(Collections.singletonList("test1"), XContentMapValues.extractValue("data_streams.name", dataStreams)); + List dataIndexNames = (List) XContentMapValues.extractValue("data_streams.indices.index_name", dataStreams); + assertThat(dataIndexNames.size(), equalTo(1)); + List failureIndexNames = (List) XContentMapValues.extractValue( + "data_streams.failure_store.indices.index_name", + dataStreams + ); + assertThat(failureIndexNames.size(), equalTo(1)); + + String dataIndexName = dataIndexNames.get(0); + String failureIndexName = failureIndexNames.get(0); + + // `*` with read access user _can_ read concrete failure index with only read + assertContainsDocIds(performRequest(STAR_READ_ONLY_USER, new Request("GET", "/" + failureIndexName + "/_search")), failedDocId); + + // user with access to failures index + assertContainsDocIds(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test1::failures/_search")), failedDocId); + assertContainsDocIds(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test*::failures/_search")), failedDocId); + assertContainsDocIds(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/*1::failures/_search")), failedDocId); + assertContainsDocIds(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/*::failures/_search")), failedDocId); + assertContainsDocIds(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/.fs*/_search")), failedDocId); + assertContainsDocIds( + performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/" + failureIndexName + "/_search")), + failedDocId + ); + assertContainsDocIds( + performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/" + failureIndexName + "/_search?ignore_unavailable=true")), + failedDocId + ); + + expectThrows404(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test12::failures/_search"))); + expectThrows404(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test2::failures/_search"))); + expectThrows404(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test12::*/_search"))); + + expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test1::data/_search"))); + expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test1/_search"))); + expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test2::data/_search"))); + expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test2/_search"))); + expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/" + dataIndexName + "/_search"))); + + assertEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test1::data/_search?ignore_unavailable=true"))); + assertEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test1/_search?ignore_unavailable=true"))); + assertEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test2::data/_search?ignore_unavailable=true"))); + assertEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test2/_search?ignore_unavailable=true"))); + assertEmpty( + performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/" + dataIndexName + "/_search?ignore_unavailable=true")) + ); + + // assertEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/*1::data/_search"))); + // assertEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/*1/_search"))); + // TODO is this correct? + assertEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/.ds*/_search"))); + + // user with access to data index + assertContainsDocIds(performRequest(DATA_ACCESS_USER, new Request("GET", "/test1/_search")), successDocId); + assertContainsDocIds(performRequest(DATA_ACCESS_USER, new Request("GET", "/test*/_search")), successDocId); + assertContainsDocIds(performRequest(DATA_ACCESS_USER, new Request("GET", "/*1/_search")), successDocId); + assertContainsDocIds(performRequest(DATA_ACCESS_USER, new Request("GET", "/*/_search")), successDocId); + assertContainsDocIds(performRequest(DATA_ACCESS_USER, new Request("GET", "/.ds*/_search")), successDocId); + assertContainsDocIds(performRequest(DATA_ACCESS_USER, new Request("GET", "/" + dataIndexName + "/_search")), successDocId); + assertContainsDocIds( + performRequest(DATA_ACCESS_USER, new Request("GET", "/" + dataIndexName + "/_search?ignore_unavailable=true")), + successDocId + ); + + expectThrows404(() -> performRequest(DATA_ACCESS_USER, new Request("GET", "/test12/_search"))); + expectThrows404(() -> performRequest(DATA_ACCESS_USER, new Request("GET", "/test2/_search"))); + expectThrows404(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test12::*/_search"))); + + expectThrows403(() -> performRequest(DATA_ACCESS_USER, new Request("GET", "/test1::failures/_search"))); + expectThrows403(() -> performRequest(DATA_ACCESS_USER, new Request("GET", "/test2::failures/_search"))); + expectThrows403(() -> performRequest(DATA_ACCESS_USER, new Request("GET", "/" + failureIndexName + "/_search"))); + // TODO is this correct? + assertEmpty(performRequest(DATA_ACCESS_USER, new Request("GET", "/.fs*/_search"))); + // assertEmpty(performRequest(DATA_ACCESS_USER, new Request("GET", "/*1::failures/_search"))); + + // user with access to everything + assertContainsDocIds(adminClient().performRequest(new Request("GET", "/test1::failures/_search")), failedDocId); + assertContainsDocIds(adminClient().performRequest(new Request("GET", "/test*::failures/_search")), failedDocId); + assertContainsDocIds(adminClient().performRequest(new Request("GET", "/*1::failures/_search")), failedDocId); + assertContainsDocIds(adminClient().performRequest(new Request("GET", "/*::failures/_search")), failedDocId); + assertContainsDocIds(adminClient().performRequest(new Request("GET", "/.fs*/_search")), failedDocId); + + expectThrows404(() -> adminClient().performRequest(new Request("GET", "/test12::failures/_search"))); + expectThrows404(() -> adminClient().performRequest(new Request("GET", "/test2::failures/_search"))); + + assertContainsDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test1::failures/_search")), failedDocId); + assertContainsDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test*::failures/_search")), failedDocId); + assertContainsDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/*1::failures/_search")), failedDocId); + assertContainsDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/*::failures/_search")), failedDocId); + assertContainsDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/.fs*/_search")), failedDocId); + + expectThrows404(() -> performRequest(BOTH_ACCESS_USER, new Request("GET", "/test12::failures/_search"))); + expectThrows404(() -> performRequest(BOTH_ACCESS_USER, new Request("GET", "/test2::failures/_search"))); + + assertContainsDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test1/_search")), successDocId); + assertContainsDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test*/_search")), successDocId); + assertContainsDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/*1/_search")), successDocId); + assertContainsDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/*/_search")), successDocId); + + expectThrows404(() -> performRequest(BOTH_ACCESS_USER, new Request("GET", "/test12/_search"))); + expectThrows404(() -> performRequest(BOTH_ACCESS_USER, new Request("GET", "/test2/_search"))); + } + + private static void expectThrows404(ThrowingRunnable get) { + var ex = expectThrows(ResponseException.class, get); + assertThat(ex.getResponse().getStatusLine().getStatusCode(), equalTo(404)); + } + + private static void expectThrows403(ThrowingRunnable get) { + var ex = expectThrows(ResponseException.class, get); + assertThat(ex.getResponse().getStatusLine().getStatusCode(), equalTo(403)); + } + + @SuppressWarnings("unchecked") + private static void assertContainsDocIds(Response response, String... docIds) throws IOException { + assertOK(response); + final SearchResponse searchResponse = SearchResponseUtils.parseSearchResponse(responseAsParser(response)); + try { + SearchHit[] hits = searchResponse.getHits().getHits(); + assertThat(hits.length, equalTo(docIds.length)); + List actualDocIds = Arrays.stream(hits).map(SearchHit::getId).toList(); + assertThat(actualDocIds, containsInAnyOrder(docIds)); + } finally { + searchResponse.decRef(); + } + } + + private static void assertEmpty(Response response) throws IOException { + assertOK(response); + final SearchResponse searchResponse = SearchResponseUtils.parseSearchResponse(responseAsParser(response)); + try { + SearchHit[] hits = searchResponse.getHits().getHits(); + assertThat(hits.length, equalTo(0)); + } finally { + searchResponse.decRef(); + } + } + + private void createTemplates() throws IOException { + var componentTemplateRequest = new Request("PUT", "/_component_template/component1"); + componentTemplateRequest.setJsonEntity(""" + { + "template": { + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "age": { + "type": "integer" + }, + "email": { + "type": "keyword" + }, + "name": { + "type": "text" + } + } + }, + "data_stream_options": { + "failure_store": { + "enabled": true + } + } + } + } + """); + assertOK(adminClient().performRequest(componentTemplateRequest)); + + var indexTemplateRequest = new Request("PUT", "/_index_template/template1"); + indexTemplateRequest.setJsonEntity(""" + { + "index_patterns": ["test*"], + "data_stream": {}, + "priority": 500, + "composed_of": ["component1"] + } + """); + assertOK(adminClient().performRequest(indexTemplateRequest)); + } + + @SuppressWarnings("unchecked") + private List populateDataStreamWithBulkRequest() throws IOException { + var bulkRequest = new Request("POST", "/_bulk?refresh=true"); + bulkRequest.setJsonEntity(""" + { "create" : { "_index" : "test1", "_id" : "1" } } + { "@timestamp": 1, "age" : 1, "name" : "jack", "email" : "jack@example.com" } + { "create" : { "_index" : "test1", "_id" : "2" } } + { "@timestamp": 2, "age" : "this should be an int", "name" : "jack", "email" : "jack@example.com" } + """); + Response response = performRequest(WRITE_ACCESS_USER, bulkRequest); + assertOK(response); + // we need this dance because the ID for the failed document is random, **not** 2 + Map stringObjectMap = responseAsMap(response); + List items = (List) stringObjectMap.get("items"); + List ids = new ArrayList<>(); + for (Object item : items) { + Map itemMap = (Map) item; + Map create = (Map) itemMap.get("create"); + assertThat(create.get("status"), equalTo(201)); + ids.add((String) create.get("_id")); + } + return ids; + } + + private Response performRequest(String user, Request request) throws IOException { + request.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", basicAuthHeaderValue(user, PASSWORD)).build()); + return client().performRequest(request); + } + private static void expectUserPrivilegesResponse(String userPrivilegesResponse) throws IOException { Request request = new Request("GET", "/_security/user/_privileges"); request.setOptions( @@ -155,4 +447,35 @@ private static void putRole(String rolePayload) throws IOException { private static Map mapFromJson(String json) { return XContentHelper.convertToMap(JsonXContent.jsonXContent, json, false); } + + protected TestSecurityClient getSecurityClient() { + if (securityClient == null) { + securityClient = new TestSecurityClient(adminClient()); + } + return securityClient; + } + + protected void createUser(String username, SecureString password, List roles) throws IOException { + getSecurityClient().putUser(new User(username, roles.toArray(String[]::new)), password); + } + + protected void upsertRole(String roleDescriptor, String roleName) throws IOException { + Request createRoleRequest = roleRequest(roleDescriptor, roleName); + Response createRoleResponse = adminClient().performRequest(createRoleRequest); + assertOK(createRoleResponse); + } + + protected Request roleRequest(String roleDescriptor, String roleName) { + Request createRoleRequest; + if (randomBoolean()) { + createRoleRequest = new Request(randomFrom(HttpPut.METHOD_NAME, HttpPost.METHOD_NAME), "/_security/role/" + roleName); + createRoleRequest.setJsonEntity(roleDescriptor); + } else { + createRoleRequest = new Request(HttpPost.METHOD_NAME, "/_security/role"); + createRoleRequest.setJsonEntity(org.elasticsearch.core.Strings.format(""" + {"roles": {"%s": %s}} + """, roleName, roleDescriptor)); + } + return createRoleRequest; + } } From 95d6369e4f26457812eb812f7dd73cf89fafe0fe Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Tue, 4 Mar 2025 11:13:52 +0100 Subject: [PATCH 053/131] Authz and test --- .../authz/privilege/IndexPrivilege.java | 8 +- .../core/security/user/InternalUsers.java | 6 ++ .../FailureStoreSecurityRestIT.java | 73 ++++++++++--------- .../RestGetBuiltinPrivilegesAction.java | 2 +- 4 files changed, 51 insertions(+), 38 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java index 73e8922dc388b..7f3b259324825 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java @@ -188,6 +188,11 @@ public final class IndexPrivilege extends Privilege { READ_AUTOMATON, IndexComponentSelectorPredicate.FAILURES ); + public static final IndexPrivilege MANAGE_FAILURE_STORE = new IndexPrivilege( + "manage_failure_store", + MANAGE_AUTOMATON, + IndexComponentSelectorPredicate.FAILURES + ); public static final IndexPrivilege READ = new IndexPrivilege("read", READ_AUTOMATON); public static final IndexPrivilege READ_CROSS_CLUSTER = new IndexPrivilege("read_cross_cluster", READ_CROSS_CLUSTER_AUTOMATON); public static final IndexPrivilege CREATE = new IndexPrivilege("create", CREATE_AUTOMATON); @@ -246,7 +251,8 @@ public final class IndexPrivilege extends Privilege { entry("auto_configure", AUTO_CONFIGURE), entry("cross_cluster_replication", CROSS_CLUSTER_REPLICATION), entry("cross_cluster_replication_internal", CROSS_CLUSTER_REPLICATION_INTERNAL), - DataStream.isFailureStoreFeatureFlagEnabled() ? entry("read_failure_store", READ_FAILURE_STORE) : null + DataStream.isFailureStoreFeatureFlagEnabled() ? entry("read_failure_store", READ_FAILURE_STORE) : null, + DataStream.isFailureStoreFeatureFlagEnabled() ? entry("manage_failure_store", MANAGE_FAILURE_STORE) : null ).filter(Objects::nonNull).collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)) ); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/InternalUsers.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/InternalUsers.java index a34c17cfee42b..5c448c2055dec 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/InternalUsers.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/InternalUsers.java @@ -248,6 +248,12 @@ public class InternalUsers { .indices("*") .privileges(LazyRolloverAction.NAME) .allowRestrictedIndices(true) + .build(), + RoleDescriptor.IndicesPrivileges.builder() + .indices("*") + // TODO consider a more granular privilege for this + .privileges("manage_failure_store") + .allowRestrictedIndices(true) .build() }, null, null, diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java index cd88803216fbc..1c2b066fcf803 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java @@ -63,6 +63,13 @@ protected Settings restAdminSettings() { return Settings.builder().put(ThreadContext.PREFIX + ".Authorization", token).build(); } + private static final String DATA_ACCESS_USER = "data_access_user"; + private static final String STAR_READ_ONLY_USER = "star_read_only_user"; + private static final String FAILURE_STORE_ACCESS_USER = "failure_store_access_user"; + private static final String BOTH_ACCESS_USER = "both_access_user"; + private static final String WRITE_ACCESS_USER = "write_access_user"; + private static final SecureString PASSWORD = new SecureString("elastic-password"); + public void testGetUserPrivileges() throws IOException { Request userRequest = new Request("PUT", "/_security/user/user"); userRequest.setJsonEntity(""" @@ -73,7 +80,7 @@ public void testGetUserPrivileges() throws IOException { """); assertOK(adminClient().performRequest(userRequest)); - putRole(""" + upsertRole(""" { "cluster": ["all"], "indices": [ @@ -83,7 +90,7 @@ public void testGetUserPrivileges() throws IOException { } ] } - """); + """, "role"); expectUserPrivilegesResponse(""" { "cluster": ["all"], @@ -102,7 +109,7 @@ public void testGetUserPrivileges() throws IOException { "run_as": [] }"""); - putRole(""" + upsertRole(""" { "cluster": ["all"], "indices": [ @@ -112,7 +119,7 @@ public void testGetUserPrivileges() throws IOException { } ] } - """); + """, "role"); expectUserPrivilegesResponse(""" { "cluster": ["all"], @@ -127,7 +134,7 @@ public void testGetUserPrivileges() throws IOException { "run_as": [] }"""); - putRole(""" + upsertRole(""" { "cluster": ["all"], "indices": [ @@ -137,7 +144,7 @@ public void testGetUserPrivileges() throws IOException { } ] } - """); + """, "role"); expectUserPrivilegesResponse(""" { "cluster": ["all"], @@ -153,13 +160,6 @@ public void testGetUserPrivileges() throws IOException { }"""); } - private static final String DATA_ACCESS_USER = "data_access_user"; - private static final String STAR_READ_ONLY_USER = "star_read_only_user"; - private static final String FAILURE_STORE_ACCESS_USER = "failure_store_access_user"; - private static final String BOTH_ACCESS_USER = "both_access_user"; - private static final String WRITE_ACCESS_USER = "write_access_user"; - private static final SecureString PASSWORD = new SecureString("elastic-password"); - @SuppressWarnings("unchecked") public void testFailureStoreAccess() throws IOException { String dataAccessRole = "data_access"; @@ -247,7 +247,6 @@ public void testFailureStoreAccess() throws IOException { expectThrows404(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test12::failures/_search"))); expectThrows404(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test2::failures/_search"))); - expectThrows404(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test12::*/_search"))); expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test1::data/_search"))); expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test1/_search"))); @@ -255,18 +254,21 @@ public void testFailureStoreAccess() throws IOException { expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test2/_search"))); expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/" + dataIndexName + "/_search"))); - assertEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test1::data/_search?ignore_unavailable=true"))); - assertEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test1/_search?ignore_unavailable=true"))); - assertEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test2::data/_search?ignore_unavailable=true"))); - assertEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test2/_search?ignore_unavailable=true"))); - assertEmpty( - performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/" + dataIndexName + "/_search?ignore_unavailable=true")) - ); - - // assertEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/*1::data/_search"))); - // assertEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/*1/_search"))); // TODO is this correct? - assertEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/.ds*/_search"))); + expectThrows403( + () -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test1::data/_search?ignore_unavailable=true")) + ); + expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test1/_search?ignore_unavailable=true"))); + expectThrows403( + () -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test2::data/_search?ignore_unavailable=true")) + ); + expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test2/_search?ignore_unavailable=true"))); + expectThrows403( + () -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/" + dataIndexName + "/_search?ignore_unavailable=true")) + ); + expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/*1::data/_search"))); + expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/*1/_search"))); + expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/.ds*/_search"))); // user with access to data index assertContainsDocIds(performRequest(DATA_ACCESS_USER, new Request("GET", "/test1/_search")), successDocId); @@ -282,14 +284,13 @@ public void testFailureStoreAccess() throws IOException { expectThrows404(() -> performRequest(DATA_ACCESS_USER, new Request("GET", "/test12/_search"))); expectThrows404(() -> performRequest(DATA_ACCESS_USER, new Request("GET", "/test2/_search"))); - expectThrows404(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test12::*/_search"))); expectThrows403(() -> performRequest(DATA_ACCESS_USER, new Request("GET", "/test1::failures/_search"))); expectThrows403(() -> performRequest(DATA_ACCESS_USER, new Request("GET", "/test2::failures/_search"))); expectThrows403(() -> performRequest(DATA_ACCESS_USER, new Request("GET", "/" + failureIndexName + "/_search"))); // TODO is this correct? - assertEmpty(performRequest(DATA_ACCESS_USER, new Request("GET", "/.fs*/_search"))); - // assertEmpty(performRequest(DATA_ACCESS_USER, new Request("GET", "/*1::failures/_search"))); + expectThrows403(() -> performRequest(DATA_ACCESS_USER, new Request("GET", "/.fs*/_search"))); + expectThrows403(() -> performRequest(DATA_ACCESS_USER, new Request("GET", "/*1::failures/_search"))); // user with access to everything assertContainsDocIds(adminClient().performRequest(new Request("GET", "/test1::failures/_search")), failedDocId); @@ -301,6 +302,9 @@ public void testFailureStoreAccess() throws IOException { expectThrows404(() -> adminClient().performRequest(new Request("GET", "/test12::failures/_search"))); expectThrows404(() -> adminClient().performRequest(new Request("GET", "/test2::failures/_search"))); + assertEmpty(adminClient().performRequest(new Request("GET", "/test12::failures/_search?ignore_unavailable=true"))); + assertEmpty(adminClient().performRequest(new Request("GET", "/test2::failures/_search?ignore_unavailable=true"))); + assertContainsDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test1::failures/_search")), failedDocId); assertContainsDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test*::failures/_search")), failedDocId); assertContainsDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/*1::failures/_search")), failedDocId); @@ -310,13 +314,16 @@ public void testFailureStoreAccess() throws IOException { expectThrows404(() -> performRequest(BOTH_ACCESS_USER, new Request("GET", "/test12::failures/_search"))); expectThrows404(() -> performRequest(BOTH_ACCESS_USER, new Request("GET", "/test2::failures/_search"))); + assertEmpty(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test12::failures/_search?ignore_unavailable=true"))); + assertEmpty(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test2::failures/_search?ignore_unavailable=true"))); + assertContainsDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test1/_search")), successDocId); assertContainsDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test*/_search")), successDocId); assertContainsDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/*1/_search")), successDocId); assertContainsDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/*/_search")), successDocId); - expectThrows404(() -> performRequest(BOTH_ACCESS_USER, new Request("GET", "/test12/_search"))); - expectThrows404(() -> performRequest(BOTH_ACCESS_USER, new Request("GET", "/test2/_search"))); + assertEmpty(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test12/_search?ignore_unavailable=true"))); + assertEmpty(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test2/_search?ignore_unavailable=true"))); } private static void expectThrows404(ThrowingRunnable get) { @@ -438,12 +445,6 @@ private static void expectUserPrivilegesResponse(String userPrivilegesResponse) assertThat(responseAsMap(response), equalTo(mapFromJson(userPrivilegesResponse))); } - private static void putRole(String rolePayload) throws IOException { - Request roleRequest = new Request("PUT", "/_security/role/role"); - roleRequest.setJsonEntity(rolePayload); - assertOK(adminClient().performRequest(roleRequest)); - } - private static Map mapFromJson(String json) { return XContentHelper.convertToMap(JsonXContent.jsonXContent, json, false); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetBuiltinPrivilegesAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetBuiltinPrivilegesAction.java index 6f9cca010a823..13e7b5446d3cf 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetBuiltinPrivilegesAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetBuiltinPrivilegesAction.java @@ -42,7 +42,7 @@ public class RestGetBuiltinPrivilegesAction extends SecurityBaseRestHandler { private static final Logger logger = LogManager.getLogger(RestGetBuiltinPrivilegesAction.class); // TODO remove this once we can update docs tests again - private static final Set FAILURE_STORE_PRIVILEGES_TO_EXCLUDE = Set.of("read_failure_store"); + private static final Set FAILURE_STORE_PRIVILEGES_TO_EXCLUDE = Set.of("read_failure_store", "manage_failure_store"); private final GetBuiltinPrivilegesResponseTranslator responseTranslator; public RestGetBuiltinPrivilegesAction( From 2a654ba4726becdfc41a5afe0d283bb74f44633c Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Tue, 4 Mar 2025 14:14:18 +0100 Subject: [PATCH 054/131] Deal with privilege ordering --- .../authz/privilege/IndexPrivilege.java | 77 ++++++++++++------- 1 file changed, 50 insertions(+), 27 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java index 7f3b259324825..b188ad8fca3b4 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java @@ -48,11 +48,13 @@ import java.util.Collection; import java.util.Collections; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.SortedMap; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -227,35 +229,56 @@ public final class IndexPrivilege extends Privilege { * If you are adding a new named index privilege, also add it to the * docs. */ - private static final Map VALUES = sortByAccessLevel( - Stream.of( - entry("none", NONE), - entry("all", ALL), - entry("manage", MANAGE), - entry("create_index", CREATE_INDEX), - entry("monitor", MONITOR), - entry("read", READ), - entry("index", INDEX), - entry("delete", DELETE), - entry("write", WRITE), - entry("create", CREATE), - entry("create_doc", CREATE_DOC), - entry("delete_index", DELETE_INDEX), - entry("view_index_metadata", VIEW_METADATA), - entry("read_cross_cluster", READ_CROSS_CLUSTER), - entry("manage_follow_index", MANAGE_FOLLOW_INDEX), - entry("manage_leader_index", MANAGE_LEADER_INDEX), - entry("manage_ilm", MANAGE_ILM), - entry("manage_data_stream_lifecycle", MANAGE_DATA_STREAM_LIFECYCLE), - entry("maintenance", MAINTENANCE), - entry("auto_configure", AUTO_CONFIGURE), - entry("cross_cluster_replication", CROSS_CLUSTER_REPLICATION), - entry("cross_cluster_replication_internal", CROSS_CLUSTER_REPLICATION_INTERNAL), - DataStream.isFailureStoreFeatureFlagEnabled() ? entry("read_failure_store", READ_FAILURE_STORE) : null, - DataStream.isFailureStoreFeatureFlagEnabled() ? entry("manage_failure_store", MANAGE_FAILURE_STORE) : null - ).filter(Objects::nonNull).collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)) + private static final Map VALUES = combineSortedInOrder( + sortByAccessLevel( + Stream.of( + DataStream.isFailureStoreFeatureFlagEnabled() ? entry("read_failure_store", READ_FAILURE_STORE) : null, + DataStream.isFailureStoreFeatureFlagEnabled() ? entry("manage_failure_store", MANAGE_FAILURE_STORE) : null + ).filter(Objects::nonNull).collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)) + ), + sortByAccessLevel( + Stream.of( + entry("none", NONE), + entry("all", ALL), + entry("manage", MANAGE), + entry("create_index", CREATE_INDEX), + entry("monitor", MONITOR), + entry("read", READ), + entry("index", INDEX), + entry("delete", DELETE), + entry("write", WRITE), + entry("create", CREATE), + entry("create_doc", CREATE_DOC), + entry("delete_index", DELETE_INDEX), + entry("view_index_metadata", VIEW_METADATA), + entry("read_cross_cluster", READ_CROSS_CLUSTER), + entry("manage_follow_index", MANAGE_FOLLOW_INDEX), + entry("manage_leader_index", MANAGE_LEADER_INDEX), + entry("manage_ilm", MANAGE_ILM), + entry("manage_data_stream_lifecycle", MANAGE_DATA_STREAM_LIFECYCLE), + entry("maintenance", MAINTENANCE), + entry("auto_configure", AUTO_CONFIGURE), + entry("cross_cluster_replication", CROSS_CLUSTER_REPLICATION), + entry("cross_cluster_replication_internal", CROSS_CLUSTER_REPLICATION_INTERNAL) + ).collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)) + ) ); + private static Map combineSortedInOrder( + SortedMap first, + SortedMap second + ) { + if (first.isEmpty()) { + return second; + } + if (second.isEmpty()) { + return first; + } + final Map combined = new LinkedHashMap<>(first); + combined.putAll(second); + return Collections.unmodifiableMap(combined); + } + public static final Predicate ACTION_MATCHER = ALL.predicate(); public static final Predicate CREATE_INDEX_MATCHER = CREATE_INDEX.predicate(); From 47c30cc57363d51e79254a1f493df398f29d90cc Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Tue, 4 Mar 2025 14:33:08 +0100 Subject: [PATCH 055/131] Comments --- .../core/security/authz/permission/IndicesPermission.java | 3 ++- .../org/elasticsearch/xpack/security/authz/RBACEngine.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index b83d8fa7e49f9..2f994137eeb74 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -505,7 +505,8 @@ public IndicesAccessControl authorize( int totalResourceCount = 0; Map lookup = metadata.getIndicesLookup(); for (String indexOrAlias : requestedIndicesOrAliases) { - // Remove any selectors from abstraction name. Discard them for this check as we do not have access control for them (yet) + // Remove any selectors from abstraction name. Selector authorization happens in conjunction with the index name, via the + // selector check in IndexResource#checkIndex Tuple expressionAndSelector = IndexNameExpressionResolver.splitSelectorExpression(indexOrAlias); indexOrAlias = expressionAndSelector.v1(); IndexComponentSelector selector = expressionAndSelector.v2() == null diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java index 24fc7480eda42..3363ab4ce6802 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java @@ -882,7 +882,8 @@ static AuthorizedIndices resolveAuthorizedIndicesFromRole( for (Index index : indexAbstraction.getIndices()) { indicesAndAliases.add(index.getName()); } - // TODO: We need to limit if a data stream's failure indices should return here. + // also add failure indices; if the role doesn't grant access to them via applicable privileges, these will + // be denied later (see `IndicesPermissions#authorize`) for (Index index : ((DataStream) indexAbstraction).getFailureIndices()) { indicesAndAliases.add(index.getName()); } From 55c88dbeb0b6c5e84f79667c9f307bf40a4718db Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Tue, 4 Mar 2025 19:14:19 +0100 Subject: [PATCH 056/131] Handle resources with same name different selector --- .../authz/permission/IndicesPermission.java | 30 ++++++++++++++----- .../FailureStoreSecurityRestIT.java | 1 + 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 2f994137eeb74..9edd90196a84e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -483,6 +483,20 @@ public Collection resolveConcreteIndices(ProjectMetadata metadata) { public boolean canHaveBackingIndices() { return indexAbstraction != null && indexAbstraction.getType() != IndexAbstraction.Type.CONCRETE_INDEX; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + IndexResource that = (IndexResource) o; + return name.equals(that.name) + && Objects.equals(selector, that.selector) + && Objects.equals(indexAbstraction, that.indexAbstraction); + } + + public int hashCode() { + return Objects.hash(name, selector, indexAbstraction); + } } /** @@ -501,7 +515,7 @@ public IndicesAccessControl authorize( } } - final Map resources = Maps.newMapWithExpectedSize(requestedIndicesOrAliases.size()); + final Set resources = Sets.newHashSetWithExpectedSize(requestedIndicesOrAliases.size()); int totalResourceCount = 0; Map lookup = metadata.getIndicesLookup(); for (String indexOrAlias : requestedIndicesOrAliases) { @@ -513,7 +527,7 @@ public IndicesAccessControl authorize( ? null : IndexComponentSelector.getByKey(expressionAndSelector.v2()); final IndexResource resource = new IndexResource(indexOrAlias, lookup.get(indexOrAlias), selector); - resources.put(resource.name, resource); + resources.add(resource); totalResourceCount += resource.size(lookup); } @@ -532,7 +546,7 @@ public IndicesAccessControl authorize( private Map buildIndicesAccessControl( final String action, - final Map requestedResources, + final Set requestedResources, final int totalResourceCount, final FieldPermissionsCache fieldPermissionsCache, final ProjectMetadata metadata @@ -546,7 +560,7 @@ private Map buildIndicesAccessC final boolean isMappingUpdateAction = isMappingUpdateAction(action); - for (IndexResource resource : requestedResources.values()) { + for (IndexResource resource : requestedResources) { // true if ANY group covers the given index AND the given action boolean granted = false; @@ -610,7 +624,8 @@ && containsPrivilegeThatGrantsMappingUpdatesForBwc(group))) { if (resource.canHaveBackingIndices()) { for (String concreteIndex : concreteIndices) { // If the name appear directly as part of the requested indices, it takes precedence over implicit access - if (false == requestedResources.containsKey(concreteIndex)) { + // TODO inefficient + if (false == requestedResources.stream().anyMatch(r -> r.name.equals(concreteIndex))) { grantedResources.add(concreteIndex); } } @@ -618,6 +633,7 @@ && containsPrivilegeThatGrantsMappingUpdatesForBwc(group))) { } } + // TODO handle failures selector for DLS/FLS Map indexPermissions = Maps.newMapWithExpectedSize(grantedResources.size()); for (String index : grantedResources) { final DocumentLevelPermissions permissions = roleQueriesByIndex.get(index); @@ -645,11 +661,11 @@ && containsPrivilegeThatGrantsMappingUpdatesForBwc(group))) { * Returns {@code true} if action is granted for all {@code requestedResources}. * If action is not granted for at least one resource, this method will return {@code false}. */ - private boolean isActionGranted(final String action, final Map requestedResources) { + private boolean isActionGranted(final String action, final Collection requestedResources) { final boolean isMappingUpdateAction = isMappingUpdateAction(action); - for (IndexResource resource : requestedResources.values()) { + for (IndexResource resource : requestedResources) { // 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 diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java index 1c2b066fcf803..1db38ee8a7a1c 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java @@ -253,6 +253,7 @@ public void testFailureStoreAccess() throws IOException { expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test2::data/_search"))); expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test2/_search"))); expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/" + dataIndexName + "/_search"))); + expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test1,test1::failures/_search"))); // TODO is this correct? expectThrows403( From 58e938b3ee2436e692f5c13c56abf2fd0dfe1b8f Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 5 Mar 2025 12:10:32 +0100 Subject: [PATCH 057/131] WIP index resolution --- .../metadata/IndexAbstractionResolver.java | 14 +++-- .../authz/permission/IndicesPermission.java | 56 +++++++++++++------ .../FailureStoreSecurityRestIT.java | 36 ++++++------ .../xpack/security/authz/RBACEngine.java | 23 ++++++-- .../accesscontrol/IndicesPermissionTests.java | 3 + 5 files changed, 87 insertions(+), 45 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java index a93366307a5a5..abafc35da1879 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java @@ -72,6 +72,7 @@ public List resolveIndexAbstractions( wildcardSeen = true; Set resolvedIndices = new HashSet<>(); for (String authorizedIndex : allAuthorizedAndAvailable.get()) { + // TODO properly handle failure selector here if (Regex.simpleMatch(indexAbstraction, authorizedIndex) && isIndexVisible( indexAbstraction, @@ -102,12 +103,13 @@ && isIndexVisible( resolveSelectorsAndCollect(indexAbstraction, selectorString, indicesOptions, resolvedIndices, projectMetadata); if (minus) { finalIndices.removeAll(resolvedIndices); - } else if (indicesOptions.ignoreUnavailable() == false || isAuthorized.test(indexAbstraction)) { - // Unauthorized names are considered unavailable, so if `ignoreUnavailable` is `true` they should be silently - // discarded from the `finalIndices` list. Other "ways of unavailable" must be handled by the action - // handler, see: https://github.com/elastic/elasticsearch/issues/90215 - finalIndices.addAll(resolvedIndices); - } + } else if (indicesOptions.ignoreUnavailable() == false + || isAuthorized.test(IndexNameExpressionResolver.combineSelectorExpression(indexAbstraction, selectorString))) { + // Unauthorized names are considered unavailable, so if `ignoreUnavailable` is `true` they should be silently + // discarded from the `finalIndices` list. Other "ways of unavailable" must be handled by the action + // handler, see: https://github.com/elastic/elasticsearch/issues/90215 + finalIndices.addAll(resolvedIndices); + } } } return finalIndices; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 9edd90196a84e..aea860a848b32 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -142,17 +142,31 @@ public boolean hasFieldOrDocumentLevelSecurity() { } private IsResourceAuthorizedPredicate buildIndexMatcherPredicateForAction(String action) { - final Set ordinaryIndices = new HashSet<>(); - final Set restrictedIndices = new HashSet<>(); + final Set dataAccessOrdinaryIndices = new HashSet<>(); + final Set failureAccessOrdinaryIndices = new HashSet<>(); + final Set dataAccessRestrictedIndices = new HashSet<>(); + final Set failureAccessRestrictedIndices = 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())); + List indexList = Arrays.asList(group.indices()); + if (group.checkSelector(IndexComponentSelector.DATA)) { + dataAccessRestrictedIndices.addAll(indexList); + } + if (group.checkSelector(IndexComponentSelector.FAILURES)) { + failureAccessRestrictedIndices.addAll(indexList); + } } else { - ordinaryIndices.addAll(Arrays.asList(group.indices())); + List indexList = Arrays.asList(group.indices()); + if (group.checkSelector(IndexComponentSelector.DATA)) { + dataAccessOrdinaryIndices.addAll(indexList); + } + if (group.checkSelector(IndexComponentSelector.FAILURES)) { + failureAccessOrdinaryIndices.addAll(indexList); + } } } 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 @@ -164,25 +178,37 @@ private IsResourceAuthorizedPredicate buildIndexMatcherPredicateForAction(String } } } - final StringMatcher nameMatcher = indexMatcher(ordinaryIndices, restrictedIndices); + final StringMatcher dataAccessNameMatcher = indexMatcher(dataAccessOrdinaryIndices, dataAccessRestrictedIndices); + final StringMatcher failureAccessNameMatcher = indexMatcher(failureAccessOrdinaryIndices, failureAccessRestrictedIndices); final StringMatcher bwcSpecialCaseMatcher = indexMatcher(grantMappingUpdatesOnIndices, grantMappingUpdatesOnRestrictedIndices); - return new IsResourceAuthorizedPredicate(nameMatcher, bwcSpecialCaseMatcher); + return new IsResourceAuthorizedPredicate(dataAccessNameMatcher, failureAccessNameMatcher, bwcSpecialCaseMatcher); } /** * This encapsulates the authorization test for resources. * There is an additional test for resources that are missing or that are not a datastream or a backing index. */ - public static class IsResourceAuthorizedPredicate implements BiPredicate { + public static class IsResourceAuthorizedPredicate { private final BiPredicate biPredicate; // public for tests - public IsResourceAuthorizedPredicate(StringMatcher resourceNameMatcher, StringMatcher additionalNonDatastreamNameMatcher) { + public IsResourceAuthorizedPredicate( + StringMatcher resourceNameMatcher, + StringMatcher failureAccessNameMatcher, + StringMatcher additionalNonDatastreamNameMatcher + ) { this((String name, @Nullable IndexAbstraction indexAbstraction) -> { - assert indexAbstraction == null || name.equals(indexAbstraction.getName()); - return resourceNameMatcher.test(name) - || (isPartOfDatastream(indexAbstraction) == false && additionalNonDatastreamNameMatcher.test(name)); + Tuple nameWithSelector = IndexNameExpressionResolver.splitSelectorExpression(name); + String indexAbstractionName = nameWithSelector.v1(); + IndexComponentSelector selector = IndexComponentSelector.getByKey(nameWithSelector.v2()); + assert indexAbstraction == null || indexAbstractionName.equals(indexAbstraction.getName()); + if (IndexComponentSelector.FAILURES.equals(selector)) { + // TODO assert isPartOfDataStream? + return failureAccessNameMatcher.test(indexAbstractionName); + } + return resourceNameMatcher.test(indexAbstractionName) + || (isPartOfDatastream(indexAbstraction) == false && additionalNonDatastreamNameMatcher.test(indexAbstractionName)); }); } @@ -195,9 +221,8 @@ private IsResourceAuthorizedPredicate(BiPredicate biPr * return a new {@link IsResourceAuthorizedPredicate} instance that is equivalent to the conjunction of * authorization tests of that other instance and this one. */ - @Override - public final IsResourceAuthorizedPredicate and(BiPredicate other) { - return new IsResourceAuthorizedPredicate(this.biPredicate.and(other)); + public final IsResourceAuthorizedPredicate and(IsResourceAuthorizedPredicate other) { + return new IsResourceAuthorizedPredicate(this.biPredicate.and(other.biPredicate)); } /** @@ -215,7 +240,6 @@ public final boolean test(IndexAbstraction indexAbstraction) { * if it doesn't. * Returns {@code true} if access to the given resource is authorized or {@code false} otherwise. */ - @Override public boolean test(String name, @Nullable IndexAbstraction indexAbstraction) { return biPredicate.test(name, indexAbstraction); } @@ -623,7 +647,7 @@ && containsPrivilegeThatGrantsMappingUpdatesForBwc(group))) { grantedResources.add(resource.name); if (resource.canHaveBackingIndices()) { for (String concreteIndex : concreteIndices) { - // If the name appear directly as part of the requested indices, it takes precedence over implicit access + // If the name appears directly as part of the requested indices, it takes precedence over implicit access // TODO inefficient if (false == requestedResources.stream().anyMatch(r -> r.name.equals(concreteIndex))) { grantedResources.add(concreteIndex); diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java index 1db38ee8a7a1c..3ab3e1ce36a5d 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java @@ -240,10 +240,11 @@ public void testFailureStoreAccess() throws IOException { performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/" + failureIndexName + "/_search")), failedDocId ); - assertContainsDocIds( - performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/" + failureIndexName + "/_search?ignore_unavailable=true")), - failedDocId - ); + // TODO fix me + // assertContainsDocIds( + // performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/" + failureIndexName + "/_search?ignore_unavailable=true")), + // failedDocId + // ); expectThrows404(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test12::failures/_search"))); expectThrows404(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test2::failures/_search"))); @@ -255,21 +256,18 @@ public void testFailureStoreAccess() throws IOException { expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/" + dataIndexName + "/_search"))); expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test1,test1::failures/_search"))); - // TODO is this correct? - expectThrows403( - () -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test1::data/_search?ignore_unavailable=true")) - ); - expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test1/_search?ignore_unavailable=true"))); - expectThrows403( - () -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test2::data/_search?ignore_unavailable=true")) - ); - expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test2/_search?ignore_unavailable=true"))); - expectThrows403( - () -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/" + dataIndexName + "/_search?ignore_unavailable=true")) + assertEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test1::data/_search?ignore_unavailable=true"))); + assertEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test1/_search?ignore_unavailable=true"))); + assertEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test2::data/_search?ignore_unavailable=true"))); + assertEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test2/_search?ignore_unavailable=true"))); + assertEmpty( + performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/" + dataIndexName + "/_search?ignore_unavailable=true")) ); + // TODO fix me expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/*1::data/_search"))); expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/*1/_search"))); - expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/.ds*/_search"))); + // TODO fix me more + assertEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/.ds*/_search"))); // user with access to data index assertContainsDocIds(performRequest(DATA_ACCESS_USER, new Request("GET", "/test1/_search")), successDocId); @@ -286,11 +284,15 @@ public void testFailureStoreAccess() throws IOException { expectThrows404(() -> performRequest(DATA_ACCESS_USER, new Request("GET", "/test12/_search"))); expectThrows404(() -> performRequest(DATA_ACCESS_USER, new Request("GET", "/test2/_search"))); + assertEmpty(performRequest(DATA_ACCESS_USER, new Request("GET", "/test1::failures/_search?ignore_unavailable=true"))); + assertEmpty(performRequest(DATA_ACCESS_USER, new Request("GET", "/test2::failures/_search?ignore_unavailable=true"))); + expectThrows403(() -> performRequest(DATA_ACCESS_USER, new Request("GET", "/test1::failures/_search"))); expectThrows403(() -> performRequest(DATA_ACCESS_USER, new Request("GET", "/test2::failures/_search"))); expectThrows403(() -> performRequest(DATA_ACCESS_USER, new Request("GET", "/" + failureIndexName + "/_search"))); // TODO is this correct? - expectThrows403(() -> performRequest(DATA_ACCESS_USER, new Request("GET", "/.fs*/_search"))); + assertEmpty(performRequest(DATA_ACCESS_USER, new Request("GET", "/.fs*/_search"))); + // TODO is this correct? expectThrows403(() -> performRequest(DATA_ACCESS_USER, new Request("GET", "/*1::failures/_search"))); // user with access to everything diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java index 3363ab4ce6802..d7f82b50bf484 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java @@ -31,9 +31,11 @@ import org.elasticsearch.action.search.TransportClosePointInTimeAction; import org.elasticsearch.action.search.TransportMultiSearchAction; import org.elasticsearch.action.search.TransportSearchScrollAction; +import org.elasticsearch.action.support.IndexComponentSelector; import org.elasticsearch.action.termvectors.MultiTermVectorsAction; import org.elasticsearch.cluster.metadata.DataStream; import org.elasticsearch.cluster.metadata.IndexAbstraction; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.metadata.ProjectMetadata; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesReference; @@ -875,18 +877,27 @@ static AuthorizedIndices resolveAuthorizedIndicesFromRole( // TODO: can this be done smarter? I think there are usually more indices/aliases in the cluster then indices defined a roles? if (includeDataStreams) { for (IndexAbstraction indexAbstraction : lookup.values()) { - if (predicate.test(indexAbstraction)) { + boolean dataAccessGranted = predicate.test(indexAbstraction); + // TODO can we also skip if the data stream does not have failure indices? + boolean failureAccessGranted = (indexAbstraction.getType() == IndexAbstraction.Type.DATA_STREAM) + && predicate.test( + IndexNameExpressionResolver.combineSelector(indexAbstraction.getName(), IndexComponentSelector.FAILURES), + indexAbstraction + ); + if (dataAccessGranted) { indicesAndAliases.add(indexAbstraction.getName()); if (indexAbstraction.getType() == IndexAbstraction.Type.DATA_STREAM) { // add data stream and its backing indices for any authorized data streams for (Index index : indexAbstraction.getIndices()) { indicesAndAliases.add(index.getName()); } - // also add failure indices; if the role doesn't grant access to them via applicable privileges, these will - // be denied later (see `IndicesPermissions#authorize`) - for (Index index : ((DataStream) indexAbstraction).getFailureIndices()) { - indicesAndAliases.add(index.getName()); - } + } + } + if (failureAccessGranted) { + // TODO we probably need to add this in _with_ the ::failures selector... + indicesAndAliases.add(indexAbstraction.getName()); + for (Index index : ((DataStream) indexAbstraction).getFailureIndices()) { + indicesAndAliases.add(index.getName()); } } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java index d3ee1fb5fdc5a..241b936bce12e 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java @@ -705,6 +705,7 @@ public void testResourceAuthorizedPredicateForDatastreams() { ); IndicesPermission.IsResourceAuthorizedPredicate predicate = new IndicesPermission.IsResourceAuthorizedPredicate( StringMatcher.of("other"), + StringMatcher.of(), StringMatcher.of(dataStreamName, backingIndex.getName(), concreteIndex.getName(), alias.getName()) ); assertThat(predicate.test(dataStream), is(false)); @@ -720,10 +721,12 @@ public void testResourceAuthorizedPredicateForDatastreams() { public void testResourceAuthorizedPredicateAnd() { IndicesPermission.IsResourceAuthorizedPredicate predicate1 = new IndicesPermission.IsResourceAuthorizedPredicate( StringMatcher.of("c", "a"), + StringMatcher.of(), StringMatcher.of("b", "d") ); IndicesPermission.IsResourceAuthorizedPredicate predicate2 = new IndicesPermission.IsResourceAuthorizedPredicate( StringMatcher.of("c", "b"), + StringMatcher.of(), StringMatcher.of("a", "d") ); Metadata.Builder mb = Metadata.builder( From 4456c2d67759bd076a301d3055c8b4ba999f6245 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 5 Mar 2025 14:06:39 +0100 Subject: [PATCH 058/131] Selectors --- .../metadata/IndexAbstractionResolver.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java index abafc35da1879..86b9c0b3709bf 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java @@ -72,8 +72,11 @@ public List resolveIndexAbstractions( wildcardSeen = true; Set resolvedIndices = new HashSet<>(); for (String authorizedIndex : allAuthorizedAndAvailable.get()) { - // TODO properly handle failure selector here - if (Regex.simpleMatch(indexAbstraction, authorizedIndex) + Tuple tuple = IndexNameExpressionResolver.splitSelectorExpression(authorizedIndex); + authorizedIndex = tuple.v1(); + String authorizedSelectorString = tuple.v2(); + if (selectorsMatch(selectorString, authorizedSelectorString) + && Regex.simpleMatch(indexAbstraction, authorizedIndex) && isIndexVisible( indexAbstraction, selectorString, @@ -115,6 +118,16 @@ && isIndexVisible( return finalIndices; } + boolean selectorsMatch(String selectorString, String authorizedSelectorString) { + IndexComponentSelector selector = IndexComponentSelector.getByKey(selectorString) == null + ? IndexComponentSelector.DATA + : IndexComponentSelector.getByKey(selectorString); + IndexComponentSelector authorizedSelector = IndexComponentSelector.getByKey(authorizedSelectorString) == null + ? IndexComponentSelector.DATA + : IndexComponentSelector.getByKey(authorizedSelectorString); + return selector == authorizedSelector; + } + private static void resolveSelectorsAndCollect( String indexAbstraction, String selectorString, From a750b38053b8df32f12d8f4c0c5305de5f3e41c8 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 5 Mar 2025 14:07:01 +0100 Subject: [PATCH 059/131] x-pack --- .../authz/permission/IndicesPermission.java | 2 +- .../FailureStoreSecurityRestIT.java | 17 +++++++---------- .../xpack/security/authz/RBACEngine.java | 17 +++++++++++++++-- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index aea860a848b32..bf961ffbb5c37 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -204,7 +204,7 @@ public IsResourceAuthorizedPredicate( IndexComponentSelector selector = IndexComponentSelector.getByKey(nameWithSelector.v2()); assert indexAbstraction == null || indexAbstractionName.equals(indexAbstraction.getName()); if (IndexComponentSelector.FAILURES.equals(selector)) { - // TODO assert isPartOfDataStream? + // TODO assert isPartOfDataStream if indexAbstraction is not null return failureAccessNameMatcher.test(indexAbstractionName); } return resourceNameMatcher.test(indexAbstractionName) diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java index 3ab3e1ce36a5d..9a96deafd8f0a 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java @@ -241,10 +241,10 @@ public void testFailureStoreAccess() throws IOException { failedDocId ); // TODO fix me - // assertContainsDocIds( - // performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/" + failureIndexName + "/_search?ignore_unavailable=true")), - // failedDocId - // ); + assertContainsDocIds( + performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/" + failureIndexName + "/_search?ignore_unavailable=true")), + failedDocId + ); expectThrows404(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test12::failures/_search"))); expectThrows404(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test2::failures/_search"))); @@ -263,10 +263,8 @@ public void testFailureStoreAccess() throws IOException { assertEmpty( performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/" + dataIndexName + "/_search?ignore_unavailable=true")) ); - // TODO fix me - expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/*1::data/_search"))); - expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/*1/_search"))); - // TODO fix me more + assertEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/*1::data/_search"))); + assertEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/*1/_search"))); assertEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/.ds*/_search"))); // user with access to data index @@ -292,8 +290,7 @@ public void testFailureStoreAccess() throws IOException { expectThrows403(() -> performRequest(DATA_ACCESS_USER, new Request("GET", "/" + failureIndexName + "/_search"))); // TODO is this correct? assertEmpty(performRequest(DATA_ACCESS_USER, new Request("GET", "/.fs*/_search"))); - // TODO is this correct? - expectThrows403(() -> performRequest(DATA_ACCESS_USER, new Request("GET", "/*1::failures/_search"))); + assertEmpty(performRequest(DATA_ACCESS_USER, new Request("GET", "/*1::failures/_search"))); // user with access to everything assertContainsDocIds(adminClient().performRequest(new Request("GET", "/test1::failures/_search")), failedDocId); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java index d7f82b50bf484..dbf41bfa17c21 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java @@ -894,8 +894,10 @@ static AuthorizedIndices resolveAuthorizedIndicesFromRole( } } if (failureAccessGranted) { - // TODO we probably need to add this in _with_ the ::failures selector... - indicesAndAliases.add(indexAbstraction.getName()); + // TODO hack hack hack + indicesAndAliases.add( + IndexNameExpressionResolver.combineSelector(indexAbstraction.getName(), IndexComponentSelector.FAILURES) + ); for (Index index : ((DataStream) indexAbstraction).getFailureIndices()) { indicesAndAliases.add(index.getName()); } @@ -917,6 +919,17 @@ static AuthorizedIndices resolveAuthorizedIndicesFromRole( // the action handler must handle the case of accessing resources that do not exist return predicate.test(name, null); } else { + // TODO remove selector, add selector, remove selector, selectors, selectors, selectors + if (indexAbstraction.isFailureIndexOfDataStream() + && predicate.test( + IndexNameExpressionResolver.combineSelector( + indexAbstraction.getParentDataStream().getName(), + IndexComponentSelector.FAILURES + ), + indexAbstraction.getParentDataStream() + )) { + return true; + } // We check the parent data stream first if there is one. For testing requested indices, this is most likely // more efficient than checking the index name first because we recommend grant privileges over data stream // instead of backing indices. From 674277aaf4a358391c9c0dc5cf5e6df8a0f9b2fa Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 5 Mar 2025 15:06:15 +0100 Subject: [PATCH 060/131] Fix resolvers --- .../metadata/IndexAbstractionResolver.java | 2 +- .../authz/IndicesAndAliasesResolver.java | 26 ++++++++++++------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java index 86b9c0b3709bf..2e9b508443606 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java @@ -118,7 +118,7 @@ && isIndexVisible( return finalIndices; } - boolean selectorsMatch(String selectorString, String authorizedSelectorString) { + public static boolean selectorsMatch(String selectorString, String authorizedSelectorString) { IndexComponentSelector selector = IndexComponentSelector.getByKey(selectorString) == null ? IndexComponentSelector.DATA : IndexComponentSelector.getByKey(selectorString); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java index d2d50d14d4a36..2e2f05d2e5755 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java @@ -324,15 +324,19 @@ ResolvedIndices resolveIndicesAndAliases( } if (indicesOptions.expandWildcardExpressions()) { for (String authorizedIndex : authorizedIndices.all().get()) { - if (IndexAbstractionResolver.isIndexVisible( - "*", - allIndicesPatternSelector, - authorizedIndex, - indicesOptions, - projectMetadata, - nameExpressionResolver, - indicesRequest.includeDataStreams() - )) { + Tuple tuple = IndexNameExpressionResolver.splitSelectorExpression(authorizedIndex); + authorizedIndex = tuple.v1(); + String authorizedSelectorString = tuple.v2(); + if (IndexAbstractionResolver.selectorsMatch(allIndicesPatternSelector, authorizedSelectorString) + && IndexAbstractionResolver.isIndexVisible( + "*", + allIndicesPatternSelector, + authorizedIndex, + indicesOptions, + projectMetadata, + nameExpressionResolver, + indicesRequest.includeDataStreams() + )) { resolvedIndicesBuilder.addLocal( IndexNameExpressionResolver.combineSelectorExpression(authorizedIndex, allIndicesPatternSelector) ); @@ -480,6 +484,10 @@ private static List loadAuthorizedAliases(Supplier> authoriz List authorizedAliases = new ArrayList<>(); SortedMap existingAliases = projectMetadata.getIndicesLookup(); for (String authorizedIndex : authorizedIndices.get()) { + Tuple tuple = IndexNameExpressionResolver.splitSelectorExpression(authorizedIndex); + authorizedIndex = tuple.v1(); + String authorizedSelectorString = tuple.v2(); + // TODO skip failures here? IndexAbstraction indexAbstraction = existingAliases.get(authorizedIndex); if (indexAbstraction != null && indexAbstraction.getType() == IndexAbstraction.Type.ALIAS) { authorizedAliases.add(authorizedIndex); From dfe957a0d51887d212c8da57e9e989beea8dc357 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Thu, 6 Mar 2025 10:46:06 +0100 Subject: [PATCH 061/131] More --- .../authz/permission/IndicesPermission.java | 2 +- .../FailureStoreSecurityRestIT.java | 54 +++++++++++++++++-- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index bf961ffbb5c37..8dab8d1189b73 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -444,7 +444,7 @@ public boolean checkIndex(Group group) { } } } - return group.checkIndex(name) && group.checkSelector(selector); + return group.checkSelector(selector) && group.checkIndex(name); } /** diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java index 9a96deafd8f0a..b8e50942b60cb 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java @@ -68,6 +68,8 @@ protected Settings restAdminSettings() { private static final String FAILURE_STORE_ACCESS_USER = "failure_store_access_user"; private static final String BOTH_ACCESS_USER = "both_access_user"; private static final String WRITE_ACCESS_USER = "write_access_user"; + private static final String MANAGE_ACCESS_USER = "manage_access_user"; + private static final String MANAGE_FAILURE_STORE_ACCESS_USER = "manage_failure_store_access_user"; private static final SecureString PASSWORD = new SecureString("elastic-password"); public void testGetUserPrivileges() throws IOException { @@ -167,12 +169,16 @@ public void testFailureStoreAccess() throws IOException { String failureStoreAccessRole = "failure_store_access"; String bothAccessRole = "both_access"; String writeAccessRole = "write_access"; + String manageAccessRole = "manage_access"; + String manageFailureStoreRole = "manage_failure_store_access"; createUser(DATA_ACCESS_USER, PASSWORD, List.of(dataAccessRole)); createUser(STAR_READ_ONLY_USER, PASSWORD, List.of(starReadOnlyRole)); createUser(FAILURE_STORE_ACCESS_USER, PASSWORD, List.of(failureStoreAccessRole)); createUser(BOTH_ACCESS_USER, PASSWORD, randomBoolean() ? List.of(bothAccessRole) : List.of(dataAccessRole, failureStoreAccessRole)); createUser(WRITE_ACCESS_USER, PASSWORD, List.of(writeAccessRole)); + createUser(MANAGE_ACCESS_USER, PASSWORD, List.of(manageAccessRole)); + createUser(MANAGE_FAILURE_STORE_ACCESS_USER, PASSWORD, List.of(manageFailureStoreRole)); upsertRole(Strings.format(""" { @@ -204,6 +210,18 @@ public void testFailureStoreAccess() throws IOException { "cluster": ["all"], "indices": [{"names": ["test*"], "privileges": ["write", "auto_configure"]}] }"""), writeAccessRole); + upsertRole(Strings.format(""" + { + "description": "Role with regular manage access without failure store access", + "cluster": ["all"], + "indices": [{"names": ["test*"], "privileges": ["manage"]}] + }"""), manageAccessRole); + upsertRole(Strings.format(""" + { + "description": "Role with failure store manage access", + "cluster": ["all"], + "indices": [{"names": ["test*"], "privileges": ["manage_failure_store"]}] + }"""), manageFailureStoreRole); createTemplates(); List docIds = populateDataStreamWithBulkRequest(); @@ -240,7 +258,6 @@ public void testFailureStoreAccess() throws IOException { performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/" + failureIndexName + "/_search")), failedDocId ); - // TODO fix me assertContainsDocIds( performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/" + failureIndexName + "/_search?ignore_unavailable=true")), failedDocId @@ -263,9 +280,9 @@ public void testFailureStoreAccess() throws IOException { assertEmpty( performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/" + dataIndexName + "/_search?ignore_unavailable=true")) ); + assertEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/.ds*/_search"))); assertEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/*1::data/_search"))); assertEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/*1/_search"))); - assertEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/.ds*/_search"))); // user with access to data index assertContainsDocIds(performRequest(DATA_ACCESS_USER, new Request("GET", "/test1/_search")), successDocId); @@ -324,16 +341,35 @@ public void testFailureStoreAccess() throws IOException { assertEmpty(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test12/_search?ignore_unavailable=true"))); assertEmpty(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test2/_search?ignore_unavailable=true"))); + + // TODO extra make sure manage_failure_store CANNOT delete the whole data stream + + // user with manage access to data stream does NOT get direct access to failure index + expectThrows403(() -> deleteIndex(MANAGE_ACCESS_USER, failureIndexName)); + expectThrows(() -> deleteIndex(MANAGE_ACCESS_USER, dataIndexName), 400); + // manage_failure_store user COULD delete failure index (not valid because it's a write index, but allow security-wise) + expectThrows403(() -> deleteIndex(MANAGE_FAILURE_STORE_ACCESS_USER, dataIndexName)); + expectThrows(() -> deleteIndex(MANAGE_FAILURE_STORE_ACCESS_USER, failureIndexName), 400); + expectThrows403(() -> deleteDataStream(MANAGE_FAILURE_STORE_ACCESS_USER, dataIndexName)); + + // manage user can delete data stream + deleteDataStream(MANAGE_ACCESS_USER, "test1"); + + expectThrows404(() -> performRequest(BOTH_ACCESS_USER, new Request("GET", "/test1/_search"))); + expectThrows404(() -> performRequest(BOTH_ACCESS_USER, new Request("GET", "/test1::failures/_search"))); } private static void expectThrows404(ThrowingRunnable get) { - var ex = expectThrows(ResponseException.class, get); - assertThat(ex.getResponse().getStatusLine().getStatusCode(), equalTo(404)); + expectThrows(get, 404); } private static void expectThrows403(ThrowingRunnable get) { + expectThrows(get, 403); + } + + private static void expectThrows(ThrowingRunnable get, int statusCode) { var ex = expectThrows(ResponseException.class, get); - assertThat(ex.getResponse().getStatusLine().getStatusCode(), equalTo(403)); + assertThat(ex.getResponse().getStatusLine().getStatusCode(), equalTo(statusCode)); } @SuppressWarnings("unchecked") @@ -428,6 +464,14 @@ private List populateDataStreamWithBulkRequest() throws IOException { return ids; } + private void deleteDataStream(String user, String dataStreamName) throws IOException { + assertOK(performRequest(user, new Request("DELETE", "/_data_stream/" + dataStreamName))); + } + + private void deleteIndex(String user, String indexName) throws IOException { + assertOK(performRequest(user, new Request("DELETE", "/" + indexName))); + } + private Response performRequest(String user, Request request) throws IOException { request.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", basicAuthHeaderValue(user, PASSWORD)).build()); return client().performRequest(request); From ba33b7c9c01787567fe53b7d25cd3dbd8156f000 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Thu, 6 Mar 2025 11:48:35 +0100 Subject: [PATCH 062/131] Write tests --- .../FailureStoreSecurityRestIT.java | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java index b8e50942b60cb..416d7267a0295 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java @@ -224,7 +224,7 @@ public void testFailureStoreAccess() throws IOException { }"""), manageFailureStoreRole); createTemplates(); - List docIds = populateDataStreamWithBulkRequest(); + List docIds = populateDataStream(); assertThat(docIds.size(), equalTo(2)); assertThat(docIds, hasItem("1")); String successDocId = "1"; @@ -352,6 +352,8 @@ public void testFailureStoreAccess() throws IOException { expectThrows(() -> deleteIndex(MANAGE_FAILURE_STORE_ACCESS_USER, failureIndexName), 400); expectThrows403(() -> deleteDataStream(MANAGE_FAILURE_STORE_ACCESS_USER, dataIndexName)); + expectThrows(() -> deleteDataStream(MANAGE_FAILURE_STORE_ACCESS_USER, "test1"), 403); + expectThrows(() -> deleteDataStream(MANAGE_FAILURE_STORE_ACCESS_USER, "test1::failures"), 403); // manage user can delete data stream deleteDataStream(MANAGE_ACCESS_USER, "test1"); @@ -440,6 +442,34 @@ private void createTemplates() throws IOException { assertOK(adminClient().performRequest(indexTemplateRequest)); } + private List populateDataStream() throws IOException { + return randomBoolean() ? populateDataStreamWithBulkRequest() : populateDataStreamWithDocRequests(); + } + + @SuppressWarnings("unchecked") + private List populateDataStreamWithDocRequests() throws IOException { + var bulkRequest = new Request("POST", "/_bulk?refresh=true"); + bulkRequest.setJsonEntity(""" + { "create" : { "_index" : "test1", "_id" : "1" } } + { "@timestamp": 1, "age" : 1, "name" : "jack", "email" : "jack@example.com" } + { "create" : { "_index" : "test1", "_id" : "2" } } + { "@timestamp": 2, "age" : "this should be an int", "name" : "jack", "email" : "jack@example.com" } + """); + Response response = performRequest(WRITE_ACCESS_USER, bulkRequest); + assertOK(response); + // we need this dance because the ID for the failed document is random, **not** 2 + Map stringObjectMap = responseAsMap(response); + List items = (List) stringObjectMap.get("items"); + List ids = new ArrayList<>(); + for (Object item : items) { + Map itemMap = (Map) item; + Map create = (Map) itemMap.get("create"); + assertThat(create.get("status"), equalTo(201)); + ids.add((String) create.get("_id")); + } + return ids; + } + @SuppressWarnings("unchecked") private List populateDataStreamWithBulkRequest() throws IOException { var bulkRequest = new Request("POST", "/_bulk?refresh=true"); From 1e33a1f85a4d917e9611be6221b298407f09300c Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Thu, 6 Mar 2025 12:58:45 +0100 Subject: [PATCH 063/131] Moar --- .../security/failurestore/FailureStoreSecurityRestIT.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java index 416d7267a0295..9855077333094 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java @@ -305,7 +305,6 @@ public void testFailureStoreAccess() throws IOException { expectThrows403(() -> performRequest(DATA_ACCESS_USER, new Request("GET", "/test1::failures/_search"))); expectThrows403(() -> performRequest(DATA_ACCESS_USER, new Request("GET", "/test2::failures/_search"))); expectThrows403(() -> performRequest(DATA_ACCESS_USER, new Request("GET", "/" + failureIndexName + "/_search"))); - // TODO is this correct? assertEmpty(performRequest(DATA_ACCESS_USER, new Request("GET", "/.fs*/_search"))); assertEmpty(performRequest(DATA_ACCESS_USER, new Request("GET", "/*1::failures/_search"))); @@ -342,8 +341,6 @@ public void testFailureStoreAccess() throws IOException { assertEmpty(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test12/_search?ignore_unavailable=true"))); assertEmpty(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test2/_search?ignore_unavailable=true"))); - // TODO extra make sure manage_failure_store CANNOT delete the whole data stream - // user with manage access to data stream does NOT get direct access to failure index expectThrows403(() -> deleteIndex(MANAGE_ACCESS_USER, failureIndexName)); expectThrows(() -> deleteIndex(MANAGE_ACCESS_USER, dataIndexName), 400); @@ -358,7 +355,9 @@ public void testFailureStoreAccess() throws IOException { deleteDataStream(MANAGE_ACCESS_USER, "test1"); expectThrows404(() -> performRequest(BOTH_ACCESS_USER, new Request("GET", "/test1/_search"))); + expectThrows404(() -> adminClient().performRequest(new Request("GET", "/" + dataIndexName + "/_search"))); expectThrows404(() -> performRequest(BOTH_ACCESS_USER, new Request("GET", "/test1::failures/_search"))); + expectThrows404(() -> adminClient().performRequest(new Request("GET", "/" + failureIndexName + "/_search"))); } private static void expectThrows404(ThrowingRunnable get) { From 2bd7437432c469c8d5b35ee4b4b497c87e346917 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Thu, 6 Mar 2025 16:19:13 +0100 Subject: [PATCH 064/131] WIP clean up tests --- .../FailureStoreSecurityRestIT.java | 555 +++++++++++++++--- 1 file changed, 469 insertions(+), 86 deletions(-) diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java index 9855077333094..8c307499f79dd 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java @@ -227,8 +227,8 @@ public void testFailureStoreAccess() throws IOException { List docIds = populateDataStream(); assertThat(docIds.size(), equalTo(2)); assertThat(docIds, hasItem("1")); - String successDocId = "1"; - String failedDocId = docIds.stream().filter(id -> false == id.equals(successDocId)).findFirst().get(); + String dataDocId = "1"; + String failuresDocId = docIds.stream().filter(id -> false == id.equals(dataDocId)).findFirst().get(); Request dataStream = new Request("GET", "/_data_stream/test1"); Response response = adminClient().performRequest(dataStream); @@ -245,101 +245,465 @@ public void testFailureStoreAccess() throws IOException { String dataIndexName = dataIndexNames.get(0); String failureIndexName = failureIndexNames.get(0); + Request aliasRequest = new Request("POST", "/_aliases"); + aliasRequest.setJsonEntity(""" + { + "actions": [ + { + "add": { + "index": "test1", + "alias": "test-alias" + } + } + ] + } + """); + assertOK(adminClient().performRequest(aliasRequest)); + // `*` with read access user _can_ read concrete failure index with only read - assertContainsDocIds(performRequest(STAR_READ_ONLY_USER, new Request("GET", "/" + failureIndexName + "/_search")), failedDocId); + expectDocIds(performRequest(STAR_READ_ONLY_USER, new Request("GET", "/" + failureIndexName + "/_search")), failuresDocId); // user with access to failures index - assertContainsDocIds(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test1::failures/_search")), failedDocId); - assertContainsDocIds(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test*::failures/_search")), failedDocId); - assertContainsDocIds(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/*1::failures/_search")), failedDocId); - assertContainsDocIds(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/*::failures/_search")), failedDocId); - assertContainsDocIds(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/.fs*/_search")), failedDocId); - assertContainsDocIds( - performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/" + failureIndexName + "/_search")), - failedDocId - ); - assertContainsDocIds( + expectDocIds(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test-alias::failures/_search")), failuresDocId); + expectDocIds(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test1::failures/_search")), failuresDocId); + expectDocIds(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test*::failures/_search")), failuresDocId); + expectDocIds(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/*1::failures/_search")), failuresDocId); + expectDocIds(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/*::failures/_search")), failuresDocId); + expectDocIds(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/.fs*/_search")), failuresDocId); + expectDocIds(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/" + failureIndexName + "/_search")), failuresDocId); + expectDocIds( performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/" + failureIndexName + "/_search?ignore_unavailable=true")), - failedDocId + failuresDocId ); + // todo also add superuser + List users = List.of(DATA_ACCESS_USER, FAILURE_STORE_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER); + + // search data + { + Request request = searchRequest("test1"); + for (var user : users) { + switch (user) { + case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: + expectDocIds(performRequest(user, request), dataDocId); + break; + case FAILURE_STORE_ACCESS_USER: + expectThrows403(() -> performRequest(user, request)); + break; + default: + fail("must cover user: " + user); + } + } + } + { + Request request = searchRequest("test1", "?ignore_unavailable=true"); + for (var user : users) { + switch (user) { + case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: + expectDocIds(performRequest(user, request), dataDocId); + break; + case FAILURE_STORE_ACCESS_USER: + expectEmpty(performRequest(user, request)); + break; + default: + fail("must cover user: " + user); + } + } + } + { + Request request = searchRequest("test-alias"); + for (var user : users) { + switch (user) { + case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: + expectDocIds(performRequest(user, request), dataDocId); + break; + case FAILURE_STORE_ACCESS_USER: + expectThrows403(() -> performRequest(user, request)); + break; + default: + fail("must cover user: " + user); + } + } + } + { + Request request = searchRequest("test-alias", "?ignore_unavailable=true"); + for (var user : users) { + switch (user) { + case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: + expectDocIds(performRequest(user, request), dataDocId); + break; + case FAILURE_STORE_ACCESS_USER: + expectEmpty(performRequest(user, request)); + break; + default: + fail("must cover user: " + user); + } + } + } + { + Request request = searchRequest("test*"); + for (var user : users) { + switch (user) { + case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: + expectDocIds(performRequest(user, request), dataDocId); + break; + case FAILURE_STORE_ACCESS_USER: + expectEmpty(performRequest(user, request)); + break; + default: + fail("must cover user: " + user); + } + } + } + { + Request request = searchRequest("*1"); + for (var user : users) { + switch (user) { + case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: + expectDocIds(performRequest(user, request), dataDocId); + break; + case FAILURE_STORE_ACCESS_USER: + expectEmpty(performRequest(user, request)); + break; + default: + fail("must cover user: " + user); + } + } + } + { + Request request = searchRequest("*"); + for (var user : users) { + switch (user) { + case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: + expectDocIds(performRequest(user, request), dataDocId); + break; + case FAILURE_STORE_ACCESS_USER: + expectEmpty(performRequest(user, request)); + break; + default: + fail("must cover user: " + user); + } + } + } + { + Request request = searchRequest(".ds*"); + for (var user : users) { + switch (user) { + case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: + expectDocIds(performRequest(user, request), dataDocId); + break; + case FAILURE_STORE_ACCESS_USER: + expectEmpty(performRequest(user, request)); + break; + default: + fail("must cover user: " + user); + } + } + } + { + Request request = searchRequest(dataIndexName); + for (var user : users) { + switch (user) { + case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: + expectDocIds(performRequest(user, request), dataDocId); + break; + case FAILURE_STORE_ACCESS_USER: + expectThrows403(() -> performRequest(user, request)); + break; + default: + fail("must cover user: " + user); + } + } + } + { + Request request = searchRequest(dataIndexName, "?ignore_unavailable=true"); + for (var user : users) { + switch (user) { + case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: + expectDocIds(performRequest(user, request), dataDocId); + break; + case FAILURE_STORE_ACCESS_USER: + expectEmpty(performRequest(user, request)); + break; + default: + fail("must cover user: " + user); + } + } + } + { + Request request = searchRequest("test2"); + for (var user : users) { + switch (user) { + case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: + expectThrows404(() -> performRequest(user, request)); + break; + case FAILURE_STORE_ACCESS_USER: + expectThrows403(() -> performRequest(user, request)); + break; + default: + fail("must cover user: " + user); + } + } + } + { + Request request = searchRequest("test2", "?ignore_unavailable=true"); + for (var user : users) { + switch (user) { + case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER, FAILURE_STORE_ACCESS_USER: + expectEmpty(performRequest(user, request)); + break; + default: + fail("must cover user: " + user); + } + } + } + + // search failures + { + Request request = searchRequest("test1::failures"); + for (var user : users) { + switch (user) { + case DATA_ACCESS_USER, STAR_READ_ONLY_USER: + expectThrows403(() -> performRequest(user, request)); + break; + case FAILURE_STORE_ACCESS_USER, BOTH_ACCESS_USER: + expectDocIds(performRequest(user, request), failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } + { + Request request = searchRequest("test1::failures", "?ignore_unavailable=true"); + for (var user : users) { + switch (user) { + case DATA_ACCESS_USER, STAR_READ_ONLY_USER: + expectEmpty(performRequest(user, request)); + break; + case FAILURE_STORE_ACCESS_USER, BOTH_ACCESS_USER: + expectDocIds(performRequest(user, request), failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } + { + Request request = searchRequest("test-alias::failures"); + for (var user : users) { + switch (user) { + case DATA_ACCESS_USER, STAR_READ_ONLY_USER: + expectThrows403(() -> performRequest(user, request)); + break; + case FAILURE_STORE_ACCESS_USER, BOTH_ACCESS_USER: + expectDocIds(performRequest(user, request), failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } + { + Request request = searchRequest("test-alias::failures", "?ignore_unavailable=true"); + for (var user : users) { + switch (user) { + case DATA_ACCESS_USER, STAR_READ_ONLY_USER: + expectEmpty(performRequest(user, request)); + break; + case FAILURE_STORE_ACCESS_USER, BOTH_ACCESS_USER: + expectDocIds(performRequest(user, request), failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } + { + Request request = searchRequest("test*::failures"); + for (var user : users) { + switch (user) { + case DATA_ACCESS_USER, STAR_READ_ONLY_USER: + expectEmpty(performRequest(user, request)); + break; + case FAILURE_STORE_ACCESS_USER, BOTH_ACCESS_USER: + expectDocIds(performRequest(user, request), failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } + { + Request request = searchRequest("*1::failures"); + for (var user : users) { + switch (user) { + case DATA_ACCESS_USER, STAR_READ_ONLY_USER: + expectEmpty(performRequest(user, request)); + break; + case FAILURE_STORE_ACCESS_USER, BOTH_ACCESS_USER: + expectDocIds(performRequest(user, request), failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } + { + Request request = searchRequest("*::failures"); + for (var user : users) { + switch (user) { + case DATA_ACCESS_USER, STAR_READ_ONLY_USER: + expectEmpty(performRequest(user, request)); + break; + case FAILURE_STORE_ACCESS_USER, BOTH_ACCESS_USER: + expectDocIds(performRequest(user, request), failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } + { + Request request = searchRequest(".fs*"); + for (var user : users) { + switch (user) { + case DATA_ACCESS_USER: + expectEmpty(performRequest(user, request)); + break; + case FAILURE_STORE_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: + expectDocIds(performRequest(user, request), failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } + { + Request request = searchRequest(failureIndexName); + for (var user : users) { + switch (user) { + case DATA_ACCESS_USER: + expectThrows403(() -> performRequest(user, request)); + break; + case FAILURE_STORE_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: + expectDocIds(performRequest(user, request), failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } + { + Request request = searchRequest(failureIndexName, "?ignore_unavailable=true"); + for (var user : users) { + switch (user) { + case DATA_ACCESS_USER: + // TODO fix + // expectEmpty(performRequest(user, request)); + break; + case FAILURE_STORE_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: + expectDocIds(performRequest(user, request), failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } + { + Request request = searchRequest("test2::failures"); + for (var user : users) { + switch (user) { + case DATA_ACCESS_USER, STAR_READ_ONLY_USER: + expectThrows403(() -> performRequest(user, request)); + break; + case FAILURE_STORE_ACCESS_USER, BOTH_ACCESS_USER: + expectThrows404(() -> performRequest(user, request)); + break; + default: + fail("must cover user: " + user); + } + } + } + { + Request request = searchRequest("test2::failures", "?ignore_unavailable=true"); + for (var user : users) { + switch (user) { + case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER, FAILURE_STORE_ACCESS_USER: + expectEmpty(performRequest(user, request)); + break; + default: + fail("must cover user: " + user); + } + } + } + expectThrows404(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test12::failures/_search"))); + expectThrows404(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test2::failures/_search"))); - expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test1::data/_search"))); - expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test1/_search"))); expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test2::data/_search"))); expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test2/_search"))); expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/" + dataIndexName + "/_search"))); expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test1,test1::failures/_search"))); - assertEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test1::data/_search?ignore_unavailable=true"))); - assertEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test1/_search?ignore_unavailable=true"))); - assertEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test2::data/_search?ignore_unavailable=true"))); - assertEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test2/_search?ignore_unavailable=true"))); - assertEmpty( + expectEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test1::data/_search?ignore_unavailable=true"))); + expectEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test1/_search?ignore_unavailable=true"))); + expectEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test2::data/_search?ignore_unavailable=true"))); + expectEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test2/_search?ignore_unavailable=true"))); + expectEmpty( performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/" + dataIndexName + "/_search?ignore_unavailable=true")) ); - assertEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/.ds*/_search"))); - assertEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/*1::data/_search"))); - assertEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/*1/_search"))); - - // user with access to data index - assertContainsDocIds(performRequest(DATA_ACCESS_USER, new Request("GET", "/test1/_search")), successDocId); - assertContainsDocIds(performRequest(DATA_ACCESS_USER, new Request("GET", "/test*/_search")), successDocId); - assertContainsDocIds(performRequest(DATA_ACCESS_USER, new Request("GET", "/*1/_search")), successDocId); - assertContainsDocIds(performRequest(DATA_ACCESS_USER, new Request("GET", "/*/_search")), successDocId); - assertContainsDocIds(performRequest(DATA_ACCESS_USER, new Request("GET", "/.ds*/_search")), successDocId); - assertContainsDocIds(performRequest(DATA_ACCESS_USER, new Request("GET", "/" + dataIndexName + "/_search")), successDocId); - assertContainsDocIds( - performRequest(DATA_ACCESS_USER, new Request("GET", "/" + dataIndexName + "/_search?ignore_unavailable=true")), - successDocId - ); + expectEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/.ds*/_search"))); + expectEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/*1::data/_search"))); + expectEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/*1/_search"))); expectThrows404(() -> performRequest(DATA_ACCESS_USER, new Request("GET", "/test12/_search"))); expectThrows404(() -> performRequest(DATA_ACCESS_USER, new Request("GET", "/test2/_search"))); - assertEmpty(performRequest(DATA_ACCESS_USER, new Request("GET", "/test1::failures/_search?ignore_unavailable=true"))); - assertEmpty(performRequest(DATA_ACCESS_USER, new Request("GET", "/test2::failures/_search?ignore_unavailable=true"))); + expectEmpty(performRequest(DATA_ACCESS_USER, new Request("GET", "/test1::failures/_search?ignore_unavailable=true"))); + expectEmpty(performRequest(DATA_ACCESS_USER, new Request("GET", "/test2::failures/_search?ignore_unavailable=true"))); expectThrows403(() -> performRequest(DATA_ACCESS_USER, new Request("GET", "/test1::failures/_search"))); expectThrows403(() -> performRequest(DATA_ACCESS_USER, new Request("GET", "/test2::failures/_search"))); expectThrows403(() -> performRequest(DATA_ACCESS_USER, new Request("GET", "/" + failureIndexName + "/_search"))); - assertEmpty(performRequest(DATA_ACCESS_USER, new Request("GET", "/.fs*/_search"))); - assertEmpty(performRequest(DATA_ACCESS_USER, new Request("GET", "/*1::failures/_search"))); + expectEmpty(performRequest(DATA_ACCESS_USER, new Request("GET", "/.fs*/_search"))); + expectEmpty(performRequest(DATA_ACCESS_USER, new Request("GET", "/*1::failures/_search"))); // user with access to everything - assertContainsDocIds(adminClient().performRequest(new Request("GET", "/test1::failures/_search")), failedDocId); - assertContainsDocIds(adminClient().performRequest(new Request("GET", "/test*::failures/_search")), failedDocId); - assertContainsDocIds(adminClient().performRequest(new Request("GET", "/*1::failures/_search")), failedDocId); - assertContainsDocIds(adminClient().performRequest(new Request("GET", "/*::failures/_search")), failedDocId); - assertContainsDocIds(adminClient().performRequest(new Request("GET", "/.fs*/_search")), failedDocId); + expectDocIds(adminClient().performRequest(new Request("GET", "/test1::failures/_search")), failuresDocId); + expectDocIds(adminClient().performRequest(new Request("GET", "/test*::failures/_search")), failuresDocId); + expectDocIds(adminClient().performRequest(new Request("GET", "/*1::failures/_search")), failuresDocId); + expectDocIds(adminClient().performRequest(new Request("GET", "/*::failures/_search")), failuresDocId); + expectDocIds(adminClient().performRequest(new Request("GET", "/.fs*/_search")), failuresDocId); expectThrows404(() -> adminClient().performRequest(new Request("GET", "/test12::failures/_search"))); expectThrows404(() -> adminClient().performRequest(new Request("GET", "/test2::failures/_search"))); - assertEmpty(adminClient().performRequest(new Request("GET", "/test12::failures/_search?ignore_unavailable=true"))); - assertEmpty(adminClient().performRequest(new Request("GET", "/test2::failures/_search?ignore_unavailable=true"))); + expectEmpty(adminClient().performRequest(new Request("GET", "/test12::failures/_search?ignore_unavailable=true"))); + expectEmpty(adminClient().performRequest(new Request("GET", "/test2::failures/_search?ignore_unavailable=true"))); - assertContainsDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test1::failures/_search")), failedDocId); - assertContainsDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test*::failures/_search")), failedDocId); - assertContainsDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/*1::failures/_search")), failedDocId); - assertContainsDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/*::failures/_search")), failedDocId); - assertContainsDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/.fs*/_search")), failedDocId); + expectDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test1::failures/_search")), failuresDocId); + expectDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test*::failures/_search")), failuresDocId); + expectDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/*1::failures/_search")), failuresDocId); + expectDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/*::failures/_search")), failuresDocId); + expectDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/.fs*/_search")), failuresDocId); expectThrows404(() -> performRequest(BOTH_ACCESS_USER, new Request("GET", "/test12::failures/_search"))); expectThrows404(() -> performRequest(BOTH_ACCESS_USER, new Request("GET", "/test2::failures/_search"))); - assertEmpty(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test12::failures/_search?ignore_unavailable=true"))); - assertEmpty(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test2::failures/_search?ignore_unavailable=true"))); + expectEmpty(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test12::failures/_search?ignore_unavailable=true"))); + expectEmpty(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test2::failures/_search?ignore_unavailable=true"))); + + expectDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test1/_search")), dataDocId); + expectDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test*/_search")), dataDocId); + expectDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/*1/_search")), dataDocId); + expectDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/*/_search")), dataDocId); - assertContainsDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test1/_search")), successDocId); - assertContainsDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test*/_search")), successDocId); - assertContainsDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/*1/_search")), successDocId); - assertContainsDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/*/_search")), successDocId); + expectEmpty(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test12/_search?ignore_unavailable=true"))); + expectEmpty(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test2/_search?ignore_unavailable=true"))); - assertEmpty(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test12/_search?ignore_unavailable=true"))); - assertEmpty(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test2/_search?ignore_unavailable=true"))); + // destructive operations below // user with manage access to data stream does NOT get direct access to failure index expectThrows403(() -> deleteIndex(MANAGE_ACCESS_USER, failureIndexName)); @@ -360,21 +724,29 @@ public void testFailureStoreAccess() throws IOException { expectThrows404(() -> adminClient().performRequest(new Request("GET", "/" + failureIndexName + "/_search"))); } - private static void expectThrows404(ThrowingRunnable get) { - expectThrows(get, 404); + private Request searchRequest(String searchTarget) { + return searchRequest(searchTarget, ""); + } + + private Request searchRequest(String searchTarget, String pathParamString) { + return new Request("GET", Strings.format("/%s/_search%s", searchTarget, pathParamString)); + } + + private static void expectThrows404(ThrowingRunnable runnable) { + expectThrows(runnable, 404); } - private static void expectThrows403(ThrowingRunnable get) { - expectThrows(get, 403); + private static void expectThrows403(ThrowingRunnable runnable) { + expectThrows(runnable, 403); } - private static void expectThrows(ThrowingRunnable get, int statusCode) { - var ex = expectThrows(ResponseException.class, get); + private static void expectThrows(ThrowingRunnable runnable, int statusCode) { + var ex = expectThrows(ResponseException.class, runnable); assertThat(ex.getResponse().getStatusLine().getStatusCode(), equalTo(statusCode)); } @SuppressWarnings("unchecked") - private static void assertContainsDocIds(Response response, String... docIds) throws IOException { + private static void expectDocIds(Response response, String... docIds) throws IOException { assertOK(response); final SearchResponse searchResponse = SearchResponseUtils.parseSearchResponse(responseAsParser(response)); try { @@ -387,7 +759,7 @@ private static void assertContainsDocIds(Response response, String... docIds) th } } - private static void assertEmpty(Response response) throws IOException { + private static void expectEmpty(Response response) throws IOException { assertOK(response); final SearchResponse searchResponse = SearchResponseUtils.parseSearchResponse(responseAsParser(response)); try { @@ -445,27 +817,38 @@ private List populateDataStream() throws IOException { return randomBoolean() ? populateDataStreamWithBulkRequest() : populateDataStreamWithDocRequests(); } - @SuppressWarnings("unchecked") private List populateDataStreamWithDocRequests() throws IOException { - var bulkRequest = new Request("POST", "/_bulk?refresh=true"); - bulkRequest.setJsonEntity(""" - { "create" : { "_index" : "test1", "_id" : "1" } } - { "@timestamp": 1, "age" : 1, "name" : "jack", "email" : "jack@example.com" } - { "create" : { "_index" : "test1", "_id" : "2" } } - { "@timestamp": 2, "age" : "this should be an int", "name" : "jack", "email" : "jack@example.com" } + List ids = new ArrayList<>(); + + var dataStreamName = "test1"; + var docRequest = new Request("PUT", "/" + dataStreamName + "/_doc/1?refresh=true&op_type=create"); + docRequest.setJsonEntity(""" + { + "@timestamp": 1, + "age" : 1, + "name" : "jack", + "email" : "jack@example.com" + } """); - Response response = performRequest(WRITE_ACCESS_USER, bulkRequest); + Response response = performRequest(WRITE_ACCESS_USER, docRequest); assertOK(response); - // we need this dance because the ID for the failed document is random, **not** 2 - Map stringObjectMap = responseAsMap(response); - List items = (List) stringObjectMap.get("items"); - List ids = new ArrayList<>(); - for (Object item : items) { - Map itemMap = (Map) item; - Map create = (Map) itemMap.get("create"); - assertThat(create.get("status"), equalTo(201)); - ids.add((String) create.get("_id")); - } + Map responseAsMap = responseAsMap(response); + ids.add((String) responseAsMap.get("_id")); + + docRequest = new Request("PUT", "/" + dataStreamName + "/_doc/2?refresh=true&op_type=create"); + docRequest.setJsonEntity(""" + { + "@timestamp": 2, + "age" : "this should be an int", + "name" : "jack", + "email" : "jack@example.com" + } + """); + response = performRequest(WRITE_ACCESS_USER, docRequest); + assertOK(response); + responseAsMap = responseAsMap(response); + ids.add((String) responseAsMap.get("_id")); + return ids; } From abcb1a3c6f9678578c9ef1af21ab7357be9a5127 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Thu, 6 Mar 2025 17:52:32 +0100 Subject: [PATCH 065/131] Moar --- .../FailureStoreSecurityRestIT.java | 134 +++++++----------- .../xpack/security/authz/RBACEngine.java | 9 +- 2 files changed, 55 insertions(+), 88 deletions(-) diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java index 8c307499f79dd..8e1cf3309ee49 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java @@ -260,22 +260,6 @@ public void testFailureStoreAccess() throws IOException { """); assertOK(adminClient().performRequest(aliasRequest)); - // `*` with read access user _can_ read concrete failure index with only read - expectDocIds(performRequest(STAR_READ_ONLY_USER, new Request("GET", "/" + failureIndexName + "/_search")), failuresDocId); - - // user with access to failures index - expectDocIds(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test-alias::failures/_search")), failuresDocId); - expectDocIds(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test1::failures/_search")), failuresDocId); - expectDocIds(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test*::failures/_search")), failuresDocId); - expectDocIds(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/*1::failures/_search")), failuresDocId); - expectDocIds(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/*::failures/_search")), failuresDocId); - expectDocIds(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/.fs*/_search")), failuresDocId); - expectDocIds(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/" + failureIndexName + "/_search")), failuresDocId); - expectDocIds( - performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/" + failureIndexName + "/_search?ignore_unavailable=true")), - failuresDocId - ); - // todo also add superuser List users = List.of(DATA_ACCESS_USER, FAILURE_STORE_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER); @@ -599,8 +583,7 @@ public void testFailureStoreAccess() throws IOException { for (var user : users) { switch (user) { case DATA_ACCESS_USER: - // TODO fix - // expectEmpty(performRequest(user, request)); + expectEmpty(performRequest(user, request)); break; case FAILURE_STORE_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: expectDocIds(performRequest(user, request), failuresDocId); @@ -638,72 +621,57 @@ public void testFailureStoreAccess() throws IOException { } } - expectThrows404(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test12::failures/_search"))); - - expectThrows404(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test2::failures/_search"))); - - expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test2::data/_search"))); - expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test2/_search"))); - expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/" + dataIndexName + "/_search"))); - expectThrows403(() -> performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test1,test1::failures/_search"))); - - expectEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test1::data/_search?ignore_unavailable=true"))); - expectEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test1/_search?ignore_unavailable=true"))); - expectEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test2::data/_search?ignore_unavailable=true"))); - expectEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/test2/_search?ignore_unavailable=true"))); - expectEmpty( - performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/" + dataIndexName + "/_search?ignore_unavailable=true")) - ); - expectEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/.ds*/_search"))); - expectEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/*1::data/_search"))); - expectEmpty(performRequest(FAILURE_STORE_ACCESS_USER, new Request("GET", "/*1/_search"))); - - expectThrows404(() -> performRequest(DATA_ACCESS_USER, new Request("GET", "/test12/_search"))); - expectThrows404(() -> performRequest(DATA_ACCESS_USER, new Request("GET", "/test2/_search"))); - - expectEmpty(performRequest(DATA_ACCESS_USER, new Request("GET", "/test1::failures/_search?ignore_unavailable=true"))); - expectEmpty(performRequest(DATA_ACCESS_USER, new Request("GET", "/test2::failures/_search?ignore_unavailable=true"))); - - expectThrows403(() -> performRequest(DATA_ACCESS_USER, new Request("GET", "/test1::failures/_search"))); - expectThrows403(() -> performRequest(DATA_ACCESS_USER, new Request("GET", "/test2::failures/_search"))); - expectThrows403(() -> performRequest(DATA_ACCESS_USER, new Request("GET", "/" + failureIndexName + "/_search"))); - expectEmpty(performRequest(DATA_ACCESS_USER, new Request("GET", "/.fs*/_search"))); - expectEmpty(performRequest(DATA_ACCESS_USER, new Request("GET", "/*1::failures/_search"))); - - // user with access to everything - expectDocIds(adminClient().performRequest(new Request("GET", "/test1::failures/_search")), failuresDocId); - expectDocIds(adminClient().performRequest(new Request("GET", "/test*::failures/_search")), failuresDocId); - expectDocIds(adminClient().performRequest(new Request("GET", "/*1::failures/_search")), failuresDocId); - expectDocIds(adminClient().performRequest(new Request("GET", "/*::failures/_search")), failuresDocId); - expectDocIds(adminClient().performRequest(new Request("GET", "/.fs*/_search")), failuresDocId); - - expectThrows404(() -> adminClient().performRequest(new Request("GET", "/test12::failures/_search"))); - expectThrows404(() -> adminClient().performRequest(new Request("GET", "/test2::failures/_search"))); - - expectEmpty(adminClient().performRequest(new Request("GET", "/test12::failures/_search?ignore_unavailable=true"))); - expectEmpty(adminClient().performRequest(new Request("GET", "/test2::failures/_search?ignore_unavailable=true"))); - - expectDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test1::failures/_search")), failuresDocId); - expectDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test*::failures/_search")), failuresDocId); - expectDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/*1::failures/_search")), failuresDocId); - expectDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/*::failures/_search")), failuresDocId); - expectDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/.fs*/_search")), failuresDocId); - - expectThrows404(() -> performRequest(BOTH_ACCESS_USER, new Request("GET", "/test12::failures/_search"))); - expectThrows404(() -> performRequest(BOTH_ACCESS_USER, new Request("GET", "/test2::failures/_search"))); - - expectEmpty(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test12::failures/_search?ignore_unavailable=true"))); - expectEmpty(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test2::failures/_search?ignore_unavailable=true"))); - - expectDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test1/_search")), dataDocId); - expectDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test*/_search")), dataDocId); - expectDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/*1/_search")), dataDocId); - expectDocIds(performRequest(BOTH_ACCESS_USER, new Request("GET", "/*/_search")), dataDocId); - - expectEmpty(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test12/_search?ignore_unavailable=true"))); - expectEmpty(performRequest(BOTH_ACCESS_USER, new Request("GET", "/test2/_search?ignore_unavailable=true"))); + // mixed access + { + Request request = searchRequest("test1,test1::failures"); + for (var user : users) { + switch (user) { + case DATA_ACCESS_USER, FAILURE_STORE_ACCESS_USER, STAR_READ_ONLY_USER: + expectThrows403(() -> performRequest(user, request)); + break; + case BOTH_ACCESS_USER: + expectDocIds(performRequest(user, request), dataDocId, failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } + { + Request request = searchRequest("test1,test1::failures", "?ignore_unavailable=true"); + for (var user : users) { + switch (user) { + case DATA_ACCESS_USER, STAR_READ_ONLY_USER: + expectDocIds(performRequest(user, request), dataDocId); + break; + case FAILURE_STORE_ACCESS_USER: + expectDocIds(performRequest(user, request), failuresDocId); + break; + case BOTH_ACCESS_USER: + expectDocIds(performRequest(user, request), dataDocId, failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } + { + Request request = searchRequest("test1,*::failures"); + for (var user : users) { + switch (user) { + case DATA_ACCESS_USER, FAILURE_STORE_ACCESS_USER, STAR_READ_ONLY_USER: + expectThrows403(() -> performRequest(user, request)); + break; + case BOTH_ACCESS_USER: + expectDocIds(performRequest(user, request), dataDocId, failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } - // destructive operations below + // write operations below // user with manage access to data stream does NOT get direct access to failure index expectThrows403(() -> deleteIndex(MANAGE_ACCESS_USER, failureIndexName)); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java index e202c9711b560..5b597d05baec2 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java @@ -926,16 +926,15 @@ static AuthorizedIndices resolveAuthorizedIndicesFromRole( // the action handler must handle the case of accessing resources that do not exist return predicate.test(name, null); } else { - // TODO remove selector, add selector, remove selector, selectors, selectors, selectors - if (indexAbstraction.isFailureIndexOfDataStream() - && predicate.test( + if (indexAbstraction.isFailureIndexOfDataStream()) { + // TODO remove selector, add selector, remove selector, selectors, selectors, selectors + return predicate.test( IndexNameExpressionResolver.combineSelector( indexAbstraction.getParentDataStream().getName(), IndexComponentSelector.FAILURES ), indexAbstraction.getParentDataStream() - )) { - return true; + ) || predicate.test(indexAbstraction); } // We check the parent data stream first if there is one. For testing requested indices, this is most likely // more efficient than checking the index name first because we recommend grant privileges over data stream From e555f7468b592ec9942d456135abb21726c6e617 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Thu, 6 Mar 2025 18:12:39 +0100 Subject: [PATCH 066/131] More tests --- .../FailureStoreSecurityRestIT.java | 77 ++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java index 8e1cf3309ee49..f2d065d51e221 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java @@ -659,9 +659,84 @@ public void testFailureStoreAccess() throws IOException { Request request = searchRequest("test1,*::failures"); for (var user : users) { switch (user) { - case DATA_ACCESS_USER, FAILURE_STORE_ACCESS_USER, STAR_READ_ONLY_USER: + case FAILURE_STORE_ACCESS_USER: expectThrows403(() -> performRequest(user, request)); break; + case DATA_ACCESS_USER, STAR_READ_ONLY_USER: + expectDocIds(performRequest(user, request), dataDocId); + break; + case BOTH_ACCESS_USER: + expectDocIds(performRequest(user, request), dataDocId, failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } + { + Request request = searchRequest("test1,*::failures", "?ignore_unavailable=true"); + for (var user : users) { + switch (user) { + case FAILURE_STORE_ACCESS_USER: + expectDocIds(performRequest(user, request), failuresDocId); + break; + case DATA_ACCESS_USER, STAR_READ_ONLY_USER: + expectDocIds(performRequest(user, request), dataDocId); + break; + case BOTH_ACCESS_USER: + expectDocIds(performRequest(user, request), dataDocId, failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } + { + Request request = searchRequest("test1::failures,*"); + for (var user : users) { + switch (user) { + case FAILURE_STORE_ACCESS_USER: + expectDocIds(performRequest(user, request), failuresDocId); + break; + case DATA_ACCESS_USER, STAR_READ_ONLY_USER: + expectThrows403(() -> performRequest(user, request)); + break; + case BOTH_ACCESS_USER: + expectDocIds(performRequest(user, request), dataDocId, failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } + { + Request request = searchRequest("test1::failures,*", "?ignore_unavailable=true"); + for (var user : users) { + switch (user) { + case FAILURE_STORE_ACCESS_USER: + expectDocIds(performRequest(user, request), failuresDocId); + break; + case DATA_ACCESS_USER, STAR_READ_ONLY_USER: + expectDocIds(performRequest(user, request), dataDocId); + break; + case BOTH_ACCESS_USER: + expectDocIds(performRequest(user, request), dataDocId, failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } + { + Request request = searchRequest("*::failures,*"); + for (var user : users) { + switch (user) { + case FAILURE_STORE_ACCESS_USER: + expectDocIds(performRequest(user, request), failuresDocId); + break; + case DATA_ACCESS_USER, STAR_READ_ONLY_USER: + expectDocIds(performRequest(user, request), dataDocId); + break; case BOTH_ACCESS_USER: expectDocIds(performRequest(user, request), dataDocId, failuresDocId); break; From 9aafa69c9f74820e4016afce3b910fed47f29e29 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Thu, 6 Mar 2025 22:43:59 +0100 Subject: [PATCH 067/131] WIP clean up --- .../metadata/IndexAbstractionResolver.java | 38 +++----- .../IndexAbstractionResolverTests.java | 10 ++- .../security/authz/AuthorizationEngine.java | 8 +- .../authz/permission/IndicesPermission.java | 12 +-- .../authz/IndicesAndAliasesResolver.java | 43 +++++----- .../xpack/security/authz/RBACEngine.java | 86 ++++++++++--------- .../authz/IndicesAndAliasesResolverTests.java | 12 ++- 7 files changed, 112 insertions(+), 97 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java index 2e9b508443606..1907ef8526e65 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java @@ -22,7 +22,8 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.function.Predicate; +import java.util.function.BiPredicate; +import java.util.function.Function; import java.util.function.Supplier; public class IndexAbstractionResolver { @@ -37,8 +38,8 @@ public List resolveIndexAbstractions( Iterable indices, IndicesOptions indicesOptions, ProjectMetadata projectMetadata, - Supplier> allAuthorizedAndAvailable, - Predicate isAuthorized, + Function>> allAuthorizedAndAvailableBySelector, + BiPredicate isAuthorized, boolean includeDataStreams ) { List finalIndices = new ArrayList<>(); @@ -71,12 +72,8 @@ public List resolveIndexAbstractions( if (indicesOptions.expandWildcardExpressions() && Regex.isSimpleMatchPattern(indexAbstraction)) { wildcardSeen = true; Set resolvedIndices = new HashSet<>(); - for (String authorizedIndex : allAuthorizedAndAvailable.get()) { - Tuple tuple = IndexNameExpressionResolver.splitSelectorExpression(authorizedIndex); - authorizedIndex = tuple.v1(); - String authorizedSelectorString = tuple.v2(); - if (selectorsMatch(selectorString, authorizedSelectorString) - && Regex.simpleMatch(indexAbstraction, authorizedIndex) + for (String authorizedIndex : allAuthorizedAndAvailableBySelector.apply(selectorString).get()) { + if (Regex.simpleMatch(indexAbstraction, authorizedIndex) && isIndexVisible( indexAbstraction, selectorString, @@ -106,28 +103,17 @@ && isIndexVisible( resolveSelectorsAndCollect(indexAbstraction, selectorString, indicesOptions, resolvedIndices, projectMetadata); if (minus) { finalIndices.removeAll(resolvedIndices); - } else if (indicesOptions.ignoreUnavailable() == false - || isAuthorized.test(IndexNameExpressionResolver.combineSelectorExpression(indexAbstraction, selectorString))) { - // Unauthorized names are considered unavailable, so if `ignoreUnavailable` is `true` they should be silently - // discarded from the `finalIndices` list. Other "ways of unavailable" must be handled by the action - // handler, see: https://github.com/elastic/elasticsearch/issues/90215 - finalIndices.addAll(resolvedIndices); - } + } else if (indicesOptions.ignoreUnavailable() == false || isAuthorized.test(indexAbstraction, selectorString)) { + // Unauthorized names are considered unavailable, so if `ignoreUnavailable` is `true` they should be silently + // discarded from the `finalIndices` list. Other "ways of unavailable" must be handled by the action + // handler, see: https://github.com/elastic/elasticsearch/issues/90215 + finalIndices.addAll(resolvedIndices); + } } } return finalIndices; } - public static boolean selectorsMatch(String selectorString, String authorizedSelectorString) { - IndexComponentSelector selector = IndexComponentSelector.getByKey(selectorString) == null - ? IndexComponentSelector.DATA - : IndexComponentSelector.getByKey(selectorString); - IndexComponentSelector authorizedSelector = IndexComponentSelector.getByKey(authorizedSelectorString) == null - ? IndexComponentSelector.DATA - : IndexComponentSelector.getByKey(authorizedSelectorString); - return selector == authorizedSelector; - } - private static void resolveSelectorsAndCollect( String indexAbstraction, String selectorString, diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolverTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolverTests.java index 96170a3c4d5fd..cbaaf3dcbc12c 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolverTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolverTests.java @@ -367,7 +367,15 @@ private List resolveAbstractionsSelectorAllowed(List expressions } private List resolveAbstractions(List expressions, IndicesOptions indicesOptions, Supplier> mask) { - return indexAbstractionResolver.resolveIndexAbstractions(expressions, indicesOptions, projectMetadata, mask, (idx) -> true, true); + return indexAbstractionResolver.resolveIndexAbstractions( + expressions, + indicesOptions, + projectMetadata, + // TODO + sel -> mask, + (idx, sel) -> true, + true + ); } private boolean isIndexVisible(String index, String selector) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java index 9f18e7915a725..0a4937cf710bf 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java @@ -291,12 +291,16 @@ interface AuthorizedIndices { * Returns all the index-like resource names that are available and accessible for an action type by a user, * at a fixed point in time (for a single cluster state view). */ - Supplier> all(); + Supplier> all(@Nullable String selector); + + default Supplier> all() { + return all(null); + } /** * Checks if an index-like resource name is authorized, for an action by a user. The resource might or might not exist. */ - boolean check(String name); + boolean check(String name, @Nullable String selector); } /** diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 8dab8d1189b73..4296a792c6e62 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -195,7 +195,7 @@ public static class IsResourceAuthorizedPredicate { // public for tests public IsResourceAuthorizedPredicate( StringMatcher resourceNameMatcher, - StringMatcher failureAccessNameMatcher, + StringMatcher failureStoreNameMatcher, StringMatcher additionalNonDatastreamNameMatcher ) { this((String name, @Nullable IndexAbstraction indexAbstraction) -> { @@ -205,7 +205,7 @@ public IsResourceAuthorizedPredicate( assert indexAbstraction == null || indexAbstractionName.equals(indexAbstraction.getName()); if (IndexComponentSelector.FAILURES.equals(selector)) { // TODO assert isPartOfDataStream if indexAbstraction is not null - return failureAccessNameMatcher.test(indexAbstractionName); + return failureStoreNameMatcher.test(indexAbstractionName); } return resourceNameMatcher.test(indexAbstractionName) || (isPartOfDatastream(indexAbstraction) == false && additionalNonDatastreamNameMatcher.test(indexAbstractionName)); @@ -227,11 +227,11 @@ public final IsResourceAuthorizedPredicate and(IsResourceAuthorizedPredicate oth /** * Verifies if access is authorized to the given {@param indexAbstraction} resource. - * The resource must exist. Otherwise, use the {@link #test(String, IndexAbstraction)} method. + * The resource must exist. Otherwise, use the {@link #test(String, IndexComponentSelector, IndexAbstraction)} method. * Returns {@code true} if access to the given resource is authorized or {@code false} otherwise. */ - public final boolean test(IndexAbstraction indexAbstraction) { - return test(indexAbstraction.getName(), indexAbstraction); + public boolean test(IndexAbstraction indexAbstraction, IndexComponentSelector selector) { + return test(indexAbstraction.getName(), selector, indexAbstraction); } /** @@ -240,7 +240,7 @@ public final boolean test(IndexAbstraction indexAbstraction) { * if it doesn't. * Returns {@code true} if access to the given resource is authorized or {@code false} otherwise. */ - public boolean test(String name, @Nullable IndexAbstraction indexAbstraction) { + public boolean test(String name, IndexComponentSelector selector, @Nullable IndexAbstraction indexAbstraction) { return biPredicate.test(name, indexAbstraction); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java index 2e2f05d2e5755..fcb5d5e426c08 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java @@ -48,7 +48,7 @@ import java.util.Set; import java.util.SortedMap; import java.util.concurrent.CopyOnWriteArraySet; -import java.util.function.Predicate; +import java.util.function.BiPredicate; import java.util.function.Supplier; import static org.elasticsearch.xpack.core.security.authz.IndicesAndAliasesResolverField.NO_INDEX_PLACEHOLDER; @@ -323,20 +323,16 @@ ResolvedIndices resolveIndicesAndAliases( ); } if (indicesOptions.expandWildcardExpressions()) { - for (String authorizedIndex : authorizedIndices.all().get()) { - Tuple tuple = IndexNameExpressionResolver.splitSelectorExpression(authorizedIndex); - authorizedIndex = tuple.v1(); - String authorizedSelectorString = tuple.v2(); - if (IndexAbstractionResolver.selectorsMatch(allIndicesPatternSelector, authorizedSelectorString) - && IndexAbstractionResolver.isIndexVisible( - "*", - allIndicesPatternSelector, - authorizedIndex, - indicesOptions, - projectMetadata, - nameExpressionResolver, - indicesRequest.includeDataStreams() - )) { + for (String authorizedIndex : authorizedIndices.all(allIndicesPatternSelector).get()) { + if (IndexAbstractionResolver.isIndexVisible( + "*", + allIndicesPatternSelector, + authorizedIndex, + indicesOptions, + projectMetadata, + nameExpressionResolver, + indicesRequest.includeDataStreams() + )) { resolvedIndicesBuilder.addLocal( IndexNameExpressionResolver.combineSelectorExpression(authorizedIndex, allIndicesPatternSelector) ); @@ -356,7 +352,7 @@ ResolvedIndices resolveIndicesAndAliases( split.getLocal(), indicesOptions, projectMetadata, - authorizedIndices.all(), + authorizedIndices::all, authorizedIndices::check, indicesRequest.includeDataStreams() ); @@ -393,7 +389,7 @@ ResolvedIndices resolveIndicesAndAliases( if (aliasesRequest.expandAliasesWildcards()) { List aliases = replaceWildcardsWithAuthorizedAliases( aliasesRequest.aliases(), - loadAuthorizedAliases(authorizedIndices.all(), projectMetadata) + loadAuthorizedAliases(authorizedIndices.all(null), projectMetadata) ); aliasesRequest.replaceAliases(aliases.toArray(new String[aliases.size()])); } @@ -435,7 +431,11 @@ ResolvedIndices resolveIndicesAndAliases( * request's concrete index is not in the list of authorized indices, then we need to look to * see if this can be authorized against an alias */ - static String getPutMappingIndexOrAlias(PutMappingRequest request, Predicate isAuthorized, ProjectMetadata projectMetadata) { + static String getPutMappingIndexOrAlias( + PutMappingRequest request, + BiPredicate isAuthorized, + ProjectMetadata projectMetadata + ) { final String concreteIndexName = request.getConcreteIndex().getName(); // validate that the concrete index exists, otherwise there is no remapping that we could do @@ -451,7 +451,7 @@ static String getPutMappingIndexOrAlias(PutMappingRequest request, Predicate> foundAliases = projectMetadata.findAllAliases(new String[] { concreteIndexName }); List aliasMetadata = foundAliases.get(concreteIndexName); if (aliasMetadata != null) { - Optional foundAlias = aliasMetadata.stream().map(AliasMetadata::alias).filter(isAuthorized).filter(aliasName -> { + Optional foundAlias = aliasMetadata.stream().map(AliasMetadata::alias).filter(aliasName -> { + if (false == isAuthorized.test(aliasName, null)) { + return false; + } IndexAbstraction alias = projectMetadata.getIndicesLookup().get(aliasName); List indices = alias.getIndices(); if (indices.size() == 1) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java index 5b597d05baec2..005835108646a 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java @@ -36,7 +36,6 @@ import org.elasticsearch.action.termvectors.MultiTermVectorsAction; import org.elasticsearch.cluster.metadata.DataStream; import org.elasticsearch.cluster.metadata.IndexAbstraction; -import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.metadata.ProjectMetadata; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesReference; @@ -44,6 +43,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.CachedSupplier; import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.core.Nullable; import org.elasticsearch.index.Index; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.transport.TransportActionProxy; @@ -109,6 +109,7 @@ import java.util.Objects; import java.util.Set; import java.util.TreeSet; +import java.util.function.BiPredicate; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.function.Supplier; @@ -884,14 +885,7 @@ static AuthorizedIndices resolveAuthorizedIndicesFromRole( // TODO: can this be done smarter? I think there are usually more indices/aliases in the cluster then indices defined a roles? if (includeDataStreams) { for (IndexAbstraction indexAbstraction : lookup.values()) { - boolean dataAccessGranted = predicate.test(indexAbstraction); - // TODO can we also skip if the data stream does not have failure indices? - boolean failureAccessGranted = (indexAbstraction.getType() == IndexAbstraction.Type.DATA_STREAM) - && predicate.test( - IndexNameExpressionResolver.combineSelector(indexAbstraction.getName(), IndexComponentSelector.FAILURES), - indexAbstraction - ); - if (dataAccessGranted) { + if (predicate.test(indexAbstraction, IndexComponentSelector.DATA)) { indicesAndAliases.add(indexAbstraction.getName()); if (indexAbstraction.getType() == IndexAbstraction.Type.DATA_STREAM) { // add data stream and its backing indices for any authorized data streams @@ -900,47 +894,51 @@ static AuthorizedIndices resolveAuthorizedIndicesFromRole( } } } - if (failureAccessGranted) { - // TODO hack hack hack - indicesAndAliases.add( - IndexNameExpressionResolver.combineSelector(indexAbstraction.getName(), IndexComponentSelector.FAILURES) - ); - for (Index index : ((DataStream) indexAbstraction).getFailureIndices()) { - indicesAndAliases.add(index.getName()); - } - } } } else { for (IndexAbstraction indexAbstraction : lookup.values()) { - if (indexAbstraction.getType() != IndexAbstraction.Type.DATA_STREAM && predicate.test(indexAbstraction)) { + if (indexAbstraction.getType() != IndexAbstraction.Type.DATA_STREAM + && predicate.test(indexAbstraction, IndexComponentSelector.DATA)) { indicesAndAliases.add(indexAbstraction.getName()); } } } timeChecker.accept(indicesAndAliases); return indicesAndAliases; - }, name -> { + }, () -> { + if (includeDataStreams == false) { + return Collections.emptySet(); + } + Set indicesAndAliases = new HashSet<>(); + for (IndexAbstraction indexAbstraction : lookup.values()) { + if (indexAbstraction.getType() == IndexAbstraction.Type.DATA_STREAM + && predicate.test(indexAbstraction, IndexComponentSelector.FAILURES)) { + indicesAndAliases.add(indexAbstraction.getName()); + // add data stream and its backing failure indices for any authorized data streams + for (Index index : ((DataStream) indexAbstraction).getFailureIndices()) { + indicesAndAliases.add(index.getName()); + } + } + } + return indicesAndAliases; + }, (name, selectorString) -> { final IndexAbstraction indexAbstraction = lookup.get(name); + final IndexComponentSelector selector = IndexComponentSelector.getByKey(selectorString); if (indexAbstraction == null) { // test access (by name) to a resource that does not currently exist // the action handler must handle the case of accessing resources that do not exist - return predicate.test(name, null); + return predicate.test(name, selector, null); } else { if (indexAbstraction.isFailureIndexOfDataStream()) { - // TODO remove selector, add selector, remove selector, selectors, selectors, selectors - return predicate.test( - IndexNameExpressionResolver.combineSelector( - indexAbstraction.getParentDataStream().getName(), - IndexComponentSelector.FAILURES - ), - indexAbstraction.getParentDataStream() - ) || predicate.test(indexAbstraction); + // TODO explain + return predicate.test(indexAbstraction.getParentDataStream(), IndexComponentSelector.FAILURES) + || predicate.test(indexAbstraction, selector); } // We check the parent data stream first if there is one. For testing requested indices, this is most likely // more efficient than checking the index name first because we recommend grant privileges over data stream // instead of backing indices. - return (indexAbstraction.getParentDataStream() != null && predicate.test(indexAbstraction.getParentDataStream())) - || predicate.test(indexAbstraction); + return (indexAbstraction.getParentDataStream() != null && predicate.test(indexAbstraction.getParentDataStream(), selector)) + || predicate.test(indexAbstraction, selector); } }); } @@ -1067,22 +1065,30 @@ private static boolean isAsyncRelatedAction(String action) { static final class AuthorizedIndices implements AuthorizationEngine.AuthorizedIndices { - private final CachedSupplier> allAuthorizedAndAvailableSupplier; - private final Predicate isAuthorizedPredicate; - - AuthorizedIndices(Supplier> allAuthorizedAndAvailableSupplier, Predicate isAuthorizedPredicate) { - this.allAuthorizedAndAvailableSupplier = CachedSupplier.wrap(allAuthorizedAndAvailableSupplier); + private final CachedSupplier> authorizedAndAvailableSupplier; + private final CachedSupplier> failureStoreAuthorizedAndAvailableSupplier; + private final BiPredicate isAuthorizedPredicate; + + AuthorizedIndices( + Supplier> authorizedAndAvailableSupplier, + Supplier> failureStoreAuthorizedAndAvailableSupplier, + BiPredicate isAuthorizedPredicate + ) { + this.authorizedAndAvailableSupplier = CachedSupplier.wrap(authorizedAndAvailableSupplier); + this.failureStoreAuthorizedAndAvailableSupplier = CachedSupplier.wrap(failureStoreAuthorizedAndAvailableSupplier); this.isAuthorizedPredicate = Objects.requireNonNull(isAuthorizedPredicate); } @Override - public Supplier> all() { - return allAuthorizedAndAvailableSupplier; + public Supplier> all(@Nullable String selector) { + return IndexComponentSelector.FAILURES.equals(IndexComponentSelector.getByKey(selector)) + ? failureStoreAuthorizedAndAvailableSupplier + : authorizedAndAvailableSupplier; } @Override - public boolean check(String name) { - return this.isAuthorizedPredicate.test(name); + public boolean check(String name, @Nullable String selector) { + return isAuthorizedPredicate.test(name, selector); } } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java index d230aeeb5666c..e2459fa38751b 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java @@ -1941,7 +1941,11 @@ public void testWhenAliasToMultipleIndicesAndUserIsAuthorizedUsingAliasReturnsAl String index = "logs-00003"; // write index PutMappingRequest request = new PutMappingRequest(Strings.EMPTY_ARRAY).setConcreteIndex(new Index(index, UUIDs.base64UUID())); assert projectMetadata.getIndicesLookup().get("logs-alias").getIndices().size() == 3; - String putMappingIndexOrAlias = IndicesAndAliasesResolver.getPutMappingIndexOrAlias(request, "logs-alias"::equals, projectMetadata); + String putMappingIndexOrAlias = IndicesAndAliasesResolver.getPutMappingIndexOrAlias( + request, + (i, s) -> i.equals("logs-alias"), + projectMetadata + ); String message = "user is authorized to access `logs-alias` and the put mapping request is for a write index" + "so this should have returned the alias name"; assertEquals(message, "logs-alias", putMappingIndexOrAlias); @@ -1951,7 +1955,11 @@ public void testWhenAliasToMultipleIndicesAndUserIsAuthorizedUsingAliasReturnsIn String index = "logs-00002"; // read index PutMappingRequest request = new PutMappingRequest(Strings.EMPTY_ARRAY).setConcreteIndex(new Index(index, UUIDs.base64UUID())); assert projectMetadata.getIndicesLookup().get("logs-alias").getIndices().size() == 3; - String putMappingIndexOrAlias = IndicesAndAliasesResolver.getPutMappingIndexOrAlias(request, "logs-alias"::equals, projectMetadata); + String putMappingIndexOrAlias = IndicesAndAliasesResolver.getPutMappingIndexOrAlias( + request, + (i, s) -> i.equals("logs-alias"), + projectMetadata + ); String message = "user is authorized to access `logs-alias` and the put mapping request is for a read index" + "so this should have returned the concrete index as fallback"; assertEquals(message, index, putMappingIndexOrAlias); From ab87f5545374085f0c155edaaa39bfc3a3794722 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Fri, 7 Mar 2025 11:02:08 +0100 Subject: [PATCH 068/131] Refactor but is it better --- .../security/authz/AuthorizationEngine.java | 6 +++ .../authz/permission/IndicesPermission.java | 46 ++++++++++++------- .../authz/permission/LimitedRoleTests.java | 38 +++++++-------- .../security/user/InternalUsersTests.java | 2 +- .../xpack/security/authz/RBACEngine.java | 28 +++++++---- .../accesscontrol/IndicesPermissionTests.java | 4 +- 6 files changed, 75 insertions(+), 49 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java index 0a4937cf710bf..ca439c6422db6 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java @@ -293,6 +293,7 @@ interface AuthorizedIndices { */ Supplier> all(@Nullable String selector); + // TODO remove me default Supplier> all() { return all(null); } @@ -301,6 +302,11 @@ default Supplier> all() { * Checks if an index-like resource name is authorized, for an action by a user. The resource might or might not exist. */ boolean check(String name, @Nullable String selector); + + // TODO remove me + default boolean check(String name) { + return check(name, null); + } } /** diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 4296a792c6e62..4b247ea562189 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -190,7 +190,8 @@ private IsResourceAuthorizedPredicate buildIndexMatcherPredicateForAction(String */ public static class IsResourceAuthorizedPredicate { - private final BiPredicate biPredicate; + private final BiPredicate isAuthorizedForData; + private final BiPredicate isAuthorizedForFailureStore; // public for tests public IsResourceAuthorizedPredicate( @@ -199,21 +200,22 @@ public IsResourceAuthorizedPredicate( StringMatcher additionalNonDatastreamNameMatcher ) { this((String name, @Nullable IndexAbstraction indexAbstraction) -> { - Tuple nameWithSelector = IndexNameExpressionResolver.splitSelectorExpression(name); - String indexAbstractionName = nameWithSelector.v1(); - IndexComponentSelector selector = IndexComponentSelector.getByKey(nameWithSelector.v2()); - assert indexAbstraction == null || indexAbstractionName.equals(indexAbstraction.getName()); - if (IndexComponentSelector.FAILURES.equals(selector)) { - // TODO assert isPartOfDataStream if indexAbstraction is not null - return failureStoreNameMatcher.test(indexAbstractionName); - } - return resourceNameMatcher.test(indexAbstractionName) - || (isPartOfDatastream(indexAbstraction) == false && additionalNonDatastreamNameMatcher.test(indexAbstractionName)); + assert indexAbstraction == null || name.equals(indexAbstraction.getName()); + return resourceNameMatcher.test(name) + || (isPartOfDatastream(indexAbstraction) == false && additionalNonDatastreamNameMatcher.test(name)); + }, (String name, @Nullable IndexAbstraction indexAbstraction) -> { + assert indexAbstraction == null || name.equals(indexAbstraction.getName()); + // we can't enforce that the abstraction is part of a data stream since we need to account for non-existent resources + return failureStoreNameMatcher.test(name); }); } - private IsResourceAuthorizedPredicate(BiPredicate biPredicate) { - this.biPredicate = biPredicate; + private IsResourceAuthorizedPredicate( + BiPredicate isAuthorizedForData, + BiPredicate isAuthorizedForFailureStore + ) { + this.isAuthorizedForData = isAuthorizedForData; + this.isAuthorizedForFailureStore = isAuthorizedForFailureStore; } /** @@ -222,7 +224,15 @@ private IsResourceAuthorizedPredicate(BiPredicate biPr * authorization tests of that other instance and this one. */ public final IsResourceAuthorizedPredicate and(IsResourceAuthorizedPredicate other) { - return new IsResourceAuthorizedPredicate(this.biPredicate.and(other.biPredicate)); + return new IsResourceAuthorizedPredicate( + this.isAuthorizedForData.and(other.isAuthorizedForData), + this.isAuthorizedForFailureStore.and(other.isAuthorizedForFailureStore) + ); + } + + // TODO remove me + public boolean test(IndexAbstraction indexAbstraction) { + return test(indexAbstraction.getName(), null, indexAbstraction); } /** @@ -230,7 +240,7 @@ public final IsResourceAuthorizedPredicate and(IsResourceAuthorizedPredicate oth * The resource must exist. Otherwise, use the {@link #test(String, IndexComponentSelector, IndexAbstraction)} method. * Returns {@code true} if access to the given resource is authorized or {@code false} otherwise. */ - public boolean test(IndexAbstraction indexAbstraction, IndexComponentSelector selector) { + public boolean test(IndexAbstraction indexAbstraction, @Nullable IndexComponentSelector selector) { return test(indexAbstraction.getName(), selector, indexAbstraction); } @@ -240,8 +250,10 @@ public boolean test(IndexAbstraction indexAbstraction, IndexComponentSelector se * if it doesn't. * Returns {@code true} if access to the given resource is authorized or {@code false} otherwise. */ - public boolean test(String name, IndexComponentSelector selector, @Nullable IndexAbstraction indexAbstraction) { - return biPredicate.test(name, indexAbstraction); + public boolean test(String name, @Nullable IndexComponentSelector selector, @Nullable IndexAbstraction indexAbstraction) { + return IndexComponentSelector.FAILURES.equals(selector) + ? isAuthorizedForFailureStore.test(name, indexAbstraction) + : isAuthorizedForData.test(name, indexAbstraction); } private static boolean isPartOfDatastream(IndexAbstraction indexAbstraction) { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java index dcc0bdec79118..acbbb0ec60fff 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java @@ -529,22 +529,22 @@ public void testCheckIndicesAction() { public void testAllowedIndicesMatcher() { Role fromRole = Role.builder(EMPTY_RESTRICTED_INDICES, "a-role").add(IndexPrivilege.READ, "ind-1*").build(); - assertThat(fromRole.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-1")), is(true)); - assertThat(fromRole.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-11")), is(true)); - assertThat(fromRole.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-2")), is(false)); + assertThat(fromRole.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-1"), null), is(true)); + assertThat(fromRole.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-11"), null), is(true)); + assertThat(fromRole.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-2"), null), is(false)); { Role limitedByRole = Role.builder(EMPTY_RESTRICTED_INDICES, "limited-role").add(IndexPrivilege.READ, "ind-1", "ind-2").build(); assertThat( - limitedByRole.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-1")), + limitedByRole.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-1"), null), is(true) ); assertThat( - limitedByRole.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-11")), + limitedByRole.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-11"), null), is(false) ); assertThat( - limitedByRole.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-2")), + limitedByRole.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-2"), null), is(true) ); Role role; @@ -553,18 +553,18 @@ public void testAllowedIndicesMatcher() { } else { role = fromRole.limitedBy(limitedByRole); } - assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-1")), is(true)); - assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-11")), is(false)); - assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-2")), is(false)); + assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-1"), null), is(true)); + assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-11"), null), is(false)); + assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-2"), null), is(false)); } { Role limitedByRole = Role.builder(EMPTY_RESTRICTED_INDICES, "limited-role").add(IndexPrivilege.READ, "ind-*").build(); assertThat( - limitedByRole.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-1")), + limitedByRole.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-1"), null), is(true) ); assertThat( - limitedByRole.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-2")), + limitedByRole.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-2"), null), is(true) ); Role role; @@ -573,16 +573,16 @@ public void testAllowedIndicesMatcher() { } else { role = fromRole.limitedBy(limitedByRole); } - assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-1")), is(true)); - assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-2")), is(false)); + assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-1"), null), is(true)); + assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-2"), null), is(false)); } } public void testAllowedIndicesMatcherWithNestedRole() { Role role = Role.builder(EMPTY_RESTRICTED_INDICES, "a-role").add(IndexPrivilege.READ, "ind-1*").build(); - assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-1")), is(true)); - assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-11")), is(true)); - assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-2")), is(false)); + assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-1"), null), is(true)); + assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-11"), null), is(true)); + assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-2"), null), is(false)); final int depth = randomIntBetween(2, 4); boolean index11Excluded = false; @@ -598,12 +598,12 @@ public void testAllowedIndicesMatcherWithNestedRole() { } else { role = role.limitedBy(limitedByRole); } - assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-1")), is(true)); + assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-1"), null), is(true)); assertThat( - role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-11")), + role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-11"), null), is(false == index11Excluded) ); - assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-2")), is(false)); + assertThat(role.allowedIndicesMatcher(TransportSearchAction.TYPE.name()).test(mockIndexAbstraction("ind-2"), null), is(false)); } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/user/InternalUsersTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/user/InternalUsersTests.java index 62dae84f30fc9..40f66f457e67a 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/user/InternalUsersTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/user/InternalUsersTests.java @@ -376,7 +376,7 @@ private static void checkIndexAccess(SimpleRole role, String action, String inde final IndexAbstraction.ConcreteIndex index = new IndexAbstraction.ConcreteIndex(metadata); assertThat( "Role " + role + ", action " + action + " access to " + indexName, - role.allowedIndicesMatcher(action).test(index), + role.allowedIndicesMatcher(action).test(index, null), is(expectedValue) ); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java index 005835108646a..1c3b64765f11d 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java @@ -885,6 +885,11 @@ static AuthorizedIndices resolveAuthorizedIndicesFromRole( // TODO: can this be done smarter? I think there are usually more indices/aliases in the cluster then indices defined a roles? if (includeDataStreams) { for (IndexAbstraction indexAbstraction : lookup.values()) { + // urgh + if (indexAbstraction.isFailureIndexOfDataStream() + && predicate.test(indexAbstraction.getParentDataStream(), IndexComponentSelector.FAILURES)) { + indicesAndAliases.add(indexAbstraction.getName()); + } if (predicate.test(indexAbstraction, IndexComponentSelector.DATA)) { indicesAndAliases.add(indexAbstraction.getName()); if (indexAbstraction.getType() == IndexAbstraction.Type.DATA_STREAM) { @@ -906,6 +911,7 @@ static AuthorizedIndices resolveAuthorizedIndicesFromRole( timeChecker.accept(indicesAndAliases); return indicesAndAliases; }, () -> { + // TODO handle timeChecker if (includeDataStreams == false) { return Collections.emptySet(); } @@ -924,22 +930,24 @@ static AuthorizedIndices resolveAuthorizedIndicesFromRole( }, (name, selectorString) -> { final IndexAbstraction indexAbstraction = lookup.get(name); final IndexComponentSelector selector = IndexComponentSelector.getByKey(selectorString); + assert selector != null || selectorString == null : "[" + selectorString + "] is not a valid selector"; if (indexAbstraction == null) { // test access (by name) to a resource that does not currently exist // the action handler must handle the case of accessing resources that do not exist return predicate.test(name, selector, null); - } else { - if (indexAbstraction.isFailureIndexOfDataStream()) { - // TODO explain - return predicate.test(indexAbstraction.getParentDataStream(), IndexComponentSelector.FAILURES) - || predicate.test(indexAbstraction, selector); + } + // We check the parent data stream first if there is one. For testing requested indices, this is most likely + // more efficient than checking the index name first because we recommend grant privileges over data stream + // instead of backing indices. + if (indexAbstraction.getParentDataStream() != null) { + if (predicate.test( + indexAbstraction.getParentDataStream(), + indexAbstraction.isFailureIndexOfDataStream() ? IndexComponentSelector.FAILURES : selector + )) { + return true; } - // We check the parent data stream first if there is one. For testing requested indices, this is most likely - // more efficient than checking the index name first because we recommend grant privileges over data stream - // instead of backing indices. - return (indexAbstraction.getParentDataStream() != null && predicate.test(indexAbstraction.getParentDataStream(), selector)) - || predicate.test(indexAbstraction, selector); } + return predicate.test(indexAbstraction, selector); }); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java index 241b936bce12e..9ff646b286bd6 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java @@ -710,10 +710,10 @@ public void testResourceAuthorizedPredicateForDatastreams() { ); assertThat(predicate.test(dataStream), is(false)); // test authorization for a missing resource with the datastream's name - assertThat(predicate.test(dataStream.getName(), null), is(true)); + assertThat(predicate.test(dataStream.getName(), null, null), is(true)); assertThat(predicate.test(backingIndex), is(false)); // test authorization for a missing resource with the backing index's name - assertThat(predicate.test(backingIndex.getName(), null), is(true)); + assertThat(predicate.test(backingIndex.getName(), null, null), is(true)); assertThat(predicate.test(concreteIndex), is(true)); assertThat(predicate.test(alias), is(true)); } From 1b96a23160d36cf6aaf74bd2f2b3fd19c1ae875f Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Fri, 7 Mar 2025 11:23:08 +0100 Subject: [PATCH 069/131] Util --- .../action/support/IndexComponentSelector.java | 13 +++++++++++++ .../xpack/security/authz/RBACEngine.java | 6 ++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/support/IndexComponentSelector.java b/server/src/main/java/org/elasticsearch/action/support/IndexComponentSelector.java index 5a0d2e3141ae1..293919eee9a93 100644 --- a/server/src/main/java/org/elasticsearch/action/support/IndexComponentSelector.java +++ b/server/src/main/java/org/elasticsearch/action/support/IndexComponentSelector.java @@ -72,6 +72,19 @@ public static IndexComponentSelector getByKey(String key) { return KEY_REGISTRY.get(key); } + public static IndexComponentSelector getByKeyOrThrow(@Nullable String key) { + if (key == null) { + return DATA; + } + IndexComponentSelector selector = getByKey(key); + if (selector == null) { + throw new IllegalArgumentException( + "Unknown key of index component selector [" + key + "], available options are: " + KEY_REGISTRY + ); + } + return selector; + } + public static IndexComponentSelector read(StreamInput in) throws IOException { byte id = in.readByte(); if (in.getTransportVersion().onOrAfter(TransportVersions.REMOVE_ALL_APPLICABLE_SELECTOR) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java index 1c3b64765f11d..b87ff640bb123 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java @@ -889,6 +889,8 @@ static AuthorizedIndices resolveAuthorizedIndicesFromRole( if (indexAbstraction.isFailureIndexOfDataStream() && predicate.test(indexAbstraction.getParentDataStream(), IndexComponentSelector.FAILURES)) { indicesAndAliases.add(indexAbstraction.getName()); + // we know this is failure index and it's authorized so no need to check further + continue; } if (predicate.test(indexAbstraction, IndexComponentSelector.DATA)) { indicesAndAliases.add(indexAbstraction.getName()); @@ -929,8 +931,7 @@ static AuthorizedIndices resolveAuthorizedIndicesFromRole( return indicesAndAliases; }, (name, selectorString) -> { final IndexAbstraction indexAbstraction = lookup.get(name); - final IndexComponentSelector selector = IndexComponentSelector.getByKey(selectorString); - assert selector != null || selectorString == null : "[" + selectorString + "] is not a valid selector"; + final IndexComponentSelector selector = IndexComponentSelector.getByKeyOrThrow(selectorString); if (indexAbstraction == null) { // test access (by name) to a resource that does not currently exist // the action handler must handle the case of accessing resources that do not exist @@ -942,6 +943,7 @@ static AuthorizedIndices resolveAuthorizedIndicesFromRole( if (indexAbstraction.getParentDataStream() != null) { if (predicate.test( indexAbstraction.getParentDataStream(), + // access to failure indices is authorized via failures-based selectors on the parent data stream _not_ via data ones indexAbstraction.isFailureIndexOfDataStream() ? IndexComponentSelector.FAILURES : selector )) { return true; From 776d49a032d7a33aa6c6c96f9e5f48672e7ba49e Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Fri, 7 Mar 2025 11:27:03 +0100 Subject: [PATCH 070/131] Check selector first --- .../security/authz/permission/IndicesPermission.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 4b247ea562189..520bf49e3914d 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -447,13 +447,11 @@ public boolean isPartOfDataStream() { public boolean checkIndex(Group group) { final DataStream ds = indexAbstraction == null ? null : indexAbstraction.getParentDataStream(); if (ds != null) { - if (group.checkIndex(ds.getName())) { - final IndexComponentSelector selectorToCheck = indexAbstraction.isFailureIndexOfDataStream() - ? IndexComponentSelector.FAILURES - : selector; - if (group.checkSelector(selectorToCheck)) { - return true; - } + final IndexComponentSelector selectorToCheck = indexAbstraction.isFailureIndexOfDataStream() + ? IndexComponentSelector.FAILURES + : selector; + if (group.checkSelector(selectorToCheck) && group.checkIndex(ds.getName())) { + return true; } } return group.checkSelector(selector) && group.checkIndex(name); From 39995f82f9761944f8e3f40f949e4b9caa15b7e0 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Fri, 7 Mar 2025 11:54:42 +0100 Subject: [PATCH 071/131] More --- .../xpack/core/security/authz/AuthorizationEngine.java | 4 ++-- .../org/elasticsearch/xpack/security/authz/RBACEngine.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java index ca439c6422db6..40eba9a0c5fce 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java @@ -281,14 +281,14 @@ default AuthorizationInfo getAuthenticatedUserAuthorizationInfo() { } /** - * Used to retrieve index-like resources that the user has access to, for a specific access action type, + * Used to retrieve index-like resources that the user has access to, for a specific access action type and selector, * at a specific point in time (for a fixed cluster state view). * It can also be used to check if a specific resource name is authorized (access to the resource name * can be authorized even if it doesn't exist). */ interface AuthorizedIndices { /** - * Returns all the index-like resource names that are available and accessible for an action type by a user, + * Returns all the index-like resource names that are available and accessible for an action type and selector by a user, * at a fixed point in time (for a single cluster state view). */ Supplier> all(@Nullable String selector); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java index b87ff640bb123..3f08eec4c7bbf 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java @@ -1091,7 +1091,7 @@ static final class AuthorizedIndices implements AuthorizationEngine.AuthorizedIn @Override public Supplier> all(@Nullable String selector) { - return IndexComponentSelector.FAILURES.equals(IndexComponentSelector.getByKey(selector)) + return IndexComponentSelector.FAILURES.equals(IndexComponentSelector.getByKeyOrThrow(selector)) ? failureStoreAuthorizedAndAvailableSupplier : authorizedAndAvailableSupplier; } From 6b0b6c9c5e2138df546f125f5f5dfd10b9bde57a Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Fri, 7 Mar 2025 16:20:09 +0100 Subject: [PATCH 072/131] Inline --- .../security/authz/AuthorizationEngine.java | 10 -- .../authz/permission/IndicesPermission.java | 2 +- .../xpack/security/authz/RBACEngine.java | 4 +- .../authz/AuthorizedIndicesTests.java | 84 ++++++------- .../authz/IndicesAndAliasesResolverTests.java | 112 +++++++++--------- .../xpack/security/authz/RBACEngineTests.java | 10 +- 6 files changed, 106 insertions(+), 116 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java index 40eba9a0c5fce..ed1663b98c0d6 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java @@ -293,20 +293,10 @@ interface AuthorizedIndices { */ Supplier> all(@Nullable String selector); - // TODO remove me - default Supplier> all() { - return all(null); - } - /** * Checks if an index-like resource name is authorized, for an action by a user. The resource might or might not exist. */ boolean check(String name, @Nullable String selector); - - // TODO remove me - default boolean check(String name) { - return check(name, null); - } } /** diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 520bf49e3914d..faedb65a5da91 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -230,7 +230,7 @@ public final IsResourceAuthorizedPredicate and(IsResourceAuthorizedPredicate oth ); } - // TODO remove me + // TODO remove me (this has >700 usages in tests which would make for a horrible diff; will remove this once the main PR is merged) public boolean test(IndexAbstraction indexAbstraction) { return test(indexAbstraction.getName(), null, indexAbstraction); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java index 3f08eec4c7bbf..b4b9ddcbab369 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java @@ -885,11 +885,11 @@ static AuthorizedIndices resolveAuthorizedIndicesFromRole( // TODO: can this be done smarter? I think there are usually more indices/aliases in the cluster then indices defined a roles? if (includeDataStreams) { for (IndexAbstraction indexAbstraction : lookup.values()) { - // urgh + // TODO clean this up if (indexAbstraction.isFailureIndexOfDataStream() && predicate.test(indexAbstraction.getParentDataStream(), IndexComponentSelector.FAILURES)) { indicesAndAliases.add(indexAbstraction.getName()); - // we know this is failure index and it's authorized so no need to check further + // we know this is a failure index, and it's authorized so no need to check further continue; } if (predicate.test(indexAbstraction, IndexComponentSelector.DATA)) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java index e91ed36331e79..a984a1a586b5b 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java @@ -55,7 +55,7 @@ public void testAuthorizedIndicesUserWithoutRoles() { Metadata.EMPTY_METADATA.getProject().getIndicesLookup(), () -> ignore -> {} ); - assertTrue(authorizedIndices.all().get().isEmpty()); + assertTrue(authorizedIndices.all(null).get().isEmpty()); } public void testAuthorizedIndicesUserWithSomeRoles() { @@ -115,15 +115,15 @@ public void testAuthorizedIndicesUserWithSomeRoles() { metadata.getProject().getIndicesLookup(), () -> ignore -> {} ); - assertThat(authorizedIndices.all().get(), containsInAnyOrder("a1", "a2", "aaaaaa", "b", "ab")); - assertThat(authorizedIndices.all().get(), not(contains("bbbbb"))); - assertThat(authorizedIndices.check("bbbbb"), is(false)); - assertThat(authorizedIndices.all().get(), not(contains("ba"))); - assertThat(authorizedIndices.check("ba"), is(false)); - assertThat(authorizedIndices.all().get(), not(contains(internalSecurityIndex))); - assertThat(authorizedIndices.check(internalSecurityIndex), is(false)); - assertThat(authorizedIndices.all().get(), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); - assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS), is(false)); + assertThat(authorizedIndices.all(null).get(), containsInAnyOrder("a1", "a2", "aaaaaa", "b", "ab")); + assertThat(authorizedIndices.all(null).get(), not(contains("bbbbb"))); + assertThat(authorizedIndices.check("bbbbb", null), is(false)); + assertThat(authorizedIndices.all(null).get(), not(contains("ba"))); + assertThat(authorizedIndices.check("ba", null), is(false)); + assertThat(authorizedIndices.all(null).get(), not(contains(internalSecurityIndex))); + assertThat(authorizedIndices.check(internalSecurityIndex, null), is(false)); + assertThat(authorizedIndices.all(null).get(), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); + assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, null), is(false)); } public void testAuthorizedIndicesUserWithSomeRolesEmptyMetadata() { @@ -134,7 +134,7 @@ public void testAuthorizedIndicesUserWithSomeRolesEmptyMetadata() { Metadata.EMPTY_METADATA.getProject().getIndicesLookup(), () -> ignore -> {} ); - assertTrue(authorizedIndices.all().get().isEmpty()); + assertTrue(authorizedIndices.all(null).get().isEmpty()); } public void testSecurityIndicesAreRemovedFromRegularUser() { @@ -145,7 +145,7 @@ public void testSecurityIndicesAreRemovedFromRegularUser() { Metadata.EMPTY_METADATA.getProject().getIndicesLookup(), () -> ignore -> {} ); - assertTrue(authorizedIndices.all().get().isEmpty()); + assertTrue(authorizedIndices.all(null).get().isEmpty()); } public void testSecurityIndicesAreRestrictedForDefaultRole() { @@ -177,13 +177,13 @@ public void testSecurityIndicesAreRestrictedForDefaultRole() { metadata.getProject().getIndicesLookup(), () -> ignore -> {} ); - assertThat(authorizedIndices.all().get(), containsInAnyOrder("an-index", "another-index")); - assertThat(authorizedIndices.check("an-index"), is(true)); - assertThat(authorizedIndices.check("another-index"), is(true)); - assertThat(authorizedIndices.all().get(), not(contains(internalSecurityIndex))); - assertThat(authorizedIndices.check(internalSecurityIndex), is(false)); - assertThat(authorizedIndices.all().get(), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); - assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS), is(false)); + assertThat(authorizedIndices.all(null).get(), containsInAnyOrder("an-index", "another-index")); + assertThat(authorizedIndices.check("an-index", null), is(true)); + assertThat(authorizedIndices.check("another-index", null), is(true)); + assertThat(authorizedIndices.all(null).get(), not(contains(internalSecurityIndex))); + assertThat(authorizedIndices.check(internalSecurityIndex, null), is(false)); + assertThat(authorizedIndices.all(null).get(), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); + assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, null), is(false)); } public void testSecurityIndicesAreNotRemovedFromUnrestrictedRole() { @@ -216,7 +216,7 @@ public void testSecurityIndicesAreNotRemovedFromUnrestrictedRole() { () -> ignore -> {} ); assertThat( - authorizedIndices.all().get(), + authorizedIndices.all(null).get(), containsInAnyOrder("an-index", "another-index", SecuritySystemIndices.SECURITY_MAIN_ALIAS, internalSecurityIndex) ); @@ -227,7 +227,7 @@ public void testSecurityIndicesAreNotRemovedFromUnrestrictedRole() { () -> ignore -> {} ); assertThat( - authorizedIndicesSuperUser.all().get(), + authorizedIndicesSuperUser.all(null).get(), containsInAnyOrder("an-index", "another-index", SecuritySystemIndices.SECURITY_MAIN_ALIAS, internalSecurityIndex) ); } @@ -297,22 +297,22 @@ public void testDataStreamsAreNotIncludedInAuthorizedIndices() { metadata.getProject().getIndicesLookup(), () -> ignore -> {} ); - assertThat(authorizedIndices.all().get(), containsInAnyOrder("a1", "a2", "aaaaaa", "b", "ab")); + assertThat(authorizedIndices.all(null).get(), containsInAnyOrder("a1", "a2", "aaaaaa", "b", "ab")); for (String resource : List.of("a1", "a2", "aaaaaa", "b", "ab")) { - assertThat(authorizedIndices.check(resource), is(true)); + assertThat(authorizedIndices.check(resource, null), is(true)); } - assertThat(authorizedIndices.all().get(), not(contains("bbbbb"))); - assertThat(authorizedIndices.check("bbbbb"), is(false)); - assertThat(authorizedIndices.all().get(), not(contains("ba"))); - assertThat(authorizedIndices.check("ba"), is(false)); + assertThat(authorizedIndices.all(null).get(), not(contains("bbbbb"))); + assertThat(authorizedIndices.check("bbbbb", null), is(false)); + assertThat(authorizedIndices.all(null).get(), not(contains("ba"))); + assertThat(authorizedIndices.check("ba", null), is(false)); // due to context, datastreams are excluded from wildcard expansion - assertThat(authorizedIndices.all().get(), not(contains("adatastream1"))); + assertThat(authorizedIndices.all(null).get(), not(contains("adatastream1"))); // but they are authorized when explicitly tested (they are not "unavailable" for the Security filter) - assertThat(authorizedIndices.check("adatastream1"), is(true)); - assertThat(authorizedIndices.all().get(), not(contains(internalSecurityIndex))); - assertThat(authorizedIndices.check(internalSecurityIndex), is(false)); - assertThat(authorizedIndices.all().get(), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); - assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS), is(false)); + assertThat(authorizedIndices.check("adatastream1", null), is(true)); + assertThat(authorizedIndices.all(null).get(), not(contains(internalSecurityIndex))); + assertThat(authorizedIndices.check(internalSecurityIndex, null), is(false)); + assertThat(authorizedIndices.all(null).get(), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); + assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, null), is(false)); } public void testDataStreamsAreIncludedInAuthorizedIndices() { @@ -382,15 +382,15 @@ public void testDataStreamsAreIncludedInAuthorizedIndices() { metadata.getProject().getIndicesLookup(), () -> ignore -> {} ); - assertThat(authorizedIndices.all().get(), containsInAnyOrder("a1", "a2", "aaaaaa", "b", "ab", "adatastream1", backingIndex)); - assertThat(authorizedIndices.all().get(), not(contains("bbbbb"))); - assertThat(authorizedIndices.check("bbbbb"), is(false)); - assertThat(authorizedIndices.all().get(), not(contains("ba"))); - assertThat(authorizedIndices.check("ba"), is(false)); - assertThat(authorizedIndices.all().get(), not(contains(internalSecurityIndex))); - assertThat(authorizedIndices.check(internalSecurityIndex), is(false)); - assertThat(authorizedIndices.all().get(), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); - assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS), is(false)); + assertThat(authorizedIndices.all(null).get(), containsInAnyOrder("a1", "a2", "aaaaaa", "b", "ab", "adatastream1", backingIndex)); + assertThat(authorizedIndices.all(null).get(), not(contains("bbbbb"))); + assertThat(authorizedIndices.check("bbbbb", null), is(false)); + assertThat(authorizedIndices.all(null).get(), not(contains("ba"))); + assertThat(authorizedIndices.check("ba", null), is(false)); + assertThat(authorizedIndices.all(null).get(), not(contains(internalSecurityIndex))); + assertThat(authorizedIndices.check(internalSecurityIndex, null), is(false)); + assertThat(authorizedIndices.all(null).get(), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); + assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, null), is(false)); } public static AuthorizationEngine.RequestInfo getRequestInfo(String action) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java index e2459fa38751b..54a5d1a8d1a64 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java @@ -2233,14 +2233,14 @@ public void testDataStreamsAreNotVisibleWhenNotIncludedByRequestWithWildcard() { List dataStreams = List.of("logs-foo", "logs-foobar"); final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); for (String dsName : dataStreams) { - assertThat(authorizedIndices.all().get(), hasItem(dsName)); - assertThat(authorizedIndices.check(dsName), is(true)); + assertThat(authorizedIndices.all(null).get(), hasItem(dsName)); + assertThat(authorizedIndices.check(dsName, null), is(true)); DataStream dataStream = projectMetadata.dataStreams().get(dsName); - assertThat(authorizedIndices.all().get(), hasItem(dsName)); - assertThat(authorizedIndices.check(dsName), is(true)); + assertThat(authorizedIndices.all(null).get(), hasItem(dsName)); + assertThat(authorizedIndices.check(dsName, null), is(true)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices.all().get(), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName()), is(true)); + assertThat(authorizedIndices.all(null).get(), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), null), is(true)); } } @@ -2272,14 +2272,14 @@ public void testDataStreamsAreNotVisibleWhenNotIncludedByRequestWithoutWildcard( // data streams and their backing indices should _not_ be in the authorized list since the backing indices // do not match the requested name final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); - assertThat(authorizedIndices.all().get(), hasItem(dataStreamName)); - assertThat(authorizedIndices.check(dataStreamName), is(true)); + assertThat(authorizedIndices.all(null).get(), hasItem(dataStreamName)); + assertThat(authorizedIndices.check(dataStreamName, null), is(true)); DataStream dataStream = projectMetadata.dataStreams().get(dataStreamName); - assertThat(authorizedIndices.all().get(), hasItem(dataStreamName)); - assertThat(authorizedIndices.check(dataStreamName), is(true)); + assertThat(authorizedIndices.all(null).get(), hasItem(dataStreamName)); + assertThat(authorizedIndices.check(dataStreamName, null), is(true)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices.all().get(), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName()), is(true)); + assertThat(authorizedIndices.all(null).get(), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), null), is(true)); } // neither data streams nor their backing indices will be in the resolved list since the backing indices do not match the @@ -2307,11 +2307,11 @@ public void testDataStreamsAreVisibleWhenIncludedByRequestWithWildcard() { final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, TransportSearchAction.TYPE.name(), request); for (String dsName : expectedDataStreams) { DataStream dataStream = projectMetadata.dataStreams().get(dsName); - assertThat(authorizedIndices.all().get(), hasItem(dsName)); - assertThat(authorizedIndices.check(dsName), is(true)); + assertThat(authorizedIndices.all(null).get(), hasItem(dsName)); + assertThat(authorizedIndices.check(dsName, null), is(true)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices.all().get(), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName()), is(true)); + assertThat(authorizedIndices.all(null).get(), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), null), is(true)); } } @@ -2348,11 +2348,11 @@ public void testDataStreamsAreVisibleWhenIncludedByRequestWithoutWildcard() { final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, TransportSearchAction.TYPE.name(), request); // data streams and their backing indices should be in the authorized list - assertThat(authorizedIndices.all().get(), hasItem(dataStreamName)); - assertThat(authorizedIndices.check(dataStreamName), is(true)); + assertThat(authorizedIndices.all(null).get(), hasItem(dataStreamName)); + assertThat(authorizedIndices.check(dataStreamName, null), is(true)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices.all().get(), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName()), is(true)); + assertThat(authorizedIndices.all(null).get(), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), null), is(true)); } ResolvedIndices resolvedIndices = defaultIndicesResolver.resolveIndicesAndAliases( @@ -2381,15 +2381,15 @@ public void testBackingIndicesAreVisibleWhenIncludedByRequestWithWildcard() { final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, TransportSearchAction.TYPE.name(), request); for (String dsName : expectedDataStreams) { DataStream dataStream = projectMetadata.dataStreams().get(dsName); - assertThat(authorizedIndices.all().get(), hasItem(dsName)); - assertThat(authorizedIndices.check(dsName), is(true)); + assertThat(authorizedIndices.all(null).get(), hasItem(dsName)); + assertThat(authorizedIndices.check(dsName, null), is(true)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices.all().get(), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName()), is(true)); + assertThat(authorizedIndices.all(null).get(), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), null), is(true)); } for (Index i : dataStream.getFailureIndices()) { - assertThat(authorizedIndices.all().get(), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName()), is(true)); + assertThat(authorizedIndices.all(null).get(), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), null), is(true)); } } @@ -2420,16 +2420,16 @@ public void testBackingIndicesAreNotVisibleWhenNotIncludedByRequestWithoutWildca // data streams and their backing indices should _not_ be in the authorized list since the backing indices // did not match the requested pattern and the request does not support data streams final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); - assertThat(authorizedIndices.all().get(), hasItem(dataStreamName)); - assertThat(authorizedIndices.check(dataStreamName), is(true)); + assertThat(authorizedIndices.all(null).get(), hasItem(dataStreamName)); + assertThat(authorizedIndices.check(dataStreamName, null), is(true)); DataStream dataStream = projectMetadata.dataStreams().get(dataStreamName); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices.all().get(), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName()), is(true)); + assertThat(authorizedIndices.all(null).get(), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), null), is(true)); } for (Index i : dataStream.getFailureIndices()) { - assertThat(authorizedIndices.all().get(), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName()), is(true)); + assertThat(authorizedIndices.all(null).get(), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), null), is(true)); } // neither data streams nor their backing indices will be in the resolved list since the request does not support data streams @@ -2460,19 +2460,19 @@ public void testDataStreamNotAuthorizedWhenBackingIndicesAreAuthorizedViaWildcar // data streams should _not_ be in the authorized list but their backing indices that matched both the requested pattern // and the authorized pattern should be in the list final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); - assertThat(authorizedIndices.all().get(), not(hasItem("logs-foobar"))); - assertThat(authorizedIndices.check("logs-foobar"), is(false)); + assertThat(authorizedIndices.all(null).get(), not(hasItem("logs-foobar"))); + assertThat(authorizedIndices.check("logs-foobar", null), is(false)); DataStream dataStream = projectMetadata.dataStreams().get("logs-foobar"); - assertThat(authorizedIndices.all().get(), not(hasItem(indexName))); + assertThat(authorizedIndices.all(null).get(), not(hasItem(indexName))); // request pattern is subset of the authorized pattern, but be aware that patterns are never passed to #check in main code - assertThat(authorizedIndices.check(indexName), is(true)); + assertThat(authorizedIndices.check(indexName, null), is(true)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices.all().get(), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName()), is(true)); + assertThat(authorizedIndices.all(null).get(), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), null), is(true)); } for (Index i : dataStream.getFailureIndices()) { - assertThat(authorizedIndices.all().get(), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName()), is(true)); + assertThat(authorizedIndices.all(null).get(), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), null), is(true)); } // only the backing indices will be in the resolved list since the request does not support data streams @@ -2500,14 +2500,14 @@ public void testDataStreamNotAuthorizedWhenBackingIndicesAreAuthorizedViaNameAnd // data streams should _not_ be in the authorized list but a single backing index that matched the requested pattern // and the authorized name should be in the list final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); - assertThat(authorizedIndices.all().get(), not(hasItem("logs-foobar"))); - assertThat(authorizedIndices.check("logs-foobar"), is(false)); + assertThat(authorizedIndices.all(null).get(), not(hasItem("logs-foobar"))); + assertThat(authorizedIndices.check("logs-foobar", null), is(false)); String expectedIndex = failureStore ? DataStream.getDefaultFailureStoreName("logs-foobar", 1, System.currentTimeMillis()) : DataStream.getDefaultBackingIndexName("logs-foobar", 1); - assertThat(authorizedIndices.all().get(), hasItem(expectedIndex)); - assertThat(authorizedIndices.check(expectedIndex), is(true)); + assertThat(authorizedIndices.all(null).get(), hasItem(expectedIndex)); + assertThat(authorizedIndices.check(expectedIndex, null), is(true)); // only the single backing index will be in the resolved list since the request does not support data streams // but one of the backing indices matched the requested pattern @@ -2532,19 +2532,19 @@ public void testDataStreamNotAuthorizedWhenBackingIndicesAreAuthorizedViaWildcar // data streams should _not_ be in the authorized list but their backing indices that matched both the requested pattern // and the authorized pattern should be in the list final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); - assertThat(authorizedIndices.all().get(), not(hasItem("logs-foobar"))); - assertThat(authorizedIndices.check("logs-foobar"), is(false)); + assertThat(authorizedIndices.all(null).get(), not(hasItem("logs-foobar"))); + assertThat(authorizedIndices.check("logs-foobar", null), is(false)); DataStream dataStream = projectMetadata.dataStreams().get("logs-foobar"); - assertThat(authorizedIndices.all().get(), not(hasItem(indexName))); + assertThat(authorizedIndices.all(null).get(), not(hasItem(indexName))); // request pattern is subset of the authorized pattern, but be aware that patterns are never passed to #check in main code - assertThat(authorizedIndices.check(indexName), is(true)); + assertThat(authorizedIndices.check(indexName, null), is(true)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices.all().get(), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName()), is(true)); + assertThat(authorizedIndices.all(null).get(), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), null), is(true)); } for (Index i : dataStream.getFailureIndices()) { - assertThat(authorizedIndices.all().get(), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName()), is(true)); + assertThat(authorizedIndices.all(null).get(), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), null), is(true)); } // only the backing indices will be in the resolved list since the request does not support data streams @@ -2575,10 +2575,10 @@ public void testDataStreamNotAuthorizedWhenBackingIndicesAreAuthorizedViaNameAnd ? DataStream.getDefaultFailureStoreName("logs-foobar", 1, System.currentTimeMillis()) : DataStream.getDefaultBackingIndexName("logs-foobar", 1); final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); - assertThat(authorizedIndices.all().get(), not(hasItem("logs-foobar"))); - assertThat(authorizedIndices.check("logs-foobar"), is(false)); - assertThat(authorizedIndices.all().get(), hasItem(expectedIndex)); - assertThat(authorizedIndices.check(expectedIndex), is(true)); + assertThat(authorizedIndices.all(null).get(), not(hasItem("logs-foobar"))); + assertThat(authorizedIndices.check("logs-foobar", null), is(false)); + assertThat(authorizedIndices.all(null).get(), hasItem(expectedIndex)); + assertThat(authorizedIndices.check(expectedIndex, null), is(true)); // only the single backing index will be in the resolved list since the request does not support data streams // but one of the backing indices matched the requested pattern diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java index 59fa2e417432c..c933c878a8123 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java @@ -1444,14 +1444,14 @@ public void testBackingIndicesAreIncludedForAuthorizedDataStreams() { lookup, () -> ignore -> {} ); - assertThat(authorizedIndices.all().get(), hasItem(dataStreamName)); - assertThat(authorizedIndices.check(dataStreamName), is(true)); + assertThat(authorizedIndices.all(null).get(), hasItem(dataStreamName)); + assertThat(authorizedIndices.check(dataStreamName, null), is(true)); assertThat( - authorizedIndices.all().get(), + authorizedIndices.all(null).get(), hasItems(backingIndices.stream().map(im -> im.getIndex().getName()).collect(Collectors.toList()).toArray(Strings.EMPTY_ARRAY)) ); for (String index : backingIndices.stream().map(im -> im.getIndex().getName()).toList()) { - assertThat(authorizedIndices.check(index), is(true)); + assertThat(authorizedIndices.check(index, null), is(true)); } } @@ -1487,7 +1487,7 @@ public void testExplicitMappingUpdatesAreNotGrantedWithIngestPrivileges() { lookup, () -> ignore -> {} ); - assertThat(authorizedIndices.all().get().isEmpty(), is(true)); + assertThat(authorizedIndices.all(null).get().isEmpty(), is(true)); } public void testNoInfiniteRecursionForRBACAuthorizationInfoHashCode() { From d728fd68b1aa8ddd4e6abecf1575103b0c71825b Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Fri, 7 Mar 2025 16:26:29 +0100 Subject: [PATCH 073/131] Fix sig --- .../core/security/authz/permission/IndicesPermission.java | 8 ++++---- .../elasticsearch/xpack/security/authz/RBACEngine.java | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index faedb65a5da91..86bab207833e7 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -232,16 +232,16 @@ public final IsResourceAuthorizedPredicate and(IsResourceAuthorizedPredicate oth // TODO remove me (this has >700 usages in tests which would make for a horrible diff; will remove this once the main PR is merged) public boolean test(IndexAbstraction indexAbstraction) { - return test(indexAbstraction.getName(), null, indexAbstraction); + return test(indexAbstraction.getName(), indexAbstraction, null); } /** * Verifies if access is authorized to the given {@param indexAbstraction} resource. - * The resource must exist. Otherwise, use the {@link #test(String, IndexComponentSelector, IndexAbstraction)} method. + * The resource must exist. Otherwise, use the {@link #test(String, IndexAbstraction, IndexComponentSelector)} method. * Returns {@code true} if access to the given resource is authorized or {@code false} otherwise. */ public boolean test(IndexAbstraction indexAbstraction, @Nullable IndexComponentSelector selector) { - return test(indexAbstraction.getName(), selector, indexAbstraction); + return test(indexAbstraction.getName(), indexAbstraction, selector); } /** @@ -250,7 +250,7 @@ public boolean test(IndexAbstraction indexAbstraction, @Nullable IndexComponentS * if it doesn't. * Returns {@code true} if access to the given resource is authorized or {@code false} otherwise. */ - public boolean test(String name, @Nullable IndexComponentSelector selector, @Nullable IndexAbstraction indexAbstraction) { + public boolean test(String name, @Nullable IndexAbstraction indexAbstraction, @Nullable IndexComponentSelector selector) { return IndexComponentSelector.FAILURES.equals(selector) ? isAuthorizedForFailureStore.test(name, indexAbstraction) : isAuthorizedForData.test(name, indexAbstraction); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java index b4b9ddcbab369..e1ce5c3fe3910 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java @@ -935,7 +935,7 @@ static AuthorizedIndices resolveAuthorizedIndicesFromRole( if (indexAbstraction == null) { // test access (by name) to a resource that does not currently exist // the action handler must handle the case of accessing resources that do not exist - return predicate.test(name, selector, null); + return predicate.test(name, null, selector); } // We check the parent data stream first if there is one. For testing requested indices, this is most likely // more efficient than checking the index name first because we recommend grant privileges over data stream From 2a22090117bf7ab4ca76491496b2d434938248c4 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Fri, 7 Mar 2025 16:33:46 +0100 Subject: [PATCH 074/131] Timer --- .../org/elasticsearch/xpack/security/authz/RBACEngine.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java index e1ce5c3fe3910..fe6a0df1ae338 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java @@ -885,7 +885,7 @@ static AuthorizedIndices resolveAuthorizedIndicesFromRole( // TODO: can this be done smarter? I think there are usually more indices/aliases in the cluster then indices defined a roles? if (includeDataStreams) { for (IndexAbstraction indexAbstraction : lookup.values()) { - // TODO clean this up + // TODO clean this up and explain if (indexAbstraction.isFailureIndexOfDataStream() && predicate.test(indexAbstraction.getParentDataStream(), IndexComponentSelector.FAILURES)) { indicesAndAliases.add(indexAbstraction.getName()); @@ -913,10 +913,11 @@ static AuthorizedIndices resolveAuthorizedIndicesFromRole( timeChecker.accept(indicesAndAliases); return indicesAndAliases; }, () -> { - // TODO handle timeChecker if (includeDataStreams == false) { return Collections.emptySet(); } + // TODO is this right? + Consumer> timeChecker = timerSupplier.get(); Set indicesAndAliases = new HashSet<>(); for (IndexAbstraction indexAbstraction : lookup.values()) { if (indexAbstraction.getType() == IndexAbstraction.Type.DATA_STREAM @@ -928,6 +929,7 @@ static AuthorizedIndices resolveAuthorizedIndicesFromRole( } } } + timeChecker.accept(indicesAndAliases); return indicesAndAliases; }, (name, selectorString) -> { final IndexAbstraction indexAbstraction = lookup.get(name); From 365b2b8ae3a174a5a8e5b2316541b13bdffc8dc6 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 10 Mar 2025 10:24:43 +0100 Subject: [PATCH 075/131] Rework name handling --- .../authz/permission/IndicesPermission.java | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 86bab207833e7..1108274b819ee 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -549,19 +549,20 @@ public IndicesAccessControl authorize( } } - final Set resources = Sets.newHashSetWithExpectedSize(requestedIndicesOrAliases.size()); + final Map resources = Maps.newMapWithExpectedSize(requestedIndicesOrAliases.size()); int totalResourceCount = 0; Map lookup = metadata.getIndicesLookup(); for (String indexOrAlias : requestedIndicesOrAliases) { - // Remove any selectors from abstraction name. Selector authorization happens in conjunction with the index name, via the - // selector check in IndexResource#checkIndex + String originalIndexOrAlias = indexOrAlias; + // Remove any selectors from abstraction name, but include it in the map key if it's ::failures. We need to do this to + // disambiguate between data streams and their failure stores. Tuple expressionAndSelector = IndexNameExpressionResolver.splitSelectorExpression(indexOrAlias); indexOrAlias = expressionAndSelector.v1(); IndexComponentSelector selector = expressionAndSelector.v2() == null ? null : IndexComponentSelector.getByKey(expressionAndSelector.v2()); final IndexResource resource = new IndexResource(indexOrAlias, lookup.get(indexOrAlias), selector); - resources.add(resource); + resources.put(IndexComponentSelector.FAILURES.equals(selector) ? originalIndexOrAlias : indexOrAlias, resource); totalResourceCount += resource.size(lookup); } @@ -580,7 +581,7 @@ public IndicesAccessControl authorize( private Map buildIndicesAccessControl( final String action, - final Set requestedResources, + final Map requestedResources, final int totalResourceCount, final FieldPermissionsCache fieldPermissionsCache, final ProjectMetadata metadata @@ -594,10 +595,11 @@ private Map buildIndicesAccessC final boolean isMappingUpdateAction = isMappingUpdateAction(action); - for (IndexResource resource : requestedResources) { + for (Map.Entry resourceEntry : requestedResources.entrySet()) { // true if ANY group covers the given index AND the given action boolean granted = false; - + final String resourceName = resourceEntry.getKey(); + final IndexResource resource = resourceEntry.getValue(); final Collection concreteIndices = resource.resolveConcreteIndices(metadata); 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 @@ -644,9 +646,9 @@ && containsPrivilegeThatGrantsMappingUpdatesForBwc(group))) { roleQueriesByIndex.put(index, docPermissions); } - if (index.equals(resource.name) == false) { - fieldPermissionsByIndex.put(resource.name, fieldPermissions); - roleQueriesByIndex.put(resource.name, docPermissions); + if (index.equals(resourceName) == false) { + fieldPermissionsByIndex.put(resourceName, fieldPermissions); + roleQueriesByIndex.put(resourceName, docPermissions); } } } @@ -654,12 +656,12 @@ && containsPrivilegeThatGrantsMappingUpdatesForBwc(group))) { } if (granted) { - grantedResources.add(resource.name); + grantedResources.add(resourceName); + if (resource.canHaveBackingIndices()) { for (String concreteIndex : concreteIndices) { // If the name appears directly as part of the requested indices, it takes precedence over implicit access - // TODO inefficient - if (false == requestedResources.stream().anyMatch(r -> r.name.equals(concreteIndex))) { + if (false == requestedResources.containsKey(concreteIndex)) { grantedResources.add(concreteIndex); } } @@ -667,7 +669,6 @@ && containsPrivilegeThatGrantsMappingUpdatesForBwc(group))) { } } - // TODO handle failures selector for DLS/FLS Map indexPermissions = Maps.newMapWithExpectedSize(grantedResources.size()); for (String index : grantedResources) { final DocumentLevelPermissions permissions = roleQueriesByIndex.get(index); @@ -695,11 +696,11 @@ && containsPrivilegeThatGrantsMappingUpdatesForBwc(group))) { * Returns {@code true} if action is granted for all {@code requestedResources}. * If action is not granted for at least one resource, this method will return {@code false}. */ - private boolean isActionGranted(final String action, final Collection requestedResources) { + private boolean isActionGranted(final String action, final Map requestedResources) { final boolean isMappingUpdateAction = isMappingUpdateAction(action); - for (IndexResource resource : requestedResources) { + for (IndexResource resource : requestedResources.values()) { // 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 From 28f207f5ab0e27ccbffb969085a624111fb63a20 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 10 Mar 2025 10:26:16 +0100 Subject: [PATCH 076/131] Spotless --- .../authz/permission/IndicesPermission.java | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 1108274b819ee..1d4183e76eceb 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -517,20 +517,6 @@ public Collection resolveConcreteIndices(ProjectMetadata metadata) { public boolean canHaveBackingIndices() { return indexAbstraction != null && indexAbstraction.getType() != IndexAbstraction.Type.CONCRETE_INDEX; } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - IndexResource that = (IndexResource) o; - return name.equals(that.name) - && Objects.equals(selector, that.selector) - && Objects.equals(indexAbstraction, that.indexAbstraction); - } - - public int hashCode() { - return Objects.hash(name, selector, indexAbstraction); - } } /** From 084ac8ccdb38f4e5eb40d0e3070046557f2d7f73 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 10 Mar 2025 10:50:36 +0100 Subject: [PATCH 077/131] More tests --- .../FailureStoreSecurityRestIT.java | 66 +++++++++++++++++++ .../authz/IndicesAndAliasesResolver.java | 4 -- .../xpack/security/authz/RBACEngine.java | 21 +++--- 3 files changed, 76 insertions(+), 15 deletions(-) diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java index f2d065d51e221..5de00e44ae2dc 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java @@ -655,6 +655,72 @@ public void testFailureStoreAccess() throws IOException { } } } + { + Request request = searchRequest("test1," + failureIndexName); + for (var user : users) { + switch (user) { + case DATA_ACCESS_USER, FAILURE_STORE_ACCESS_USER: + expectThrows403(() -> performRequest(user, request)); + break; + case BOTH_ACCESS_USER, STAR_READ_ONLY_USER: + expectDocIds(performRequest(user, request), dataDocId, failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } + { + Request request = searchRequest("test1," + failureIndexName, "?ignore_unavailable=true"); + for (var user : users) { + switch (user) { + case DATA_ACCESS_USER: + expectDocIds(performRequest(user, request), dataDocId); + break; + case FAILURE_STORE_ACCESS_USER: + expectDocIds(performRequest(user, request), failuresDocId); + break; + case BOTH_ACCESS_USER, STAR_READ_ONLY_USER: + expectDocIds(performRequest(user, request), dataDocId, failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } + { + Request request = searchRequest("test1::failures," + dataIndexName); + for (var user : users) { + switch (user) { + case DATA_ACCESS_USER, FAILURE_STORE_ACCESS_USER, STAR_READ_ONLY_USER: + expectThrows403(() -> performRequest(user, request)); + break; + case BOTH_ACCESS_USER: + expectDocIds(performRequest(user, request), dataDocId, failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } + { + Request request = searchRequest("test1::failures," + dataIndexName, "?ignore_unavailable=true"); + for (var user : users) { + switch (user) { + case DATA_ACCESS_USER, STAR_READ_ONLY_USER: + expectDocIds(performRequest(user, request), dataDocId); + break; + case FAILURE_STORE_ACCESS_USER: + expectDocIds(performRequest(user, request), failuresDocId); + break; + case BOTH_ACCESS_USER: + expectDocIds(performRequest(user, request), dataDocId, failuresDocId); + break; + default: + fail("must cover user: " + user); + } + } + } { Request request = searchRequest("test1,*::failures"); for (var user : users) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java index fcb5d5e426c08..bed91547bbbff 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java @@ -487,10 +487,6 @@ private static List loadAuthorizedAliases(Supplier> authoriz List authorizedAliases = new ArrayList<>(); SortedMap existingAliases = projectMetadata.getIndicesLookup(); for (String authorizedIndex : authorizedIndices.get()) { - Tuple tuple = IndexNameExpressionResolver.splitSelectorExpression(authorizedIndex); - authorizedIndex = tuple.v1(); - String authorizedSelectorString = tuple.v2(); - // TODO skip failures here? IndexAbstraction indexAbstraction = existingAliases.get(authorizedIndex); if (indexAbstraction != null && indexAbstraction.getType() == IndexAbstraction.Type.ALIAS) { authorizedAliases.add(authorizedIndex); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java index fe6a0df1ae338..eee2515535708 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java @@ -913,23 +913,22 @@ static AuthorizedIndices resolveAuthorizedIndicesFromRole( timeChecker.accept(indicesAndAliases); return indicesAndAliases; }, () -> { - if (includeDataStreams == false) { - return Collections.emptySet(); - } // TODO is this right? Consumer> timeChecker = timerSupplier.get(); Set indicesAndAliases = new HashSet<>(); - for (IndexAbstraction indexAbstraction : lookup.values()) { - if (indexAbstraction.getType() == IndexAbstraction.Type.DATA_STREAM - && predicate.test(indexAbstraction, IndexComponentSelector.FAILURES)) { - indicesAndAliases.add(indexAbstraction.getName()); - // add data stream and its backing failure indices for any authorized data streams - for (Index index : ((DataStream) indexAbstraction).getFailureIndices()) { - indicesAndAliases.add(index.getName()); + if (includeDataStreams) { + for (IndexAbstraction indexAbstraction : lookup.values()) { + if (indexAbstraction.getType() == IndexAbstraction.Type.DATA_STREAM + && predicate.test(indexAbstraction, IndexComponentSelector.FAILURES)) { + indicesAndAliases.add(indexAbstraction.getName()); + // add data stream and its backing failure indices for any authorized data streams + for (Index index : ((DataStream) indexAbstraction).getFailureIndices()) { + indicesAndAliases.add(index.getName()); + } } } + timeChecker.accept(indicesAndAliases); } - timeChecker.accept(indicesAndAliases); return indicesAndAliases; }, (name, selectorString) -> { final IndexAbstraction indexAbstraction = lookup.get(name); From d8ed014da310bb6e9f3ecceaa3efe77d4a32f2f4 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 10 Mar 2025 11:22:37 +0100 Subject: [PATCH 078/131] Toss supplier --- .../metadata/IndexAbstractionResolver.java | 5 +- .../security/authz/AuthorizationEngine.java | 4 +- .../authz/IndicesAndAliasesResolver.java | 12 ++-- .../xpack/security/authz/RBACEngine.java | 6 +- .../authz/AuthorizedIndicesTests.java | 48 ++++++++-------- .../authz/IndicesAndAliasesResolverTests.java | 56 +++++++++---------- .../xpack/security/authz/RBACEngineTests.java | 6 +- 7 files changed, 69 insertions(+), 68 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java index 1907ef8526e65..805c956229725 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java @@ -24,7 +24,6 @@ import java.util.Set; import java.util.function.BiPredicate; import java.util.function.Function; -import java.util.function.Supplier; public class IndexAbstractionResolver { @@ -38,7 +37,7 @@ public List resolveIndexAbstractions( Iterable indices, IndicesOptions indicesOptions, ProjectMetadata projectMetadata, - Function>> allAuthorizedAndAvailableBySelector, + Function> allAuthorizedAndAvailableBySelector, BiPredicate isAuthorized, boolean includeDataStreams ) { @@ -72,7 +71,7 @@ public List resolveIndexAbstractions( if (indicesOptions.expandWildcardExpressions() && Regex.isSimpleMatchPattern(indexAbstraction)) { wildcardSeen = true; Set resolvedIndices = new HashSet<>(); - for (String authorizedIndex : allAuthorizedAndAvailableBySelector.apply(selectorString).get()) { + for (String authorizedIndex : allAuthorizedAndAvailableBySelector.apply(selectorString)) { if (Regex.simpleMatch(indexAbstraction, authorizedIndex) && isIndexVisible( indexAbstraction, diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java index ed1663b98c0d6..07525f1032871 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java @@ -39,7 +39,6 @@ import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.function.Supplier; import java.util.stream.Collectors; import static org.elasticsearch.action.ValidateActions.addValidationError; @@ -290,8 +289,9 @@ interface AuthorizedIndices { /** * Returns all the index-like resource names that are available and accessible for an action type and selector by a user, * at a fixed point in time (for a single cluster state view). + * The result is cached and subsequent calls to this method are idempotent. */ - Supplier> all(@Nullable String selector); + Set all(@Nullable String selector); /** * Checks if an index-like resource name is authorized, for an action by a user. The resource might or might not exist. diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java index bed91547bbbff..a1fdccae78c4b 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java @@ -49,7 +49,6 @@ import java.util.SortedMap; import java.util.concurrent.CopyOnWriteArraySet; import java.util.function.BiPredicate; -import java.util.function.Supplier; import static org.elasticsearch.xpack.core.security.authz.IndicesAndAliasesResolverField.NO_INDEX_PLACEHOLDER; @@ -323,7 +322,7 @@ ResolvedIndices resolveIndicesAndAliases( ); } if (indicesOptions.expandWildcardExpressions()) { - for (String authorizedIndex : authorizedIndices.all(allIndicesPatternSelector).get()) { + for (String authorizedIndex : authorizedIndices.all(allIndicesPatternSelector)) { if (IndexAbstractionResolver.isIndexVisible( "*", allIndicesPatternSelector, @@ -389,7 +388,7 @@ ResolvedIndices resolveIndicesAndAliases( if (aliasesRequest.expandAliasesWildcards()) { List aliases = replaceWildcardsWithAuthorizedAliases( aliasesRequest.aliases(), - loadAuthorizedAliases(authorizedIndices.all(null), projectMetadata) + loadAuthorizedAliases(authorizedIndices, projectMetadata) ); aliasesRequest.replaceAliases(aliases.toArray(new String[aliases.size()])); } @@ -483,10 +482,13 @@ static String getPutMappingIndexOrAlias( return resolvedAliasOrIndex; } - private static List loadAuthorizedAliases(Supplier> authorizedIndices, ProjectMetadata projectMetadata) { + private static List loadAuthorizedAliases( + AuthorizationEngine.AuthorizedIndices authorizedIndices, + ProjectMetadata projectMetadata + ) { List authorizedAliases = new ArrayList<>(); SortedMap existingAliases = projectMetadata.getIndicesLookup(); - for (String authorizedIndex : authorizedIndices.get()) { + for (String authorizedIndex : authorizedIndices.all(null)) { IndexAbstraction indexAbstraction = existingAliases.get(authorizedIndex); if (indexAbstraction != null && indexAbstraction.getType() == IndexAbstraction.Type.ALIAS) { authorizedAliases.add(authorizedIndex); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java index eee2515535708..953603026b607 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java @@ -1091,10 +1091,10 @@ static final class AuthorizedIndices implements AuthorizationEngine.AuthorizedIn } @Override - public Supplier> all(@Nullable String selector) { + public Set all(@Nullable String selector) { return IndexComponentSelector.FAILURES.equals(IndexComponentSelector.getByKeyOrThrow(selector)) - ? failureStoreAuthorizedAndAvailableSupplier - : authorizedAndAvailableSupplier; + ? failureStoreAuthorizedAndAvailableSupplier.get() + : authorizedAndAvailableSupplier.get(); } @Override diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java index a984a1a586b5b..b379881c506b7 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java @@ -55,7 +55,7 @@ public void testAuthorizedIndicesUserWithoutRoles() { Metadata.EMPTY_METADATA.getProject().getIndicesLookup(), () -> ignore -> {} ); - assertTrue(authorizedIndices.all(null).get().isEmpty()); + assertTrue(authorizedIndices.all(null).isEmpty()); } public void testAuthorizedIndicesUserWithSomeRoles() { @@ -115,14 +115,14 @@ public void testAuthorizedIndicesUserWithSomeRoles() { metadata.getProject().getIndicesLookup(), () -> ignore -> {} ); - assertThat(authorizedIndices.all(null).get(), containsInAnyOrder("a1", "a2", "aaaaaa", "b", "ab")); - assertThat(authorizedIndices.all(null).get(), not(contains("bbbbb"))); + assertThat(authorizedIndices.all(null), containsInAnyOrder("a1", "a2", "aaaaaa", "b", "ab")); + assertThat(authorizedIndices.all(null), not(contains("bbbbb"))); assertThat(authorizedIndices.check("bbbbb", null), is(false)); - assertThat(authorizedIndices.all(null).get(), not(contains("ba"))); + assertThat(authorizedIndices.all(null), not(contains("ba"))); assertThat(authorizedIndices.check("ba", null), is(false)); - assertThat(authorizedIndices.all(null).get(), not(contains(internalSecurityIndex))); + assertThat(authorizedIndices.all(null), not(contains(internalSecurityIndex))); assertThat(authorizedIndices.check(internalSecurityIndex, null), is(false)); - assertThat(authorizedIndices.all(null).get(), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); + assertThat(authorizedIndices.all(null), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, null), is(false)); } @@ -134,7 +134,7 @@ public void testAuthorizedIndicesUserWithSomeRolesEmptyMetadata() { Metadata.EMPTY_METADATA.getProject().getIndicesLookup(), () -> ignore -> {} ); - assertTrue(authorizedIndices.all(null).get().isEmpty()); + assertTrue(authorizedIndices.all(null).isEmpty()); } public void testSecurityIndicesAreRemovedFromRegularUser() { @@ -145,7 +145,7 @@ public void testSecurityIndicesAreRemovedFromRegularUser() { Metadata.EMPTY_METADATA.getProject().getIndicesLookup(), () -> ignore -> {} ); - assertTrue(authorizedIndices.all(null).get().isEmpty()); + assertTrue(authorizedIndices.all(null).isEmpty()); } public void testSecurityIndicesAreRestrictedForDefaultRole() { @@ -177,12 +177,12 @@ public void testSecurityIndicesAreRestrictedForDefaultRole() { metadata.getProject().getIndicesLookup(), () -> ignore -> {} ); - assertThat(authorizedIndices.all(null).get(), containsInAnyOrder("an-index", "another-index")); + assertThat(authorizedIndices.all(null), containsInAnyOrder("an-index", "another-index")); assertThat(authorizedIndices.check("an-index", null), is(true)); assertThat(authorizedIndices.check("another-index", null), is(true)); - assertThat(authorizedIndices.all(null).get(), not(contains(internalSecurityIndex))); + assertThat(authorizedIndices.all(null), not(contains(internalSecurityIndex))); assertThat(authorizedIndices.check(internalSecurityIndex, null), is(false)); - assertThat(authorizedIndices.all(null).get(), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); + assertThat(authorizedIndices.all(null), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, null), is(false)); } @@ -216,7 +216,7 @@ public void testSecurityIndicesAreNotRemovedFromUnrestrictedRole() { () -> ignore -> {} ); assertThat( - authorizedIndices.all(null).get(), + authorizedIndices.all(null), containsInAnyOrder("an-index", "another-index", SecuritySystemIndices.SECURITY_MAIN_ALIAS, internalSecurityIndex) ); @@ -227,7 +227,7 @@ public void testSecurityIndicesAreNotRemovedFromUnrestrictedRole() { () -> ignore -> {} ); assertThat( - authorizedIndicesSuperUser.all(null).get(), + authorizedIndicesSuperUser.all(null), containsInAnyOrder("an-index", "another-index", SecuritySystemIndices.SECURITY_MAIN_ALIAS, internalSecurityIndex) ); } @@ -297,21 +297,21 @@ public void testDataStreamsAreNotIncludedInAuthorizedIndices() { metadata.getProject().getIndicesLookup(), () -> ignore -> {} ); - assertThat(authorizedIndices.all(null).get(), containsInAnyOrder("a1", "a2", "aaaaaa", "b", "ab")); + assertThat(authorizedIndices.all(null), containsInAnyOrder("a1", "a2", "aaaaaa", "b", "ab")); for (String resource : List.of("a1", "a2", "aaaaaa", "b", "ab")) { assertThat(authorizedIndices.check(resource, null), is(true)); } - assertThat(authorizedIndices.all(null).get(), not(contains("bbbbb"))); + assertThat(authorizedIndices.all(null), not(contains("bbbbb"))); assertThat(authorizedIndices.check("bbbbb", null), is(false)); - assertThat(authorizedIndices.all(null).get(), not(contains("ba"))); + assertThat(authorizedIndices.all(null), not(contains("ba"))); assertThat(authorizedIndices.check("ba", null), is(false)); // due to context, datastreams are excluded from wildcard expansion - assertThat(authorizedIndices.all(null).get(), not(contains("adatastream1"))); + assertThat(authorizedIndices.all(null), not(contains("adatastream1"))); // but they are authorized when explicitly tested (they are not "unavailable" for the Security filter) assertThat(authorizedIndices.check("adatastream1", null), is(true)); - assertThat(authorizedIndices.all(null).get(), not(contains(internalSecurityIndex))); + assertThat(authorizedIndices.all(null), not(contains(internalSecurityIndex))); assertThat(authorizedIndices.check(internalSecurityIndex, null), is(false)); - assertThat(authorizedIndices.all(null).get(), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); + assertThat(authorizedIndices.all(null), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, null), is(false)); } @@ -382,14 +382,14 @@ public void testDataStreamsAreIncludedInAuthorizedIndices() { metadata.getProject().getIndicesLookup(), () -> ignore -> {} ); - assertThat(authorizedIndices.all(null).get(), containsInAnyOrder("a1", "a2", "aaaaaa", "b", "ab", "adatastream1", backingIndex)); - assertThat(authorizedIndices.all(null).get(), not(contains("bbbbb"))); + assertThat(authorizedIndices.all(null), containsInAnyOrder("a1", "a2", "aaaaaa", "b", "ab", "adatastream1", backingIndex)); + assertThat(authorizedIndices.all(null), not(contains("bbbbb"))); assertThat(authorizedIndices.check("bbbbb", null), is(false)); - assertThat(authorizedIndices.all(null).get(), not(contains("ba"))); + assertThat(authorizedIndices.all(null), not(contains("ba"))); assertThat(authorizedIndices.check("ba", null), is(false)); - assertThat(authorizedIndices.all(null).get(), not(contains(internalSecurityIndex))); + assertThat(authorizedIndices.all(null), not(contains(internalSecurityIndex))); assertThat(authorizedIndices.check(internalSecurityIndex, null), is(false)); - assertThat(authorizedIndices.all(null).get(), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); + assertThat(authorizedIndices.all(null), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, null), is(false)); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java index 54a5d1a8d1a64..906e6d8a025bd 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java @@ -2233,13 +2233,13 @@ public void testDataStreamsAreNotVisibleWhenNotIncludedByRequestWithWildcard() { List dataStreams = List.of("logs-foo", "logs-foobar"); final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); for (String dsName : dataStreams) { - assertThat(authorizedIndices.all(null).get(), hasItem(dsName)); + assertThat(authorizedIndices.all(null), hasItem(dsName)); assertThat(authorizedIndices.check(dsName, null), is(true)); DataStream dataStream = projectMetadata.dataStreams().get(dsName); - assertThat(authorizedIndices.all(null).get(), hasItem(dsName)); + assertThat(authorizedIndices.all(null), hasItem(dsName)); assertThat(authorizedIndices.check(dsName, null), is(true)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices.all(null).get(), hasItem(i.getName())); + assertThat(authorizedIndices.all(null), hasItem(i.getName())); assertThat(authorizedIndices.check(i.getName(), null), is(true)); } } @@ -2272,13 +2272,13 @@ public void testDataStreamsAreNotVisibleWhenNotIncludedByRequestWithoutWildcard( // data streams and their backing indices should _not_ be in the authorized list since the backing indices // do not match the requested name final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); - assertThat(authorizedIndices.all(null).get(), hasItem(dataStreamName)); + assertThat(authorizedIndices.all(null), hasItem(dataStreamName)); assertThat(authorizedIndices.check(dataStreamName, null), is(true)); DataStream dataStream = projectMetadata.dataStreams().get(dataStreamName); - assertThat(authorizedIndices.all(null).get(), hasItem(dataStreamName)); + assertThat(authorizedIndices.all(null), hasItem(dataStreamName)); assertThat(authorizedIndices.check(dataStreamName, null), is(true)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices.all(null).get(), hasItem(i.getName())); + assertThat(authorizedIndices.all(null), hasItem(i.getName())); assertThat(authorizedIndices.check(i.getName(), null), is(true)); } @@ -2307,10 +2307,10 @@ public void testDataStreamsAreVisibleWhenIncludedByRequestWithWildcard() { final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, TransportSearchAction.TYPE.name(), request); for (String dsName : expectedDataStreams) { DataStream dataStream = projectMetadata.dataStreams().get(dsName); - assertThat(authorizedIndices.all(null).get(), hasItem(dsName)); + assertThat(authorizedIndices.all(null), hasItem(dsName)); assertThat(authorizedIndices.check(dsName, null), is(true)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices.all(null).get(), hasItem(i.getName())); + assertThat(authorizedIndices.all(null), hasItem(i.getName())); assertThat(authorizedIndices.check(i.getName(), null), is(true)); } } @@ -2348,10 +2348,10 @@ public void testDataStreamsAreVisibleWhenIncludedByRequestWithoutWildcard() { final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, TransportSearchAction.TYPE.name(), request); // data streams and their backing indices should be in the authorized list - assertThat(authorizedIndices.all(null).get(), hasItem(dataStreamName)); + assertThat(authorizedIndices.all(null), hasItem(dataStreamName)); assertThat(authorizedIndices.check(dataStreamName, null), is(true)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices.all(null).get(), hasItem(i.getName())); + assertThat(authorizedIndices.all(null), hasItem(i.getName())); assertThat(authorizedIndices.check(i.getName(), null), is(true)); } @@ -2381,14 +2381,14 @@ public void testBackingIndicesAreVisibleWhenIncludedByRequestWithWildcard() { final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, TransportSearchAction.TYPE.name(), request); for (String dsName : expectedDataStreams) { DataStream dataStream = projectMetadata.dataStreams().get(dsName); - assertThat(authorizedIndices.all(null).get(), hasItem(dsName)); + assertThat(authorizedIndices.all(null), hasItem(dsName)); assertThat(authorizedIndices.check(dsName, null), is(true)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices.all(null).get(), hasItem(i.getName())); + assertThat(authorizedIndices.all(null), hasItem(i.getName())); assertThat(authorizedIndices.check(i.getName(), null), is(true)); } for (Index i : dataStream.getFailureIndices()) { - assertThat(authorizedIndices.all(null).get(), hasItem(i.getName())); + assertThat(authorizedIndices.all(null), hasItem(i.getName())); assertThat(authorizedIndices.check(i.getName(), null), is(true)); } } @@ -2420,15 +2420,15 @@ public void testBackingIndicesAreNotVisibleWhenNotIncludedByRequestWithoutWildca // data streams and their backing indices should _not_ be in the authorized list since the backing indices // did not match the requested pattern and the request does not support data streams final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); - assertThat(authorizedIndices.all(null).get(), hasItem(dataStreamName)); + assertThat(authorizedIndices.all(null), hasItem(dataStreamName)); assertThat(authorizedIndices.check(dataStreamName, null), is(true)); DataStream dataStream = projectMetadata.dataStreams().get(dataStreamName); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices.all(null).get(), hasItem(i.getName())); + assertThat(authorizedIndices.all(null), hasItem(i.getName())); assertThat(authorizedIndices.check(i.getName(), null), is(true)); } for (Index i : dataStream.getFailureIndices()) { - assertThat(authorizedIndices.all(null).get(), hasItem(i.getName())); + assertThat(authorizedIndices.all(null), hasItem(i.getName())); assertThat(authorizedIndices.check(i.getName(), null), is(true)); } @@ -2460,18 +2460,18 @@ public void testDataStreamNotAuthorizedWhenBackingIndicesAreAuthorizedViaWildcar // data streams should _not_ be in the authorized list but their backing indices that matched both the requested pattern // and the authorized pattern should be in the list final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); - assertThat(authorizedIndices.all(null).get(), not(hasItem("logs-foobar"))); + assertThat(authorizedIndices.all(null), not(hasItem("logs-foobar"))); assertThat(authorizedIndices.check("logs-foobar", null), is(false)); DataStream dataStream = projectMetadata.dataStreams().get("logs-foobar"); - assertThat(authorizedIndices.all(null).get(), not(hasItem(indexName))); + assertThat(authorizedIndices.all(null), not(hasItem(indexName))); // request pattern is subset of the authorized pattern, but be aware that patterns are never passed to #check in main code assertThat(authorizedIndices.check(indexName, null), is(true)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices.all(null).get(), hasItem(i.getName())); + assertThat(authorizedIndices.all(null), hasItem(i.getName())); assertThat(authorizedIndices.check(i.getName(), null), is(true)); } for (Index i : dataStream.getFailureIndices()) { - assertThat(authorizedIndices.all(null).get(), hasItem(i.getName())); + assertThat(authorizedIndices.all(null), hasItem(i.getName())); assertThat(authorizedIndices.check(i.getName(), null), is(true)); } @@ -2500,13 +2500,13 @@ public void testDataStreamNotAuthorizedWhenBackingIndicesAreAuthorizedViaNameAnd // data streams should _not_ be in the authorized list but a single backing index that matched the requested pattern // and the authorized name should be in the list final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); - assertThat(authorizedIndices.all(null).get(), not(hasItem("logs-foobar"))); + assertThat(authorizedIndices.all(null), not(hasItem("logs-foobar"))); assertThat(authorizedIndices.check("logs-foobar", null), is(false)); String expectedIndex = failureStore ? DataStream.getDefaultFailureStoreName("logs-foobar", 1, System.currentTimeMillis()) : DataStream.getDefaultBackingIndexName("logs-foobar", 1); - assertThat(authorizedIndices.all(null).get(), hasItem(expectedIndex)); + assertThat(authorizedIndices.all(null), hasItem(expectedIndex)); assertThat(authorizedIndices.check(expectedIndex, null), is(true)); // only the single backing index will be in the resolved list since the request does not support data streams @@ -2532,18 +2532,18 @@ public void testDataStreamNotAuthorizedWhenBackingIndicesAreAuthorizedViaWildcar // data streams should _not_ be in the authorized list but their backing indices that matched both the requested pattern // and the authorized pattern should be in the list final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); - assertThat(authorizedIndices.all(null).get(), not(hasItem("logs-foobar"))); + assertThat(authorizedIndices.all(null), not(hasItem("logs-foobar"))); assertThat(authorizedIndices.check("logs-foobar", null), is(false)); DataStream dataStream = projectMetadata.dataStreams().get("logs-foobar"); - assertThat(authorizedIndices.all(null).get(), not(hasItem(indexName))); + assertThat(authorizedIndices.all(null), not(hasItem(indexName))); // request pattern is subset of the authorized pattern, but be aware that patterns are never passed to #check in main code assertThat(authorizedIndices.check(indexName, null), is(true)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices.all(null).get(), hasItem(i.getName())); + assertThat(authorizedIndices.all(null), hasItem(i.getName())); assertThat(authorizedIndices.check(i.getName(), null), is(true)); } for (Index i : dataStream.getFailureIndices()) { - assertThat(authorizedIndices.all(null).get(), hasItem(i.getName())); + assertThat(authorizedIndices.all(null), hasItem(i.getName())); assertThat(authorizedIndices.check(i.getName(), null), is(true)); } @@ -2575,9 +2575,9 @@ public void testDataStreamNotAuthorizedWhenBackingIndicesAreAuthorizedViaNameAnd ? DataStream.getDefaultFailureStoreName("logs-foobar", 1, System.currentTimeMillis()) : DataStream.getDefaultBackingIndexName("logs-foobar", 1); final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); - assertThat(authorizedIndices.all(null).get(), not(hasItem("logs-foobar"))); + assertThat(authorizedIndices.all(null), not(hasItem("logs-foobar"))); assertThat(authorizedIndices.check("logs-foobar", null), is(false)); - assertThat(authorizedIndices.all(null).get(), hasItem(expectedIndex)); + assertThat(authorizedIndices.all(null), hasItem(expectedIndex)); assertThat(authorizedIndices.check(expectedIndex, null), is(true)); // only the single backing index will be in the resolved list since the request does not support data streams diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java index c933c878a8123..10d2173c0f201 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java @@ -1444,10 +1444,10 @@ public void testBackingIndicesAreIncludedForAuthorizedDataStreams() { lookup, () -> ignore -> {} ); - assertThat(authorizedIndices.all(null).get(), hasItem(dataStreamName)); + assertThat(authorizedIndices.all(null), hasItem(dataStreamName)); assertThat(authorizedIndices.check(dataStreamName, null), is(true)); assertThat( - authorizedIndices.all(null).get(), + authorizedIndices.all(null), hasItems(backingIndices.stream().map(im -> im.getIndex().getName()).collect(Collectors.toList()).toArray(Strings.EMPTY_ARRAY)) ); for (String index : backingIndices.stream().map(im -> im.getIndex().getName()).toList()) { @@ -1487,7 +1487,7 @@ public void testExplicitMappingUpdatesAreNotGrantedWithIngestPrivileges() { lookup, () -> ignore -> {} ); - assertThat(authorizedIndices.all(null).get().isEmpty(), is(true)); + assertThat(authorizedIndices.all(null).isEmpty(), is(true)); } public void testNoInfiniteRecursionForRBACAuthorizationInfoHashCode() { From be718df3c804967c6f890ea2a651d09098c9ee9a Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 10 Mar 2025 11:25:42 +0100 Subject: [PATCH 079/131] Moar --- .../org/elasticsearch/xpack/security/authz/RBACEngine.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java index 953603026b607..c12d1bdab249e 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java @@ -885,7 +885,8 @@ static AuthorizedIndices resolveAuthorizedIndicesFromRole( // TODO: can this be done smarter? I think there are usually more indices/aliases in the cluster then indices defined a roles? if (includeDataStreams) { for (IndexAbstraction indexAbstraction : lookup.values()) { - // TODO clean this up and explain + // failure indices are special: when accessed directly (not through ::failures on parent data stream) they are accessed + // as implicitly as data. However, authz to the parent data stream happens via the failures selector if (indexAbstraction.isFailureIndexOfDataStream() && predicate.test(indexAbstraction.getParentDataStream(), IndexComponentSelector.FAILURES)) { indicesAndAliases.add(indexAbstraction.getName()); @@ -927,8 +928,8 @@ static AuthorizedIndices resolveAuthorizedIndicesFromRole( } } } - timeChecker.accept(indicesAndAliases); } + timeChecker.accept(indicesAndAliases); return indicesAndAliases; }, (name, selectorString) -> { final IndexAbstraction indexAbstraction = lookup.get(name); From 400a1f74a03a09c8d541e8fd46e0378f34f693e0 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 10 Mar 2025 11:45:25 +0100 Subject: [PATCH 080/131] Fix --- .../IndexAbstractionResolverTests.java | 48 +++++++++---------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolverTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolverTests.java index cbaaf3dcbc12c..ee2119e2c3b56 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolverTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolverTests.java @@ -27,7 +27,6 @@ import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; -import java.util.function.Supplier; import static org.elasticsearch.index.mapper.MapperService.SINGLE_MAPPING_NAME; import static org.elasticsearch.indices.SystemIndices.EXTERNAL_SYSTEM_INDEX_ACCESS_CONTROL_HEADER_KEY; @@ -48,7 +47,7 @@ public class IndexAbstractionResolverTests extends ESTestCase { private String dateTimeIndexTomorrow; // Only used when resolving wildcard expressions - private final Supplier> defaultMask = () -> Set.of("index1", "index2", "data-stream1"); + private final Set defaultMask = Set.of("index1", "index2", "data-stream1"); @Override public void setUp() throws Exception { @@ -215,13 +214,11 @@ public void testResolveIndexAbstractions() { public void testIsIndexVisible() { assertThat(isIndexVisible("index1", null), is(true)); - assertThat(isIndexVisible("index1", "*"), is(true)); assertThat(isIndexVisible("index1", "data"), is(true)); assertThat(isIndexVisible("index1", "failures"), is(false)); // * // * Indices don't have failure components so the failure component is not visible assertThat(isIndexVisible("data-stream1", null), is(true)); - assertThat(isIndexVisible("data-stream1", "*"), is(true)); assertThat(isIndexVisible("data-stream1", "data"), is(true)); assertThat(isIndexVisible("data-stream1", "failures"), is(true)); } @@ -290,14 +287,14 @@ public void testIsNetNewSystemIndexVisible() { indexAbstractionResolver = new IndexAbstractionResolver(indexNameExpressionResolver); // this covers the GET * case -- with system access, you can see everything - assertThat(isIndexVisible("other", "*"), is(true)); - assertThat(isIndexVisible(".foo", "*"), is(true)); - assertThat(isIndexVisible(".bar", "*"), is(true)); + assertThat(isIndexVisible("other", null), is(true)); + assertThat(isIndexVisible(".foo", null), is(true)); + assertThat(isIndexVisible(".bar", null), is(true)); // but if you don't ask for hidden and aliases, you won't see hidden indices or aliases, naturally - assertThat(isIndexVisible("other", "*", noHiddenNoAliases), is(true)); - assertThat(isIndexVisible(".foo", "*", noHiddenNoAliases), is(false)); - assertThat(isIndexVisible(".bar", "*", noHiddenNoAliases), is(false)); + assertThat(isIndexVisible("other", null, noHiddenNoAliases), is(true)); + assertThat(isIndexVisible(".foo", null, noHiddenNoAliases), is(false)); + assertThat(isIndexVisible(".bar", null, noHiddenNoAliases), is(false)); } { @@ -311,14 +308,14 @@ public void testIsNetNewSystemIndexVisible() { indexAbstractionResolver = new IndexAbstractionResolver(indexNameExpressionResolver); // this covers the GET * case -- without system access, you can't see everything - assertThat(isIndexVisible("other", "*"), is(true)); - assertThat(isIndexVisible(".foo", "*"), is(false)); - assertThat(isIndexVisible(".bar", "*"), is(false)); + assertThat(isIndexVisible("other", null), is(true)); + assertThat(isIndexVisible(".foo", null), is(false)); + assertThat(isIndexVisible(".bar", null), is(false)); // no difference here in the datastream case, you can't see these then, either - assertThat(isIndexVisible("other", "*", noHiddenNoAliases), is(true)); - assertThat(isIndexVisible(".foo", "*", noHiddenNoAliases), is(false)); - assertThat(isIndexVisible(".bar", "*", noHiddenNoAliases), is(false)); + assertThat(isIndexVisible("other", null, noHiddenNoAliases), is(true)); + assertThat(isIndexVisible(".foo", null, noHiddenNoAliases), is(false)); + assertThat(isIndexVisible(".bar", null, noHiddenNoAliases), is(false)); } { @@ -333,14 +330,14 @@ public void testIsNetNewSystemIndexVisible() { indexAbstractionResolver = new IndexAbstractionResolver(indexNameExpressionResolver); // this covers the GET * case -- with product (only) access, you can't see everything - assertThat(isIndexVisible("other", "*"), is(true)); - assertThat(isIndexVisible(".foo", "*"), is(false)); - assertThat(isIndexVisible(".bar", "*"), is(false)); + assertThat(isIndexVisible("other", null), is(true)); + assertThat(isIndexVisible(".foo", null), is(false)); + assertThat(isIndexVisible(".bar", null), is(false)); // no difference here in the datastream case, you can't see these then, either - assertThat(isIndexVisible("other", "*", noHiddenNoAliases), is(true)); - assertThat(isIndexVisible(".foo", "*", noHiddenNoAliases), is(false)); - assertThat(isIndexVisible(".bar", "*", noHiddenNoAliases), is(false)); + assertThat(isIndexVisible("other", null, noHiddenNoAliases), is(true)); + assertThat(isIndexVisible(".foo", null, noHiddenNoAliases), is(false)); + assertThat(isIndexVisible(".bar", null, noHiddenNoAliases), is(false)); } } @@ -366,14 +363,13 @@ private List resolveAbstractionsSelectorAllowed(List expressions return resolveAbstractions(expressions, IndicesOptions.strictExpandOpen(), defaultMask); } - private List resolveAbstractions(List expressions, IndicesOptions indicesOptions, Supplier> mask) { + private List resolveAbstractions(List expressions, IndicesOptions indicesOptions, Set mask) { return indexAbstractionResolver.resolveIndexAbstractions( expressions, indicesOptions, projectMetadata, - // TODO - sel -> mask, - (idx, sel) -> true, + (ignored) -> mask, + (ignored, nothing) -> true, true ); } From fb599ca19853ed6008a8df2570f5b92dffb6eb10 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 10 Mar 2025 11:57:31 +0100 Subject: [PATCH 081/131] Prevent privilege checks --- .../xpack/core/security/authz/AuthorizationEngine.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java index 07525f1032871..45b71e9da3a6f 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java @@ -366,6 +366,14 @@ public ActionRequestValidationException validate(ActionRequestValidationExceptio && application.length == 0) { validationException = addValidationError("must specify at least one privilege", validationException); } + if (index != null) { + for (RoleDescriptor.IndicesPrivileges indexPrivilege : index) { + if (Arrays.stream(indexPrivilege.getPrivileges()) + .anyMatch(p -> "read_failure_store".equals(p) || "manage_failure_store".equals(p))) { + validationException = addValidationError("checking failure store privileges is not supported", validationException); + } + } + } return validationException; } From 79d50bef892b101591acbec9f6d39cc1e672fc9b Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 10 Mar 2025 11:58:15 +0100 Subject: [PATCH 082/131] Null check --- .../xpack/core/security/authz/AuthorizationEngine.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java index 45b71e9da3a6f..6af704475b1f9 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java @@ -368,8 +368,9 @@ public ActionRequestValidationException validate(ActionRequestValidationExceptio } if (index != null) { for (RoleDescriptor.IndicesPrivileges indexPrivilege : index) { - if (Arrays.stream(indexPrivilege.getPrivileges()) - .anyMatch(p -> "read_failure_store".equals(p) || "manage_failure_store".equals(p))) { + if (indexPrivilege.getPrivileges() != null + && Arrays.stream(indexPrivilege.getPrivileges()) + .anyMatch(p -> "read_failure_store".equals(p) || "manage_failure_store".equals(p))) { validationException = addValidationError("checking failure store privileges is not supported", validationException); } } From 0c4ec24ebd4919796f59e97dd452b767d017f35d Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 10 Mar 2025 12:57:45 +0100 Subject: [PATCH 083/131] WIP unit tests --- .../accesscontrol/IndicesPermissionTests.java | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java index 9ff646b286bd6..0a7552ab31837 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java @@ -11,6 +11,7 @@ import org.elasticsearch.action.admin.indices.mapping.put.TransportAutoPutMappingAction; import org.elasticsearch.action.admin.indices.mapping.put.TransportPutMappingAction; import org.elasticsearch.action.search.TransportSearchAction; +import org.elasticsearch.action.support.IndexComponentSelector; import org.elasticsearch.cluster.metadata.AliasMetadata; import org.elasticsearch.cluster.metadata.DataStream; import org.elasticsearch.cluster.metadata.DataStreamTestHelper; @@ -757,6 +758,75 @@ public void testResourceAuthorizedPredicateAnd() { assertThat(predicate.test(concreteIndexD), is(true)); } + public void testResourceAuthorizedPredicateAndWithFailures() { + IndicesPermission.IsResourceAuthorizedPredicate predicate1 = new IndicesPermission.IsResourceAuthorizedPredicate( + StringMatcher.of("c", "a"), + StringMatcher.of("e", "f"), + StringMatcher.of("b", "d") + ); + IndicesPermission.IsResourceAuthorizedPredicate predicate2 = new IndicesPermission.IsResourceAuthorizedPredicate( + StringMatcher.of("c", "b"), + StringMatcher.of("a", "f", "g"), + StringMatcher.of("a", "d") + ); + Metadata.Builder mb = Metadata.builder( + DataStreamTestHelper.getClusterStateWithDataStreams( + List.of( + Tuple.tuple("a", 1), + Tuple.tuple("b", 1), + Tuple.tuple("c", 1), + Tuple.tuple("d", 1), + Tuple.tuple("e", 1), + Tuple.tuple("f", 1) + ), + List.of(), + Instant.now().toEpochMilli(), + builder().build(), + 1 + ).getMetadata() + ); + DataStream dataStreamA = mb.dataStream("a"); + DataStream dataStreamB = mb.dataStream("b"); + DataStream dataStreamC = mb.dataStream("c"); + DataStream dataStreamD = mb.dataStream("d"); + DataStream dataStreamE = mb.dataStream("e"); + DataStream dataStreamF = mb.dataStream("f"); + IndexAbstraction concreteIndexA = concreteIndexAbstraction("a"); + IndexAbstraction concreteIndexB = concreteIndexAbstraction("b"); + IndexAbstraction concreteIndexC = concreteIndexAbstraction("c"); + IndexAbstraction concreteIndexD = concreteIndexAbstraction("d"); + IndexAbstraction concreteIndexE = concreteIndexAbstraction("e"); + IndexAbstraction concreteIndexF = concreteIndexAbstraction("f"); + IndicesPermission.IsResourceAuthorizedPredicate predicate = predicate1.and(predicate2); + assertThat(predicate.test(dataStreamA), is(false)); + assertThat(predicate.test(dataStreamB), is(false)); + assertThat(predicate.test(dataStreamC), is(true)); + assertThat(predicate.test(dataStreamD), is(false)); + assertThat(predicate.test(dataStreamE), is(false)); + assertThat(predicate.test(dataStreamF), is(false)); + + assertThat(predicate.test(dataStreamA, IndexComponentSelector.FAILURES), is(false)); + assertThat(predicate.test(dataStreamB, IndexComponentSelector.FAILURES), is(false)); + assertThat(predicate.test(dataStreamC, IndexComponentSelector.FAILURES), is(false)); + assertThat(predicate.test(dataStreamD, IndexComponentSelector.FAILURES), is(false)); + assertThat(predicate.test(dataStreamE, IndexComponentSelector.FAILURES), is(false)); + assertThat(predicate.test(dataStreamF, IndexComponentSelector.FAILURES), is(true)); + + assertThat(predicate.test(concreteIndexA), is(true)); + assertThat(predicate.test(concreteIndexB), is(true)); + assertThat(predicate.test(concreteIndexC), is(true)); + assertThat(predicate.test(concreteIndexD), is(true)); + assertThat(predicate.test(concreteIndexE), is(false)); + assertThat(predicate.test(concreteIndexF), is(false)); + + assertThat(predicate.test(concreteIndexA, IndexComponentSelector.FAILURES), is(false)); + assertThat(predicate.test(concreteIndexB, IndexComponentSelector.FAILURES), is(false)); + assertThat(predicate.test(concreteIndexC, IndexComponentSelector.FAILURES), is(false)); + assertThat(predicate.test(concreteIndexD, IndexComponentSelector.FAILURES), is(false)); + assertThat(predicate.test(concreteIndexE, IndexComponentSelector.FAILURES), is(false)); + assertThat(predicate.test(concreteIndexF, IndexComponentSelector.FAILURES), is(true)); + } + private static IndexAbstraction concreteIndexAbstraction(String name) { return new IndexAbstraction.ConcreteIndex( IndexMetadata.builder(name).settings(indexSettings(IndexVersion.current(), 1, 0)).build() From c8205fb8cb3f5cb070bae7554a79dfbf824dbe23 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 10 Mar 2025 13:54:09 +0100 Subject: [PATCH 084/131] Fix --- .../profile/ProfileHasPrivilegesRequestTests.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/profile/ProfileHasPrivilegesRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/profile/ProfileHasPrivilegesRequestTests.java index 0d84eb041ee0c..e14539bbc9c12 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/profile/ProfileHasPrivilegesRequestTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/profile/ProfileHasPrivilegesRequestTests.java @@ -18,6 +18,8 @@ import java.util.List; import java.util.Locale; +import java.util.Set; +import java.util.stream.Collectors; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.hasItem; @@ -134,13 +136,20 @@ private static RoleDescriptor.IndicesPrivileges[] randomIndicesPrivileges(boolea )]; for (int i = 0; i < indicesPrivileges.length; i++) { indicesPrivileges[i] = RoleDescriptor.IndicesPrivileges.builder() - .privileges(randomSubsetOf(randomIntBetween(1, 5), IndexPrivilege.names())) + .privileges(randomSubsetOf(randomIntBetween(1, 5), validPrivilegeNames())) .indices(randomList(1, 3, () -> randomAlphaOfLengthBetween(2, 8) + (randomBoolean() ? "*" : ""))) .build(); } return indicesPrivileges; } + private static Set validPrivilegeNames() { + return IndexPrivilege.names() + .stream() + .filter(name -> false == name.equals("read_failure_store") && false == name.equals("manage_failure_store")) + .collect(Collectors.toSet()); + } + private static RoleDescriptor.ApplicationResourcePrivileges[] randomApplicationResourcePrivileges(boolean allowEmpty) { RoleDescriptor.ApplicationResourcePrivileges[] appPrivileges = new RoleDescriptor.ApplicationResourcePrivileges[randomIntBetween( allowEmpty ? 0 : 1, From aa741472242cdefe15ae62b0041f3d139920104f Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 10 Mar 2025 16:32:59 +0100 Subject: [PATCH 085/131] More selector handling --- .../authz/accesscontrol/IndicesAccessControl.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/IndicesAccessControl.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/IndicesAccessControl.java index 3f0fb18e825c0..3bc2a942b2586 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/IndicesAccessControl.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/IndicesAccessControl.java @@ -6,6 +6,7 @@ */ package org.elasticsearch.xpack.core.security.authz.accesscontrol; +import org.elasticsearch.action.support.IndexComponentSelector; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.util.CachedSupplier; @@ -62,7 +63,12 @@ protected IndicesAccessControl(IndicesAccessControl copy) { @Nullable public IndexAccessControl getIndexPermissions(String index) { Tuple indexAndSelector = IndexNameExpressionResolver.splitSelectorExpression(index); - return this.getAllIndexPermissions().get(indexAndSelector.v1()); + return this.getAllIndexPermissions() + .get( + IndexComponentSelector.FAILURES.equals(IndexComponentSelector.getByKey(indexAndSelector.v2())) + ? index + : indexAndSelector.v1() + ); } public boolean hasIndexPermissions(String index) { From a3e6429a3ccdc6aea74b9638ed1c700ecaf10e0c Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 10 Mar 2025 21:20:13 +0100 Subject: [PATCH 086/131] WIP test clean up and async search --- .../FailureStoreSecurityRestIT.java | 270 +++++++++--------- 1 file changed, 139 insertions(+), 131 deletions(-) diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java index 5de00e44ae2dc..631248beec55f 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java @@ -163,7 +163,7 @@ public void testGetUserPrivileges() throws IOException { } @SuppressWarnings("unchecked") - public void testFailureStoreAccess() throws IOException { + public void testFailureStoreAccess() throws Exception { String dataAccessRole = "data_access"; String starReadOnlyRole = "star_read_only_access"; String failureStoreAccessRole = "failure_store_access"; @@ -265,14 +265,14 @@ public void testFailureStoreAccess() throws IOException { // search data { - Request request = searchRequest("test1"); + var request = new Search("test1"); for (var user : users) { switch (user) { case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: - expectDocIds(performRequest(user, request), dataDocId); + expect(user, request, dataDocId); break; case FAILURE_STORE_ACCESS_USER: - expectThrows403(() -> performRequest(user, request)); + expect(user, request, 403); break; default: fail("must cover user: " + user); @@ -280,14 +280,14 @@ public void testFailureStoreAccess() throws IOException { } } { - Request request = searchRequest("test1", "?ignore_unavailable=true"); + var request = new Search("test1", "?ignore_unavailable=true"); for (var user : users) { switch (user) { case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: - expectDocIds(performRequest(user, request), dataDocId); + expect(user, request, dataDocId); break; case FAILURE_STORE_ACCESS_USER: - expectEmpty(performRequest(user, request)); + expect(user, request); break; default: fail("must cover user: " + user); @@ -295,14 +295,14 @@ public void testFailureStoreAccess() throws IOException { } } { - Request request = searchRequest("test-alias"); + var request = new Search("test-alias"); for (var user : users) { switch (user) { case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: - expectDocIds(performRequest(user, request), dataDocId); + expect(user, request, dataDocId); break; case FAILURE_STORE_ACCESS_USER: - expectThrows403(() -> performRequest(user, request)); + expect(user, request, 403); break; default: fail("must cover user: " + user); @@ -310,14 +310,14 @@ public void testFailureStoreAccess() throws IOException { } } { - Request request = searchRequest("test-alias", "?ignore_unavailable=true"); + var request = new Search("test-alias", "?ignore_unavailable=true"); for (var user : users) { switch (user) { case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: - expectDocIds(performRequest(user, request), dataDocId); + expect(user, request, dataDocId); break; case FAILURE_STORE_ACCESS_USER: - expectEmpty(performRequest(user, request)); + expect(user, request); break; default: fail("must cover user: " + user); @@ -325,14 +325,14 @@ public void testFailureStoreAccess() throws IOException { } } { - Request request = searchRequest("test*"); + var request = new Search("test*"); for (var user : users) { switch (user) { case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: - expectDocIds(performRequest(user, request), dataDocId); + expect(user, request, dataDocId); break; case FAILURE_STORE_ACCESS_USER: - expectEmpty(performRequest(user, request)); + expect(user, request); break; default: fail("must cover user: " + user); @@ -340,14 +340,14 @@ public void testFailureStoreAccess() throws IOException { } } { - Request request = searchRequest("*1"); + var request = new Search("*1"); for (var user : users) { switch (user) { case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: - expectDocIds(performRequest(user, request), dataDocId); + expect(user, request, dataDocId); break; case FAILURE_STORE_ACCESS_USER: - expectEmpty(performRequest(user, request)); + expect(user, request); break; default: fail("must cover user: " + user); @@ -355,14 +355,14 @@ public void testFailureStoreAccess() throws IOException { } } { - Request request = searchRequest("*"); + var request = new Search("*"); for (var user : users) { switch (user) { case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: - expectDocIds(performRequest(user, request), dataDocId); + expect(user, request, dataDocId); break; case FAILURE_STORE_ACCESS_USER: - expectEmpty(performRequest(user, request)); + expect(user, request); break; default: fail("must cover user: " + user); @@ -370,14 +370,14 @@ public void testFailureStoreAccess() throws IOException { } } { - Request request = searchRequest(".ds*"); + var request = new Search(".ds*"); for (var user : users) { switch (user) { case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: - expectDocIds(performRequest(user, request), dataDocId); + expect(user, request, dataDocId); break; case FAILURE_STORE_ACCESS_USER: - expectEmpty(performRequest(user, request)); + expect(user, request); break; default: fail("must cover user: " + user); @@ -385,14 +385,14 @@ public void testFailureStoreAccess() throws IOException { } } { - Request request = searchRequest(dataIndexName); + var request = new Search(dataIndexName); for (var user : users) { switch (user) { case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: - expectDocIds(performRequest(user, request), dataDocId); + expect(user, request, dataDocId); break; case FAILURE_STORE_ACCESS_USER: - expectThrows403(() -> performRequest(user, request)); + expect(user, request, 403); break; default: fail("must cover user: " + user); @@ -400,14 +400,14 @@ public void testFailureStoreAccess() throws IOException { } } { - Request request = searchRequest(dataIndexName, "?ignore_unavailable=true"); + var request = new Search(dataIndexName, "?ignore_unavailable=true"); for (var user : users) { switch (user) { case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: - expectDocIds(performRequest(user, request), dataDocId); + expect(user, request, dataDocId); break; case FAILURE_STORE_ACCESS_USER: - expectEmpty(performRequest(user, request)); + expect(user, request); break; default: fail("must cover user: " + user); @@ -415,14 +415,14 @@ public void testFailureStoreAccess() throws IOException { } } { - Request request = searchRequest("test2"); + var request = new Search("test2"); for (var user : users) { switch (user) { case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: - expectThrows404(() -> performRequest(user, request)); + expect(user, request, 404); break; case FAILURE_STORE_ACCESS_USER: - expectThrows403(() -> performRequest(user, request)); + expect(user, request, 403); break; default: fail("must cover user: " + user); @@ -430,11 +430,11 @@ public void testFailureStoreAccess() throws IOException { } } { - Request request = searchRequest("test2", "?ignore_unavailable=true"); + var request = new Search("test2", "?ignore_unavailable=true"); for (var user : users) { switch (user) { case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER, FAILURE_STORE_ACCESS_USER: - expectEmpty(performRequest(user, request)); + expect(user, request); break; default: fail("must cover user: " + user); @@ -444,14 +444,14 @@ public void testFailureStoreAccess() throws IOException { // search failures { - Request request = searchRequest("test1::failures"); + var request = new Search("test1::failures"); for (var user : users) { switch (user) { case DATA_ACCESS_USER, STAR_READ_ONLY_USER: - expectThrows403(() -> performRequest(user, request)); + expect(user, request, 403); break; case FAILURE_STORE_ACCESS_USER, BOTH_ACCESS_USER: - expectDocIds(performRequest(user, request), failuresDocId); + expect(user, request, failuresDocId); break; default: fail("must cover user: " + user); @@ -459,14 +459,14 @@ public void testFailureStoreAccess() throws IOException { } } { - Request request = searchRequest("test1::failures", "?ignore_unavailable=true"); + var request = new Search("test1::failures", "?ignore_unavailable=true"); for (var user : users) { switch (user) { case DATA_ACCESS_USER, STAR_READ_ONLY_USER: - expectEmpty(performRequest(user, request)); + expect(user, request); break; case FAILURE_STORE_ACCESS_USER, BOTH_ACCESS_USER: - expectDocIds(performRequest(user, request), failuresDocId); + expect(user, request, failuresDocId); break; default: fail("must cover user: " + user); @@ -474,14 +474,14 @@ public void testFailureStoreAccess() throws IOException { } } { - Request request = searchRequest("test-alias::failures"); + var request = new Search("test-alias::failures"); for (var user : users) { switch (user) { case DATA_ACCESS_USER, STAR_READ_ONLY_USER: - expectThrows403(() -> performRequest(user, request)); + expect(user, request, 403); break; case FAILURE_STORE_ACCESS_USER, BOTH_ACCESS_USER: - expectDocIds(performRequest(user, request), failuresDocId); + expect(user, request, failuresDocId); break; default: fail("must cover user: " + user); @@ -489,14 +489,14 @@ public void testFailureStoreAccess() throws IOException { } } { - Request request = searchRequest("test-alias::failures", "?ignore_unavailable=true"); + var request = new Search("test-alias::failures", "?ignore_unavailable=true"); for (var user : users) { switch (user) { case DATA_ACCESS_USER, STAR_READ_ONLY_USER: - expectEmpty(performRequest(user, request)); + expect(user, request); break; case FAILURE_STORE_ACCESS_USER, BOTH_ACCESS_USER: - expectDocIds(performRequest(user, request), failuresDocId); + expect(user, request, failuresDocId); break; default: fail("must cover user: " + user); @@ -504,14 +504,14 @@ public void testFailureStoreAccess() throws IOException { } } { - Request request = searchRequest("test*::failures"); + var request = new Search("test*::failures"); for (var user : users) { switch (user) { case DATA_ACCESS_USER, STAR_READ_ONLY_USER: - expectEmpty(performRequest(user, request)); + expect(user, request); break; case FAILURE_STORE_ACCESS_USER, BOTH_ACCESS_USER: - expectDocIds(performRequest(user, request), failuresDocId); + expect(user, request, failuresDocId); break; default: fail("must cover user: " + user); @@ -519,14 +519,14 @@ public void testFailureStoreAccess() throws IOException { } } { - Request request = searchRequest("*1::failures"); + var request = new Search("*1::failures"); for (var user : users) { switch (user) { case DATA_ACCESS_USER, STAR_READ_ONLY_USER: - expectEmpty(performRequest(user, request)); + expect(user, request); break; case FAILURE_STORE_ACCESS_USER, BOTH_ACCESS_USER: - expectDocIds(performRequest(user, request), failuresDocId); + expect(user, request, failuresDocId); break; default: fail("must cover user: " + user); @@ -534,14 +534,14 @@ public void testFailureStoreAccess() throws IOException { } } { - Request request = searchRequest("*::failures"); + var request = new Search("*::failures"); for (var user : users) { switch (user) { case DATA_ACCESS_USER, STAR_READ_ONLY_USER: - expectEmpty(performRequest(user, request)); + expect(user, request); break; case FAILURE_STORE_ACCESS_USER, BOTH_ACCESS_USER: - expectDocIds(performRequest(user, request), failuresDocId); + expect(user, request, failuresDocId); break; default: fail("must cover user: " + user); @@ -549,14 +549,14 @@ public void testFailureStoreAccess() throws IOException { } } { - Request request = searchRequest(".fs*"); + var request = new Search(".fs*"); for (var user : users) { switch (user) { case DATA_ACCESS_USER: - expectEmpty(performRequest(user, request)); + expect(user, request); break; case FAILURE_STORE_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: - expectDocIds(performRequest(user, request), failuresDocId); + expect(user, request, failuresDocId); break; default: fail("must cover user: " + user); @@ -564,14 +564,14 @@ public void testFailureStoreAccess() throws IOException { } } { - Request request = searchRequest(failureIndexName); + var request = new Search(failureIndexName); for (var user : users) { switch (user) { case DATA_ACCESS_USER: - expectThrows403(() -> performRequest(user, request)); + expect(user, request, 403); break; case FAILURE_STORE_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: - expectDocIds(performRequest(user, request), failuresDocId); + expect(user, request, failuresDocId); break; default: fail("must cover user: " + user); @@ -579,14 +579,14 @@ public void testFailureStoreAccess() throws IOException { } } { - Request request = searchRequest(failureIndexName, "?ignore_unavailable=true"); + var request = new Search(failureIndexName, "?ignore_unavailable=true"); for (var user : users) { switch (user) { case DATA_ACCESS_USER: - expectEmpty(performRequest(user, request)); + expect(user, request); break; case FAILURE_STORE_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: - expectDocIds(performRequest(user, request), failuresDocId); + expect(user, request, failuresDocId); break; default: fail("must cover user: " + user); @@ -594,14 +594,14 @@ public void testFailureStoreAccess() throws IOException { } } { - Request request = searchRequest("test2::failures"); + var request = new Search("test2::failures"); for (var user : users) { switch (user) { case DATA_ACCESS_USER, STAR_READ_ONLY_USER: - expectThrows403(() -> performRequest(user, request)); + expect(user, request, 403); break; case FAILURE_STORE_ACCESS_USER, BOTH_ACCESS_USER: - expectThrows404(() -> performRequest(user, request)); + expect(user, request, 404); break; default: fail("must cover user: " + user); @@ -609,11 +609,11 @@ public void testFailureStoreAccess() throws IOException { } } { - Request request = searchRequest("test2::failures", "?ignore_unavailable=true"); + var request = new Search("test2::failures", "?ignore_unavailable=true"); for (var user : users) { switch (user) { case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER, FAILURE_STORE_ACCESS_USER: - expectEmpty(performRequest(user, request)); + expect(user, request); break; default: fail("must cover user: " + user); @@ -623,14 +623,14 @@ public void testFailureStoreAccess() throws IOException { // mixed access { - Request request = searchRequest("test1,test1::failures"); + var request = new Search("test1,test1::failures"); for (var user : users) { switch (user) { case DATA_ACCESS_USER, FAILURE_STORE_ACCESS_USER, STAR_READ_ONLY_USER: - expectThrows403(() -> performRequest(user, request)); + expect(user, request, 403); break; case BOTH_ACCESS_USER: - expectDocIds(performRequest(user, request), dataDocId, failuresDocId); + expect(user, request, dataDocId, failuresDocId); break; default: fail("must cover user: " + user); @@ -638,17 +638,17 @@ public void testFailureStoreAccess() throws IOException { } } { - Request request = searchRequest("test1,test1::failures", "?ignore_unavailable=true"); + var request = new Search("test1,test1::failures", "?ignore_unavailable=true"); for (var user : users) { switch (user) { case DATA_ACCESS_USER, STAR_READ_ONLY_USER: - expectDocIds(performRequest(user, request), dataDocId); + expect(user, request, dataDocId); break; case FAILURE_STORE_ACCESS_USER: - expectDocIds(performRequest(user, request), failuresDocId); + expect(user, request, failuresDocId); break; case BOTH_ACCESS_USER: - expectDocIds(performRequest(user, request), dataDocId, failuresDocId); + expect(user, request, dataDocId, failuresDocId); break; default: fail("must cover user: " + user); @@ -656,14 +656,14 @@ public void testFailureStoreAccess() throws IOException { } } { - Request request = searchRequest("test1," + failureIndexName); + var request = new Search("test1," + failureIndexName); for (var user : users) { switch (user) { case DATA_ACCESS_USER, FAILURE_STORE_ACCESS_USER: - expectThrows403(() -> performRequest(user, request)); + expect(user, request, 403); break; case BOTH_ACCESS_USER, STAR_READ_ONLY_USER: - expectDocIds(performRequest(user, request), dataDocId, failuresDocId); + expect(user, request, dataDocId, failuresDocId); break; default: fail("must cover user: " + user); @@ -671,17 +671,17 @@ public void testFailureStoreAccess() throws IOException { } } { - Request request = searchRequest("test1," + failureIndexName, "?ignore_unavailable=true"); + var request = new Search("test1," + failureIndexName, "?ignore_unavailable=true"); for (var user : users) { switch (user) { case DATA_ACCESS_USER: - expectDocIds(performRequest(user, request), dataDocId); + expect(user, request, dataDocId); break; case FAILURE_STORE_ACCESS_USER: - expectDocIds(performRequest(user, request), failuresDocId); + expect(user, request, failuresDocId); break; case BOTH_ACCESS_USER, STAR_READ_ONLY_USER: - expectDocIds(performRequest(user, request), dataDocId, failuresDocId); + expect(user, request, dataDocId, failuresDocId); break; default: fail("must cover user: " + user); @@ -689,14 +689,14 @@ public void testFailureStoreAccess() throws IOException { } } { - Request request = searchRequest("test1::failures," + dataIndexName); + var request = new Search("test1::failures," + dataIndexName); for (var user : users) { switch (user) { case DATA_ACCESS_USER, FAILURE_STORE_ACCESS_USER, STAR_READ_ONLY_USER: - expectThrows403(() -> performRequest(user, request)); + expect(user, request, 403); break; case BOTH_ACCESS_USER: - expectDocIds(performRequest(user, request), dataDocId, failuresDocId); + expect(user, request, dataDocId, failuresDocId); break; default: fail("must cover user: " + user); @@ -704,17 +704,17 @@ public void testFailureStoreAccess() throws IOException { } } { - Request request = searchRequest("test1::failures," + dataIndexName, "?ignore_unavailable=true"); + var request = new Search("test1::failures," + dataIndexName, "?ignore_unavailable=true"); for (var user : users) { switch (user) { case DATA_ACCESS_USER, STAR_READ_ONLY_USER: - expectDocIds(performRequest(user, request), dataDocId); + expect(user, request, dataDocId); break; case FAILURE_STORE_ACCESS_USER: - expectDocIds(performRequest(user, request), failuresDocId); + expect(user, request, failuresDocId); break; case BOTH_ACCESS_USER: - expectDocIds(performRequest(user, request), dataDocId, failuresDocId); + expect(user, request, dataDocId, failuresDocId); break; default: fail("must cover user: " + user); @@ -722,17 +722,17 @@ public void testFailureStoreAccess() throws IOException { } } { - Request request = searchRequest("test1,*::failures"); + var request = new Search("test1,*::failures"); for (var user : users) { switch (user) { case FAILURE_STORE_ACCESS_USER: - expectThrows403(() -> performRequest(user, request)); + expect(user, request, 403); break; case DATA_ACCESS_USER, STAR_READ_ONLY_USER: - expectDocIds(performRequest(user, request), dataDocId); + expect(user, request, dataDocId); break; case BOTH_ACCESS_USER: - expectDocIds(performRequest(user, request), dataDocId, failuresDocId); + expect(user, request, dataDocId, failuresDocId); break; default: fail("must cover user: " + user); @@ -740,17 +740,17 @@ public void testFailureStoreAccess() throws IOException { } } { - Request request = searchRequest("test1,*::failures", "?ignore_unavailable=true"); + var request = new Search("test1,*::failures", "?ignore_unavailable=true"); for (var user : users) { switch (user) { case FAILURE_STORE_ACCESS_USER: - expectDocIds(performRequest(user, request), failuresDocId); + expect(user, request, failuresDocId); break; case DATA_ACCESS_USER, STAR_READ_ONLY_USER: - expectDocIds(performRequest(user, request), dataDocId); + expect(user, request, dataDocId); break; case BOTH_ACCESS_USER: - expectDocIds(performRequest(user, request), dataDocId, failuresDocId); + expect(user, request, dataDocId, failuresDocId); break; default: fail("must cover user: " + user); @@ -758,17 +758,17 @@ public void testFailureStoreAccess() throws IOException { } } { - Request request = searchRequest("test1::failures,*"); + var request = new Search("test1::failures,*"); for (var user : users) { switch (user) { case FAILURE_STORE_ACCESS_USER: - expectDocIds(performRequest(user, request), failuresDocId); + expect(user, request, failuresDocId); break; case DATA_ACCESS_USER, STAR_READ_ONLY_USER: - expectThrows403(() -> performRequest(user, request)); + expect(user, request, 403); break; case BOTH_ACCESS_USER: - expectDocIds(performRequest(user, request), dataDocId, failuresDocId); + expect(user, request, dataDocId, failuresDocId); break; default: fail("must cover user: " + user); @@ -776,17 +776,17 @@ public void testFailureStoreAccess() throws IOException { } } { - Request request = searchRequest("test1::failures,*", "?ignore_unavailable=true"); + var request = new Search("test1::failures,*", "?ignore_unavailable=true"); for (var user : users) { switch (user) { case FAILURE_STORE_ACCESS_USER: - expectDocIds(performRequest(user, request), failuresDocId); + expect(user, request, failuresDocId); break; case DATA_ACCESS_USER, STAR_READ_ONLY_USER: - expectDocIds(performRequest(user, request), dataDocId); + expect(user, request, dataDocId); break; case BOTH_ACCESS_USER: - expectDocIds(performRequest(user, request), dataDocId, failuresDocId); + expect(user, request, dataDocId, failuresDocId); break; default: fail("must cover user: " + user); @@ -794,17 +794,17 @@ public void testFailureStoreAccess() throws IOException { } } { - Request request = searchRequest("*::failures,*"); + Search request = new Search("*::failures,*"); for (var user : users) { switch (user) { case FAILURE_STORE_ACCESS_USER: - expectDocIds(performRequest(user, request), failuresDocId); + expect(user, request, failuresDocId); break; case DATA_ACCESS_USER, STAR_READ_ONLY_USER: - expectDocIds(performRequest(user, request), dataDocId); + expect(user, request, dataDocId); break; case BOTH_ACCESS_USER: - expectDocIds(performRequest(user, request), dataDocId, failuresDocId); + expect(user, request, dataDocId, failuresDocId); break; default: fail("must cover user: " + user); @@ -833,14 +833,6 @@ public void testFailureStoreAccess() throws IOException { expectThrows404(() -> adminClient().performRequest(new Request("GET", "/" + failureIndexName + "/_search"))); } - private Request searchRequest(String searchTarget) { - return searchRequest(searchTarget, ""); - } - - private Request searchRequest(String searchTarget, String pathParamString) { - return new Request("GET", Strings.format("/%s/_search%s", searchTarget, pathParamString)); - } - private static void expectThrows404(ThrowingRunnable runnable) { expectThrows(runnable, 404); } @@ -854,6 +846,33 @@ private static void expectThrows(ThrowingRunnable runnable, int statusCode) { assertThat(ex.getResponse().getStatusLine().getStatusCode(), equalTo(statusCode)); } + private void expect(String user, Search search, int statusCode) { + expectThrows(() -> performRequest(user, search.toSearchRequest()), statusCode); + expectThrows(() -> performRequest(user, search.toAsyncSearchRequest()), statusCode); + } + + private void expect(String user, Search search, String... docIds) throws Exception { + expect(user, search, response -> expectDocIds(response, docIds)); + } + + private void expect(String user, Search search, ThrowingConsumer consumer) throws Exception { + consumer.accept(performRequest(user, search.toSearchRequest())); + } + + private record Search(String searchTarget, String pathParamString) { + Search(String searchTarget) { + this(searchTarget, ""); + } + + Request toSearchRequest() { + return new Request("POST", Strings.format("/%s/_search%s", searchTarget, pathParamString)); + } + + Request toAsyncSearchRequest() { + return new Request("POST", Strings.format("/%s/_async_search%s", searchTarget, pathParamString)); + } + } + @SuppressWarnings("unchecked") private static void expectDocIds(Response response, String... docIds) throws IOException { assertOK(response); @@ -868,17 +887,6 @@ private static void expectDocIds(Response response, String... docIds) throws IOE } } - private static void expectEmpty(Response response) throws IOException { - assertOK(response); - final SearchResponse searchResponse = SearchResponseUtils.parseSearchResponse(responseAsParser(response)); - try { - SearchHit[] hits = searchResponse.getHits().getHits(); - assertThat(hits.length, equalTo(0)); - } finally { - searchResponse.decRef(); - } - } - private void createTemplates() throws IOException { var componentTemplateRequest = new Request("PUT", "/_component_template/component1"); componentTemplateRequest.setJsonEntity(""" From e77fb81cca50b6d30a93dab4ea32d57ba4168803 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Tue, 11 Mar 2025 11:00:25 +0100 Subject: [PATCH 087/131] Tests and flags --- .../core/security/user/InternalUsers.java | 32 ++++++++++------- .../FailureStoreSecurityRestIT.java | 35 +++++++++++++++---- 2 files changed, 49 insertions(+), 18 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/InternalUsers.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/InternalUsers.java index b67c9756f5ea4..eaba08c0aad83 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/InternalUsers.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/InternalUsers.java @@ -28,6 +28,7 @@ import org.elasticsearch.action.index.TransportIndexAction; import org.elasticsearch.action.search.TransportSearchAction; import org.elasticsearch.action.search.TransportSearchScrollAction; +import org.elasticsearch.cluster.metadata.DataStream; import org.elasticsearch.index.reindex.ReindexAction; import org.elasticsearch.xpack.core.XPackPlugin; import org.elasticsearch.xpack.core.ilm.action.ILMActions; @@ -245,18 +246,25 @@ public class InternalUsers { new RoleDescriptor( UsernamesField.LAZY_ROLLOVER_ROLE, new String[] {}, - new RoleDescriptor.IndicesPrivileges[] { - RoleDescriptor.IndicesPrivileges.builder() - .indices("*") - .privileges(LazyRolloverAction.NAME) - .allowRestrictedIndices(true) - .build(), - RoleDescriptor.IndicesPrivileges.builder() - .indices("*") - // TODO consider a more granular privilege for this - .privileges("manage_failure_store") - .allowRestrictedIndices(true) - .build() }, + DataStream.isFailureStoreFeatureFlagEnabled() + ? new RoleDescriptor.IndicesPrivileges[] { + RoleDescriptor.IndicesPrivileges.builder() + .indices("*") + .privileges(LazyRolloverAction.NAME) + .allowRestrictedIndices(true) + .build(), + RoleDescriptor.IndicesPrivileges.builder() + .indices("*") + // needed to rollover failure store + .privileges("manage_failure_store") + .allowRestrictedIndices(true) + .build() } + : new RoleDescriptor.IndicesPrivileges[] { + RoleDescriptor.IndicesPrivileges.builder() + .indices("*") + .privileges(LazyRolloverAction.NAME) + .allowRestrictedIndices(true) + .build(), }, null, null, new String[] {}, diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java index 631248beec55f..df09cd0db6a18 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java @@ -26,6 +26,7 @@ import org.elasticsearch.test.cluster.ElasticsearchCluster; import org.elasticsearch.test.cluster.FeatureFlag; import org.elasticsearch.test.rest.ESRestTestCase; +import org.elasticsearch.test.rest.ObjectPath; import org.elasticsearch.xcontent.json.JsonXContent; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.SecurityOnTrialLicenseRestTestCase; @@ -41,6 +42,7 @@ import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.is; public class FailureStoreSecurityRestIT extends ESRestTestCase { @@ -63,6 +65,8 @@ protected Settings restAdminSettings() { return Settings.builder().put(ThreadContext.PREFIX + ".Authorization", token).build(); } + private static final String ASYNC_SEARCH_TIMEOUT = "30s"; + private static final String DATA_ACCESS_USER = "data_access_user"; private static final String STAR_READ_ONLY_USER = "star_read_only_user"; private static final String FAILURE_STORE_ACCESS_USER = "failure_store_access_user"; @@ -794,7 +798,7 @@ public void testFailureStoreAccess() throws Exception { } } { - Search request = new Search("*::failures,*"); + var request = new Search("*::failures,*"); for (var user : users) { switch (user) { case FAILURE_STORE_ACCESS_USER: @@ -852,11 +856,28 @@ private void expect(String user, Search search, int statusCode) { } private void expect(String user, Search search, String... docIds) throws Exception { - expect(user, search, response -> expectDocIds(response, docIds)); + expectSearch(user, search.toSearchRequest(), response -> expectDocIds(response, docIds)); + expectAsyncSearch(user, search.toAsyncSearchRequest(), docIds); + } + + @SuppressWarnings("unchecked") + private void expectAsyncSearch(String user, Request request, String... docIds) throws IOException { + Response response = performRequest(user, request); + assertOK(response); + ObjectPath resp = ObjectPath.createFromResponse(response); + Boolean isRunning = resp.evaluate("is_running"); + Boolean isPartial = resp.evaluate("is_partial"); + assertThat(isRunning, is(false)); + assertThat(isPartial, is(false)); + + List hits = resp.evaluate("response.hits.hits"); + List actual = hits.stream().map(h -> (String) ((Map) h).get("_id")).toList(); + + assertThat(actual, containsInAnyOrder(docIds)); } - private void expect(String user, Search search, ThrowingConsumer consumer) throws Exception { - consumer.accept(performRequest(user, search.toSearchRequest())); + private void expectSearch(String user, Request request, ThrowingConsumer consumer) throws Exception { + consumer.accept(performRequest(user, request)); } private record Search(String searchTarget, String pathParamString) { @@ -869,11 +890,13 @@ Request toSearchRequest() { } Request toAsyncSearchRequest() { - return new Request("POST", Strings.format("/%s/_async_search%s", searchTarget, pathParamString)); + var pathParam = pathParamString.isEmpty() + ? "?wait_for_completion_timeout=" + ASYNC_SEARCH_TIMEOUT + : pathParamString + "&wait_for_completion_timeout=" + ASYNC_SEARCH_TIMEOUT; + return new Request("POST", Strings.format("/%s/_async_search%s", searchTarget, pathParam)); } } - @SuppressWarnings("unchecked") private static void expectDocIds(Response response, String... docIds) throws IOException { assertOK(response); final SearchResponse searchResponse = SearchResponseUtils.parseSearchResponse(responseAsParser(response)); From e8d9542a3e912b831b4b5790b9e9e8be758537a8 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Tue, 11 Mar 2025 12:11:38 +0100 Subject: [PATCH 088/131] Use API keys --- .../FailureStoreSecurityRestIT.java | 319 ++++++++++-------- 1 file changed, 176 insertions(+), 143 deletions(-) diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java index df09cd0db6a18..0f2ce7cf97e26 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java @@ -36,6 +36,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -43,11 +44,14 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; public class FailureStoreSecurityRestIT extends ESRestTestCase { private TestSecurityClient securityClient; + private Map apiKeys = new HashMap<>(); + @ClassRule public static ElasticsearchCluster cluster = ElasticsearchCluster.local() .apply(SecurityOnTrialLicenseRestTestCase.commonTrialSecurityClusterConfig) @@ -67,13 +71,13 @@ protected Settings restAdminSettings() { private static final String ASYNC_SEARCH_TIMEOUT = "30s"; - private static final String DATA_ACCESS_USER = "data_access_user"; - private static final String STAR_READ_ONLY_USER = "star_read_only_user"; - private static final String FAILURE_STORE_ACCESS_USER = "failure_store_access_user"; - private static final String BOTH_ACCESS_USER = "both_access_user"; - private static final String WRITE_ACCESS_USER = "write_access_user"; - private static final String MANAGE_ACCESS_USER = "manage_access_user"; - private static final String MANAGE_FAILURE_STORE_ACCESS_USER = "manage_failure_store_access_user"; + private static final String DATA_ACCESS = "data_access"; + private static final String STAR_READ_ONLY_ACCESS = "star_read_only"; + private static final String FAILURE_STORE_ACCESS = "failure_store_access"; + private static final String BOTH_ACCESS = "both_access"; + private static final String WRITE_ACCESS = "write_access"; + private static final String MANAGE_ACCESS = "manage_access"; + private static final String MANAGE_FAILURE_STORE_ACCESS = "manage_failure_store_access"; private static final SecureString PASSWORD = new SecureString("elastic-password"); public void testGetUserPrivileges() throws IOException { @@ -168,66 +172,70 @@ public void testGetUserPrivileges() throws IOException { @SuppressWarnings("unchecked") public void testFailureStoreAccess() throws Exception { - String dataAccessRole = "data_access"; - String starReadOnlyRole = "star_read_only_access"; - String failureStoreAccessRole = "failure_store_access"; - String bothAccessRole = "both_access"; - String writeAccessRole = "write_access"; - String manageAccessRole = "manage_access"; - String manageFailureStoreRole = "manage_failure_store_access"; - - createUser(DATA_ACCESS_USER, PASSWORD, List.of(dataAccessRole)); - createUser(STAR_READ_ONLY_USER, PASSWORD, List.of(starReadOnlyRole)); - createUser(FAILURE_STORE_ACCESS_USER, PASSWORD, List.of(failureStoreAccessRole)); - createUser(BOTH_ACCESS_USER, PASSWORD, randomBoolean() ? List.of(bothAccessRole) : List.of(dataAccessRole, failureStoreAccessRole)); - createUser(WRITE_ACCESS_USER, PASSWORD, List.of(writeAccessRole)); - createUser(MANAGE_ACCESS_USER, PASSWORD, List.of(manageAccessRole)); - createUser(MANAGE_FAILURE_STORE_ACCESS_USER, PASSWORD, List.of(manageFailureStoreRole)); + apiKeys = new HashMap<>(); + createUser(DATA_ACCESS, PASSWORD, DATA_ACCESS); upsertRole(Strings.format(""" { "description": "Role with data access", "cluster": ["all"], "indices": [{"names": ["test*"], "privileges": ["read"]}] - }"""), dataAccessRole); + }"""), DATA_ACCESS); + + createUser(STAR_READ_ONLY_ACCESS, PASSWORD, STAR_READ_ONLY_ACCESS); upsertRole(Strings.format(""" { "description": "Role with data access", "cluster": ["all"], "indices": [{"names": ["*"], "privileges": ["read"]}] - }"""), starReadOnlyRole); + }"""), STAR_READ_ONLY_ACCESS); + + createUser(FAILURE_STORE_ACCESS, PASSWORD, FAILURE_STORE_ACCESS); upsertRole(Strings.format(""" { "description": "Role with failure store access", "cluster": ["all"], "indices": [{"names": ["test*"], "privileges": ["read_failure_store"]}] - }"""), failureStoreAccessRole); - upsertRole(Strings.format(""" - { - "description": "Role with both data and failure store access", - "cluster": ["all"], - "indices": [{"names": ["test*"], "privileges": ["read", "read_failure_store"]}] - }"""), bothAccessRole); + }"""), FAILURE_STORE_ACCESS); + + if (randomBoolean()) { + createUser(BOTH_ACCESS, PASSWORD, BOTH_ACCESS); + upsertRole(Strings.format(""" + { + "description": "Role with both data and failure store access", + "cluster": ["all"], + "indices": [{"names": ["test*"], "privileges": ["read", "read_failure_store"]}] + }"""), BOTH_ACCESS); + } else { + createUser(BOTH_ACCESS, PASSWORD, DATA_ACCESS, FAILURE_STORE_ACCESS); + } + + createUser(WRITE_ACCESS, PASSWORD, WRITE_ACCESS); upsertRole(Strings.format(""" { "description": "Role with regular write access without failure store access", "cluster": ["all"], "indices": [{"names": ["test*"], "privileges": ["write", "auto_configure"]}] - }"""), writeAccessRole); + }"""), WRITE_ACCESS); + + createUser(MANAGE_ACCESS, PASSWORD, MANAGE_ACCESS); upsertRole(Strings.format(""" { "description": "Role with regular manage access without failure store access", "cluster": ["all"], "indices": [{"names": ["test*"], "privileges": ["manage"]}] - }"""), manageAccessRole); + }"""), MANAGE_ACCESS); + + createUser(MANAGE_FAILURE_STORE_ACCESS, PASSWORD, MANAGE_FAILURE_STORE_ACCESS); upsertRole(Strings.format(""" { "description": "Role with failure store manage access", "cluster": ["all"], "indices": [{"names": ["test*"], "privileges": ["manage_failure_store"]}] - }"""), manageFailureStoreRole); + }"""), MANAGE_FAILURE_STORE_ACCESS); createTemplates(); + List docIds = populateDataStream(); assertThat(docIds.size(), equalTo(2)); assertThat(docIds, hasItem("1")); @@ -265,17 +273,20 @@ public void testFailureStoreAccess() throws Exception { assertOK(adminClient().performRequest(aliasRequest)); // todo also add superuser - List users = List.of(DATA_ACCESS_USER, FAILURE_STORE_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER); + List users = List.of(DATA_ACCESS, FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS); + for (var user : users) { + createAndStoreApiKey(user); + } // search data { var request = new Search("test1"); for (var user : users) { switch (user) { - case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expect(user, request, dataDocId); break; - case FAILURE_STORE_ACCESS_USER: + case FAILURE_STORE_ACCESS: expect(user, request, 403); break; default: @@ -287,10 +298,10 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test1", "?ignore_unavailable=true"); for (var user : users) { switch (user) { - case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expect(user, request, dataDocId); break; - case FAILURE_STORE_ACCESS_USER: + case FAILURE_STORE_ACCESS: expect(user, request); break; default: @@ -302,10 +313,10 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test-alias"); for (var user : users) { switch (user) { - case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expect(user, request, dataDocId); break; - case FAILURE_STORE_ACCESS_USER: + case FAILURE_STORE_ACCESS: expect(user, request, 403); break; default: @@ -317,10 +328,10 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test-alias", "?ignore_unavailable=true"); for (var user : users) { switch (user) { - case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expect(user, request, dataDocId); break; - case FAILURE_STORE_ACCESS_USER: + case FAILURE_STORE_ACCESS: expect(user, request); break; default: @@ -332,10 +343,10 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test*"); for (var user : users) { switch (user) { - case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expect(user, request, dataDocId); break; - case FAILURE_STORE_ACCESS_USER: + case FAILURE_STORE_ACCESS: expect(user, request); break; default: @@ -347,10 +358,10 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("*1"); for (var user : users) { switch (user) { - case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expect(user, request, dataDocId); break; - case FAILURE_STORE_ACCESS_USER: + case FAILURE_STORE_ACCESS: expect(user, request); break; default: @@ -362,10 +373,10 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("*"); for (var user : users) { switch (user) { - case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expect(user, request, dataDocId); break; - case FAILURE_STORE_ACCESS_USER: + case FAILURE_STORE_ACCESS: expect(user, request); break; default: @@ -377,10 +388,10 @@ public void testFailureStoreAccess() throws Exception { var request = new Search(".ds*"); for (var user : users) { switch (user) { - case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expect(user, request, dataDocId); break; - case FAILURE_STORE_ACCESS_USER: + case FAILURE_STORE_ACCESS: expect(user, request); break; default: @@ -392,10 +403,10 @@ public void testFailureStoreAccess() throws Exception { var request = new Search(dataIndexName); for (var user : users) { switch (user) { - case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expect(user, request, dataDocId); break; - case FAILURE_STORE_ACCESS_USER: + case FAILURE_STORE_ACCESS: expect(user, request, 403); break; default: @@ -407,10 +418,10 @@ public void testFailureStoreAccess() throws Exception { var request = new Search(dataIndexName, "?ignore_unavailable=true"); for (var user : users) { switch (user) { - case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expect(user, request, dataDocId); break; - case FAILURE_STORE_ACCESS_USER: + case FAILURE_STORE_ACCESS: expect(user, request); break; default: @@ -422,10 +433,10 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test2"); for (var user : users) { switch (user) { - case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expect(user, request, 404); break; - case FAILURE_STORE_ACCESS_USER: + case FAILURE_STORE_ACCESS: expect(user, request, 403); break; default: @@ -437,7 +448,7 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test2", "?ignore_unavailable=true"); for (var user : users) { switch (user) { - case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER, FAILURE_STORE_ACCESS_USER: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, FAILURE_STORE_ACCESS: expect(user, request); break; default: @@ -451,10 +462,10 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test1::failures"); for (var user : users) { switch (user) { - case DATA_ACCESS_USER, STAR_READ_ONLY_USER: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request, 403); break; - case FAILURE_STORE_ACCESS_USER, BOTH_ACCESS_USER: + case FAILURE_STORE_ACCESS, BOTH_ACCESS: expect(user, request, failuresDocId); break; default: @@ -466,10 +477,10 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test1::failures", "?ignore_unavailable=true"); for (var user : users) { switch (user) { - case DATA_ACCESS_USER, STAR_READ_ONLY_USER: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request); break; - case FAILURE_STORE_ACCESS_USER, BOTH_ACCESS_USER: + case FAILURE_STORE_ACCESS, BOTH_ACCESS: expect(user, request, failuresDocId); break; default: @@ -481,10 +492,10 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test-alias::failures"); for (var user : users) { switch (user) { - case DATA_ACCESS_USER, STAR_READ_ONLY_USER: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request, 403); break; - case FAILURE_STORE_ACCESS_USER, BOTH_ACCESS_USER: + case FAILURE_STORE_ACCESS, BOTH_ACCESS: expect(user, request, failuresDocId); break; default: @@ -496,10 +507,10 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test-alias::failures", "?ignore_unavailable=true"); for (var user : users) { switch (user) { - case DATA_ACCESS_USER, STAR_READ_ONLY_USER: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request); break; - case FAILURE_STORE_ACCESS_USER, BOTH_ACCESS_USER: + case FAILURE_STORE_ACCESS, BOTH_ACCESS: expect(user, request, failuresDocId); break; default: @@ -511,10 +522,10 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test*::failures"); for (var user : users) { switch (user) { - case DATA_ACCESS_USER, STAR_READ_ONLY_USER: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request); break; - case FAILURE_STORE_ACCESS_USER, BOTH_ACCESS_USER: + case FAILURE_STORE_ACCESS, BOTH_ACCESS: expect(user, request, failuresDocId); break; default: @@ -526,10 +537,10 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("*1::failures"); for (var user : users) { switch (user) { - case DATA_ACCESS_USER, STAR_READ_ONLY_USER: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request); break; - case FAILURE_STORE_ACCESS_USER, BOTH_ACCESS_USER: + case FAILURE_STORE_ACCESS, BOTH_ACCESS: expect(user, request, failuresDocId); break; default: @@ -541,10 +552,10 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("*::failures"); for (var user : users) { switch (user) { - case DATA_ACCESS_USER, STAR_READ_ONLY_USER: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request); break; - case FAILURE_STORE_ACCESS_USER, BOTH_ACCESS_USER: + case FAILURE_STORE_ACCESS, BOTH_ACCESS: expect(user, request, failuresDocId); break; default: @@ -556,10 +567,10 @@ public void testFailureStoreAccess() throws Exception { var request = new Search(".fs*"); for (var user : users) { switch (user) { - case DATA_ACCESS_USER: + case DATA_ACCESS: expect(user, request); break; - case FAILURE_STORE_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: + case FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expect(user, request, failuresDocId); break; default: @@ -571,10 +582,10 @@ public void testFailureStoreAccess() throws Exception { var request = new Search(failureIndexName); for (var user : users) { switch (user) { - case DATA_ACCESS_USER: + case DATA_ACCESS: expect(user, request, 403); break; - case FAILURE_STORE_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: + case FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expect(user, request, failuresDocId); break; default: @@ -586,10 +597,10 @@ public void testFailureStoreAccess() throws Exception { var request = new Search(failureIndexName, "?ignore_unavailable=true"); for (var user : users) { switch (user) { - case DATA_ACCESS_USER: + case DATA_ACCESS: expect(user, request); break; - case FAILURE_STORE_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER: + case FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expect(user, request, failuresDocId); break; default: @@ -601,10 +612,10 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test2::failures"); for (var user : users) { switch (user) { - case DATA_ACCESS_USER, STAR_READ_ONLY_USER: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request, 403); break; - case FAILURE_STORE_ACCESS_USER, BOTH_ACCESS_USER: + case FAILURE_STORE_ACCESS, BOTH_ACCESS: expect(user, request, 404); break; default: @@ -616,7 +627,7 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test2::failures", "?ignore_unavailable=true"); for (var user : users) { switch (user) { - case DATA_ACCESS_USER, STAR_READ_ONLY_USER, BOTH_ACCESS_USER, FAILURE_STORE_ACCESS_USER: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, FAILURE_STORE_ACCESS: expect(user, request); break; default: @@ -630,10 +641,10 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test1,test1::failures"); for (var user : users) { switch (user) { - case DATA_ACCESS_USER, FAILURE_STORE_ACCESS_USER, STAR_READ_ONLY_USER: + case DATA_ACCESS, FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request, 403); break; - case BOTH_ACCESS_USER: + case BOTH_ACCESS: expect(user, request, dataDocId, failuresDocId); break; default: @@ -645,13 +656,13 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test1,test1::failures", "?ignore_unavailable=true"); for (var user : users) { switch (user) { - case DATA_ACCESS_USER, STAR_READ_ONLY_USER: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request, dataDocId); break; - case FAILURE_STORE_ACCESS_USER: + case FAILURE_STORE_ACCESS: expect(user, request, failuresDocId); break; - case BOTH_ACCESS_USER: + case BOTH_ACCESS: expect(user, request, dataDocId, failuresDocId); break; default: @@ -663,10 +674,10 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test1," + failureIndexName); for (var user : users) { switch (user) { - case DATA_ACCESS_USER, FAILURE_STORE_ACCESS_USER: + case DATA_ACCESS, FAILURE_STORE_ACCESS: expect(user, request, 403); break; - case BOTH_ACCESS_USER, STAR_READ_ONLY_USER: + case BOTH_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request, dataDocId, failuresDocId); break; default: @@ -678,13 +689,13 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test1," + failureIndexName, "?ignore_unavailable=true"); for (var user : users) { switch (user) { - case DATA_ACCESS_USER: + case DATA_ACCESS: expect(user, request, dataDocId); break; - case FAILURE_STORE_ACCESS_USER: + case FAILURE_STORE_ACCESS: expect(user, request, failuresDocId); break; - case BOTH_ACCESS_USER, STAR_READ_ONLY_USER: + case BOTH_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request, dataDocId, failuresDocId); break; default: @@ -696,10 +707,10 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test1::failures," + dataIndexName); for (var user : users) { switch (user) { - case DATA_ACCESS_USER, FAILURE_STORE_ACCESS_USER, STAR_READ_ONLY_USER: + case DATA_ACCESS, FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request, 403); break; - case BOTH_ACCESS_USER: + case BOTH_ACCESS: expect(user, request, dataDocId, failuresDocId); break; default: @@ -711,13 +722,13 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test1::failures," + dataIndexName, "?ignore_unavailable=true"); for (var user : users) { switch (user) { - case DATA_ACCESS_USER, STAR_READ_ONLY_USER: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request, dataDocId); break; - case FAILURE_STORE_ACCESS_USER: + case FAILURE_STORE_ACCESS: expect(user, request, failuresDocId); break; - case BOTH_ACCESS_USER: + case BOTH_ACCESS: expect(user, request, dataDocId, failuresDocId); break; default: @@ -729,13 +740,13 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test1,*::failures"); for (var user : users) { switch (user) { - case FAILURE_STORE_ACCESS_USER: + case FAILURE_STORE_ACCESS: expect(user, request, 403); break; - case DATA_ACCESS_USER, STAR_READ_ONLY_USER: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request, dataDocId); break; - case BOTH_ACCESS_USER: + case BOTH_ACCESS: expect(user, request, dataDocId, failuresDocId); break; default: @@ -747,13 +758,13 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test1,*::failures", "?ignore_unavailable=true"); for (var user : users) { switch (user) { - case FAILURE_STORE_ACCESS_USER: + case FAILURE_STORE_ACCESS: expect(user, request, failuresDocId); break; - case DATA_ACCESS_USER, STAR_READ_ONLY_USER: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request, dataDocId); break; - case BOTH_ACCESS_USER: + case BOTH_ACCESS: expect(user, request, dataDocId, failuresDocId); break; default: @@ -765,13 +776,13 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test1::failures,*"); for (var user : users) { switch (user) { - case FAILURE_STORE_ACCESS_USER: + case FAILURE_STORE_ACCESS: expect(user, request, failuresDocId); break; - case DATA_ACCESS_USER, STAR_READ_ONLY_USER: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request, 403); break; - case BOTH_ACCESS_USER: + case BOTH_ACCESS: expect(user, request, dataDocId, failuresDocId); break; default: @@ -783,13 +794,13 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test1::failures,*", "?ignore_unavailable=true"); for (var user : users) { switch (user) { - case FAILURE_STORE_ACCESS_USER: + case FAILURE_STORE_ACCESS: expect(user, request, failuresDocId); break; - case DATA_ACCESS_USER, STAR_READ_ONLY_USER: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request, dataDocId); break; - case BOTH_ACCESS_USER: + case BOTH_ACCESS: expect(user, request, dataDocId, failuresDocId); break; default: @@ -801,13 +812,13 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("*::failures,*"); for (var user : users) { switch (user) { - case FAILURE_STORE_ACCESS_USER: + case FAILURE_STORE_ACCESS: expect(user, request, failuresDocId); break; - case DATA_ACCESS_USER, STAR_READ_ONLY_USER: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request, dataDocId); break; - case BOTH_ACCESS_USER: + case BOTH_ACCESS: expect(user, request, dataDocId, failuresDocId); break; default: @@ -819,21 +830,21 @@ public void testFailureStoreAccess() throws Exception { // write operations below // user with manage access to data stream does NOT get direct access to failure index - expectThrows403(() -> deleteIndex(MANAGE_ACCESS_USER, failureIndexName)); - expectThrows(() -> deleteIndex(MANAGE_ACCESS_USER, dataIndexName), 400); + expectThrows403(() -> deleteIndex(MANAGE_ACCESS, failureIndexName)); + expectThrows(() -> deleteIndex(MANAGE_ACCESS, dataIndexName), 400); // manage_failure_store user COULD delete failure index (not valid because it's a write index, but allow security-wise) - expectThrows403(() -> deleteIndex(MANAGE_FAILURE_STORE_ACCESS_USER, dataIndexName)); - expectThrows(() -> deleteIndex(MANAGE_FAILURE_STORE_ACCESS_USER, failureIndexName), 400); - expectThrows403(() -> deleteDataStream(MANAGE_FAILURE_STORE_ACCESS_USER, dataIndexName)); + expectThrows403(() -> deleteIndex(MANAGE_FAILURE_STORE_ACCESS, dataIndexName)); + expectThrows(() -> deleteIndex(MANAGE_FAILURE_STORE_ACCESS, failureIndexName), 400); + expectThrows403(() -> deleteDataStream(MANAGE_FAILURE_STORE_ACCESS, dataIndexName)); - expectThrows(() -> deleteDataStream(MANAGE_FAILURE_STORE_ACCESS_USER, "test1"), 403); - expectThrows(() -> deleteDataStream(MANAGE_FAILURE_STORE_ACCESS_USER, "test1::failures"), 403); + expectThrows(() -> deleteDataStream(MANAGE_FAILURE_STORE_ACCESS, "test1"), 403); + expectThrows(() -> deleteDataStream(MANAGE_FAILURE_STORE_ACCESS, "test1::failures"), 403); // manage user can delete data stream - deleteDataStream(MANAGE_ACCESS_USER, "test1"); + deleteDataStream(MANAGE_ACCESS, "test1"); - expectThrows404(() -> performRequest(BOTH_ACCESS_USER, new Request("GET", "/test1/_search"))); + expectThrows404(() -> performRequest(BOTH_ACCESS, new Request("GET", "/test1/_search"))); expectThrows404(() -> adminClient().performRequest(new Request("GET", "/" + dataIndexName + "/_search"))); - expectThrows404(() -> performRequest(BOTH_ACCESS_USER, new Request("GET", "/test1::failures/_search"))); + expectThrows404(() -> performRequest(BOTH_ACCESS, new Request("GET", "/test1::failures/_search"))); expectThrows404(() -> adminClient().performRequest(new Request("GET", "/" + failureIndexName + "/_search"))); } @@ -856,13 +867,13 @@ private void expect(String user, Search search, int statusCode) { } private void expect(String user, Search search, String... docIds) throws Exception { - expectSearch(user, search.toSearchRequest(), response -> expectDocIds(response, docIds)); + expectSearch(user, search.toSearchRequest(), docIds); expectAsyncSearch(user, search.toAsyncSearchRequest(), docIds); } @SuppressWarnings("unchecked") private void expectAsyncSearch(String user, Request request, String... docIds) throws IOException { - Response response = performRequest(user, request); + Response response = performRequestMaybeUsingApiKey(user, request); assertOK(response); ObjectPath resp = ObjectPath.createFromResponse(response); Boolean isRunning = resp.evaluate("is_running"); @@ -876,8 +887,18 @@ private void expectAsyncSearch(String user, Request request, String... docIds) t assertThat(actual, containsInAnyOrder(docIds)); } - private void expectSearch(String user, Request request, ThrowingConsumer consumer) throws Exception { - consumer.accept(performRequest(user, request)); + private void expectSearch(String user, Request request, String... docIds) throws Exception { + Response response = performRequestMaybeUsingApiKey(user, request); + assertOK(response); + final SearchResponse searchResponse = SearchResponseUtils.parseSearchResponse(responseAsParser(response)); + try { + SearchHit[] hits = searchResponse.getHits().getHits(); + assertThat(hits.length, equalTo(docIds.length)); + List actualDocIds = Arrays.stream(hits).map(SearchHit::getId).toList(); + assertThat(actualDocIds, containsInAnyOrder(docIds)); + } finally { + searchResponse.decRef(); + } } private record Search(String searchTarget, String pathParamString) { @@ -897,19 +918,6 @@ Request toAsyncSearchRequest() { } } - private static void expectDocIds(Response response, String... docIds) throws IOException { - assertOK(response); - final SearchResponse searchResponse = SearchResponseUtils.parseSearchResponse(responseAsParser(response)); - try { - SearchHit[] hits = searchResponse.getHits().getHits(); - assertThat(hits.length, equalTo(docIds.length)); - List actualDocIds = Arrays.stream(hits).map(SearchHit::getId).toList(); - assertThat(actualDocIds, containsInAnyOrder(docIds)); - } finally { - searchResponse.decRef(); - } - } - private void createTemplates() throws IOException { var componentTemplateRequest = new Request("PUT", "/_component_template/component1"); componentTemplateRequest.setJsonEntity(""" @@ -970,7 +978,7 @@ private List populateDataStreamWithDocRequests() throws IOException { "email" : "jack@example.com" } """); - Response response = performRequest(WRITE_ACCESS_USER, docRequest); + Response response = performRequest(WRITE_ACCESS, docRequest); assertOK(response); Map responseAsMap = responseAsMap(response); ids.add((String) responseAsMap.get("_id")); @@ -984,7 +992,7 @@ private List populateDataStreamWithDocRequests() throws IOException { "email" : "jack@example.com" } """); - response = performRequest(WRITE_ACCESS_USER, docRequest); + response = performRequest(WRITE_ACCESS, docRequest); assertOK(response); responseAsMap = responseAsMap(response); ids.add((String) responseAsMap.get("_id")); @@ -1001,7 +1009,7 @@ private List populateDataStreamWithBulkRequest() throws IOException { { "create" : { "_index" : "test1", "_id" : "2" } } { "@timestamp": 2, "age" : "this should be an int", "name" : "jack", "email" : "jack@example.com" } """); - Response response = performRequest(WRITE_ACCESS_USER, bulkRequest); + Response response = performRequest(WRITE_ACCESS, bulkRequest); assertOK(response); // we need this dance because the ID for the failed document is random, **not** 2 Map stringObjectMap = responseAsMap(response); @@ -1029,6 +1037,17 @@ private Response performRequest(String user, Request request) throws IOException return client().performRequest(request); } + private Response performRequestMaybeUsingApiKey(String user, Request request) throws IOException { + if (randomBoolean()) { + return performRequest(user, request); + } else { + String apiKey = apiKeys.get(user); + assertThat("expected to have an API key stored for user: " + user, apiKey, is(notNullValue())); + request.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", "ApiKey " + apiKey).build()); + return client().performRequest(request); + } + } + private static void expectUserPrivilegesResponse(String userPrivilegesResponse) throws IOException { Request request = new Request("GET", "/_security/user/_privileges"); request.setOptions( @@ -1052,8 +1071,22 @@ protected TestSecurityClient getSecurityClient() { return securityClient; } - protected void createUser(String username, SecureString password, List roles) throws IOException { - getSecurityClient().putUser(new User(username, roles.toArray(String[]::new)), password); + protected void createUser(String username, SecureString password, String... roles) throws IOException { + getSecurityClient().putUser(new User(username, roles), password); + } + + protected void createAndStoreApiKey(String username) throws IOException { + var request = new Request("POST", "/_security/api_key"); + request.setJsonEntity(""" + { + "name": "test-api-key" + } + """); + Response response = performRequest(username, request); + assertOK(response); + Map responseAsMap = responseAsMap(response); + String encoded = (String) responseAsMap.get("encoded"); + apiKeys.put(username, encoded); } protected void upsertRole(String roleDescriptor, String roleName) throws IOException { From 4d7d0f5c7f0249b2b823e6e0a7d3bcc23b75e772 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Tue, 11 Mar 2025 12:32:05 +0100 Subject: [PATCH 089/131] API keys with assigned role descriptors --- .../FailureStoreSecurityRestIT.java | 99 ++++++++++++++++--- 1 file changed, 84 insertions(+), 15 deletions(-) diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java index 0f2ce7cf97e26..3cd1796ea5d63 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java @@ -20,6 +20,7 @@ import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.support.XContentMapValues; +import org.elasticsearch.core.Nullable; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchResponseUtils; import org.elasticsearch.test.TestSecurityClient; @@ -177,62 +178,120 @@ public void testFailureStoreAccess() throws Exception { createUser(DATA_ACCESS, PASSWORD, DATA_ACCESS); upsertRole(Strings.format(""" { - "description": "Role with data access", "cluster": ["all"], "indices": [{"names": ["test*"], "privileges": ["read"]}] }"""), DATA_ACCESS); + createAndStoreApiKey(DATA_ACCESS, randomBoolean() ? null : """ + { + "role": { + "cluster": ["all"], + "indices": [{"names": ["test*"], "privileges": ["read"]}] + } + } + """); createUser(STAR_READ_ONLY_ACCESS, PASSWORD, STAR_READ_ONLY_ACCESS); upsertRole(Strings.format(""" { - "description": "Role with data access", "cluster": ["all"], "indices": [{"names": ["*"], "privileges": ["read"]}] }"""), STAR_READ_ONLY_ACCESS); + createAndStoreApiKey(STAR_READ_ONLY_ACCESS, randomBoolean() ? null : """ + { + "role": { + "cluster": ["all"], + "indices": [{"names": ["*"], "privileges": ["read"]}] + } + } + """); createUser(FAILURE_STORE_ACCESS, PASSWORD, FAILURE_STORE_ACCESS); upsertRole(Strings.format(""" { - "description": "Role with failure store access", "cluster": ["all"], "indices": [{"names": ["test*"], "privileges": ["read_failure_store"]}] }"""), FAILURE_STORE_ACCESS); + createAndStoreApiKey(FAILURE_STORE_ACCESS, randomBoolean() ? null : """ + { + "role": { + "cluster": ["all"], + "indices": [{"names": ["test*"], "privileges": ["read_failure_store"]}] + } + } + """); if (randomBoolean()) { createUser(BOTH_ACCESS, PASSWORD, BOTH_ACCESS); upsertRole(Strings.format(""" { - "description": "Role with both data and failure store access", "cluster": ["all"], "indices": [{"names": ["test*"], "privileges": ["read", "read_failure_store"]}] }"""), BOTH_ACCESS); + createAndStoreApiKey(BOTH_ACCESS, randomBoolean() ? null : """ + { + "role": { + "cluster": ["all"], + "indices": [{"names": ["test*"], "privileges": ["read", "read_failure_store"]}] + } + } + """); } else { createUser(BOTH_ACCESS, PASSWORD, DATA_ACCESS, FAILURE_STORE_ACCESS); + // TODO + createAndStoreApiKey(BOTH_ACCESS, randomBoolean() ? null : """ + { + "role": { + "cluster": ["all"], + "indices": [{"names": ["test*"], "privileges": ["read", "read_failure_store"]}] + } + } + """); } createUser(WRITE_ACCESS, PASSWORD, WRITE_ACCESS); upsertRole(Strings.format(""" { - "description": "Role with regular write access without failure store access", "cluster": ["all"], "indices": [{"names": ["test*"], "privileges": ["write", "auto_configure"]}] }"""), WRITE_ACCESS); + createAndStoreApiKey(WRITE_ACCESS, randomBoolean() ? null : """ + { + "role": { + "cluster": ["all"], + "indices": [{"names": ["test*"], "privileges": ["write", "auto_configure"]}] + } + } + """); createUser(MANAGE_ACCESS, PASSWORD, MANAGE_ACCESS); upsertRole(Strings.format(""" { - "description": "Role with regular manage access without failure store access", "cluster": ["all"], "indices": [{"names": ["test*"], "privileges": ["manage"]}] }"""), MANAGE_ACCESS); + createAndStoreApiKey(MANAGE_ACCESS, randomBoolean() ? null : """ + { + "role": { + "cluster": ["all"], + "indices": [{"names": ["test*"], "privileges": ["manage"]}] + } + } + """); createUser(MANAGE_FAILURE_STORE_ACCESS, PASSWORD, MANAGE_FAILURE_STORE_ACCESS); upsertRole(Strings.format(""" { - "description": "Role with failure store manage access", "cluster": ["all"], "indices": [{"names": ["test*"], "privileges": ["manage_failure_store"]}] }"""), MANAGE_FAILURE_STORE_ACCESS); + createAndStoreApiKey(MANAGE_FAILURE_STORE_ACCESS, randomBoolean() ? null : """ + { + "role": { + "cluster": ["all"], + "indices": [{"names": ["test*"], "privileges": ["manage_failure_store"]}] + } + } + """); createTemplates(); @@ -274,9 +333,6 @@ public void testFailureStoreAccess() throws Exception { // todo also add superuser List users = List.of(DATA_ACCESS, FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS); - for (var user : users) { - createAndStoreApiKey(user); - } // search data { @@ -1076,12 +1132,25 @@ protected void createUser(String username, SecureString password, String... role } protected void createAndStoreApiKey(String username) throws IOException { + createAndStoreApiKey(username, null); + } + + protected void createAndStoreApiKey(String username, @Nullable String roleDescriptors) throws IOException { var request = new Request("POST", "/_security/api_key"); - request.setJsonEntity(""" - { - "name": "test-api-key" - } - """); + if (roleDescriptors == null) { + request.setJsonEntity(""" + { + "name": "test-api-key" + } + """); + } else { + request.setJsonEntity(Strings.format(""" + { + "name": "test-api-key", + "role_descriptors": %s + } + """, roleDescriptors)); + } Response response = performRequest(username, request); assertOK(response); Map responseAsMap = responseAsMap(response); From a1e9c5ba19322c0d7945ccb001c4c40c498f9ad9 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Tue, 11 Mar 2025 12:33:42 +0100 Subject: [PATCH 090/131] Nit --- .../xpack/security/failurestore/FailureStoreSecurityRestIT.java | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java index 3cd1796ea5d63..f6077e6dcd7da 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java @@ -237,7 +237,6 @@ public void testFailureStoreAccess() throws Exception { """); } else { createUser(BOTH_ACCESS, PASSWORD, DATA_ACCESS, FAILURE_STORE_ACCESS); - // TODO createAndStoreApiKey(BOTH_ACCESS, randomBoolean() ? null : """ { "role": { From 6a938f1d0dfa2deb0d96640a2995d6763901e50d Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Tue, 11 Mar 2025 12:42:01 +0100 Subject: [PATCH 091/131] Assert --- .../security/failurestore/FailureStoreSecurityRestIT.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java index f6077e6dcd7da..462a482e92d3a 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java @@ -1130,11 +1130,8 @@ protected void createUser(String username, SecureString password, String... role getSecurityClient().putUser(new User(username, roles), password); } - protected void createAndStoreApiKey(String username) throws IOException { - createAndStoreApiKey(username, null); - } - protected void createAndStoreApiKey(String username, @Nullable String roleDescriptors) throws IOException { + assertThat("API key already registered for user: " + username, apiKeys.containsKey(username), is(false)); var request = new Request("POST", "/_security/api_key"); if (roleDescriptors == null) { request.setJsonEntity(""" From f7e16901b4798814d4065dd6ce39c5bea691edcd Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Tue, 11 Mar 2025 12:46:09 +0100 Subject: [PATCH 092/131] Fix customer authz engine --- .../example/CustomAuthorizationEngine.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java b/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java index ea99880117f17..e9ae9fc787da5 100644 --- a/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java +++ b/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java @@ -13,6 +13,7 @@ import org.elasticsearch.action.support.SubscribableListener; import org.elasticsearch.cluster.metadata.IndexAbstraction; import org.elasticsearch.cluster.metadata.ProjectMetadata; +import org.elasticsearch.core.Nullable; import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse; import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse.Indices; import org.elasticsearch.xpack.core.security.authc.Authentication; @@ -35,7 +36,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.function.Supplier; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; @@ -119,19 +119,19 @@ public void loadAuthorizedIndices( ) { if (isSuperuser(requestInfo.getAuthentication().getEffectiveSubject().getUser())) { listener.onResponse(new AuthorizedIndices() { - public Supplier> all() { + public Set all(@Nullable String selector) { return () -> indicesLookup.keySet(); } - public boolean check(String name) { + public boolean check(String name, @Nullable String selector) { return indicesLookup.containsKey(name); } }); } else { listener.onResponse(new AuthorizedIndices() { - public Supplier> all() { + public Set all(@Nullable String selector) { return () -> Set.of(); } - public boolean check(String name) { + public boolean check(String name, @Nullable String selector) { return false; } }); From 49f9c4c1f466e4e965d782f37a64443e7098f05e Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Tue, 11 Mar 2025 14:33:35 +0100 Subject: [PATCH 093/131] Clean up --- .../authz/permission/IndicesPermission.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 1d4183e76eceb..79750d0ae4a3c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -517,6 +517,13 @@ public Collection resolveConcreteIndices(ProjectMetadata metadata) { public boolean canHaveBackingIndices() { return indexAbstraction != null && indexAbstraction.getType() != IndexAbstraction.Type.CONCRETE_INDEX; } + + public String nameCombinedWithSelector() { + if (IndexComponentSelector.FAILURES.equals(selector)) { + return IndexNameExpressionResolver.combineSelector(name, selector); + } + return name; + } } /** @@ -539,16 +546,16 @@ public IndicesAccessControl authorize( int totalResourceCount = 0; Map lookup = metadata.getIndicesLookup(); for (String indexOrAlias : requestedIndicesOrAliases) { - String originalIndexOrAlias = indexOrAlias; - // Remove any selectors from abstraction name, but include it in the map key if it's ::failures. We need to do this to - // disambiguate between data streams and their failure stores. + // Remove any selectors from abstraction name. Tuple expressionAndSelector = IndexNameExpressionResolver.splitSelectorExpression(indexOrAlias); indexOrAlias = expressionAndSelector.v1(); IndexComponentSelector selector = expressionAndSelector.v2() == null ? null : IndexComponentSelector.getByKey(expressionAndSelector.v2()); final IndexResource resource = new IndexResource(indexOrAlias, lookup.get(indexOrAlias), selector); - resources.put(IndexComponentSelector.FAILURES.equals(selector) ? originalIndexOrAlias : indexOrAlias, resource); + // We can't use resource.name here because we may be accessing a data stream _and_ its failure store, + // where the selector-free name is the same for both and thus ambiguous. + resources.put(resource.nameCombinedWithSelector(), resource); totalResourceCount += resource.size(lookup); } From ca566e8feaa7ed84ed33868fbcac8f8d6cda6eb5 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Tue, 11 Mar 2025 14:38:40 +0100 Subject: [PATCH 094/131] More --- .../core/security/authz/permission/IndicesPermission.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 79750d0ae4a3c..03a78deac1902 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -519,10 +519,10 @@ public boolean canHaveBackingIndices() { } public String nameCombinedWithSelector() { - if (IndexComponentSelector.FAILURES.equals(selector)) { - return IndexNameExpressionResolver.combineSelector(name, selector); - } - return name; + String combined = IndexNameExpressionResolver.combineSelector(name, selector); + assert false != IndexComponentSelector.FAILURES.equals(selector) || name.equals(combined) + : "Only failures selectors should result in explicit selectors suffix"; + return combined; } } From 8efd2d26f83919db073e37ea1663decb78881e39 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Tue, 11 Mar 2025 15:07:58 +0100 Subject: [PATCH 095/131] Moar --- .../core/security/authz/permission/IndicesPermission.java | 2 +- .../security/failurestore/FailureStoreSecurityRestIT.java | 3 +-- .../org/elasticsearch/xpack/security/authz/RBACEngine.java | 4 +--- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 03a78deac1902..4ee80761a2d39 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -546,7 +546,7 @@ public IndicesAccessControl authorize( int totalResourceCount = 0; Map lookup = metadata.getIndicesLookup(); for (String indexOrAlias : requestedIndicesOrAliases) { - // Remove any selectors from abstraction name. + // Remove any selectors from abstraction name. Access control is based on the `selector` field of the IndexResource Tuple expressionAndSelector = IndexNameExpressionResolver.splitSelectorExpression(indexOrAlias); indexOrAlias = expressionAndSelector.v1(); IndexComponentSelector selector = expressionAndSelector.v2() == null diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java index 462a482e92d3a..803bc328ffb1a 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java @@ -424,8 +424,7 @@ public void testFailureStoreAccess() throws Exception { } } } - { - var request = new Search("*"); + for (var request : List.of(new Search("*"), new Search("_all"), new Search(""))) { for (var user : users) { switch (user) { case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java index c12d1bdab249e..73a7979d61ddb 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java @@ -914,8 +914,7 @@ static AuthorizedIndices resolveAuthorizedIndicesFromRole( timeChecker.accept(indicesAndAliases); return indicesAndAliases; }, () -> { - // TODO is this right? - Consumer> timeChecker = timerSupplier.get(); + // TODO handle time checking in a follow-up Set indicesAndAliases = new HashSet<>(); if (includeDataStreams) { for (IndexAbstraction indexAbstraction : lookup.values()) { @@ -929,7 +928,6 @@ static AuthorizedIndices resolveAuthorizedIndicesFromRole( } } } - timeChecker.accept(indicesAndAliases); return indicesAndAliases; }, (name, selectorString) -> { final IndexAbstraction indexAbstraction = lookup.get(name); From 24db96104229c4171d1893d615849002950e9fbd Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Tue, 11 Mar 2025 15:43:57 +0100 Subject: [PATCH 096/131] More comments --- .../xpack/security/authz/IndicesAndAliasesResolver.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java index a1fdccae78c4b..557e3676ff519 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java @@ -450,6 +450,7 @@ static String getPutMappingIndexOrAlias( + indexAbstraction.getType().getDisplayName() + "], but a concrete index is expected" ); + // we know this is implicit data access (as opposed to another selector) so the default selector check is correct } else if (isAuthorized.test(concreteIndexName, null)) { // user is authorized to put mappings for this index resolvedAliasOrIndex = concreteIndexName; @@ -460,6 +461,7 @@ static String getPutMappingIndexOrAlias( List aliasMetadata = foundAliases.get(concreteIndexName); if (aliasMetadata != null) { Optional foundAlias = aliasMetadata.stream().map(AliasMetadata::alias).filter(aliasName -> { + // we know this is implicit data access (as opposed to another selector) so the default selector check is correct if (false == isAuthorized.test(aliasName, null)) { return false; } From 240007ed319428980d29810484055bb21669e375 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Tue, 11 Mar 2025 17:02:23 +0100 Subject: [PATCH 097/131] Assert --- .../security/authz/IndicesAndAliasesResolver.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java index 557e3676ff519..45fcc6c0a2021 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java @@ -26,6 +26,7 @@ import org.elasticsearch.common.regex.Regex; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.core.Assertions; import org.elasticsearch.core.Nullable; import org.elasticsearch.core.Tuple; import org.elasticsearch.index.Index; @@ -509,6 +510,7 @@ private static List replaceWildcardsWithAuthorizedAliases(String[] alias } for (String aliasExpression : aliases) { + assertOnlyDataSelector(aliasExpression); boolean include = true; if (aliasExpression.charAt(0) == '-') { include = false; @@ -536,6 +538,14 @@ private static List replaceWildcardsWithAuthorizedAliases(String[] alias return finalAliases; } + private static void assertOnlyDataSelector(String expression) { + if (Assertions.ENABLED) { + Tuple tuple = IndexNameExpressionResolver.splitSelectorExpression(expression); + assert tuple.v2() == null || IndexComponentSelector.getByKey(tuple.v2()) == IndexComponentSelector.DATA + : "Selector [" + tuple.v2() + "] is not allowed in expression [" + expression + "]"; + } + } + private static List indicesList(String[] list) { return (list == null) ? null : Arrays.asList(list); } From a5b5ac4a7e6dce9bac355a06ea5afed31553c1f2 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Tue, 11 Mar 2025 17:08:16 +0100 Subject: [PATCH 098/131] Better method name --- .../xpack/security/authz/IndicesAndAliasesResolver.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java index 45fcc6c0a2021..1b601901a1078 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java @@ -510,7 +510,7 @@ private static List replaceWildcardsWithAuthorizedAliases(String[] alias } for (String aliasExpression : aliases) { - assertOnlyDataSelector(aliasExpression); + assertNoUnsupportedSelectors(aliasExpression); boolean include = true; if (aliasExpression.charAt(0) == '-') { include = false; @@ -538,7 +538,7 @@ private static List replaceWildcardsWithAuthorizedAliases(String[] alias return finalAliases; } - private static void assertOnlyDataSelector(String expression) { + private static void assertNoUnsupportedSelectors(String expression) { if (Assertions.ENABLED) { Tuple tuple = IndexNameExpressionResolver.splitSelectorExpression(expression); assert tuple.v2() == null || IndexComponentSelector.getByKey(tuple.v2()) == IndexComponentSelector.DATA From 092371e6565fb47c13ff8df98b1c6ac72a260d00 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 12 Mar 2025 11:11:03 +0100 Subject: [PATCH 099/131] Tests tests tests --- .../FailureStoreSecurityRestIT.java | 71 +++++++ .../accesscontrol/IndicesPermissionTests.java | 176 ++++++++++++++++++ 2 files changed, 247 insertions(+) diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java index 803bc328ffb1a..67ae995828df9 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java @@ -171,6 +171,77 @@ public void testGetUserPrivileges() throws IOException { }"""); } + public void testRoleWithSelectorInIndexPattern() throws Exception { + apiKeys = new HashMap<>(); + + createUser(WRITE_ACCESS, PASSWORD, WRITE_ACCESS); + upsertRole(Strings.format(""" + { + "cluster": ["all"], + "indices": [{"names": ["test*"], "privileges": ["write", "auto_configure"]}] + }"""), WRITE_ACCESS); + createAndStoreApiKey(WRITE_ACCESS, null); + + createTemplates(); + populateDataStream(); + + createUser("user", PASSWORD, "role"); + upsertRole(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["*::failures"], + "privileges": ["read"] + } + ] + }""", "role"); + createAndStoreApiKey("user", null); + + expect("user", new Search("test1::failures"), 403); + expect("user", new Search("*::failures")); + + upsertRole(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["test1::failures"], + "privileges": ["read"] + } + ] + }""", "role"); + + expect("user", new Search("test1::failures"), 403); + expect("user", new Search("*::failures")); + + upsertRole(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["*::failures"], + "privileges": ["read_failure_store"] + } + ] + }""", "role"); + expect("user", new Search("test1::failures"), 403); + expect("user", new Search("*::failures")); + + upsertRole(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["test1::failures"], + "privileges": ["read_failure_store"] + } + ] + }""", "role"); + expect("user", new Search("test1::failures"), 403); + expect("user", new Search("*::failures")); + } + @SuppressWarnings("unchecked") public void testFailureStoreAccess() throws Exception { apiKeys = new HashMap<>(); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java index 0a7552ab31837..a0d4264490004 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java @@ -186,6 +186,182 @@ public void testAuthorize() { } + public void testAuthorizeWithFailuresSelector() { + Metadata.Builder builder = Metadata.builder(); + String dataStreamName = randomAlphaOfLength(6); + int numBackingIndices = randomIntBetween(1, 3); + List backingIndices = new ArrayList<>(); + for (int backingIndexNumber = 1; backingIndexNumber <= numBackingIndices; backingIndexNumber++) { + backingIndices.add(createBackingIndexMetadata(DataStream.getDefaultBackingIndexName(dataStreamName, backingIndexNumber))); + } + DataStream ds = DataStreamTestHelper.newInstance( + dataStreamName, + backingIndices.stream().map(IndexMetadata::getIndex).collect(Collectors.toList()) + ); + builder.put(ds); + for (IndexMetadata index : backingIndices) { + builder.put(index, false); + } + var metadata = builder.build().getProject(); + FieldPermissionsCache fieldPermissionsCache = new FieldPermissionsCache(Settings.EMPTY); + + for (var privilege : List.of(IndexPrivilege.ALL, IndexPrivilege.READ)) { + Role role = Role.builder(RESTRICTED_INDICES, "_role") + .add( + new FieldPermissions(fieldPermissionDef(null, null)), + null, + privilege, + randomBoolean(), + randomFrom(dataStreamName, dataStreamName + "*") + ) + .build(); + IndicesAccessControl permissions = role.authorize( + TransportSearchAction.TYPE.name(), + Sets.newHashSet(dataStreamName), + metadata, + fieldPermissionsCache + ); + assertThat("for privilege " + privilege, permissions.isGranted(), is(true)); + assertThat("for privilege " + privilege, permissions.hasIndexPermissions(dataStreamName + "::failures"), is(false)); + assertThat("for privilege " + privilege, permissions.hasIndexPermissions(dataStreamName), is(true)); + } + + for (var privilege : List.of(IndexPrivilege.ALL, IndexPrivilege.READ_FAILURE_STORE)) { + Role role = Role.builder(RESTRICTED_INDICES, "_role") + .add( + new FieldPermissions(fieldPermissionDef(null, null)), + null, + privilege, + randomBoolean(), + randomFrom(dataStreamName, dataStreamName + "*") + ) + .build(); + IndicesAccessControl permissions = role.authorize( + TransportSearchAction.TYPE.name(), + Sets.newHashSet(dataStreamName + "::failures"), + metadata, + fieldPermissionsCache + ); + assertThat("for privilege " + privilege, permissions.isGranted(), is(true)); + assertThat("for privilege " + privilege, permissions.hasIndexPermissions(dataStreamName + "::failures"), is(true)); + assertThat("for privilege " + privilege, permissions.hasIndexPermissions(dataStreamName), is(false)); + } + + for (var privilege : List.of(IndexPrivilege.ALL, IndexPrivilege.READ_FAILURE_STORE)) { + Role role = Role.builder(RESTRICTED_INDICES, "_role") + .add( + new FieldPermissions(fieldPermissionDef(null, null)), + null, + privilege, + randomBoolean(), + randomFrom(dataStreamName, dataStreamName + "*") + ) + .build(); + IndicesAccessControl permissions = role.authorize( + TransportSearchAction.TYPE.name(), + Sets.newHashSet(dataStreamName + "::failures"), + metadata, + fieldPermissionsCache + ); + + assertThat("for privilege " + privilege, permissions.isGranted(), is(true)); + assertThat("for privilege " + privilege, permissions.hasIndexPermissions(dataStreamName + "::failures"), is(true)); + assertThat("for privilege " + privilege, permissions.hasIndexPermissions(dataStreamName), is(false)); + } + + { + Role role = Role.builder(RESTRICTED_INDICES, "_role") + .add( + new FieldPermissions(fieldPermissionDef(null, null)), + null, + IndexPrivilege.READ, + randomBoolean(), + randomFrom(dataStreamName, dataStreamName + "*") + ) + .add( + new FieldPermissions(fieldPermissionDef(null, null)), + null, + IndexPrivilege.READ_FAILURE_STORE, + randomBoolean(), + randomFrom(dataStreamName, dataStreamName + "*") + ) + .build(); + IndicesAccessControl permissions = role.authorize( + TransportSearchAction.TYPE.name(), + Sets.newHashSet(dataStreamName, dataStreamName + "::failures"), + metadata, + fieldPermissionsCache + ); + assertThat(permissions.isGranted(), is(true)); + assertThat(permissions.hasIndexPermissions(dataStreamName + "::failures"), is(true)); + assertThat(permissions.hasIndexPermissions(dataStreamName), is(true)); + } + + for (var privilege : List.of(IndexPrivilege.ALL)) { + Role role = Role.builder(RESTRICTED_INDICES, "_role") + .add( + new FieldPermissions(fieldPermissionDef(null, null)), + null, + privilege, + randomBoolean(), + randomFrom(dataStreamName, dataStreamName + "*") + ) + .build(); + IndicesAccessControl permissions = role.authorize( + TransportSearchAction.TYPE.name(), + Sets.newHashSet(dataStreamName, dataStreamName + "::failures"), + metadata, + fieldPermissionsCache + ); + assertThat("for privilege " + privilege, permissions.isGranted(), is(true)); + assertThat("for privilege " + privilege, permissions.hasIndexPermissions(dataStreamName + "::failures"), is(true)); + assertThat("for privilege " + privilege, permissions.hasIndexPermissions(dataStreamName), is(true)); + } + + for (var privilege : List.of(IndexPrivilege.READ_FAILURE_STORE)) { + Role role = Role.builder(RESTRICTED_INDICES, "_role") + .add( + new FieldPermissions(fieldPermissionDef(null, null)), + null, + privilege, + randomBoolean(), + randomFrom(dataStreamName, dataStreamName + "*") + ) + .build(); + IndicesAccessControl permissions = role.authorize( + TransportSearchAction.TYPE.name(), + Sets.newHashSet(dataStreamName, dataStreamName + "::failures"), + metadata, + fieldPermissionsCache + ); + assertThat("for privilege " + privilege, permissions.isGranted(), is(false)); + assertThat("for privilege " + privilege, permissions.hasIndexPermissions(dataStreamName + "::failures"), is(true)); + assertThat("for privilege " + privilege, permissions.hasIndexPermissions(dataStreamName), is(false)); + } + + for (var privilege : List.of(IndexPrivilege.READ)) { + Role role = Role.builder(RESTRICTED_INDICES, "_role") + .add( + new FieldPermissions(fieldPermissionDef(null, null)), + null, + privilege, + randomBoolean(), + randomFrom(dataStreamName, dataStreamName + "*") + ) + .build(); + IndicesAccessControl permissions = role.authorize( + TransportSearchAction.TYPE.name(), + Sets.newHashSet(dataStreamName, dataStreamName + "::failures"), + metadata, + fieldPermissionsCache + ); + assertThat("for privilege " + privilege, permissions.isGranted(), is(false)); + assertThat("for privilege " + privilege, permissions.hasIndexPermissions(dataStreamName + "::failures"), is(false)); + assertThat("for privilege " + privilege, permissions.hasIndexPermissions(dataStreamName), is(true)); + } + + } + public void testAuthorizeMultipleGroupsMixedDls() { IndexMetadata.Builder imbBuilder = IndexMetadata.builder("_index") .settings(indexSettings(IndexVersion.current(), 1, 1)) From 903941e1fd9ee2a441b0d992ceabfa25b8057128 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 12 Mar 2025 12:03:24 +0100 Subject: [PATCH 100/131] More --- .../accesscontrol/IndicesPermissionTests.java | 123 +++++++++++++++--- 1 file changed, 105 insertions(+), 18 deletions(-) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java index a0d4264490004..afc6d1b0b246e 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java @@ -186,7 +186,7 @@ public void testAuthorize() { } - public void testAuthorizeWithFailuresSelector() { + public void testAuthorizeDataStreamAccessWithFailuresSelector() { Metadata.Builder builder = Metadata.builder(); String dataStreamName = randomAlphaOfLength(6); int numBackingIndices = randomIntBetween(1, 3); @@ -217,7 +217,7 @@ public void testAuthorizeWithFailuresSelector() { .build(); IndicesAccessControl permissions = role.authorize( TransportSearchAction.TYPE.name(), - Sets.newHashSet(dataStreamName), + Sets.newHashSet(randomFrom(dataStreamName, dataStreamName + "::data")), metadata, fieldPermissionsCache ); @@ -288,7 +288,7 @@ public void testAuthorizeWithFailuresSelector() { .build(); IndicesAccessControl permissions = role.authorize( TransportSearchAction.TYPE.name(), - Sets.newHashSet(dataStreamName, dataStreamName + "::failures"), + Sets.newHashSet(randomFrom(dataStreamName, dataStreamName + "::data"), dataStreamName + "::failures"), metadata, fieldPermissionsCache ); @@ -297,47 +297,100 @@ public void testAuthorizeWithFailuresSelector() { assertThat(permissions.hasIndexPermissions(dataStreamName), is(true)); } - for (var privilege : List.of(IndexPrivilege.ALL)) { + { Role role = Role.builder(RESTRICTED_INDICES, "_role") .add( new FieldPermissions(fieldPermissionDef(null, null)), null, - privilege, + IndexPrivilege.ALL, randomBoolean(), randomFrom(dataStreamName, dataStreamName + "*") ) .build(); IndicesAccessControl permissions = role.authorize( TransportSearchAction.TYPE.name(), - Sets.newHashSet(dataStreamName, dataStreamName + "::failures"), + Sets.newHashSet(randomFrom(dataStreamName, dataStreamName + "::data"), dataStreamName + "::failures"), metadata, fieldPermissionsCache ); - assertThat("for privilege " + privilege, permissions.isGranted(), is(true)); - assertThat("for privilege " + privilege, permissions.hasIndexPermissions(dataStreamName + "::failures"), is(true)); - assertThat("for privilege " + privilege, permissions.hasIndexPermissions(dataStreamName), is(true)); + assertThat("for privilege " + IndexPrivilege.ALL, permissions.isGranted(), is(true)); + assertThat("for privilege " + IndexPrivilege.ALL, permissions.hasIndexPermissions(dataStreamName + "::failures"), is(true)); + assertThat("for privilege " + IndexPrivilege.ALL, permissions.hasIndexPermissions(dataStreamName), is(true)); + } + { + Role role = Role.builder(RESTRICTED_INDICES, "_role") + .add( + new FieldPermissions(fieldPermissionDef(null, null)), + null, + IndexPrivilege.READ_FAILURE_STORE, + randomBoolean(), + randomFrom(dataStreamName, dataStreamName + "*") + ) + .build(); + IndicesAccessControl permissions = role.authorize( + TransportSearchAction.TYPE.name(), + Sets.newHashSet(randomFrom(dataStreamName, dataStreamName + "::data"), dataStreamName + "::failures"), + metadata, + fieldPermissionsCache + ); + assertThat("for privilege " + IndexPrivilege.READ_FAILURE_STORE, permissions.isGranted(), is(false)); + assertThat( + "for privilege " + IndexPrivilege.READ_FAILURE_STORE, + permissions.hasIndexPermissions(dataStreamName + "::failures"), + is(true) + ); + assertThat("for privilege " + IndexPrivilege.READ_FAILURE_STORE, permissions.hasIndexPermissions(dataStreamName), is(false)); } - for (var privilege : List.of(IndexPrivilege.READ_FAILURE_STORE)) { + { Role role = Role.builder(RESTRICTED_INDICES, "_role") .add( new FieldPermissions(fieldPermissionDef(null, null)), null, - privilege, + IndexPrivilege.READ, randomBoolean(), randomFrom(dataStreamName, dataStreamName + "*") ) .build(); IndicesAccessControl permissions = role.authorize( TransportSearchAction.TYPE.name(), - Sets.newHashSet(dataStreamName, dataStreamName + "::failures"), + Sets.newHashSet(randomFrom(dataStreamName, dataStreamName + "::data"), dataStreamName + "::failures"), metadata, fieldPermissionsCache ); - assertThat("for privilege " + privilege, permissions.isGranted(), is(false)); - assertThat("for privilege " + privilege, permissions.hasIndexPermissions(dataStreamName + "::failures"), is(true)); - assertThat("for privilege " + privilege, permissions.hasIndexPermissions(dataStreamName), is(false)); + assertThat("for privilege " + IndexPrivilege.READ, permissions.isGranted(), is(false)); + assertThat("for privilege " + IndexPrivilege.READ, permissions.hasIndexPermissions(dataStreamName + "::failures"), is(false)); + assertThat("for privilege " + IndexPrivilege.READ, permissions.hasIndexPermissions(dataStreamName), is(true)); } + } + + public void testAuthorizeDataStreamFailureIndices() { + Metadata.Builder builder = Metadata.builder(); + String dataStreamName = randomAlphaOfLength(6); + int numBackingIndices = randomIntBetween(1, 3); + List backingIndices = new ArrayList<>(); + for (int backingIndexNumber = 1; backingIndexNumber <= numBackingIndices; backingIndexNumber++) { + backingIndices.add(createBackingIndexMetadata(DataStream.getDefaultBackingIndexName(dataStreamName, backingIndexNumber))); + } + List failureIndices = new ArrayList<>(); + int numFailureIndices = randomIntBetween(1, 3); + for (int failureIndexNumber = 1; failureIndexNumber <= numFailureIndices; failureIndexNumber++) { + failureIndices.add(createBackingIndexMetadata(DataStream.getDefaultFailureStoreName(dataStreamName, failureIndexNumber, 1L))); + } + DataStream ds = DataStreamTestHelper.newInstance( + dataStreamName, + backingIndices.stream().map(IndexMetadata::getIndex).collect(Collectors.toList()), + failureIndices.stream().map(IndexMetadata::getIndex).collect(Collectors.toList()) + ); + builder.put(ds); + for (IndexMetadata index : backingIndices) { + builder.put(index, false); + } + for (IndexMetadata index : failureIndices) { + builder.put(index, false); + } + var metadata = builder.build().getProject(); + FieldPermissionsCache fieldPermissionsCache = new FieldPermissionsCache(Settings.EMPTY); for (var privilege : List.of(IndexPrivilege.READ)) { Role role = Role.builder(RESTRICTED_INDICES, "_role") @@ -349,17 +402,51 @@ public void testAuthorizeWithFailuresSelector() { randomFrom(dataStreamName, dataStreamName + "*") ) .build(); + String failureIndex = randomFrom(failureIndices).getIndex().getName(); IndicesAccessControl permissions = role.authorize( TransportSearchAction.TYPE.name(), - Sets.newHashSet(dataStreamName, dataStreamName + "::failures"), + Sets.newHashSet(failureIndex), metadata, fieldPermissionsCache ); assertThat("for privilege " + privilege, permissions.isGranted(), is(false)); - assertThat("for privilege " + privilege, permissions.hasIndexPermissions(dataStreamName + "::failures"), is(false)); - assertThat("for privilege " + privilege, permissions.hasIndexPermissions(dataStreamName), is(true)); + assertThat("for privilege " + privilege, permissions.hasIndexPermissions(failureIndex), is(false)); + + String dataIndex = randomFrom(backingIndices).getIndex().getName(); + permissions = role.authorize(TransportSearchAction.TYPE.name(), Sets.newHashSet(dataIndex), metadata, fieldPermissionsCache); + + assertThat("for privilege " + privilege, permissions.isGranted(), is(true)); + assertThat("for privilege " + privilege, permissions.hasIndexPermissions(dataIndex), is(true)); } + for (var privilege : List.of(IndexPrivilege.READ_FAILURE_STORE)) { + Role role = Role.builder(RESTRICTED_INDICES, "_role") + .add( + new FieldPermissions(fieldPermissionDef(null, null)), + null, + privilege, + randomBoolean(), + randomFrom(dataStreamName, dataStreamName + "*") + ) + .build(); + + String failureIndex = randomFrom(failureIndices).getIndex().getName(); + IndicesAccessControl permissions = role.authorize( + TransportSearchAction.TYPE.name(), + Sets.newHashSet(failureIndex), + metadata, + fieldPermissionsCache + ); + + assertThat("for privilege " + privilege, permissions.isGranted(), is(true)); + assertThat("for privilege " + privilege, permissions.hasIndexPermissions(failureIndex), is(true)); + + String dataIndex = randomFrom(backingIndices).getIndex().getName(); + permissions = role.authorize(TransportSearchAction.TYPE.name(), Sets.newHashSet(dataIndex), metadata, fieldPermissionsCache); + + assertThat("for privilege " + privilege, permissions.isGranted(), is(false)); + assertThat("for privilege " + privilege, permissions.hasIndexPermissions(dataIndex), is(false)); + } } public void testAuthorizeMultipleGroupsMixedDls() { From 5d0d5d8c86c033acc09be9dbbbcd4f5d95c15483 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 12 Mar 2025 12:17:21 +0100 Subject: [PATCH 101/131] Action matcher --- .../core/security/authz/permission/IndicesPermission.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 4ee80761a2d39..430b069ff60e3 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -374,9 +374,14 @@ public boolean checkResourcePrivileges( } public Automaton allowedActionsMatcher(String index) { + Tuple tuple = IndexNameExpressionResolver.splitSelectorExpression(index); + String indexName = tuple.v1(); + IndexComponentSelector selector = IndexComponentSelector.getByKey(tuple.v2()); + // TODO is this correct? + assert selector == null || IndexComponentSelector.DATA.equals(selector) : "unexpected selector [" + selector + "]"; List automatonList = new ArrayList<>(); for (Group group : groups) { - if (group.indexNameMatcher.test(index)) { + if (group.checkSelector(selector) && group.indexNameMatcher.test(indexName)) { automatonList.add(group.privilege.getAutomaton()); } } From da81d5030937d15d9200cb51664a861ac1315870 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 12 Mar 2025 14:26:59 +0100 Subject: [PATCH 102/131] Assertions and WIP API key tests --- .../metadata/IndexNameExpressionResolver.java | 9 ++ .../FailureStoreSecurityRestIT.java | 153 ++++++++++++++++-- .../authz/IndicesAndAliasesResolver.java | 11 +- 3 files changed, 147 insertions(+), 26 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java index e221cb5c08e5b..6ba51c8f0cc71 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java @@ -28,6 +28,7 @@ import org.elasticsearch.common.util.CollectionUtils; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.core.Assertions; import org.elasticsearch.core.Nullable; import org.elasticsearch.core.Predicates; import org.elasticsearch.core.Tuple; @@ -1022,6 +1023,14 @@ public static String combineSelectorExpression(String baseExpression, @Nullable : (baseExpression + SelectorResolver.SELECTOR_SEPARATOR + selectorExpression); } + public static void assertDefaultOrDataSelector(String expression) { + if (Assertions.ENABLED) { + var tuple = splitSelectorExpression(expression); + assert tuple.v2() == null || IndexComponentSelector.DATA.getKey().equals(tuple.v2()) + : "Expected expression [" + expression + "] to have a data selector but found [" + tuple.v2() + "]"; + } + } + /** * Resolve an array of expressions to the set of indices and aliases that these expressions match. */ diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java index 67ae995828df9..4a3348efae76a 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java @@ -45,7 +45,6 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; public class FailureStoreSecurityRestIT extends ESRestTestCase { @@ -973,6 +972,114 @@ public void testFailureStoreAccess() throws Exception { expectThrows404(() -> adminClient().performRequest(new Request("GET", "/" + failureIndexName + "/_search"))); } + @SuppressWarnings("unchecked") + public void testFailureStoreAccessWithApiKeys() throws Exception { + apiKeys = new HashMap<>(); + + createUser(WRITE_ACCESS, PASSWORD, WRITE_ACCESS); + upsertRole(Strings.format(""" + { + "cluster": ["all"], + "indices": [{"names": ["test*"], "privileges": ["write", "auto_configure"]}] + }"""), WRITE_ACCESS); + + createTemplates(); + + List docIds = populateDataStream(); + assertThat(docIds.size(), equalTo(2)); + assertThat(docIds, hasItem("1")); + String dataDocId = "1"; + String failuresDocId = docIds.stream().filter(id -> false == id.equals(dataDocId)).findFirst().get(); + + Request dataStream = new Request("GET", "/_data_stream/test1"); + Response response = adminClient().performRequest(dataStream); + Map dataStreams = entityAsMap(response); + assertEquals(Collections.singletonList("test1"), XContentMapValues.extractValue("data_streams.name", dataStreams)); + List dataIndexNames = (List) XContentMapValues.extractValue("data_streams.indices.index_name", dataStreams); + assertThat(dataIndexNames.size(), equalTo(1)); + List failureIndexNames = (List) XContentMapValues.extractValue( + "data_streams.failure_store.indices.index_name", + dataStreams + ); + assertThat(failureIndexNames.size(), equalTo(1)); + + String dataIndexName = dataIndexNames.get(0); + String failureIndexName = failureIndexNames.get(0); + + var user = "user"; + var role = "role"; + createUser(user, PASSWORD, role); + upsertRole(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["*"], + "privileges": ["read_failure_store"] + } + ] + } + """, role); + + String apiKey = createApiKey(user, """ + { + "role": { + "cluster": ["all"], + "indices": [{"names": ["test1"], "privileges": ["read_failure_store"]}] + } + }"""); + + expectUsingApiKey(apiKey, new Search("test1::failures"), failuresDocId); + expectUsingApiKey(apiKey, new Search(failureIndexName), failuresDocId); + expectUsingApiKey(apiKey, new Search(dataIndexName), 403); + expectUsingApiKey(apiKey, new Search("test1"), 403); + + apiKey = createApiKey(user, """ + { + "role": { + "cluster": ["all"], + "indices": [{"names": ["test1"], "privileges": ["read_failure_store", "read"]}] + } + }"""); + + expectUsingApiKey(apiKey, new Search("test1::failures"), failuresDocId); + expectUsingApiKey(apiKey, new Search(failureIndexName), failuresDocId); + expectUsingApiKey(apiKey, new Search(dataIndexName), 403); + expectUsingApiKey(apiKey, new Search("test1"), 403); + + apiKey = createApiKey(user, """ + { + "role": { + "cluster": ["all"], + "indices": [ + {"names": ["test1"], "privileges": ["read_failure_store"]}, + {"names": ["*"], "privileges": ["read"]} + ] + } + }"""); + + expectUsingApiKey(apiKey, new Search("test1::failures"), failuresDocId); + expectUsingApiKey(apiKey, new Search(failureIndexName), failuresDocId); + expectUsingApiKey(apiKey, new Search(dataIndexName), 403); + expectUsingApiKey(apiKey, new Search("test1"), 403); + + apiKey = createApiKey(user, """ + { + "role": { + "cluster": ["all"], + "indices": [ + {"names": ["*"], "privileges": ["read"]} + ] + } + }"""); + + expectUsingApiKey(apiKey, new Search("test1::failures"), 403); + // funky but correct: assigned role descriptors grant direct access to failure index, limited-by to failure store + expectUsingApiKey(apiKey, new Search(failureIndexName), failuresDocId); + expectUsingApiKey(apiKey, new Search(dataIndexName), 403); + expectUsingApiKey(apiKey, new Search("test1"), 403); + } + private static void expectThrows404(ThrowingRunnable runnable) { expectThrows(runnable, 404); } @@ -992,13 +1099,22 @@ private void expect(String user, Search search, int statusCode) { } private void expect(String user, Search search, String... docIds) throws Exception { - expectSearch(user, search.toSearchRequest(), docIds); - expectAsyncSearch(user, search.toAsyncSearchRequest(), docIds); + expectSearch(performRequestMaybeUsingApiKey(user, search.toSearchRequest()), docIds); + expectAsyncSearch(performRequestMaybeUsingApiKey(user, search.toAsyncSearchRequest()), docIds); + } + + private void expectUsingApiKey(String apiKey, Search search, String... docIds) throws Exception { + expectSearch(performRequestWithApiKey(apiKey, search.toSearchRequest()), docIds); + expectAsyncSearch(performRequestWithApiKey(apiKey, search.toAsyncSearchRequest()), docIds); + } + + private void expectUsingApiKey(String apiKey, Search search, int statusCode) { + expectThrows(() -> performRequestWithApiKey(apiKey, search.toSearchRequest()), statusCode); + expectThrows(() -> performRequestWithApiKey(apiKey, search.toAsyncSearchRequest()), statusCode); } @SuppressWarnings("unchecked") - private void expectAsyncSearch(String user, Request request, String... docIds) throws IOException { - Response response = performRequestMaybeUsingApiKey(user, request); + private static void expectAsyncSearch(Response response, String... docIds) throws IOException { assertOK(response); ObjectPath resp = ObjectPath.createFromResponse(response); Boolean isRunning = resp.evaluate("is_running"); @@ -1012,8 +1128,7 @@ private void expectAsyncSearch(String user, Request request, String... docIds) t assertThat(actual, containsInAnyOrder(docIds)); } - private void expectSearch(String user, Request request, String... docIds) throws Exception { - Response response = performRequestMaybeUsingApiKey(user, request); + private static void expectSearch(Response response, String... docIds) throws IOException { assertOK(response); final SearchResponse searchResponse = SearchResponseUtils.parseSearchResponse(responseAsParser(response)); try { @@ -1163,16 +1278,18 @@ private Response performRequest(String user, Request request) throws IOException } private Response performRequestMaybeUsingApiKey(String user, Request request) throws IOException { - if (randomBoolean()) { - return performRequest(user, request); + if (randomBoolean() && apiKeys.containsKey(user)) { + return performRequestWithApiKey(apiKeys.get(user), request); } else { - String apiKey = apiKeys.get(user); - assertThat("expected to have an API key stored for user: " + user, apiKey, is(notNullValue())); - request.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", "ApiKey " + apiKey).build()); - return client().performRequest(request); + return performRequest(user, request); } } + private static Response performRequestWithApiKey(String apiKey, Request request) throws IOException { + request.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", "ApiKey " + apiKey).build()); + return client().performRequest(request); + } + private static void expectUserPrivilegesResponse(String userPrivilegesResponse) throws IOException { Request request = new Request("GET", "/_security/user/_privileges"); request.setOptions( @@ -1200,8 +1317,13 @@ protected void createUser(String username, SecureString password, String... role getSecurityClient().putUser(new User(username, roles), password); } - protected void createAndStoreApiKey(String username, @Nullable String roleDescriptors) throws IOException { + protected String createAndStoreApiKey(String username, @Nullable String roleDescriptors) throws IOException { assertThat("API key already registered for user: " + username, apiKeys.containsKey(username), is(false)); + apiKeys.put(username, createApiKey(username, roleDescriptors)); + return createApiKey(username, roleDescriptors); + } + + private String createApiKey(String username, String roleDescriptors) throws IOException { var request = new Request("POST", "/_security/api_key"); if (roleDescriptors == null) { request.setJsonEntity(""" @@ -1220,8 +1342,7 @@ protected void createAndStoreApiKey(String username, @Nullable String roleDescri Response response = performRequest(username, request); assertOK(response); Map responseAsMap = responseAsMap(response); - String encoded = (String) responseAsMap.get("encoded"); - apiKeys.put(username, encoded); + return (String) responseAsMap.get("encoded"); } protected void upsertRole(String roleDescriptor, String roleName) throws IOException { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java index 1b601901a1078..d4409274e6bf0 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java @@ -26,7 +26,6 @@ import org.elasticsearch.common.regex.Regex; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.core.Assertions; import org.elasticsearch.core.Nullable; import org.elasticsearch.core.Tuple; import org.elasticsearch.index.Index; @@ -510,7 +509,7 @@ private static List replaceWildcardsWithAuthorizedAliases(String[] alias } for (String aliasExpression : aliases) { - assertNoUnsupportedSelectors(aliasExpression); + IndexNameExpressionResolver.assertDefaultOrDataSelector(aliasExpression); boolean include = true; if (aliasExpression.charAt(0) == '-') { include = false; @@ -538,14 +537,6 @@ private static List replaceWildcardsWithAuthorizedAliases(String[] alias return finalAliases; } - private static void assertNoUnsupportedSelectors(String expression) { - if (Assertions.ENABLED) { - Tuple tuple = IndexNameExpressionResolver.splitSelectorExpression(expression); - assert tuple.v2() == null || IndexComponentSelector.getByKey(tuple.v2()) == IndexComponentSelector.DATA - : "Selector [" + tuple.v2() + "] is not allowed in expression [" + expression + "]"; - } - } - private static List indicesList(String[] list) { return (list == null) ? null : Arrays.asList(list); } From cd6e74ae1b455cfcb1ae7b9f5b3b3a3d8b8b59f4 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 12 Mar 2025 14:32:30 +0100 Subject: [PATCH 103/131] More api key tests --- .../FailureStoreSecurityRestIT.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java index 4a3348efae76a..057f90f02bf08 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java @@ -1047,6 +1047,19 @@ public void testFailureStoreAccessWithApiKeys() throws Exception { expectUsingApiKey(apiKey, new Search(dataIndexName), 403); expectUsingApiKey(apiKey, new Search("test1"), 403); + apiKey = createApiKey(user, """ + { + "role": { + "cluster": ["all"], + "indices": [{"names": ["test2"], "privileges": ["read_failure_store", "read"]}] + } + }"""); + + expectUsingApiKey(apiKey, new Search("test1::failures"), 403); + expectUsingApiKey(apiKey, new Search(failureIndexName), 403); + expectUsingApiKey(apiKey, new Search(dataIndexName), 403); + expectUsingApiKey(apiKey, new Search("test1"), 403); + apiKey = createApiKey(user, """ { "role": { @@ -1078,6 +1091,33 @@ public void testFailureStoreAccessWithApiKeys() throws Exception { expectUsingApiKey(apiKey, new Search(failureIndexName), failuresDocId); expectUsingApiKey(apiKey, new Search(dataIndexName), 403); expectUsingApiKey(apiKey, new Search("test1"), 403); + + upsertRole(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["*"], + "privileges": ["read"] + } + ] + } + """, role); + apiKey = createApiKey(user, """ + { + "role": { + "cluster": ["all"], + "indices": [ + {"names": ["test1"], "privileges": ["read_failure_store"]} + ] + } + }"""); + expectUsingApiKey(apiKey, new Search("test1::failures"), 403); + // funky but correct: limited-by role descriptors grant direct access to failure index, assigned to failure store + expectUsingApiKey(apiKey, new Search(failureIndexName), failuresDocId); + expectUsingApiKey(apiKey, new Search(dataIndexName), 403); + expectUsingApiKey(apiKey, new Search("test1"), 403); + } private static void expectThrows404(ThrowingRunnable runnable) { From fc5c4054b0d17cd9b9c73696a802a685d1ae8285 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 12 Mar 2025 14:54:01 +0100 Subject: [PATCH 104/131] Nit --- .../xpack/security/failurestore/FailureStoreSecurityRestIT.java | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java index 057f90f02bf08..4f908ebc7038a 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java @@ -1117,7 +1117,6 @@ public void testFailureStoreAccessWithApiKeys() throws Exception { expectUsingApiKey(apiKey, new Search(failureIndexName), failuresDocId); expectUsingApiKey(apiKey, new Search(dataIndexName), 403); expectUsingApiKey(apiKey, new Search("test1"), 403); - } private static void expectThrows404(ThrowingRunnable runnable) { From af90738834c44bdab68628f3897adbe3f719678f Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 12 Mar 2025 16:39:11 +0100 Subject: [PATCH 105/131] Support selectors in allowedActionsMatcher --- .../metadata/IndexNameExpressionResolver.java | 2 +- .../authz/permission/IndicesPermission.java | 2 - .../authz/permission/LimitedRoleTests.java | 95 +++++++++++++++++++ .../authz/IndicesAndAliasesResolver.java | 2 +- 4 files changed, 97 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java index 6ba51c8f0cc71..71afaba6659fb 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java @@ -1023,7 +1023,7 @@ public static String combineSelectorExpression(String baseExpression, @Nullable : (baseExpression + SelectorResolver.SELECTOR_SEPARATOR + selectorExpression); } - public static void assertDefaultOrDataSelector(String expression) { + public static void assertExpressionHasDefaultOrDataSelector(String expression) { if (Assertions.ENABLED) { var tuple = splitSelectorExpression(expression); assert tuple.v2() == null || IndexComponentSelector.DATA.getKey().equals(tuple.v2()) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 430b069ff60e3..00deeb4062844 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -377,8 +377,6 @@ public Automaton allowedActionsMatcher(String index) { Tuple tuple = IndexNameExpressionResolver.splitSelectorExpression(index); String indexName = tuple.v1(); IndexComponentSelector selector = IndexComponentSelector.getByKey(tuple.v2()); - // TODO is this correct? - assert selector == null || IndexComponentSelector.DATA.equals(selector) : "unexpected selector [" + selector + "]"; List automatonList = new ArrayList<>(); for (Group group : groups) { if (group.checkSelector(selector) && group.indexNameMatcher.test(indexName)) { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java index acbbb0ec60fff..9472f0da9e790 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java @@ -646,6 +646,101 @@ public void testAllowedActionsMatcher() { assertThat(rolePredicate.test(TransportBulkAction.NAME), is(false)); } + public void testAllowedActionsMatcherWithSelectors() { + Role fromRole = Role.builder(EMPTY_RESTRICTED_INDICES, "fromRole") + .add(IndexPrivilege.READ_FAILURE_STORE, "ind*") + .add(IndexPrivilege.READ, "ind*") + .add(IndexPrivilege.READ_FAILURE_STORE, "metric") + .add(IndexPrivilege.READ, "logs") + .build(); + Automaton fromRoleAutomaton = fromRole.allowedActionsMatcher("index1"); + Predicate fromRolePredicate = Automatons.predicate(fromRoleAutomaton); + assertThat(fromRolePredicate.test(TransportSearchAction.TYPE.name()), is(true)); + + fromRoleAutomaton = fromRole.allowedActionsMatcher("index1::failures"); + fromRolePredicate = Automatons.predicate(fromRoleAutomaton); + assertThat(fromRolePredicate.test(TransportSearchAction.TYPE.name()), is(true)); + + fromRoleAutomaton = fromRole.allowedActionsMatcher("metric"); + fromRolePredicate = Automatons.predicate(fromRoleAutomaton); + assertThat(fromRolePredicate.test(TransportSearchAction.TYPE.name()), is(false)); + + fromRoleAutomaton = fromRole.allowedActionsMatcher("metric::failures"); + fromRolePredicate = Automatons.predicate(fromRoleAutomaton); + assertThat(fromRolePredicate.test(TransportSearchAction.TYPE.name()), is(true)); + + fromRoleAutomaton = fromRole.allowedActionsMatcher("logs"); + fromRolePredicate = Automatons.predicate(fromRoleAutomaton); + assertThat(fromRolePredicate.test(TransportSearchAction.TYPE.name()), is(true)); + + fromRoleAutomaton = fromRole.allowedActionsMatcher("logs::failures"); + fromRolePredicate = Automatons.predicate(fromRoleAutomaton); + assertThat(fromRolePredicate.test(TransportSearchAction.TYPE.name()), is(false)); + + Role limitedByRole = Role.builder(EMPTY_RESTRICTED_INDICES, "limitedRole") + .add(IndexPrivilege.READ, "index1", "index2") + .add(IndexPrivilege.READ_FAILURE_STORE, "index3") + .build(); + Automaton limitedByRoleAutomaton = limitedByRole.allowedActionsMatcher("index1"); + Predicate limitedByRolePredicated = Automatons.predicate(limitedByRoleAutomaton); + assertThat(limitedByRolePredicated.test(TransportSearchAction.TYPE.name()), is(true)); + + limitedByRoleAutomaton = limitedByRole.allowedActionsMatcher("index1"); + limitedByRolePredicated = Automatons.predicate(limitedByRoleAutomaton); + assertThat(limitedByRolePredicated.test(TransportSearchAction.TYPE.name()), is(true)); + + limitedByRoleAutomaton = limitedByRole.allowedActionsMatcher("index1::failures"); + limitedByRolePredicated = Automatons.predicate(limitedByRoleAutomaton); + assertThat(limitedByRolePredicated.test(TransportSearchAction.TYPE.name()), is(false)); + + limitedByRoleAutomaton = limitedByRole.allowedActionsMatcher("index3"); + limitedByRolePredicated = Automatons.predicate(limitedByRoleAutomaton); + assertThat(limitedByRolePredicated.test(TransportSearchAction.TYPE.name()), is(false)); + + limitedByRoleAutomaton = limitedByRole.allowedActionsMatcher("index3::failures"); + limitedByRolePredicated = Automatons.predicate(limitedByRoleAutomaton); + assertThat(limitedByRolePredicated.test(TransportSearchAction.TYPE.name()), is(true)); + + Role role; + if (randomBoolean()) { + role = limitedByRole.limitedBy(fromRole); + } else { + role = fromRole.limitedBy(limitedByRole); + } + + Automaton roleAutomaton = role.allowedActionsMatcher("index1"); + Predicate rolePredicate = Automatons.predicate(roleAutomaton); + assertThat(rolePredicate.test(TransportSearchAction.TYPE.name()), is(true)); + + roleAutomaton = role.allowedActionsMatcher("index1::failures"); + rolePredicate = Automatons.predicate(roleAutomaton); + assertThat(rolePredicate.test(TransportSearchAction.TYPE.name()), is(false)); + + roleAutomaton = role.allowedActionsMatcher("index3"); + rolePredicate = Automatons.predicate(roleAutomaton); + assertThat(rolePredicate.test(TransportSearchAction.TYPE.name()), is(false)); + + roleAutomaton = role.allowedActionsMatcher("index3::failures"); + rolePredicate = Automatons.predicate(roleAutomaton); + assertThat(rolePredicate.test(TransportSearchAction.TYPE.name()), is(true)); + + roleAutomaton = role.allowedActionsMatcher("metric"); + rolePredicate = Automatons.predicate(roleAutomaton); + assertThat(rolePredicate.test(TransportSearchAction.TYPE.name()), is(false)); + + roleAutomaton = role.allowedActionsMatcher("metric::failures"); + rolePredicate = Automatons.predicate(roleAutomaton); + assertThat(rolePredicate.test(TransportSearchAction.TYPE.name()), is(false)); + + roleAutomaton = role.allowedActionsMatcher("logs"); + rolePredicate = Automatons.predicate(roleAutomaton); + assertThat(rolePredicate.test(TransportSearchAction.TYPE.name()), is(false)); + + roleAutomaton = role.allowedActionsMatcher("logs::failures"); + rolePredicate = Automatons.predicate(roleAutomaton); + assertThat(rolePredicate.test(TransportSearchAction.TYPE.name()), is(false)); + } + public void testCheckClusterPrivilege() { Role fromRole = Role.builder(EMPTY_RESTRICTED_INDICES, "a-role") .cluster(Collections.singleton("manage_security"), Collections.emptyList()) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java index d4409274e6bf0..df9f67db4e72f 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java @@ -509,7 +509,7 @@ private static List replaceWildcardsWithAuthorizedAliases(String[] alias } for (String aliasExpression : aliases) { - IndexNameExpressionResolver.assertDefaultOrDataSelector(aliasExpression); + IndexNameExpressionResolver.assertExpressionHasDefaultOrDataSelector(aliasExpression); boolean include = true; if (aliasExpression.charAt(0) == '-') { include = false; From 82959d74c1fa5a8ac31efc494840b84ad96905b8 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 12 Mar 2025 17:05:27 +0100 Subject: [PATCH 106/131] More unsupported places --- .../core/security/authz/permission/IndicesPermission.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 00deeb4062844..1e134844900e3 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -319,6 +319,7 @@ public boolean checkResourcePrivileges( combineIndexGroups && checkForIndexPatterns.stream().anyMatch(Automatons::isLuceneRegex) ); for (String forIndexPattern : checkForIndexPatterns) { + IndexNameExpressionResolver.assertExpressionHasDefaultOrDataSelector(forIndexPattern); Automaton checkIndexAutomaton = Automatons.patterns(forIndexPattern); if (false == allowRestrictedIndices && false == isConcreteRestrictedIndex(forIndexPattern)) { checkIndexAutomaton = Automatons.minusAndMinimize(checkIndexAutomaton, restrictedIndices.getAutomaton()); @@ -811,6 +812,11 @@ private Map indexGroupAutomatons(boolean combine) { // Map of privilege automaton object references (cached by IndexPrivilege::CACHE) Map allAutomatons = new HashMap<>(); for (Group group : groups) { + // TODO support failure store privileges + // we also check that the group does not support data access to avoid erroneously filtering out `all` privilege groups + if (group.checkSelector(IndexComponentSelector.FAILURES) && false == group.checkSelector(IndexComponentSelector.DATA)) { + continue; + } Automaton indexAutomaton = group.getIndexMatcherAutomaton(); allAutomatons.compute( group.privilege().getAutomaton(), From 3d66d16b5d8ca6d7e7c2ddb432e8bbb2b89e1d10 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 12 Mar 2025 17:19:33 +0100 Subject: [PATCH 107/131] HasPrivileges validation --- .../cluster/metadata/IndexNameExpressionResolver.java | 7 +++++++ .../core/security/authz/AuthorizationEngine.java | 11 +++++++++++ 2 files changed, 18 insertions(+) diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java index 71afaba6659fb..363a8f5c67713 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java @@ -1002,6 +1002,13 @@ public static boolean hasSelectorSuffix(String expression) { return expression.contains(SelectorResolver.SELECTOR_SEPARATOR); } + public static boolean hasSelector(String expression, IndexComponentSelector selector) { + if (expression == null) { + return false; + } + return expression.endsWith(SelectorResolver.SELECTOR_SEPARATOR + selector.getKey()); + } + /** * @return If the specified string is a selector expression then this method returns the base expression and its selector part. */ diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java index 6af704475b1f9..058ceed91cc95 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java @@ -11,8 +11,10 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.IndicesRequest; +import org.elasticsearch.action.support.IndexComponentSelector; import org.elasticsearch.action.support.SubscribableListener; import org.elasticsearch.cluster.metadata.IndexAbstraction; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.metadata.ProjectMetadata; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.BytesStreamOutput; @@ -368,6 +370,15 @@ public ActionRequestValidationException validate(ActionRequestValidationExceptio } if (index != null) { for (RoleDescriptor.IndicesPrivileges indexPrivilege : index) { + if (indexPrivilege.getIndices() != null + && Arrays.stream(indexPrivilege.getIndices()) + // best effort prevent users from attempting to check failure selectors + .anyMatch(idx -> IndexNameExpressionResolver.hasSelector(idx, IndexComponentSelector.FAILURES))) { + validationException = addValidationError( + "non-data selectors are not supported in index patterns", + validationException + ); + } if (indexPrivilege.getPrivileges() != null && Arrays.stream(indexPrivilege.getPrivileges()) .anyMatch(p -> "read_failure_store".equals(p) || "manage_failure_store".equals(p))) { From 8020982d5f68a432706adab8c6b53b7e43fb5411 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 12 Mar 2025 17:24:47 +0100 Subject: [PATCH 108/131] TODO --- .../xpack/core/security/authz/AuthorizationEngine.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java index 058ceed91cc95..6358fd0c3eeba 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java @@ -375,7 +375,8 @@ public ActionRequestValidationException validate(ActionRequestValidationExceptio // best effort prevent users from attempting to check failure selectors .anyMatch(idx -> IndexNameExpressionResolver.hasSelector(idx, IndexComponentSelector.FAILURES))) { validationException = addValidationError( - "non-data selectors are not supported in index patterns", + // TODO adjust message once HasPrivileges check support checking failure store privileges + "failures selectors are not supported in index patterns", validationException ); } From 063954b9148de38cb666ee859e194f59dd0b138b Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 12 Mar 2025 17:33:50 +0100 Subject: [PATCH 109/131] Nits --- .../cluster/metadata/IndexNameExpressionResolver.java | 3 ++- .../xpack/core/security/authz/AuthorizationEngine.java | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java index 363a8f5c67713..8a8e15f285f74 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java @@ -1002,7 +1002,8 @@ public static boolean hasSelectorSuffix(String expression) { return expression.contains(SelectorResolver.SELECTOR_SEPARATOR); } - public static boolean hasSelector(String expression, IndexComponentSelector selector) { + public static boolean hasSelector(@Nullable String expression, IndexComponentSelector selector) { + Objects.requireNonNull(selector, "null selectors not supported"); if (expression == null) { return false; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java index 6358fd0c3eeba..0c993aaab122b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java @@ -375,8 +375,8 @@ public ActionRequestValidationException validate(ActionRequestValidationExceptio // best effort prevent users from attempting to check failure selectors .anyMatch(idx -> IndexNameExpressionResolver.hasSelector(idx, IndexComponentSelector.FAILURES))) { validationException = addValidationError( - // TODO adjust message once HasPrivileges check support checking failure store privileges - "failures selectors are not supported in index patterns", + // TODO adjust message once HasPrivileges check supports checking failure store privileges + "failures selector is not supported in index patterns", validationException ); } From 4590f0410b75ba7d489e5ba6774041ef0c0329e0 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Thu, 13 Mar 2025 10:51:38 +0100 Subject: [PATCH 110/131] Basic FLS tests --- .../authz/permission/IndicesPermission.java | 4 +- .../FailureStoreSecurityRestIT.java | 242 +++++++++++++----- 2 files changed, 175 insertions(+), 71 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 1e134844900e3..dc22a79b83c06 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -522,7 +522,7 @@ public boolean canHaveBackingIndices() { return indexAbstraction != null && indexAbstraction.getType() != IndexAbstraction.Type.CONCRETE_INDEX; } - public String nameCombinedWithSelector() { + public String nameWithSelector() { String combined = IndexNameExpressionResolver.combineSelector(name, selector); assert false != IndexComponentSelector.FAILURES.equals(selector) || name.equals(combined) : "Only failures selectors should result in explicit selectors suffix"; @@ -559,7 +559,7 @@ public IndicesAccessControl authorize( final IndexResource resource = new IndexResource(indexOrAlias, lookup.get(indexOrAlias), selector); // We can't use resource.name here because we may be accessing a data stream _and_ its failure store, // where the selector-free name is the same for both and thus ambiguous. - resources.put(resource.nameCombinedWithSelector(), resource); + resources.put(resource.nameWithSelector(), resource); totalResourceCount += resource.size(lookup); } diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java index 4f908ebc7038a..5ac851f88f532 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java @@ -21,6 +21,7 @@ import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.support.XContentMapValues; import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.Tuple; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchResponseUtils; import org.elasticsearch.test.TestSecurityClient; @@ -31,15 +32,19 @@ import org.elasticsearch.xcontent.json.JsonXContent; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.SecurityOnTrialLicenseRestTestCase; +import org.junit.Before; import org.junit.ClassRule; import java.io.IOException; +import java.io.UncheckedIOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; @@ -80,6 +85,17 @@ protected Settings restAdminSettings() { private static final String MANAGE_FAILURE_STORE_ACCESS = "manage_failure_store_access"; private static final SecureString PASSWORD = new SecureString("elastic-password"); + @Before + public void setup() throws IOException { + apiKeys = new HashMap<>(); + createUser(WRITE_ACCESS, PASSWORD, WRITE_ACCESS); + upsertRole(Strings.format(""" + { + "cluster": ["all"], + "indices": [{"names": ["test*"], "privileges": ["write", "auto_configure"]}] + }"""), WRITE_ACCESS); + } + public void testGetUserPrivileges() throws IOException { Request userRequest = new Request("PUT", "/_security/user/user"); userRequest.setJsonEntity(""" @@ -171,16 +187,6 @@ public void testGetUserPrivileges() throws IOException { } public void testRoleWithSelectorInIndexPattern() throws Exception { - apiKeys = new HashMap<>(); - - createUser(WRITE_ACCESS, PASSWORD, WRITE_ACCESS); - upsertRole(Strings.format(""" - { - "cluster": ["all"], - "indices": [{"names": ["test*"], "privileges": ["write", "auto_configure"]}] - }"""), WRITE_ACCESS); - createAndStoreApiKey(WRITE_ACCESS, null); - createTemplates(); populateDataStream(); @@ -243,8 +249,6 @@ public void testRoleWithSelectorInIndexPattern() throws Exception { @SuppressWarnings("unchecked") public void testFailureStoreAccess() throws Exception { - apiKeys = new HashMap<>(); - createUser(DATA_ACCESS, PASSWORD, DATA_ACCESS); upsertRole(Strings.format(""" { @@ -317,12 +321,6 @@ public void testFailureStoreAccess() throws Exception { """); } - createUser(WRITE_ACCESS, PASSWORD, WRITE_ACCESS); - upsertRole(Strings.format(""" - { - "cluster": ["all"], - "indices": [{"names": ["test*"], "privileges": ["write", "auto_configure"]}] - }"""), WRITE_ACCESS); createAndStoreApiKey(WRITE_ACCESS, randomBoolean() ? null : """ { "role": { @@ -363,27 +361,15 @@ public void testFailureStoreAccess() throws Exception { """); createTemplates(); - List docIds = populateDataStream(); assertThat(docIds.size(), equalTo(2)); assertThat(docIds, hasItem("1")); String dataDocId = "1"; String failuresDocId = docIds.stream().filter(id -> false == id.equals(dataDocId)).findFirst().get(); - Request dataStream = new Request("GET", "/_data_stream/test1"); - Response response = adminClient().performRequest(dataStream); - Map dataStreams = entityAsMap(response); - assertEquals(Collections.singletonList("test1"), XContentMapValues.extractValue("data_streams.name", dataStreams)); - List dataIndexNames = (List) XContentMapValues.extractValue("data_streams.indices.index_name", dataStreams); - assertThat(dataIndexNames.size(), equalTo(1)); - List failureIndexNames = (List) XContentMapValues.extractValue( - "data_streams.failure_store.indices.index_name", - dataStreams - ); - assertThat(failureIndexNames.size(), equalTo(1)); - - String dataIndexName = dataIndexNames.get(0); - String failureIndexName = failureIndexNames.get(0); + Tuple backingIndices = getSingleDataAndFailureIndices("test1"); + String dataIndexName = backingIndices.v1(); + String failureIndexName = backingIndices.v2(); Request aliasRequest = new Request("POST", "/_aliases"); aliasRequest.setJsonEntity(""" @@ -954,35 +940,25 @@ public void testFailureStoreAccess() throws Exception { // write operations below // user with manage access to data stream does NOT get direct access to failure index - expectThrows403(() -> deleteIndex(MANAGE_ACCESS, failureIndexName)); + expectThrows(() -> deleteIndex(MANAGE_ACCESS, failureIndexName), 403); expectThrows(() -> deleteIndex(MANAGE_ACCESS, dataIndexName), 400); // manage_failure_store user COULD delete failure index (not valid because it's a write index, but allow security-wise) - expectThrows403(() -> deleteIndex(MANAGE_FAILURE_STORE_ACCESS, dataIndexName)); + expectThrows(() -> deleteIndex(MANAGE_FAILURE_STORE_ACCESS, dataIndexName), 403); expectThrows(() -> deleteIndex(MANAGE_FAILURE_STORE_ACCESS, failureIndexName), 400); - expectThrows403(() -> deleteDataStream(MANAGE_FAILURE_STORE_ACCESS, dataIndexName)); + expectThrows(() -> deleteDataStream(MANAGE_FAILURE_STORE_ACCESS, dataIndexName), 403); expectThrows(() -> deleteDataStream(MANAGE_FAILURE_STORE_ACCESS, "test1"), 403); expectThrows(() -> deleteDataStream(MANAGE_FAILURE_STORE_ACCESS, "test1::failures"), 403); // manage user can delete data stream deleteDataStream(MANAGE_ACCESS, "test1"); - expectThrows404(() -> performRequest(BOTH_ACCESS, new Request("GET", "/test1/_search"))); - expectThrows404(() -> adminClient().performRequest(new Request("GET", "/" + dataIndexName + "/_search"))); - expectThrows404(() -> performRequest(BOTH_ACCESS, new Request("GET", "/test1::failures/_search"))); - expectThrows404(() -> adminClient().performRequest(new Request("GET", "/" + failureIndexName + "/_search"))); + expectThrows(() -> performRequest(BOTH_ACCESS, new Request("GET", "/test1/_search")), 404); + expectThrows(() -> adminClient().performRequest(new Request("GET", "/" + dataIndexName + "/_search")), 404); + expectThrows(() -> performRequest(BOTH_ACCESS, new Request("GET", "/test1::failures/_search")), 404); + expectThrows(() -> adminClient().performRequest(new Request("GET", "/" + failureIndexName + "/_search")), 404); } - @SuppressWarnings("unchecked") public void testFailureStoreAccessWithApiKeys() throws Exception { - apiKeys = new HashMap<>(); - - createUser(WRITE_ACCESS, PASSWORD, WRITE_ACCESS); - upsertRole(Strings.format(""" - { - "cluster": ["all"], - "indices": [{"names": ["test*"], "privileges": ["write", "auto_configure"]}] - }"""), WRITE_ACCESS); - createTemplates(); List docIds = populateDataStream(); @@ -991,20 +967,9 @@ public void testFailureStoreAccessWithApiKeys() throws Exception { String dataDocId = "1"; String failuresDocId = docIds.stream().filter(id -> false == id.equals(dataDocId)).findFirst().get(); - Request dataStream = new Request("GET", "/_data_stream/test1"); - Response response = adminClient().performRequest(dataStream); - Map dataStreams = entityAsMap(response); - assertEquals(Collections.singletonList("test1"), XContentMapValues.extractValue("data_streams.name", dataStreams)); - List dataIndexNames = (List) XContentMapValues.extractValue("data_streams.indices.index_name", dataStreams); - assertThat(dataIndexNames.size(), equalTo(1)); - List failureIndexNames = (List) XContentMapValues.extractValue( - "data_streams.failure_store.indices.index_name", - dataStreams - ); - assertThat(failureIndexNames.size(), equalTo(1)); - - String dataIndexName = dataIndexNames.get(0); - String failureIndexName = failureIndexNames.get(0); + Tuple backingIndices = getSingleDataAndFailureIndices("test1"); + String dataIndexName = backingIndices.v1(); + String failureIndexName = backingIndices.v2(); var user = "user"; var role = "role"; @@ -1119,12 +1084,106 @@ public void testFailureStoreAccessWithApiKeys() throws Exception { expectUsingApiKey(apiKey, new Search("test1"), 403); } - private static void expectThrows404(ThrowingRunnable runnable) { - expectThrows(runnable, 404); - } + public void testFlsDls() throws IOException { + createTemplates(); + populateDataStream(); + + Tuple backingIndices = getSingleDataAndFailureIndices("test1"); + String dataIndexName = backingIndices.v1(); + String failureIndexName = backingIndices.v2(); + + String user = "user"; + String role = "role"; + createUser(user, PASSWORD, role); + upsertRole(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["test*"], + "privileges": ["read", "read_failure_store"], + "field_security": { + "grant": ["@timestamp", "age"] + } + } + ] + }""", role); + + // FLS applies to regular data stream + assertSearchResponseContainsExpectedIndicesAndFields( + performRequest(user, new Search("test1").toSearchRequest()), + Map.of(dataIndexName, Set.of("@timestamp", "age")) + ); + + // FLS sort of applies to failure store + // TODO this will change with FLS handling + assertSearchResponseContainsExpectedIndicesAndFields( + performRequest(user, new Search("test1::failures").toSearchRequest()), + Map.of(failureIndexName, Set.of("@timestamp")) + ); + + upsertRole(Strings.format(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["%s"], + "privileges": ["read"], + "field_security": { + "grant": ["@timestamp", "age"] + } + }, + { + "names": ["test*"], + "privileges": ["read_failure_store"], + "field_security": { + "grant": ["@timestamp", "age"] + } + } + ] + }""", randomFrom("test*", "test1")), role); + + // FLS applies to regular data stream + assertSearchResponseContainsExpectedIndicesAndFields( + performRequest(user, new Search("test1").toSearchRequest()), + Map.of(dataIndexName, Set.of("@timestamp", "age")) + ); + + // FLS sort of applies to failure store + // TODO this will change with FLS handling + assertSearchResponseContainsExpectedIndicesAndFields( + performRequest(user, new Search("test1::failures").toSearchRequest()), + Map.of(failureIndexName, Set.of("@timestamp")) + ); + + upsertRole(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["test*"], + "privileges": ["read"], + "field_security": { + "grant": ["@timestamp", "age"] + } + }, + { + "names": ["test*"], + "privileges": ["read_failure_store"] + } + ] + }""", role); + + // since there is a section without FLS, no FLS applies + assertSearchResponseContainsExpectedIndicesAndFields( + performRequest(user, new Search("test1").toSearchRequest()), + Map.of(dataIndexName, Set.of("@timestamp", "age", "name", "email")) + ); - private static void expectThrows403(ThrowingRunnable runnable) { - expectThrows(runnable, 403); + assertSearchResponseContainsExpectedIndicesAndFields( + performRequest(user, new Search("test1::failures").toSearchRequest()), + Map.of(failureIndexName, Set.of("@timestamp", "document", "error")) + ); } private static void expectThrows(ThrowingRunnable runnable, int statusCode) { @@ -1403,4 +1462,49 @@ protected Request roleRequest(String roleDescriptor, String roleName) { } return createRoleRequest; } + + protected void assertSearchResponseContainsExpectedIndicesAndFields( + Response searchResponse, + Map> expectedIndicesAndFields + ) { + try { + assertOK(searchResponse); + var response = SearchResponseUtils.responseAsSearchResponse(searchResponse); + try { + final var searchResult = Arrays.stream(response.getHits().getHits()) + .collect(Collectors.toMap(SearchHit::getIndex, SearchHit::getSourceAsMap)); + + assertThat(searchResult.keySet(), equalTo(expectedIndicesAndFields.keySet())); + for (String index : expectedIndicesAndFields.keySet()) { + Set expectedFields = expectedIndicesAndFields.get(index); + assertThat(searchResult.get(index).keySet(), equalTo(expectedFields)); + } + } finally { + response.decRef(); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @SuppressWarnings("unchecked") + private Tuple, List> getDataAndFailureIndices(String dataStreamName) throws IOException { + Request dataStream = new Request("GET", "/_data_stream/" + dataStreamName); + Response response = adminClient().performRequest(dataStream); + Map dataStreams = entityAsMap(response); + assertEquals(Collections.singletonList("test1"), XContentMapValues.extractValue("data_streams.name", dataStreams)); + List dataIndexNames = (List) XContentMapValues.extractValue("data_streams.indices.index_name", dataStreams); + List failureIndexNames = (List) XContentMapValues.extractValue( + "data_streams.failure_store.indices.index_name", + dataStreams + ); + return new Tuple<>(dataIndexNames, failureIndexNames); + } + + private Tuple getSingleDataAndFailureIndices(String dataStreamName) throws IOException { + Tuple, List> indices = getDataAndFailureIndices(dataStreamName); + assertThat(indices.v1().size(), equalTo(1)); + assertThat(indices.v2().size(), equalTo(1)); + return new Tuple<>(indices.v1().get(0), indices.v2().get(0)); + } } From d78cc3e8830cf5769ee4a6e1a4d354b2f26e63a2 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Thu, 13 Mar 2025 11:46:04 +0100 Subject: [PATCH 111/131] DLS --- .../FailureStoreSecurityRestIT.java | 101 +++++++++++++++--- 1 file changed, 87 insertions(+), 14 deletions(-) diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java index 5ac851f88f532..67f76db8ed178 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java @@ -97,14 +97,7 @@ public void setup() throws IOException { } public void testGetUserPrivileges() throws IOException { - Request userRequest = new Request("PUT", "/_security/user/user"); - userRequest.setJsonEntity(""" - { - "password": "x-pack-test-password", - "roles": ["role"] - } - """); - assertOK(adminClient().performRequest(userRequest)); + createUser("user", PASSWORD, "role"); upsertRole(""" { @@ -184,6 +177,40 @@ public void testGetUserPrivileges() throws IOException { "applications": [], "run_as": [] }"""); + + upsertRole(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["*"], + "privileges": ["read", "read_failure_store"] + }, + { + "names": ["*"], + "privileges": ["write", "manage_failure_store"] + } + ] + } + """, "role"); + expectUserPrivilegesResponse(""" + { + "cluster": ["all"], + "global": [], + "indices": [ + { + "names": ["*"], + "privileges": ["read", "write"], + "allow_restricted_indices": false + }, + { + "names": ["*"], + "privileges": ["manage_failure_store", "read_failure_store"], + "allow_restricted_indices": false + }], + "applications": [], + "run_as": [] + }"""); } public void testRoleWithSelectorInIndexPattern() throws Exception { @@ -1084,7 +1111,7 @@ public void testFailureStoreAccessWithApiKeys() throws Exception { expectUsingApiKey(apiKey, new Search("test1"), 403); } - public void testFlsDls() throws IOException { + public void testDlsFls() throws Exception { createTemplates(); populateDataStream(); @@ -1184,6 +1211,56 @@ public void testFlsDls() throws IOException { performRequest(user, new Search("test1::failures").toSearchRequest()), Map.of(failureIndexName, Set.of("@timestamp", "document", "error")) ); + + // DLS + String dataIndexDocId = "1"; + upsertRole(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["test*"], + "privileges": ["read", "read_failure_store"], + "query":{"term":{"name":{"value":"not-jack"}}} + } + ] + }""", role); + // DLS applies and no docs match the query + expect(user, new Search("test1")); + expect(user, new Search("test1::failures")); + + upsertRole(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["test*"], + "privileges": ["read", "read_failure_store"], + "query":{"term":{"name":{"value":"jack"}}} + } + ] + }""", role); + // DLS applies and doc matches the query + expect(user, new Search("test1"), dataIndexDocId); + expect(user, new Search("test1::failures")); + + upsertRole(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["test*"], + "privileges": ["read"], + "query":{"term":{"name":{"value":"not-jack"}}} + }, + { + "names": ["test*"], + "privileges": ["read_failure_store"] + } + ] + }""", role); + // DLS does not apply because there is a section without DLS + expect(user, new Search("test1"), dataIndexDocId); } private static void expectThrows(ThrowingRunnable runnable, int statusCode) { @@ -1390,11 +1467,7 @@ private static Response performRequestWithApiKey(String apiKey, Request request) private static void expectUserPrivilegesResponse(String userPrivilegesResponse) throws IOException { Request request = new Request("GET", "/_security/user/_privileges"); - request.setOptions( - request.getOptions() - .toBuilder() - .addHeader("Authorization", basicAuthHeaderValue("user", new SecureString("x-pack-test-password".toCharArray()))) - ); + request.setOptions(request.getOptions().toBuilder().addHeader("Authorization", basicAuthHeaderValue("user", PASSWORD))); Response response = client().performRequest(request); assertOK(response); assertThat(responseAsMap(response), equalTo(mapFromJson(userPrivilegesResponse))); From 3b6e0305e84d2025baa1125e18963712e23f40a0 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Thu, 13 Mar 2025 12:57:27 +0100 Subject: [PATCH 112/131] More assertions --- .../xpack/downsample/TransportDownsampleAction.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/downsample/src/main/java/org/elasticsearch/xpack/downsample/TransportDownsampleAction.java b/x-pack/plugin/downsample/src/main/java/org/elasticsearch/xpack/downsample/TransportDownsampleAction.java index 8ea64ab38a7a5..9fe4ebe22e79d 100644 --- a/x-pack/plugin/downsample/src/main/java/org/elasticsearch/xpack/downsample/TransportDownsampleAction.java +++ b/x-pack/plugin/downsample/src/main/java/org/elasticsearch/xpack/downsample/TransportDownsampleAction.java @@ -34,6 +34,7 @@ import org.elasticsearch.cluster.block.ClusterBlockLevel; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.metadata.IndexMetadata.DownsampleTaskStatus; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.metadata.MetadataCreateIndexService; import org.elasticsearch.cluster.metadata.ProjectId; import org.elasticsearch.cluster.metadata.ProjectMetadata; @@ -209,7 +210,8 @@ protected void masterOperation( ) { long startTime = client.threadPool().relativeTimeInMillis(); String sourceIndexName = request.getSourceIndex(); - + IndexNameExpressionResolver.assertExpressionHasDefaultOrDataSelector(sourceIndexName); + IndexNameExpressionResolver.assertExpressionHasDefaultOrDataSelector(request.getTargetIndex()); final IndicesAccessControl indicesAccessControl = threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY); if (indicesAccessControl != null) { final IndicesAccessControl.IndexAccessControl indexPermissions = indicesAccessControl.getIndexPermissions(sourceIndexName); From 0b0948319beed0dfed230c55f5b0dd0ed0950750 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Thu, 13 Mar 2025 15:51:04 +0100 Subject: [PATCH 113/131] Tweak interface --- .../example/CustomAuthorizationEngine.java | 9 +- .../metadata/IndexAbstractionResolver.java | 9 +- .../security/authz/AuthorizationEngine.java | 4 +- .../authz/IndicesAndAliasesResolver.java | 12 +-- .../xpack/security/authz/RBACEngine.java | 14 ++- .../authz/AuthorizedIndicesTests.java | 86 ++++++++++--------- .../xpack/security/authz/RBACEngineTests.java | 11 +-- 7 files changed, 76 insertions(+), 69 deletions(-) diff --git a/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java b/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java index e9ae9fc787da5..309435a8e7dab 100644 --- a/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java +++ b/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java @@ -10,6 +10,7 @@ package org.elasticsearch.example; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.IndexComponentSelector; import org.elasticsearch.action.support.SubscribableListener; import org.elasticsearch.cluster.metadata.IndexAbstraction; import org.elasticsearch.cluster.metadata.ProjectMetadata; @@ -119,19 +120,19 @@ public void loadAuthorizedIndices( ) { if (isSuperuser(requestInfo.getAuthentication().getEffectiveSubject().getUser())) { listener.onResponse(new AuthorizedIndices() { - public Set all(@Nullable String selector) { + public Set all(IndexComponentSelector selector) { return () -> indicesLookup.keySet(); } - public boolean check(String name, @Nullable String selector) { + public boolean check(String name, IndexComponentSelector selector) { return indicesLookup.containsKey(name); } }); } else { listener.onResponse(new AuthorizedIndices() { - public Set all(@Nullable String selector) { + public Set all(IndexComponentSelector selector) { return () -> Set.of(); } - public boolean check(String name, @Nullable String selector) { + public boolean check(String name, IndexComponentSelector selector) { return false; } }); diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java index e279bf3986425..97b649aac1497 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java @@ -37,8 +37,8 @@ public List resolveIndexAbstractions( Iterable indices, IndicesOptions indicesOptions, ProjectMetadata projectMetadata, - Function> allAuthorizedAndAvailableBySelector, - BiPredicate isAuthorized, + Function> allAuthorizedAndAvailableBySelector, + BiPredicate isAuthorized, boolean includeDataStreams ) { List finalIndices = new ArrayList<>(); @@ -63,6 +63,7 @@ public List resolveIndexAbstractions( + "]" ); } + IndexComponentSelector selector = IndexComponentSelector.getByKeyOrThrow(selectorString); indexAbstraction = expressionAndSelector.v1(); // we always need to check for date math expressions @@ -71,7 +72,7 @@ public List resolveIndexAbstractions( if (indicesOptions.expandWildcardExpressions() && Regex.isSimpleMatchPattern(indexAbstraction)) { wildcardSeen = true; Set resolvedIndices = new HashSet<>(); - for (String authorizedIndex : allAuthorizedAndAvailableBySelector.apply(selectorString)) { + for (String authorizedIndex : allAuthorizedAndAvailableBySelector.apply(selector)) { if (Regex.simpleMatch(indexAbstraction, authorizedIndex) && isIndexVisible( indexAbstraction, @@ -102,7 +103,7 @@ && isIndexVisible( resolveSelectorsAndCollect(indexAbstraction, selectorString, indicesOptions, resolvedIndices, projectMetadata); if (minus) { finalIndices.removeAll(resolvedIndices); - } else if (indicesOptions.ignoreUnavailable() == false || isAuthorized.test(indexAbstraction, selectorString)) { + } else if (indicesOptions.ignoreUnavailable() == false || isAuthorized.test(indexAbstraction, selector)) { // Unauthorized names are considered unavailable, so if `ignoreUnavailable` is `true` they should be silently // discarded from the `finalIndices` list. Other "ways of unavailable" must be handled by the action // handler, see: https://github.com/elastic/elasticsearch/issues/90215 diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java index 0c993aaab122b..cc173bee44fe8 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java @@ -293,12 +293,12 @@ interface AuthorizedIndices { * at a fixed point in time (for a single cluster state view). * The result is cached and subsequent calls to this method are idempotent. */ - Set all(@Nullable String selector); + Set all(IndexComponentSelector selector); /** * Checks if an index-like resource name is authorized, for an action by a user. The resource might or might not exist. */ - boolean check(String name, @Nullable String selector); + boolean check(String name, IndexComponentSelector selector); } /** diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java index df9f67db4e72f..0636b81d00162 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java @@ -322,7 +322,9 @@ ResolvedIndices resolveIndicesAndAliases( ); } if (indicesOptions.expandWildcardExpressions()) { - for (String authorizedIndex : authorizedIndices.all(allIndicesPatternSelector)) { + for (String authorizedIndex : authorizedIndices.all( + IndexComponentSelector.getByKeyOrThrow(allIndicesPatternSelector) + )) { if (IndexAbstractionResolver.isIndexVisible( "*", allIndicesPatternSelector, @@ -432,7 +434,7 @@ ResolvedIndices resolveIndicesAndAliases( */ static String getPutMappingIndexOrAlias( PutMappingRequest request, - BiPredicate isAuthorized, + BiPredicate isAuthorized, ProjectMetadata projectMetadata ) { final String concreteIndexName = request.getConcreteIndex().getName(); @@ -451,7 +453,7 @@ static String getPutMappingIndexOrAlias( + "], but a concrete index is expected" ); // we know this is implicit data access (as opposed to another selector) so the default selector check is correct - } else if (isAuthorized.test(concreteIndexName, null)) { + } else if (isAuthorized.test(concreteIndexName, IndexComponentSelector.DATA)) { // user is authorized to put mappings for this index resolvedAliasOrIndex = concreteIndexName; } else { @@ -462,7 +464,7 @@ static String getPutMappingIndexOrAlias( if (aliasMetadata != null) { Optional foundAlias = aliasMetadata.stream().map(AliasMetadata::alias).filter(aliasName -> { // we know this is implicit data access (as opposed to another selector) so the default selector check is correct - if (false == isAuthorized.test(aliasName, null)) { + if (false == isAuthorized.test(aliasName, IndexComponentSelector.DATA)) { return false; } IndexAbstraction alias = projectMetadata.getIndicesLookup().get(aliasName); @@ -490,7 +492,7 @@ private static List loadAuthorizedAliases( ) { List authorizedAliases = new ArrayList<>(); SortedMap existingAliases = projectMetadata.getIndicesLookup(); - for (String authorizedIndex : authorizedIndices.all(null)) { + for (String authorizedIndex : authorizedIndices.all(IndexComponentSelector.DATA)) { IndexAbstraction indexAbstraction = existingAliases.get(authorizedIndex); if (indexAbstraction != null && indexAbstraction.getType() == IndexAbstraction.Type.ALIAS) { authorizedAliases.add(authorizedIndex); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java index 73a7979d61ddb..591db4a7f347f 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java @@ -43,7 +43,6 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.CachedSupplier; import org.elasticsearch.common.util.set.Sets; -import org.elasticsearch.core.Nullable; import org.elasticsearch.index.Index; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.transport.TransportActionProxy; @@ -929,9 +928,8 @@ static AuthorizedIndices resolveAuthorizedIndicesFromRole( } } return indicesAndAliases; - }, (name, selectorString) -> { + }, (name, selector) -> { final IndexAbstraction indexAbstraction = lookup.get(name); - final IndexComponentSelector selector = IndexComponentSelector.getByKeyOrThrow(selectorString); if (indexAbstraction == null) { // test access (by name) to a resource that does not currently exist // the action handler must handle the case of accessing resources that do not exist @@ -1077,12 +1075,12 @@ static final class AuthorizedIndices implements AuthorizationEngine.AuthorizedIn private final CachedSupplier> authorizedAndAvailableSupplier; private final CachedSupplier> failureStoreAuthorizedAndAvailableSupplier; - private final BiPredicate isAuthorizedPredicate; + private final BiPredicate isAuthorizedPredicate; AuthorizedIndices( Supplier> authorizedAndAvailableSupplier, Supplier> failureStoreAuthorizedAndAvailableSupplier, - BiPredicate isAuthorizedPredicate + BiPredicate isAuthorizedPredicate ) { this.authorizedAndAvailableSupplier = CachedSupplier.wrap(authorizedAndAvailableSupplier); this.failureStoreAuthorizedAndAvailableSupplier = CachedSupplier.wrap(failureStoreAuthorizedAndAvailableSupplier); @@ -1090,14 +1088,14 @@ static final class AuthorizedIndices implements AuthorizationEngine.AuthorizedIn } @Override - public Set all(@Nullable String selector) { - return IndexComponentSelector.FAILURES.equals(IndexComponentSelector.getByKeyOrThrow(selector)) + public Set all(IndexComponentSelector selector) { + return IndexComponentSelector.FAILURES.equals(selector) ? failureStoreAuthorizedAndAvailableSupplier.get() : authorizedAndAvailableSupplier.get(); } @Override - public boolean check(String name, @Nullable String selector) { + public boolean check(String name, IndexComponentSelector selector) { return isAuthorizedPredicate.test(name, selector); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java index b379881c506b7..f4776637abee6 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java @@ -8,6 +8,7 @@ import org.elasticsearch.action.admin.indices.resolve.ResolveIndexAction; import org.elasticsearch.action.search.TransportSearchAction; +import org.elasticsearch.action.support.IndexComponentSelector; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.cluster.metadata.AliasMetadata; import org.elasticsearch.cluster.metadata.DataStream; @@ -55,7 +56,7 @@ public void testAuthorizedIndicesUserWithoutRoles() { Metadata.EMPTY_METADATA.getProject().getIndicesLookup(), () -> ignore -> {} ); - assertTrue(authorizedIndices.all(null).isEmpty()); + assertTrue(authorizedIndices.all(IndexComponentSelector.DATA).isEmpty()); } public void testAuthorizedIndicesUserWithSomeRoles() { @@ -115,14 +116,14 @@ public void testAuthorizedIndicesUserWithSomeRoles() { metadata.getProject().getIndicesLookup(), () -> ignore -> {} ); - assertThat(authorizedIndices.all(null), containsInAnyOrder("a1", "a2", "aaaaaa", "b", "ab")); - assertThat(authorizedIndices.all(null), not(contains("bbbbb"))); - assertThat(authorizedIndices.check("bbbbb", null), is(false)); - assertThat(authorizedIndices.all(null), not(contains("ba"))); - assertThat(authorizedIndices.check("ba", null), is(false)); - assertThat(authorizedIndices.all(null), not(contains(internalSecurityIndex))); - assertThat(authorizedIndices.check(internalSecurityIndex, null), is(false)); - assertThat(authorizedIndices.all(null), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), containsInAnyOrder("a1", "a2", "aaaaaa", "b", "ab")); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains("bbbbb"))); + assertThat(authorizedIndices.check("bbbbb", IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains("ba"))); + assertThat(authorizedIndices.check("ba", IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains(internalSecurityIndex))); + assertThat(authorizedIndices.check(internalSecurityIndex, IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, null), is(false)); } @@ -134,7 +135,7 @@ public void testAuthorizedIndicesUserWithSomeRolesEmptyMetadata() { Metadata.EMPTY_METADATA.getProject().getIndicesLookup(), () -> ignore -> {} ); - assertTrue(authorizedIndices.all(null).isEmpty()); + assertTrue(authorizedIndices.all(IndexComponentSelector.DATA).isEmpty()); } public void testSecurityIndicesAreRemovedFromRegularUser() { @@ -145,7 +146,7 @@ public void testSecurityIndicesAreRemovedFromRegularUser() { Metadata.EMPTY_METADATA.getProject().getIndicesLookup(), () -> ignore -> {} ); - assertTrue(authorizedIndices.all(null).isEmpty()); + assertTrue(authorizedIndices.all(IndexComponentSelector.DATA).isEmpty()); } public void testSecurityIndicesAreRestrictedForDefaultRole() { @@ -177,13 +178,13 @@ public void testSecurityIndicesAreRestrictedForDefaultRole() { metadata.getProject().getIndicesLookup(), () -> ignore -> {} ); - assertThat(authorizedIndices.all(null), containsInAnyOrder("an-index", "another-index")); - assertThat(authorizedIndices.check("an-index", null), is(true)); - assertThat(authorizedIndices.check("another-index", null), is(true)); - assertThat(authorizedIndices.all(null), not(contains(internalSecurityIndex))); - assertThat(authorizedIndices.check(internalSecurityIndex, null), is(false)); - assertThat(authorizedIndices.all(null), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); - assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, null), is(false)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), containsInAnyOrder("an-index", "another-index")); + assertThat(authorizedIndices.check("an-index", IndexComponentSelector.DATA), is(true)); + assertThat(authorizedIndices.check("another-index", IndexComponentSelector.DATA), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains(internalSecurityIndex))); + assertThat(authorizedIndices.check(internalSecurityIndex, IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); + assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, IndexComponentSelector.DATA), is(false)); } public void testSecurityIndicesAreNotRemovedFromUnrestrictedRole() { @@ -216,7 +217,7 @@ public void testSecurityIndicesAreNotRemovedFromUnrestrictedRole() { () -> ignore -> {} ); assertThat( - authorizedIndices.all(null), + authorizedIndices.all(IndexComponentSelector.DATA), containsInAnyOrder("an-index", "another-index", SecuritySystemIndices.SECURITY_MAIN_ALIAS, internalSecurityIndex) ); @@ -227,7 +228,7 @@ public void testSecurityIndicesAreNotRemovedFromUnrestrictedRole() { () -> ignore -> {} ); assertThat( - authorizedIndicesSuperUser.all(null), + authorizedIndicesSuperUser.all(IndexComponentSelector.DATA), containsInAnyOrder("an-index", "another-index", SecuritySystemIndices.SECURITY_MAIN_ALIAS, internalSecurityIndex) ); } @@ -297,22 +298,22 @@ public void testDataStreamsAreNotIncludedInAuthorizedIndices() { metadata.getProject().getIndicesLookup(), () -> ignore -> {} ); - assertThat(authorizedIndices.all(null), containsInAnyOrder("a1", "a2", "aaaaaa", "b", "ab")); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), containsInAnyOrder("a1", "a2", "aaaaaa", "b", "ab")); for (String resource : List.of("a1", "a2", "aaaaaa", "b", "ab")) { - assertThat(authorizedIndices.check(resource, null), is(true)); + assertThat(authorizedIndices.check(resource, IndexComponentSelector.DATA), is(true)); } - assertThat(authorizedIndices.all(null), not(contains("bbbbb"))); - assertThat(authorizedIndices.check("bbbbb", null), is(false)); - assertThat(authorizedIndices.all(null), not(contains("ba"))); - assertThat(authorizedIndices.check("ba", null), is(false)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains("bbbbb"))); + assertThat(authorizedIndices.check("bbbbb", IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains("ba"))); + assertThat(authorizedIndices.check("ba", IndexComponentSelector.DATA), is(false)); // due to context, datastreams are excluded from wildcard expansion - assertThat(authorizedIndices.all(null), not(contains("adatastream1"))); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains("adatastream1"))); // but they are authorized when explicitly tested (they are not "unavailable" for the Security filter) - assertThat(authorizedIndices.check("adatastream1", null), is(true)); - assertThat(authorizedIndices.all(null), not(contains(internalSecurityIndex))); - assertThat(authorizedIndices.check(internalSecurityIndex, null), is(false)); - assertThat(authorizedIndices.all(null), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); - assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, null), is(false)); + assertThat(authorizedIndices.check("adatastream1", IndexComponentSelector.DATA), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains(internalSecurityIndex))); + assertThat(authorizedIndices.check(internalSecurityIndex, IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); + assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, IndexComponentSelector.DATA), is(false)); } public void testDataStreamsAreIncludedInAuthorizedIndices() { @@ -382,15 +383,18 @@ public void testDataStreamsAreIncludedInAuthorizedIndices() { metadata.getProject().getIndicesLookup(), () -> ignore -> {} ); - assertThat(authorizedIndices.all(null), containsInAnyOrder("a1", "a2", "aaaaaa", "b", "ab", "adatastream1", backingIndex)); - assertThat(authorizedIndices.all(null), not(contains("bbbbb"))); - assertThat(authorizedIndices.check("bbbbb", null), is(false)); - assertThat(authorizedIndices.all(null), not(contains("ba"))); - assertThat(authorizedIndices.check("ba", null), is(false)); - assertThat(authorizedIndices.all(null), not(contains(internalSecurityIndex))); - assertThat(authorizedIndices.check(internalSecurityIndex, null), is(false)); - assertThat(authorizedIndices.all(null), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); - assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, null), is(false)); + assertThat( + authorizedIndices.all(IndexComponentSelector.DATA), + containsInAnyOrder("a1", "a2", "aaaaaa", "b", "ab", "adatastream1", backingIndex) + ); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains("bbbbb"))); + assertThat(authorizedIndices.check("bbbbb", IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains("ba"))); + assertThat(authorizedIndices.check("ba", IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains(internalSecurityIndex))); + assertThat(authorizedIndices.check(internalSecurityIndex, IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); + assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, IndexComponentSelector.DATA), is(false)); } public static AuthorizationEngine.RequestInfo getRequestInfo(String action) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java index 10d2173c0f201..93963a2745dbe 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java @@ -19,6 +19,7 @@ import org.elasticsearch.action.index.TransportIndexAction; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.TransportSearchAction; +import org.elasticsearch.action.support.IndexComponentSelector; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.action.support.SubscribableListener; import org.elasticsearch.client.internal.Client; @@ -1444,14 +1445,14 @@ public void testBackingIndicesAreIncludedForAuthorizedDataStreams() { lookup, () -> ignore -> {} ); - assertThat(authorizedIndices.all(null), hasItem(dataStreamName)); - assertThat(authorizedIndices.check(dataStreamName, null), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(dataStreamName)); + assertThat(authorizedIndices.check(dataStreamName, IndexComponentSelector.DATA), is(true)); assertThat( - authorizedIndices.all(null), - hasItems(backingIndices.stream().map(im -> im.getIndex().getName()).collect(Collectors.toList()).toArray(Strings.EMPTY_ARRAY)) + authorizedIndices.all(IndexComponentSelector.DATA), + hasItems(backingIndices.stream().map(im -> im.getIndex().getName()).toList().toArray(Strings.EMPTY_ARRAY)) ); for (String index : backingIndices.stream().map(im -> im.getIndex().getName()).toList()) { - assertThat(authorizedIndices.check(index, null), is(true)); + assertThat(authorizedIndices.check(index, IndexComponentSelector.DATA), is(true)); } } From 2ccbcdfc85a4d37a888c3ad9186b125dbf9ec3d9 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Thu, 13 Mar 2025 16:57:13 +0100 Subject: [PATCH 114/131] More fixes --- .../elasticsearch/action/support/IndexComponentSelector.java | 2 +- .../cluster/metadata/IndexAbstractionResolver.java | 2 +- .../security/authz/accesscontrol/IndicesAccessControl.java | 2 +- .../core/security/authz/permission/IndicesPermission.java | 2 ++ .../main/java/org/elasticsearch/xpack/security/Security.java | 1 + .../org/elasticsearch/xpack/security/authz/RBACEngine.java | 5 +++-- 6 files changed, 9 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/support/IndexComponentSelector.java b/server/src/main/java/org/elasticsearch/action/support/IndexComponentSelector.java index 293919eee9a93..5b1239479eaa5 100644 --- a/server/src/main/java/org/elasticsearch/action/support/IndexComponentSelector.java +++ b/server/src/main/java/org/elasticsearch/action/support/IndexComponentSelector.java @@ -79,7 +79,7 @@ public static IndexComponentSelector getByKeyOrThrow(@Nullable String key) { IndexComponentSelector selector = getByKey(key); if (selector == null) { throw new IllegalArgumentException( - "Unknown key of index component selector [" + key + "], available options are: " + KEY_REGISTRY + "Unknown key of index component selector [" + key + "], available options are: " + KEY_REGISTRY.keySet() ); } return selector; diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java index 97b649aac1497..6b8619cec34f9 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java @@ -63,8 +63,8 @@ public List resolveIndexAbstractions( + "]" ); } - IndexComponentSelector selector = IndexComponentSelector.getByKeyOrThrow(selectorString); indexAbstraction = expressionAndSelector.v1(); + IndexComponentSelector selector = IndexComponentSelector.getByKeyOrThrow(selectorString); // we always need to check for date math expressions indexAbstraction = IndexNameExpressionResolver.resolveDateMathExpression(indexAbstraction); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/IndicesAccessControl.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/IndicesAccessControl.java index 3bc2a942b2586..4672b6d849877 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/IndicesAccessControl.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/IndicesAccessControl.java @@ -57,7 +57,7 @@ protected IndicesAccessControl(IndicesAccessControl copy) { } /** - * @return The document and field permissions for an index if exist, otherwise null is returned. + * @return The document and field permissions for an index if they exist, otherwise null is returned. * If null is being returned this means that there are no field or document level restrictions. */ @Nullable diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index dc22a79b83c06..5b6e8fcc9025e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -451,6 +451,8 @@ public boolean isPartOfDataStream() { public boolean checkIndex(Group group) { final DataStream ds = indexAbstraction == null ? null : indexAbstraction.getParentDataStream(); if (ds != null) { + // failure indices are special: when accessed directly (not through ::failures on parent data stream) they are accessed + // implicitly as data. However, authz to the parent data stream happens via the failures selector final IndexComponentSelector selectorToCheck = indexAbstraction.isFailureIndexOfDataStream() ? IndexComponentSelector.FAILURES : selector; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index f3db0a3d2f630..5f42f4322b2ca 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -2211,6 +2211,7 @@ public Function getFieldFilter() { return FieldPredicate.ACCEPT_ALL; } assert indicesAccessControl.isGranted(); + IndexNameExpressionResolver.assertExpressionHasDefaultOrDataSelector(index); IndicesAccessControl.IndexAccessControl indexPermissions = indicesAccessControl.getIndexPermissions(index); if (indexPermissions == null) { return FieldPredicate.ACCEPT_ALL; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java index 591db4a7f347f..f200b4714f712 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java @@ -885,7 +885,7 @@ static AuthorizedIndices resolveAuthorizedIndicesFromRole( if (includeDataStreams) { for (IndexAbstraction indexAbstraction : lookup.values()) { // failure indices are special: when accessed directly (not through ::failures on parent data stream) they are accessed - // as implicitly as data. However, authz to the parent data stream happens via the failures selector + // implicitly as data. However, authz to the parent data stream happens via the failures selector if (indexAbstraction.isFailureIndexOfDataStream() && predicate.test(indexAbstraction.getParentDataStream(), IndexComponentSelector.FAILURES)) { indicesAndAliases.add(indexAbstraction.getName()); @@ -1072,7 +1072,6 @@ private static boolean isAsyncRelatedAction(String action) { } static final class AuthorizedIndices implements AuthorizationEngine.AuthorizedIndices { - private final CachedSupplier> authorizedAndAvailableSupplier; private final CachedSupplier> failureStoreAuthorizedAndAvailableSupplier; private final BiPredicate isAuthorizedPredicate; @@ -1089,6 +1088,7 @@ static final class AuthorizedIndices implements AuthorizationEngine.AuthorizedIn @Override public Set all(IndexComponentSelector selector) { + Objects.requireNonNull(selector); return IndexComponentSelector.FAILURES.equals(selector) ? failureStoreAuthorizedAndAvailableSupplier.get() : authorizedAndAvailableSupplier.get(); @@ -1096,6 +1096,7 @@ public Set all(IndexComponentSelector selector) { @Override public boolean check(String name, IndexComponentSelector selector) { + Objects.requireNonNull(selector); return isAuthorizedPredicate.test(name, selector); } } From a2ea6f223615eb20a7788dd5cc88102672670262 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Thu, 13 Mar 2025 17:06:42 +0100 Subject: [PATCH 115/131] Skip pointless map --- .../core/security/authz/permission/IndicesPermission.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 5b6e8fcc9025e..09bc0853b626b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -565,7 +565,7 @@ public IndicesAccessControl authorize( totalResourceCount += resource.size(lookup); } - final boolean overallGranted = isActionGranted(action, resources); + final boolean overallGranted = isActionGranted(action, resources.values()); final int finalTotalResourceCount = totalResourceCount; final Supplier> indexPermissions = () -> buildIndicesAccessControl( action, @@ -695,11 +695,11 @@ && containsPrivilegeThatGrantsMappingUpdatesForBwc(group))) { * Returns {@code true} if action is granted for all {@code requestedResources}. * If action is not granted for at least one resource, this method will return {@code false}. */ - private boolean isActionGranted(final String action, final Map requestedResources) { + private boolean isActionGranted(final String action, final Collection requestedResources) { final boolean isMappingUpdateAction = isMappingUpdateAction(action); - for (IndexResource resource : requestedResources.values()) { + for (IndexResource resource : requestedResources) { // 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 From d3f50dcae13ff9d562112cfd108375d11e010d69 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Thu, 13 Mar 2025 18:15:24 +0100 Subject: [PATCH 116/131] Fix tests --- .../authz/AuthorizedIndicesTests.java | 2 +- .../authz/IndicesAndAliasesResolverTests.java | 113 +++++++++--------- .../xpack/security/authz/RBACEngineTests.java | 3 +- 3 files changed, 60 insertions(+), 58 deletions(-) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java index f4776637abee6..11c4fcf75eebe 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java @@ -124,7 +124,7 @@ public void testAuthorizedIndicesUserWithSomeRoles() { assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains(internalSecurityIndex))); assertThat(authorizedIndices.check(internalSecurityIndex, IndexComponentSelector.DATA), is(false)); assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); - assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, null), is(false)); + assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, IndexComponentSelector.DATA), is(false)); } public void testAuthorizedIndicesUserWithSomeRolesEmptyMetadata() { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java index 906e6d8a025bd..aea8528ef0d5b 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java @@ -30,6 +30,7 @@ import org.elasticsearch.action.search.TransportMultiSearchAction; import org.elasticsearch.action.search.TransportSearchAction; import org.elasticsearch.action.search.TransportSearchShardsAction; +import org.elasticsearch.action.support.IndexComponentSelector; import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.action.termvectors.MultiTermVectorsRequest; @@ -2233,14 +2234,14 @@ public void testDataStreamsAreNotVisibleWhenNotIncludedByRequestWithWildcard() { List dataStreams = List.of("logs-foo", "logs-foobar"); final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); for (String dsName : dataStreams) { - assertThat(authorizedIndices.all(null), hasItem(dsName)); - assertThat(authorizedIndices.check(dsName, null), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(dsName)); + assertThat(authorizedIndices.check(dsName, IndexComponentSelector.DATA), is(true)); DataStream dataStream = projectMetadata.dataStreams().get(dsName); - assertThat(authorizedIndices.all(null), hasItem(dsName)); - assertThat(authorizedIndices.check(dsName, null), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(dsName)); + assertThat(authorizedIndices.check(dsName, IndexComponentSelector.DATA), is(true)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices.all(null), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName(), null), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), IndexComponentSelector.DATA), is(true)); } } @@ -2272,14 +2273,14 @@ public void testDataStreamsAreNotVisibleWhenNotIncludedByRequestWithoutWildcard( // data streams and their backing indices should _not_ be in the authorized list since the backing indices // do not match the requested name final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); - assertThat(authorizedIndices.all(null), hasItem(dataStreamName)); - assertThat(authorizedIndices.check(dataStreamName, null), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(dataStreamName)); + assertThat(authorizedIndices.check(dataStreamName, IndexComponentSelector.DATA), is(true)); DataStream dataStream = projectMetadata.dataStreams().get(dataStreamName); - assertThat(authorizedIndices.all(null), hasItem(dataStreamName)); - assertThat(authorizedIndices.check(dataStreamName, null), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(dataStreamName)); + assertThat(authorizedIndices.check(dataStreamName, IndexComponentSelector.DATA), is(true)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices.all(null), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName(), null), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), IndexComponentSelector.DATA), is(true)); } // neither data streams nor their backing indices will be in the resolved list since the backing indices do not match the @@ -2307,11 +2308,11 @@ public void testDataStreamsAreVisibleWhenIncludedByRequestWithWildcard() { final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, TransportSearchAction.TYPE.name(), request); for (String dsName : expectedDataStreams) { DataStream dataStream = projectMetadata.dataStreams().get(dsName); - assertThat(authorizedIndices.all(null), hasItem(dsName)); - assertThat(authorizedIndices.check(dsName, null), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(dsName)); + assertThat(authorizedIndices.check(dsName, IndexComponentSelector.DATA), is(true)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices.all(null), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName(), null), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), IndexComponentSelector.DATA), is(true)); } } @@ -2348,11 +2349,11 @@ public void testDataStreamsAreVisibleWhenIncludedByRequestWithoutWildcard() { final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, TransportSearchAction.TYPE.name(), request); // data streams and their backing indices should be in the authorized list - assertThat(authorizedIndices.all(null), hasItem(dataStreamName)); - assertThat(authorizedIndices.check(dataStreamName, null), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(dataStreamName)); + assertThat(authorizedIndices.check(dataStreamName, IndexComponentSelector.DATA), is(true)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices.all(null), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName(), null), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), IndexComponentSelector.DATA), is(true)); } ResolvedIndices resolvedIndices = defaultIndicesResolver.resolveIndicesAndAliases( @@ -2381,15 +2382,15 @@ public void testBackingIndicesAreVisibleWhenIncludedByRequestWithWildcard() { final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, TransportSearchAction.TYPE.name(), request); for (String dsName : expectedDataStreams) { DataStream dataStream = projectMetadata.dataStreams().get(dsName); - assertThat(authorizedIndices.all(null), hasItem(dsName)); - assertThat(authorizedIndices.check(dsName, null), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(dsName)); + assertThat(authorizedIndices.check(dsName, IndexComponentSelector.DATA), is(true)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices.all(null), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName(), null), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), IndexComponentSelector.DATA), is(true)); } for (Index i : dataStream.getFailureIndices()) { - assertThat(authorizedIndices.all(null), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName(), null), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), IndexComponentSelector.DATA), is(true)); } } @@ -2420,16 +2421,16 @@ public void testBackingIndicesAreNotVisibleWhenNotIncludedByRequestWithoutWildca // data streams and their backing indices should _not_ be in the authorized list since the backing indices // did not match the requested pattern and the request does not support data streams final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); - assertThat(authorizedIndices.all(null), hasItem(dataStreamName)); - assertThat(authorizedIndices.check(dataStreamName, null), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(dataStreamName)); + assertThat(authorizedIndices.check(dataStreamName, IndexComponentSelector.DATA), is(true)); DataStream dataStream = projectMetadata.dataStreams().get(dataStreamName); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices.all(null), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName(), null), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), IndexComponentSelector.DATA), is(true)); } for (Index i : dataStream.getFailureIndices()) { - assertThat(authorizedIndices.all(null), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName(), null), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), IndexComponentSelector.DATA), is(true)); } // neither data streams nor their backing indices will be in the resolved list since the request does not support data streams @@ -2460,19 +2461,19 @@ public void testDataStreamNotAuthorizedWhenBackingIndicesAreAuthorizedViaWildcar // data streams should _not_ be in the authorized list but their backing indices that matched both the requested pattern // and the authorized pattern should be in the list final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); - assertThat(authorizedIndices.all(null), not(hasItem("logs-foobar"))); - assertThat(authorizedIndices.check("logs-foobar", null), is(false)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(hasItem("logs-foobar"))); + assertThat(authorizedIndices.check("logs-foobar", IndexComponentSelector.DATA), is(false)); DataStream dataStream = projectMetadata.dataStreams().get("logs-foobar"); - assertThat(authorizedIndices.all(null), not(hasItem(indexName))); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(hasItem(indexName))); // request pattern is subset of the authorized pattern, but be aware that patterns are never passed to #check in main code - assertThat(authorizedIndices.check(indexName, null), is(true)); + assertThat(authorizedIndices.check(indexName, IndexComponentSelector.DATA), is(true)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices.all(null), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName(), null), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), IndexComponentSelector.DATA), is(true)); } for (Index i : dataStream.getFailureIndices()) { - assertThat(authorizedIndices.all(null), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName(), null), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), IndexComponentSelector.DATA), is(true)); } // only the backing indices will be in the resolved list since the request does not support data streams @@ -2500,14 +2501,14 @@ public void testDataStreamNotAuthorizedWhenBackingIndicesAreAuthorizedViaNameAnd // data streams should _not_ be in the authorized list but a single backing index that matched the requested pattern // and the authorized name should be in the list final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); - assertThat(authorizedIndices.all(null), not(hasItem("logs-foobar"))); - assertThat(authorizedIndices.check("logs-foobar", null), is(false)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(hasItem("logs-foobar"))); + assertThat(authorizedIndices.check("logs-foobar", IndexComponentSelector.DATA), is(false)); String expectedIndex = failureStore ? DataStream.getDefaultFailureStoreName("logs-foobar", 1, System.currentTimeMillis()) : DataStream.getDefaultBackingIndexName("logs-foobar", 1); - assertThat(authorizedIndices.all(null), hasItem(expectedIndex)); - assertThat(authorizedIndices.check(expectedIndex, null), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(expectedIndex)); + assertThat(authorizedIndices.check(expectedIndex, IndexComponentSelector.DATA), is(true)); // only the single backing index will be in the resolved list since the request does not support data streams // but one of the backing indices matched the requested pattern @@ -2532,19 +2533,19 @@ public void testDataStreamNotAuthorizedWhenBackingIndicesAreAuthorizedViaWildcar // data streams should _not_ be in the authorized list but their backing indices that matched both the requested pattern // and the authorized pattern should be in the list final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); - assertThat(authorizedIndices.all(null), not(hasItem("logs-foobar"))); - assertThat(authorizedIndices.check("logs-foobar", null), is(false)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(hasItem("logs-foobar"))); + assertThat(authorizedIndices.check("logs-foobar", IndexComponentSelector.DATA), is(false)); DataStream dataStream = projectMetadata.dataStreams().get("logs-foobar"); - assertThat(authorizedIndices.all(null), not(hasItem(indexName))); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(hasItem(indexName))); // request pattern is subset of the authorized pattern, but be aware that patterns are never passed to #check in main code - assertThat(authorizedIndices.check(indexName, null), is(true)); + assertThat(authorizedIndices.check(indexName, IndexComponentSelector.DATA), is(true)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices.all(null), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName(), null), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), IndexComponentSelector.DATA), is(true)); } for (Index i : dataStream.getFailureIndices()) { - assertThat(authorizedIndices.all(null), hasItem(i.getName())); - assertThat(authorizedIndices.check(i.getName(), null), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName(), IndexComponentSelector.DATA), is(true)); } // only the backing indices will be in the resolved list since the request does not support data streams @@ -2575,10 +2576,10 @@ public void testDataStreamNotAuthorizedWhenBackingIndicesAreAuthorizedViaNameAnd ? DataStream.getDefaultFailureStoreName("logs-foobar", 1, System.currentTimeMillis()) : DataStream.getDefaultBackingIndexName("logs-foobar", 1); final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); - assertThat(authorizedIndices.all(null), not(hasItem("logs-foobar"))); - assertThat(authorizedIndices.check("logs-foobar", null), is(false)); - assertThat(authorizedIndices.all(null), hasItem(expectedIndex)); - assertThat(authorizedIndices.check(expectedIndex, null), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(hasItem("logs-foobar"))); + assertThat(authorizedIndices.check("logs-foobar", IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), hasItem(expectedIndex)); + assertThat(authorizedIndices.check(expectedIndex, IndexComponentSelector.DATA), is(true)); // only the single backing index will be in the resolved list since the request does not support data streams // but one of the backing indices matched the requested pattern diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java index 93963a2745dbe..9ff64723704e5 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java @@ -1488,7 +1488,8 @@ public void testExplicitMappingUpdatesAreNotGrantedWithIngestPrivileges() { lookup, () -> ignore -> {} ); - assertThat(authorizedIndices.all(null).isEmpty(), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA).isEmpty(), is(true)); + assertThat(authorizedIndices.all(IndexComponentSelector.FAILURES).isEmpty(), is(true)); } public void testNoInfiniteRecursionForRBACAuthorizationInfoHashCode() { From 4ca997869f3114090d3b963ef732d95af402cf0e Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Thu, 13 Mar 2025 19:15:22 +0100 Subject: [PATCH 117/131] Assert --- .../authz/accesscontrol/IndicesAccessControl.java | 12 ++++-------- .../failurestore/FailureStoreSecurityRestIT.java | 2 +- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/IndicesAccessControl.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/IndicesAccessControl.java index 4672b6d849877..fe7cbc11df5a3 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/IndicesAccessControl.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/IndicesAccessControl.java @@ -13,7 +13,6 @@ import org.elasticsearch.common.util.Maps; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.core.Nullable; -import org.elasticsearch.core.Tuple; import org.elasticsearch.xpack.core.security.authz.IndicesAndAliasesResolverField; import org.elasticsearch.xpack.core.security.authz.permission.DocumentPermissions; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions; @@ -62,13 +61,10 @@ protected IndicesAccessControl(IndicesAccessControl copy) { */ @Nullable public IndexAccessControl getIndexPermissions(String index) { - Tuple indexAndSelector = IndexNameExpressionResolver.splitSelectorExpression(index); - return this.getAllIndexPermissions() - .get( - IndexComponentSelector.FAILURES.equals(IndexComponentSelector.getByKey(indexAndSelector.v2())) - ? index - : indexAndSelector.v1() - ); + assert false == IndexNameExpressionResolver.hasSelectorSuffix(index) + || IndexNameExpressionResolver.hasSelector(index, IndexComponentSelector.FAILURES) + : "index name [" + index + "] cannot have explicit selector other than ::failures"; + return getAllIndexPermissions().get(index); } public boolean hasIndexPermissions(String index) { diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java index 67f76db8ed178..c3969694a0dde 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java @@ -418,7 +418,7 @@ public void testFailureStoreAccess() throws Exception { // search data { - var request = new Search("test1"); + var request = new Search(randomFrom("test1::data", "test1")); for (var user : users) { switch (user) { case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: From b23f04cb9516d7be8cc8f4ef1b7899ac65e7a576 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Fri, 14 Mar 2025 14:28:10 +0100 Subject: [PATCH 118/131] Tests and comments --- .../authz/permission/IndicesPermission.java | 42 ++-- .../xpack/security/authz/RBACEngine.java | 22 +- .../authz/AuthorizedIndicesTests.java | 205 ++++++++++++++++++ 3 files changed, 238 insertions(+), 31 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 09bc0853b626b..7de449d84aa67 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -143,9 +143,9 @@ public boolean hasFieldOrDocumentLevelSecurity() { private IsResourceAuthorizedPredicate buildIndexMatcherPredicateForAction(String action) { final Set dataAccessOrdinaryIndices = new HashSet<>(); - final Set failureAccessOrdinaryIndices = new HashSet<>(); + final Set failuresAccessOrdinaryIndices = new HashSet<>(); final Set dataAccessRestrictedIndices = new HashSet<>(); - final Set failureAccessRestrictedIndices = new HashSet<>(); + final Set failuresAccessRestrictedIndices = new HashSet<>(); final Set grantMappingUpdatesOnIndices = new HashSet<>(); final Set grantMappingUpdatesOnRestrictedIndices = new HashSet<>(); final boolean isMappingUpdateAction = isMappingUpdateAction(action); @@ -157,7 +157,7 @@ private IsResourceAuthorizedPredicate buildIndexMatcherPredicateForAction(String dataAccessRestrictedIndices.addAll(indexList); } if (group.checkSelector(IndexComponentSelector.FAILURES)) { - failureAccessRestrictedIndices.addAll(indexList); + failuresAccessRestrictedIndices.addAll(indexList); } } else { List indexList = Arrays.asList(group.indices()); @@ -165,7 +165,7 @@ private IsResourceAuthorizedPredicate buildIndexMatcherPredicateForAction(String dataAccessOrdinaryIndices.addAll(indexList); } if (group.checkSelector(IndexComponentSelector.FAILURES)) { - failureAccessOrdinaryIndices.addAll(indexList); + failuresAccessOrdinaryIndices.addAll(indexList); } } } else if (isMappingUpdateAction && containsPrivilegeThatGrantsMappingUpdatesForBwc(group)) { @@ -179,9 +179,9 @@ private IsResourceAuthorizedPredicate buildIndexMatcherPredicateForAction(String } } final StringMatcher dataAccessNameMatcher = indexMatcher(dataAccessOrdinaryIndices, dataAccessRestrictedIndices); - final StringMatcher failureAccessNameMatcher = indexMatcher(failureAccessOrdinaryIndices, failureAccessRestrictedIndices); + final StringMatcher failuresAccessNameMatcher = indexMatcher(failuresAccessOrdinaryIndices, failuresAccessRestrictedIndices); final StringMatcher bwcSpecialCaseMatcher = indexMatcher(grantMappingUpdatesOnIndices, grantMappingUpdatesOnRestrictedIndices); - return new IsResourceAuthorizedPredicate(dataAccessNameMatcher, failureAccessNameMatcher, bwcSpecialCaseMatcher); + return new IsResourceAuthorizedPredicate(dataAccessNameMatcher, failuresAccessNameMatcher, bwcSpecialCaseMatcher); } /** @@ -190,32 +190,32 @@ private IsResourceAuthorizedPredicate buildIndexMatcherPredicateForAction(String */ public static class IsResourceAuthorizedPredicate { - private final BiPredicate isAuthorizedForData; - private final BiPredicate isAuthorizedForFailureStore; + private final BiPredicate isAuthorizedForDataAccess; + private final BiPredicate isAuthorizedForFailuresAccess; // public for tests public IsResourceAuthorizedPredicate( - StringMatcher resourceNameMatcher, - StringMatcher failureStoreNameMatcher, + StringMatcher dataResourceNameMatcher, + StringMatcher failuresResourceNameMatcher, StringMatcher additionalNonDatastreamNameMatcher ) { this((String name, @Nullable IndexAbstraction indexAbstraction) -> { assert indexAbstraction == null || name.equals(indexAbstraction.getName()); - return resourceNameMatcher.test(name) + return dataResourceNameMatcher.test(name) || (isPartOfDatastream(indexAbstraction) == false && additionalNonDatastreamNameMatcher.test(name)); }, (String name, @Nullable IndexAbstraction indexAbstraction) -> { assert indexAbstraction == null || name.equals(indexAbstraction.getName()); // we can't enforce that the abstraction is part of a data stream since we need to account for non-existent resources - return failureStoreNameMatcher.test(name); + return failuresResourceNameMatcher.test(name); }); } private IsResourceAuthorizedPredicate( - BiPredicate isAuthorizedForData, - BiPredicate isAuthorizedForFailureStore + BiPredicate isAuthorizedForDataAccess, + BiPredicate isAuthorizedForFailuresAccess ) { - this.isAuthorizedForData = isAuthorizedForData; - this.isAuthorizedForFailureStore = isAuthorizedForFailureStore; + this.isAuthorizedForDataAccess = isAuthorizedForDataAccess; + this.isAuthorizedForFailuresAccess = isAuthorizedForFailuresAccess; } /** @@ -225,14 +225,14 @@ private IsResourceAuthorizedPredicate( */ public final IsResourceAuthorizedPredicate and(IsResourceAuthorizedPredicate other) { return new IsResourceAuthorizedPredicate( - this.isAuthorizedForData.and(other.isAuthorizedForData), - this.isAuthorizedForFailureStore.and(other.isAuthorizedForFailureStore) + this.isAuthorizedForDataAccess.and(other.isAuthorizedForDataAccess), + this.isAuthorizedForFailuresAccess.and(other.isAuthorizedForFailuresAccess) ); } // TODO remove me (this has >700 usages in tests which would make for a horrible diff; will remove this once the main PR is merged) public boolean test(IndexAbstraction indexAbstraction) { - return test(indexAbstraction.getName(), indexAbstraction, null); + return test(indexAbstraction.getName(), indexAbstraction, IndexComponentSelector.DATA); } /** @@ -252,8 +252,8 @@ public boolean test(IndexAbstraction indexAbstraction, @Nullable IndexComponentS */ public boolean test(String name, @Nullable IndexAbstraction indexAbstraction, @Nullable IndexComponentSelector selector) { return IndexComponentSelector.FAILURES.equals(selector) - ? isAuthorizedForFailureStore.test(name, indexAbstraction) - : isAuthorizedForData.test(name, indexAbstraction); + ? isAuthorizedForFailuresAccess.test(name, indexAbstraction) + : isAuthorizedForDataAccess.test(name, indexAbstraction); } private static boolean isPartOfDatastream(IndexAbstraction indexAbstraction) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java index f200b4714f712..7a1fedb0805eb 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java @@ -905,6 +905,8 @@ static AuthorizedIndices resolveAuthorizedIndicesFromRole( } else { for (IndexAbstraction indexAbstraction : lookup.values()) { if (indexAbstraction.getType() != IndexAbstraction.Type.DATA_STREAM + // data check is correct, even for failure indices -- in this context, we treat them as regular indices, and + // only include them if direct data access to them is granted (e.g., if a role has `read` over "*") && predicate.test(indexAbstraction, IndexComponentSelector.DATA)) { indicesAndAliases.add(indexAbstraction.getName()); } @@ -1072,31 +1074,31 @@ private static boolean isAsyncRelatedAction(String action) { } static final class AuthorizedIndices implements AuthorizationEngine.AuthorizedIndices { - private final CachedSupplier> authorizedAndAvailableSupplier; - private final CachedSupplier> failureStoreAuthorizedAndAvailableSupplier; + private final CachedSupplier> authorizedAndAvailableDataResources; + private final CachedSupplier> authorizedAndAvailableFailuresResources; private final BiPredicate isAuthorizedPredicate; AuthorizedIndices( - Supplier> authorizedAndAvailableSupplier, - Supplier> failureStoreAuthorizedAndAvailableSupplier, + Supplier> authorizedAndAvailableDataResources, + Supplier> authorizedAndAvailableFailuresResources, BiPredicate isAuthorizedPredicate ) { - this.authorizedAndAvailableSupplier = CachedSupplier.wrap(authorizedAndAvailableSupplier); - this.failureStoreAuthorizedAndAvailableSupplier = CachedSupplier.wrap(failureStoreAuthorizedAndAvailableSupplier); + this.authorizedAndAvailableDataResources = CachedSupplier.wrap(authorizedAndAvailableDataResources); + this.authorizedAndAvailableFailuresResources = CachedSupplier.wrap(authorizedAndAvailableFailuresResources); this.isAuthorizedPredicate = Objects.requireNonNull(isAuthorizedPredicate); } @Override public Set all(IndexComponentSelector selector) { - Objects.requireNonNull(selector); + Objects.requireNonNull(selector, "must specify a selector to get authorized indices"); return IndexComponentSelector.FAILURES.equals(selector) - ? failureStoreAuthorizedAndAvailableSupplier.get() - : authorizedAndAvailableSupplier.get(); + ? authorizedAndAvailableFailuresResources.get() + : authorizedAndAvailableDataResources.get(); } @Override public boolean check(String name, IndexComponentSelector selector) { - Objects.requireNonNull(selector); + Objects.requireNonNull(selector, "must specify a selector for authorization check"); return isAuthorizedPredicate.test(name, selector); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java index 11c4fcf75eebe..5be000bf5f925 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java @@ -316,6 +316,200 @@ public void testDataStreamsAreNotIncludedInAuthorizedIndices() { assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, IndexComponentSelector.DATA), is(false)); } + public void testDataStreamsAreNotIncludedInAuthorizedIndicesWithFailuresSelector() { + assumeTrue("requires failure store", DataStream.isFailureStoreFeatureFlagEnabled()); + RoleDescriptor aStarRole = new RoleDescriptor( + "a_star", + null, + new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a*").privileges("all").build() }, + null + ); + RoleDescriptor bRole = new RoleDescriptor( + "b", + null, + new IndicesPrivileges[] { IndicesPrivileges.builder().indices("b").privileges("READ").build() }, + null + ); + RoleDescriptor cRole = new RoleDescriptor( + "c", + null, + new IndicesPrivileges[] { IndicesPrivileges.builder().indices("b").privileges("READ_FAILURE_STORE").build() }, + null + ); + Settings indexSettings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersion.current()).build(); + final String internalSecurityIndex = randomFrom( + TestRestrictedIndices.INTERNAL_SECURITY_MAIN_INDEX_6, + TestRestrictedIndices.INTERNAL_SECURITY_MAIN_INDEX_7 + ); + String backingIndex = DataStream.getDefaultBackingIndexName("adatastream1", 1); + String failureIndex = DataStream.getDefaultFailureStoreName("adatastream1", 1, 1); + Metadata metadata = Metadata.builder() + .put(new IndexMetadata.Builder("a1").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetadata.Builder("a2").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetadata.Builder("aaaaaa").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetadata.Builder("bbbbb").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put( + new IndexMetadata.Builder("b").settings(indexSettings) + .numberOfShards(1) + .numberOfReplicas(0) + .putAlias(new AliasMetadata.Builder("ab").build()) + .putAlias(new AliasMetadata.Builder("ba").build()) + .build(), + true + ) + .put( + new IndexMetadata.Builder(internalSecurityIndex).settings(indexSettings) + .numberOfShards(1) + .numberOfReplicas(0) + .putAlias(new AliasMetadata.Builder(SecuritySystemIndices.SECURITY_MAIN_ALIAS).build()) + .build(), + true + ) + .put(new IndexMetadata.Builder(backingIndex).settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetadata.Builder(failureIndex).settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put( + DataStreamTestHelper.newInstance( + "adatastream1", + List.of(new Index(DataStream.getDefaultBackingIndexName("adatastream1", 1), "_na_")), + List.of(new Index(DataStream.getDefaultFailureStoreName("adatastream1", 1, 1), "_na_")) + ) + ) + .build(); + final PlainActionFuture future = new PlainActionFuture<>(); + final Set descriptors = Sets.newHashSet(aStarRole, bRole, cRole); + CompositeRolesStore.buildRoleFromDescriptors( + descriptors, + new FieldPermissionsCache(Settings.EMPTY), + null, + RESTRICTED_INDICES, + future + ); + Role roles = future.actionGet(); + AuthorizedIndices authorizedIndices = RBACEngine.resolveAuthorizedIndicesFromRole( + roles, + getRequestInfo(TransportSearchAction.TYPE.name()), + metadata.getProject().getIndicesLookup(), + () -> ignore -> {} + ); + assertAuthorizedFor(authorizedIndices, IndexComponentSelector.DATA, "a1", "a2", "aaaaaa", "b", "ab"); + assertAuthorizedFor(authorizedIndices, IndexComponentSelector.FAILURES); + + assertThat(authorizedIndices.check("bbbbb", IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.check("bbbbb", IndexComponentSelector.FAILURES), is(false)); + + assertThat(authorizedIndices.check("ba", IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.check("ba", IndexComponentSelector.FAILURES), is(false)); + + // data are authorized when explicitly tested (they are not "unavailable" for the Security filter) + assertThat(authorizedIndices.check("adatastream1", IndexComponentSelector.DATA), is(true)); + assertThat(authorizedIndices.check("adatastream1", IndexComponentSelector.FAILURES), is(true)); + + assertThat(authorizedIndices.check(internalSecurityIndex, IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.check(internalSecurityIndex, IndexComponentSelector.FAILURES), is(false)); + assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, IndexComponentSelector.FAILURES), is(false)); + } + + public void testDataStreamsAreIncludedInAuthorizedIndicesWithFailuresSelector() { + assumeTrue("requires failure store", DataStream.isFailureStoreFeatureFlagEnabled()); + RoleDescriptor aStarRole = new RoleDescriptor( + "a_star", + null, + new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a*").privileges("all").build() }, + null + ); + RoleDescriptor bRole = new RoleDescriptor( + "b", + null, + new IndicesPrivileges[] { IndicesPrivileges.builder().indices("b").privileges("READ").build() }, + null + ); + RoleDescriptor cRole = new RoleDescriptor( + "b", + null, + new IndicesPrivileges[] { IndicesPrivileges.builder().indices("b").privileges("READ_FAILURE_STORE").build() }, + null + ); + Settings indexSettings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersion.current()).build(); + final String internalSecurityIndex = randomFrom( + TestRestrictedIndices.INTERNAL_SECURITY_MAIN_INDEX_6, + TestRestrictedIndices.INTERNAL_SECURITY_MAIN_INDEX_7 + ); + String backingIndex = DataStream.getDefaultBackingIndexName("adatastream1", 1); + String failureIndex = DataStream.getDefaultFailureStoreName("adatastream1", 1, 1); + Metadata metadata = Metadata.builder() + .put(new IndexMetadata.Builder("a1").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetadata.Builder("a2").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetadata.Builder("aaaaaa").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetadata.Builder("bbbbb").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put( + new IndexMetadata.Builder("b").settings(indexSettings) + .numberOfShards(1) + .numberOfReplicas(0) + .putAlias(new AliasMetadata.Builder("ab").build()) + .putAlias(new AliasMetadata.Builder("ba").build()) + .build(), + true + ) + .put( + new IndexMetadata.Builder(internalSecurityIndex).settings(indexSettings) + .numberOfShards(1) + .numberOfReplicas(0) + .putAlias(new AliasMetadata.Builder(SecuritySystemIndices.SECURITY_MAIN_ALIAS).build()) + .build(), + true + ) + .put(new IndexMetadata.Builder(backingIndex).settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetadata.Builder(failureIndex).settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put( + DataStreamTestHelper.newInstance( + "adatastream1", + List.of(new Index(DataStream.getDefaultBackingIndexName("adatastream1", 1), "_na_")), + List.of(new Index(DataStream.getDefaultFailureStoreName("adatastream1", 1, 1), "_na_")) + ) + ) + .build(); + final PlainActionFuture future = new PlainActionFuture<>(); + final Set descriptors = Sets.newHashSet(aStarRole, bRole, cRole); + CompositeRolesStore.buildRoleFromDescriptors( + descriptors, + new FieldPermissionsCache(Settings.EMPTY), + null, + RESTRICTED_INDICES, + future + ); + Role roles = future.actionGet(); + TransportRequest request = new ResolveIndexAction.Request(new String[] { "a*" }); + AuthorizationEngine.RequestInfo requestInfo = getRequestInfo(request, TransportSearchAction.TYPE.name()); + AuthorizedIndices authorizedIndices = RBACEngine.resolveAuthorizedIndicesFromRole( + roles, + requestInfo, + metadata.getProject().getIndicesLookup(), + () -> ignore -> {} + ); + assertAuthorizedFor( + authorizedIndices, + IndexComponentSelector.DATA, + "a1", + "a2", + "aaaaaa", + "b", + "ab", + "adatastream1", + backingIndex, + failureIndex + ); + assertAuthorizedFor(authorizedIndices, IndexComponentSelector.FAILURES, "adatastream1", failureIndex); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains("bbbbb"))); + assertThat(authorizedIndices.check("bbbbb", IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.check("bbbbb", IndexComponentSelector.FAILURES), is(false)); + assertThat(authorizedIndices.check("ba", IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.check("ba", IndexComponentSelector.FAILURES), is(false)); + assertThat(authorizedIndices.check(internalSecurityIndex, IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.check(internalSecurityIndex, IndexComponentSelector.FAILURES), is(false)); + assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, IndexComponentSelector.FAILURES), is(false)); + } + public void testDataStreamsAreIncludedInAuthorizedIndices() { RoleDescriptor aStarRole = new RoleDescriptor( "a_star", @@ -414,4 +608,15 @@ public static AuthorizationEngine.RequestInfo getRequestInfo(TransportRequest re null ); } + + private static void assertAuthorizedFor( + AuthorizedIndices authorizedIndices, + IndexComponentSelector selector, + String... expectedIndices + ) { + assertThat(authorizedIndices.all(selector), containsInAnyOrder(expectedIndices)); + for (String resource : expectedIndices) { + assertThat(authorizedIndices.check(resource, selector), is(true)); + } + } } From fa467f46f1faf8989fdb1428e0c36140f2ad5e8d Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Fri, 14 Mar 2025 14:59:53 +0100 Subject: [PATCH 119/131] Assert --- .../cluster/metadata/IndexNameExpressionResolver.java | 2 +- .../core/security/authz/permission/IndicesPermission.java | 2 +- .../xpack/downsample/TransportDownsampleAction.java | 4 ++-- .../main/java/org/elasticsearch/xpack/security/Security.java | 2 +- .../xpack/security/authz/IndicesAndAliasesResolver.java | 4 +++- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java index 8a8e15f285f74..fa359794166fa 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java @@ -1031,7 +1031,7 @@ public static String combineSelectorExpression(String baseExpression, @Nullable : (baseExpression + SelectorResolver.SELECTOR_SEPARATOR + selectorExpression); } - public static void assertExpressionHasDefaultOrDataSelector(String expression) { + public static void assertExpressionHasNullOrDataSelector(String expression) { if (Assertions.ENABLED) { var tuple = splitSelectorExpression(expression); assert tuple.v2() == null || IndexComponentSelector.DATA.getKey().equals(tuple.v2()) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 7de449d84aa67..00a25fdad4d77 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -319,7 +319,7 @@ public boolean checkResourcePrivileges( combineIndexGroups && checkForIndexPatterns.stream().anyMatch(Automatons::isLuceneRegex) ); for (String forIndexPattern : checkForIndexPatterns) { - IndexNameExpressionResolver.assertExpressionHasDefaultOrDataSelector(forIndexPattern); + IndexNameExpressionResolver.assertExpressionHasNullOrDataSelector(forIndexPattern); Automaton checkIndexAutomaton = Automatons.patterns(forIndexPattern); if (false == allowRestrictedIndices && false == isConcreteRestrictedIndex(forIndexPattern)) { checkIndexAutomaton = Automatons.minusAndMinimize(checkIndexAutomaton, restrictedIndices.getAutomaton()); diff --git a/x-pack/plugin/downsample/src/main/java/org/elasticsearch/xpack/downsample/TransportDownsampleAction.java b/x-pack/plugin/downsample/src/main/java/org/elasticsearch/xpack/downsample/TransportDownsampleAction.java index 9fe4ebe22e79d..8486963a5daee 100644 --- a/x-pack/plugin/downsample/src/main/java/org/elasticsearch/xpack/downsample/TransportDownsampleAction.java +++ b/x-pack/plugin/downsample/src/main/java/org/elasticsearch/xpack/downsample/TransportDownsampleAction.java @@ -210,8 +210,8 @@ protected void masterOperation( ) { long startTime = client.threadPool().relativeTimeInMillis(); String sourceIndexName = request.getSourceIndex(); - IndexNameExpressionResolver.assertExpressionHasDefaultOrDataSelector(sourceIndexName); - IndexNameExpressionResolver.assertExpressionHasDefaultOrDataSelector(request.getTargetIndex()); + IndexNameExpressionResolver.assertExpressionHasNullOrDataSelector(sourceIndexName); + IndexNameExpressionResolver.assertExpressionHasNullOrDataSelector(request.getTargetIndex()); final IndicesAccessControl indicesAccessControl = threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY); if (indicesAccessControl != null) { final IndicesAccessControl.IndexAccessControl indexPermissions = indicesAccessControl.getIndexPermissions(sourceIndexName); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 5f42f4322b2ca..b4035a36e15e6 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -2211,7 +2211,7 @@ public Function getFieldFilter() { return FieldPredicate.ACCEPT_ALL; } assert indicesAccessControl.isGranted(); - IndexNameExpressionResolver.assertExpressionHasDefaultOrDataSelector(index); + IndexNameExpressionResolver.assertExpressionHasNullOrDataSelector(index); IndicesAccessControl.IndexAccessControl indexPermissions = indicesAccessControl.getIndexPermissions(index); if (indexPermissions == null) { return FieldPredicate.ACCEPT_ALL; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java index 0636b81d00162..9b3a509b6315f 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java @@ -438,6 +438,7 @@ static String getPutMappingIndexOrAlias( ProjectMetadata projectMetadata ) { final String concreteIndexName = request.getConcreteIndex().getName(); + assert IndexNameExpressionResolver.hasSelectorSuffix(concreteIndexName) == false : "selectors are not allowed in this context"; // validate that the concrete index exists, otherwise there is no remapping that we could do final IndexAbstraction indexAbstraction = projectMetadata.getIndicesLookup().get(concreteIndexName); @@ -464,6 +465,7 @@ static String getPutMappingIndexOrAlias( if (aliasMetadata != null) { Optional foundAlias = aliasMetadata.stream().map(AliasMetadata::alias).filter(aliasName -> { // we know this is implicit data access (as opposed to another selector) so the default selector check is correct + assert IndexNameExpressionResolver.hasSelectorSuffix(aliasName) == false : "selectors are not allowed in this context"; if (false == isAuthorized.test(aliasName, IndexComponentSelector.DATA)) { return false; } @@ -511,7 +513,7 @@ private static List replaceWildcardsWithAuthorizedAliases(String[] alias } for (String aliasExpression : aliases) { - IndexNameExpressionResolver.assertExpressionHasDefaultOrDataSelector(aliasExpression); + IndexNameExpressionResolver.assertExpressionHasNullOrDataSelector(aliasExpression); boolean include = true; if (aliasExpression.charAt(0) == '-') { include = false; From 2931c8594f0f899e4cffd55e21aa45dffc3d6f80 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Fri, 14 Mar 2025 15:24:03 +0100 Subject: [PATCH 120/131] Javadoc --- .../org/elasticsearch/example/CustomAuthorizationEngine.java | 1 - .../elasticsearch/action/support/IndexComponentSelector.java | 5 +++++ .../xpack/security/authz/IndicesAndAliasesResolver.java | 5 ++--- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java b/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java index 309435a8e7dab..a59db7a8ea565 100644 --- a/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java +++ b/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java @@ -14,7 +14,6 @@ import org.elasticsearch.action.support.SubscribableListener; import org.elasticsearch.cluster.metadata.IndexAbstraction; import org.elasticsearch.cluster.metadata.ProjectMetadata; -import org.elasticsearch.core.Nullable; import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse; import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse.Indices; import org.elasticsearch.xpack.core.security.authc.Authentication; diff --git a/server/src/main/java/org/elasticsearch/action/support/IndexComponentSelector.java b/server/src/main/java/org/elasticsearch/action/support/IndexComponentSelector.java index 5b1239479eaa5..0f8533b5c5d55 100644 --- a/server/src/main/java/org/elasticsearch/action/support/IndexComponentSelector.java +++ b/server/src/main/java/org/elasticsearch/action/support/IndexComponentSelector.java @@ -72,6 +72,11 @@ public static IndexComponentSelector getByKey(String key) { return KEY_REGISTRY.get(key); } + /** + * Like {@link #getByKey(String)} but throws an exception if the key is not recognised. + * @return the selector if recognized. `null` input will return `DATA`. + * @throws IllegalArgumentException if the key was not recognised. + */ public static IndexComponentSelector getByKeyOrThrow(@Nullable String key) { if (key == null) { return DATA; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java index 9b3a509b6315f..a7fee63ff5b0d 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java @@ -322,9 +322,8 @@ ResolvedIndices resolveIndicesAndAliases( ); } if (indicesOptions.expandWildcardExpressions()) { - for (String authorizedIndex : authorizedIndices.all( - IndexComponentSelector.getByKeyOrThrow(allIndicesPatternSelector) - )) { + IndexComponentSelector selector = IndexComponentSelector.getByKeyOrThrow(allIndicesPatternSelector); + for (String authorizedIndex : authorizedIndices.all(selector)) { if (IndexAbstractionResolver.isIndexVisible( "*", allIndicesPatternSelector, From 21f12185101d5a573bc9d20e52a06728f04bf572 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Fri, 14 Mar 2025 15:38:11 +0100 Subject: [PATCH 121/131] Rm nullable annotations --- .../core/security/authz/permission/IndicesPermission.java | 4 ++-- .../security/authz/accesscontrol/IndicesPermissionTests.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 00a25fdad4d77..34410c7116201 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -240,7 +240,7 @@ public boolean test(IndexAbstraction indexAbstraction) { * The resource must exist. Otherwise, use the {@link #test(String, IndexAbstraction, IndexComponentSelector)} method. * Returns {@code true} if access to the given resource is authorized or {@code false} otherwise. */ - public boolean test(IndexAbstraction indexAbstraction, @Nullable IndexComponentSelector selector) { + public boolean test(IndexAbstraction indexAbstraction, IndexComponentSelector selector) { return test(indexAbstraction.getName(), indexAbstraction, selector); } @@ -250,7 +250,7 @@ public boolean test(IndexAbstraction indexAbstraction, @Nullable IndexComponentS * if it doesn't. * Returns {@code true} if access to the given resource is authorized or {@code false} otherwise. */ - public boolean test(String name, @Nullable IndexAbstraction indexAbstraction, @Nullable IndexComponentSelector selector) { + public boolean test(String name, @Nullable IndexAbstraction indexAbstraction, IndexComponentSelector selector) { return IndexComponentSelector.FAILURES.equals(selector) ? isAuthorizedForFailuresAccess.test(name, indexAbstraction) : isAuthorizedForDataAccess.test(name, indexAbstraction); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java index afc6d1b0b246e..5ac5452fae900 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java @@ -974,10 +974,10 @@ public void testResourceAuthorizedPredicateForDatastreams() { ); assertThat(predicate.test(dataStream), is(false)); // test authorization for a missing resource with the datastream's name - assertThat(predicate.test(dataStream.getName(), null, null), is(true)); + assertThat(predicate.test(dataStream.getName(), null, IndexComponentSelector.DATA), is(true)); assertThat(predicate.test(backingIndex), is(false)); // test authorization for a missing resource with the backing index's name - assertThat(predicate.test(backingIndex.getName(), null, null), is(true)); + assertThat(predicate.test(backingIndex.getName(), null, IndexComponentSelector.DATA), is(true)); assertThat(predicate.test(concreteIndex), is(true)); assertThat(predicate.test(alias), is(true)); } From 75dbd7baa180440a44614eb0af260af5745a002a Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Fri, 14 Mar 2025 15:56:51 +0100 Subject: [PATCH 122/131] More tests --- .../FailureStoreSecurityRestIT.java | 172 +++++++++--------- 1 file changed, 88 insertions(+), 84 deletions(-) diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java index c3969694a0dde..78e504d464624 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java @@ -76,6 +76,8 @@ protected Settings restAdminSettings() { private static final String ASYNC_SEARCH_TIMEOUT = "30s"; + private static final String ADMIN_USER_NAME = "admin_user"; + private static final String DATA_ACCESS = "data_access"; private static final String STAR_READ_ONLY_ACCESS = "star_read_only"; private static final String FAILURE_STORE_ACCESS = "failure_store_access"; @@ -83,7 +85,7 @@ protected Settings restAdminSettings() { private static final String WRITE_ACCESS = "write_access"; private static final String MANAGE_ACCESS = "manage_access"; private static final String MANAGE_FAILURE_STORE_ACCESS = "manage_failure_store_access"; - private static final SecureString PASSWORD = new SecureString("elastic-password"); + private static final SecureString PASSWORD = new SecureString("admin-password"); @Before public void setup() throws IOException { @@ -214,8 +216,7 @@ public void testGetUserPrivileges() throws IOException { } public void testRoleWithSelectorInIndexPattern() throws Exception { - createTemplates(); - populateDataStream(); + setupDataStream(); createUser("user", PASSWORD, "role"); upsertRole(""" @@ -357,38 +358,7 @@ public void testFailureStoreAccess() throws Exception { } """); - createUser(MANAGE_ACCESS, PASSWORD, MANAGE_ACCESS); - upsertRole(Strings.format(""" - { - "cluster": ["all"], - "indices": [{"names": ["test*"], "privileges": ["manage"]}] - }"""), MANAGE_ACCESS); - createAndStoreApiKey(MANAGE_ACCESS, randomBoolean() ? null : """ - { - "role": { - "cluster": ["all"], - "indices": [{"names": ["test*"], "privileges": ["manage"]}] - } - } - """); - - createUser(MANAGE_FAILURE_STORE_ACCESS, PASSWORD, MANAGE_FAILURE_STORE_ACCESS); - upsertRole(Strings.format(""" - { - "cluster": ["all"], - "indices": [{"names": ["test*"], "privileges": ["manage_failure_store"]}] - }"""), MANAGE_FAILURE_STORE_ACCESS); - createAndStoreApiKey(MANAGE_FAILURE_STORE_ACCESS, randomBoolean() ? null : """ - { - "role": { - "cluster": ["all"], - "indices": [{"names": ["test*"], "privileges": ["manage_failure_store"]}] - } - } - """); - - createTemplates(); - List docIds = populateDataStream(); + List docIds = setupDataStream(); assertThat(docIds.size(), equalTo(2)); assertThat(docIds, hasItem("1")); String dataDocId = "1"; @@ -413,15 +383,14 @@ public void testFailureStoreAccess() throws Exception { """); assertOK(adminClient().performRequest(aliasRequest)); - // todo also add superuser - List users = List.of(DATA_ACCESS, FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS); + List users = List.of(DATA_ACCESS, FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, ADMIN_USER_NAME); // search data { var request = new Search(randomFrom("test1::data", "test1")); for (var user : users) { switch (user) { - case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expect(user, request, dataDocId); break; case FAILURE_STORE_ACCESS: @@ -436,7 +405,7 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test1", "?ignore_unavailable=true"); for (var user : users) { switch (user) { - case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expect(user, request, dataDocId); break; case FAILURE_STORE_ACCESS: @@ -451,7 +420,7 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test-alias"); for (var user : users) { switch (user) { - case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expect(user, request, dataDocId); break; case FAILURE_STORE_ACCESS: @@ -466,7 +435,7 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test-alias", "?ignore_unavailable=true"); for (var user : users) { switch (user) { - case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expect(user, request, dataDocId); break; case FAILURE_STORE_ACCESS: @@ -481,7 +450,7 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test*"); for (var user : users) { switch (user) { - case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expect(user, request, dataDocId); break; case FAILURE_STORE_ACCESS: @@ -496,7 +465,7 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("*1"); for (var user : users) { switch (user) { - case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expect(user, request, dataDocId); break; case FAILURE_STORE_ACCESS: @@ -510,7 +479,7 @@ public void testFailureStoreAccess() throws Exception { for (var request : List.of(new Search("*"), new Search("_all"), new Search(""))) { for (var user : users) { switch (user) { - case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expect(user, request, dataDocId); break; case FAILURE_STORE_ACCESS: @@ -525,7 +494,7 @@ public void testFailureStoreAccess() throws Exception { var request = new Search(".ds*"); for (var user : users) { switch (user) { - case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expect(user, request, dataDocId); break; case FAILURE_STORE_ACCESS: @@ -540,7 +509,7 @@ public void testFailureStoreAccess() throws Exception { var request = new Search(dataIndexName); for (var user : users) { switch (user) { - case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expect(user, request, dataDocId); break; case FAILURE_STORE_ACCESS: @@ -555,7 +524,7 @@ public void testFailureStoreAccess() throws Exception { var request = new Search(dataIndexName, "?ignore_unavailable=true"); for (var user : users) { switch (user) { - case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expect(user, request, dataDocId); break; case FAILURE_STORE_ACCESS: @@ -570,7 +539,7 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test2"); for (var user : users) { switch (user) { - case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expect(user, request, 404); break; case FAILURE_STORE_ACCESS: @@ -585,7 +554,7 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test2", "?ignore_unavailable=true"); for (var user : users) { switch (user) { - case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, FAILURE_STORE_ACCESS: + case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, FAILURE_STORE_ACCESS: expect(user, request); break; default: @@ -602,7 +571,7 @@ public void testFailureStoreAccess() throws Exception { case DATA_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request, 403); break; - case FAILURE_STORE_ACCESS, BOTH_ACCESS: + case ADMIN_USER_NAME, FAILURE_STORE_ACCESS, BOTH_ACCESS: expect(user, request, failuresDocId); break; default: @@ -617,7 +586,7 @@ public void testFailureStoreAccess() throws Exception { case DATA_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request); break; - case FAILURE_STORE_ACCESS, BOTH_ACCESS: + case ADMIN_USER_NAME, FAILURE_STORE_ACCESS, BOTH_ACCESS: expect(user, request, failuresDocId); break; default: @@ -632,7 +601,7 @@ public void testFailureStoreAccess() throws Exception { case DATA_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request, 403); break; - case FAILURE_STORE_ACCESS, BOTH_ACCESS: + case ADMIN_USER_NAME, FAILURE_STORE_ACCESS, BOTH_ACCESS: expect(user, request, failuresDocId); break; default: @@ -647,7 +616,7 @@ public void testFailureStoreAccess() throws Exception { case DATA_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request); break; - case FAILURE_STORE_ACCESS, BOTH_ACCESS: + case ADMIN_USER_NAME, FAILURE_STORE_ACCESS, BOTH_ACCESS: expect(user, request, failuresDocId); break; default: @@ -662,7 +631,7 @@ public void testFailureStoreAccess() throws Exception { case DATA_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request); break; - case FAILURE_STORE_ACCESS, BOTH_ACCESS: + case ADMIN_USER_NAME, FAILURE_STORE_ACCESS, BOTH_ACCESS: expect(user, request, failuresDocId); break; default: @@ -677,7 +646,7 @@ public void testFailureStoreAccess() throws Exception { case DATA_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request); break; - case FAILURE_STORE_ACCESS, BOTH_ACCESS: + case ADMIN_USER_NAME, FAILURE_STORE_ACCESS, BOTH_ACCESS: expect(user, request, failuresDocId); break; default: @@ -692,7 +661,7 @@ public void testFailureStoreAccess() throws Exception { case DATA_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request); break; - case FAILURE_STORE_ACCESS, BOTH_ACCESS: + case ADMIN_USER_NAME, FAILURE_STORE_ACCESS, BOTH_ACCESS: expect(user, request, failuresDocId); break; default: @@ -707,7 +676,7 @@ public void testFailureStoreAccess() throws Exception { case DATA_ACCESS: expect(user, request); break; - case FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + case ADMIN_USER_NAME, FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expect(user, request, failuresDocId); break; default: @@ -722,7 +691,7 @@ public void testFailureStoreAccess() throws Exception { case DATA_ACCESS: expect(user, request, 403); break; - case FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + case ADMIN_USER_NAME, FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expect(user, request, failuresDocId); break; default: @@ -737,7 +706,7 @@ public void testFailureStoreAccess() throws Exception { case DATA_ACCESS: expect(user, request); break; - case FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + case ADMIN_USER_NAME, FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expect(user, request, failuresDocId); break; default: @@ -752,7 +721,7 @@ public void testFailureStoreAccess() throws Exception { case DATA_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request, 403); break; - case FAILURE_STORE_ACCESS, BOTH_ACCESS: + case ADMIN_USER_NAME, FAILURE_STORE_ACCESS, BOTH_ACCESS: expect(user, request, 404); break; default: @@ -764,7 +733,7 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test2::failures", "?ignore_unavailable=true"); for (var user : users) { switch (user) { - case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, FAILURE_STORE_ACCESS: + case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, FAILURE_STORE_ACCESS: expect(user, request); break; default: @@ -781,7 +750,7 @@ public void testFailureStoreAccess() throws Exception { case DATA_ACCESS, FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request, 403); break; - case BOTH_ACCESS: + case ADMIN_USER_NAME, BOTH_ACCESS: expect(user, request, dataDocId, failuresDocId); break; default: @@ -799,7 +768,7 @@ public void testFailureStoreAccess() throws Exception { case FAILURE_STORE_ACCESS: expect(user, request, failuresDocId); break; - case BOTH_ACCESS: + case ADMIN_USER_NAME, BOTH_ACCESS: expect(user, request, dataDocId, failuresDocId); break; default: @@ -814,7 +783,7 @@ public void testFailureStoreAccess() throws Exception { case DATA_ACCESS, FAILURE_STORE_ACCESS: expect(user, request, 403); break; - case BOTH_ACCESS, STAR_READ_ONLY_ACCESS: + case ADMIN_USER_NAME, BOTH_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request, dataDocId, failuresDocId); break; default: @@ -832,7 +801,7 @@ public void testFailureStoreAccess() throws Exception { case FAILURE_STORE_ACCESS: expect(user, request, failuresDocId); break; - case BOTH_ACCESS, STAR_READ_ONLY_ACCESS: + case ADMIN_USER_NAME, BOTH_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request, dataDocId, failuresDocId); break; default: @@ -847,7 +816,7 @@ public void testFailureStoreAccess() throws Exception { case DATA_ACCESS, FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request, 403); break; - case BOTH_ACCESS: + case ADMIN_USER_NAME, BOTH_ACCESS: expect(user, request, dataDocId, failuresDocId); break; default: @@ -865,7 +834,7 @@ public void testFailureStoreAccess() throws Exception { case FAILURE_STORE_ACCESS: expect(user, request, failuresDocId); break; - case BOTH_ACCESS: + case ADMIN_USER_NAME, BOTH_ACCESS: expect(user, request, dataDocId, failuresDocId); break; default: @@ -883,7 +852,7 @@ public void testFailureStoreAccess() throws Exception { case DATA_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request, dataDocId); break; - case BOTH_ACCESS: + case ADMIN_USER_NAME, BOTH_ACCESS: expect(user, request, dataDocId, failuresDocId); break; default: @@ -901,7 +870,7 @@ public void testFailureStoreAccess() throws Exception { case DATA_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request, dataDocId); break; - case BOTH_ACCESS: + case ADMIN_USER_NAME, BOTH_ACCESS: expect(user, request, dataDocId, failuresDocId); break; default: @@ -919,7 +888,7 @@ public void testFailureStoreAccess() throws Exception { case DATA_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request, 403); break; - case BOTH_ACCESS: + case ADMIN_USER_NAME, BOTH_ACCESS: expect(user, request, dataDocId, failuresDocId); break; default: @@ -937,7 +906,7 @@ public void testFailureStoreAccess() throws Exception { case DATA_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request, dataDocId); break; - case BOTH_ACCESS: + case ADMIN_USER_NAME, BOTH_ACCESS: expect(user, request, dataDocId, failuresDocId); break; default: @@ -955,7 +924,7 @@ public void testFailureStoreAccess() throws Exception { case DATA_ACCESS, STAR_READ_ONLY_ACCESS: expect(user, request, dataDocId); break; - case BOTH_ACCESS: + case ADMIN_USER_NAME, BOTH_ACCESS: expect(user, request, dataDocId, failuresDocId); break; default: @@ -963,8 +932,43 @@ public void testFailureStoreAccess() throws Exception { } } } + } + + public void testWriteOperations() throws IOException { + setupDataStream(); + Tuple backingIndices = getSingleDataAndFailureIndices("test1"); + String dataIndexName = backingIndices.v1(); + String failureIndexName = backingIndices.v2(); + + createUser(MANAGE_ACCESS, PASSWORD, MANAGE_ACCESS); + upsertRole(Strings.format(""" + { + "cluster": ["all"], + "indices": [{"names": ["test*"], "privileges": ["manage"]}] + }"""), MANAGE_ACCESS); + createAndStoreApiKey(MANAGE_ACCESS, randomBoolean() ? null : """ + { + "role": { + "cluster": ["all"], + "indices": [{"names": ["test*"], "privileges": ["manage"]}] + } + } + """); - // write operations below + createUser(MANAGE_FAILURE_STORE_ACCESS, PASSWORD, MANAGE_FAILURE_STORE_ACCESS); + upsertRole(Strings.format(""" + { + "cluster": ["all"], + "indices": [{"names": ["test*"], "privileges": ["manage_failure_store"]}] + }"""), MANAGE_FAILURE_STORE_ACCESS); + createAndStoreApiKey(MANAGE_FAILURE_STORE_ACCESS, randomBoolean() ? null : """ + { + "role": { + "cluster": ["all"], + "indices": [{"names": ["test*"], "privileges": ["manage_failure_store"]}] + } + } + """); // user with manage access to data stream does NOT get direct access to failure index expectThrows(() -> deleteIndex(MANAGE_ACCESS, failureIndexName), 403); @@ -976,19 +980,19 @@ public void testFailureStoreAccess() throws Exception { expectThrows(() -> deleteDataStream(MANAGE_FAILURE_STORE_ACCESS, "test1"), 403); expectThrows(() -> deleteDataStream(MANAGE_FAILURE_STORE_ACCESS, "test1::failures"), 403); + // manage user can delete data stream deleteDataStream(MANAGE_ACCESS, "test1"); - expectThrows(() -> performRequest(BOTH_ACCESS, new Request("GET", "/test1/_search")), 404); + // deleting data stream deletes everything, including failure index + expectThrows(() -> adminClient().performRequest(new Request("GET", "/test1/_search")), 404); expectThrows(() -> adminClient().performRequest(new Request("GET", "/" + dataIndexName + "/_search")), 404); - expectThrows(() -> performRequest(BOTH_ACCESS, new Request("GET", "/test1::failures/_search")), 404); + expectThrows(() -> adminClient().performRequest(new Request("GET", "/test1::failures/_search")), 404); expectThrows(() -> adminClient().performRequest(new Request("GET", "/" + failureIndexName + "/_search")), 404); } public void testFailureStoreAccessWithApiKeys() throws Exception { - createTemplates(); - - List docIds = populateDataStream(); + List docIds = setupDataStream(); assertThat(docIds.size(), equalTo(2)); assertThat(docIds, hasItem("1")); String dataDocId = "1"; @@ -1112,8 +1116,7 @@ public void testFailureStoreAccessWithApiKeys() throws Exception { } public void testDlsFls() throws Exception { - createTemplates(); - populateDataStream(); + setupDataStream(); Tuple backingIndices = getSingleDataAndFailureIndices("test1"); String dataIndexName = backingIndices.v1(); @@ -1333,6 +1336,11 @@ Request toAsyncSearchRequest() { } } + private List setupDataStream() throws IOException { + createTemplates(); + return randomBoolean() ? populateDataStreamWithBulkRequest() : populateDataStreamWithDocRequests(); + } + private void createTemplates() throws IOException { var componentTemplateRequest = new Request("PUT", "/_component_template/component1"); componentTemplateRequest.setJsonEntity(""" @@ -1376,10 +1384,6 @@ private void createTemplates() throws IOException { assertOK(adminClient().performRequest(indexTemplateRequest)); } - private List populateDataStream() throws IOException { - return randomBoolean() ? populateDataStreamWithBulkRequest() : populateDataStreamWithDocRequests(); - } - private List populateDataStreamWithDocRequests() throws IOException { List ids = new ArrayList<>(); From 917b2c72aeea1a458faac3666d6cfbb3a6957e48 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 17 Mar 2025 10:42:46 +0100 Subject: [PATCH 123/131] Update docs/changelog/123986.yaml --- docs/changelog/123986.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/changelog/123986.yaml diff --git a/docs/changelog/123986.yaml b/docs/changelog/123986.yaml new file mode 100644 index 0000000000000..3166d10fff6eb --- /dev/null +++ b/docs/changelog/123986.yaml @@ -0,0 +1,5 @@ +pr: 123986 +summary: Failure store access - selector-aware authorization +area: Authorization +type: enhancement +issues: [] From 2bb84a036929c8091f0706769a67e4d10946a02b Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 17 Mar 2025 20:57:46 +0100 Subject: [PATCH 124/131] PIT tests --- .../FailureStoreSecurityRestIT.java | 305 +++++++++++------- 1 file changed, 188 insertions(+), 117 deletions(-) diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java index 78e504d464624..12dab945a54e1 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java @@ -50,6 +50,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; public class FailureStoreSecurityRestIT extends ESRestTestCase { @@ -231,8 +232,8 @@ public void testRoleWithSelectorInIndexPattern() throws Exception { }""", "role"); createAndStoreApiKey("user", null); - expect("user", new Search("test1::failures"), 403); - expect("user", new Search("*::failures")); + expectThrows("user", new Search("test1::failures"), 403); + expectSearch("user", new Search("*::failures")); upsertRole(""" { @@ -245,8 +246,8 @@ public void testRoleWithSelectorInIndexPattern() throws Exception { ] }""", "role"); - expect("user", new Search("test1::failures"), 403); - expect("user", new Search("*::failures")); + expectThrows("user", new Search("test1::failures"), 403); + expectSearch("user", new Search("*::failures")); upsertRole(""" { @@ -258,8 +259,8 @@ public void testRoleWithSelectorInIndexPattern() throws Exception { } ] }""", "role"); - expect("user", new Search("test1::failures"), 403); - expect("user", new Search("*::failures")); + expectThrows("user", new Search("test1::failures"), 403); + expectSearch("user", new Search("*::failures")); upsertRole(""" { @@ -271,8 +272,8 @@ public void testRoleWithSelectorInIndexPattern() throws Exception { } ] }""", "role"); - expect("user", new Search("test1::failures"), 403); - expect("user", new Search("*::failures")); + expectThrows("user", new Search("test1::failures"), 403); + expectSearch("user", new Search("*::failures")); } @SuppressWarnings("unchecked") @@ -391,10 +392,10 @@ public void testFailureStoreAccess() throws Exception { for (var user : users) { switch (user) { case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: - expect(user, request, dataDocId); + expectSearch(user, request, dataDocId); break; case FAILURE_STORE_ACCESS: - expect(user, request, 403); + expectThrows(user, request, 403); break; default: fail("must cover user: " + user); @@ -406,10 +407,10 @@ public void testFailureStoreAccess() throws Exception { for (var user : users) { switch (user) { case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: - expect(user, request, dataDocId); + expectSearch(user, request, dataDocId); break; case FAILURE_STORE_ACCESS: - expect(user, request); + expectSearch(user, request); break; default: fail("must cover user: " + user); @@ -421,10 +422,10 @@ public void testFailureStoreAccess() throws Exception { for (var user : users) { switch (user) { case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: - expect(user, request, dataDocId); + expectSearch(user, request, dataDocId); break; case FAILURE_STORE_ACCESS: - expect(user, request, 403); + expectThrows(user, request, 403); break; default: fail("must cover user: " + user); @@ -436,10 +437,10 @@ public void testFailureStoreAccess() throws Exception { for (var user : users) { switch (user) { case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: - expect(user, request, dataDocId); + expectSearch(user, request, dataDocId); break; case FAILURE_STORE_ACCESS: - expect(user, request); + expectSearch(user, request); break; default: fail("must cover user: " + user); @@ -451,10 +452,10 @@ public void testFailureStoreAccess() throws Exception { for (var user : users) { switch (user) { case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: - expect(user, request, dataDocId); + expectSearch(user, request, dataDocId); break; case FAILURE_STORE_ACCESS: - expect(user, request); + expectSearch(user, request); break; default: fail("must cover user: " + user); @@ -466,10 +467,10 @@ public void testFailureStoreAccess() throws Exception { for (var user : users) { switch (user) { case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: - expect(user, request, dataDocId); + expectSearch(user, request, dataDocId); break; case FAILURE_STORE_ACCESS: - expect(user, request); + expectSearch(user, request); break; default: fail("must cover user: " + user); @@ -480,10 +481,10 @@ public void testFailureStoreAccess() throws Exception { for (var user : users) { switch (user) { case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: - expect(user, request, dataDocId); + expectSearch(user, request, dataDocId); break; case FAILURE_STORE_ACCESS: - expect(user, request); + expectSearch(user, request); break; default: fail("must cover user: " + user); @@ -495,10 +496,10 @@ public void testFailureStoreAccess() throws Exception { for (var user : users) { switch (user) { case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: - expect(user, request, dataDocId); + expectSearch(user, request, dataDocId); break; case FAILURE_STORE_ACCESS: - expect(user, request); + expectSearch(user, request); break; default: fail("must cover user: " + user); @@ -510,10 +511,10 @@ public void testFailureStoreAccess() throws Exception { for (var user : users) { switch (user) { case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: - expect(user, request, dataDocId); + expectSearch(user, request, dataDocId); break; case FAILURE_STORE_ACCESS: - expect(user, request, 403); + expectThrows(user, request, 403); break; default: fail("must cover user: " + user); @@ -525,10 +526,10 @@ public void testFailureStoreAccess() throws Exception { for (var user : users) { switch (user) { case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: - expect(user, request, dataDocId); + expectSearch(user, request, dataDocId); break; case FAILURE_STORE_ACCESS: - expect(user, request); + expectSearch(user, request); break; default: fail("must cover user: " + user); @@ -540,10 +541,10 @@ public void testFailureStoreAccess() throws Exception { for (var user : users) { switch (user) { case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: - expect(user, request, 404); + expectThrows(user, request, 404); break; case FAILURE_STORE_ACCESS: - expect(user, request, 403); + expectThrows(user, request, 403); break; default: fail("must cover user: " + user); @@ -555,7 +556,7 @@ public void testFailureStoreAccess() throws Exception { for (var user : users) { switch (user) { case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, FAILURE_STORE_ACCESS: - expect(user, request); + expectSearch(user, request); break; default: fail("must cover user: " + user); @@ -569,10 +570,10 @@ public void testFailureStoreAccess() throws Exception { for (var user : users) { switch (user) { case DATA_ACCESS, STAR_READ_ONLY_ACCESS: - expect(user, request, 403); + expectThrows(user, request, 403); break; case ADMIN_USER_NAME, FAILURE_STORE_ACCESS, BOTH_ACCESS: - expect(user, request, failuresDocId); + expectSearch(user, request, failuresDocId); break; default: fail("must cover user: " + user); @@ -584,10 +585,10 @@ public void testFailureStoreAccess() throws Exception { for (var user : users) { switch (user) { case DATA_ACCESS, STAR_READ_ONLY_ACCESS: - expect(user, request); + expectSearch(user, request); break; case ADMIN_USER_NAME, FAILURE_STORE_ACCESS, BOTH_ACCESS: - expect(user, request, failuresDocId); + expectSearch(user, request, failuresDocId); break; default: fail("must cover user: " + user); @@ -599,10 +600,10 @@ public void testFailureStoreAccess() throws Exception { for (var user : users) { switch (user) { case DATA_ACCESS, STAR_READ_ONLY_ACCESS: - expect(user, request, 403); + expectThrows(user, request, 403); break; case ADMIN_USER_NAME, FAILURE_STORE_ACCESS, BOTH_ACCESS: - expect(user, request, failuresDocId); + expectSearch(user, request, failuresDocId); break; default: fail("must cover user: " + user); @@ -614,10 +615,10 @@ public void testFailureStoreAccess() throws Exception { for (var user : users) { switch (user) { case DATA_ACCESS, STAR_READ_ONLY_ACCESS: - expect(user, request); + expectSearch(user, request); break; case ADMIN_USER_NAME, FAILURE_STORE_ACCESS, BOTH_ACCESS: - expect(user, request, failuresDocId); + expectSearch(user, request, failuresDocId); break; default: fail("must cover user: " + user); @@ -629,10 +630,10 @@ public void testFailureStoreAccess() throws Exception { for (var user : users) { switch (user) { case DATA_ACCESS, STAR_READ_ONLY_ACCESS: - expect(user, request); + expectSearch(user, request); break; case ADMIN_USER_NAME, FAILURE_STORE_ACCESS, BOTH_ACCESS: - expect(user, request, failuresDocId); + expectSearch(user, request, failuresDocId); break; default: fail("must cover user: " + user); @@ -644,10 +645,10 @@ public void testFailureStoreAccess() throws Exception { for (var user : users) { switch (user) { case DATA_ACCESS, STAR_READ_ONLY_ACCESS: - expect(user, request); + expectSearch(user, request); break; case ADMIN_USER_NAME, FAILURE_STORE_ACCESS, BOTH_ACCESS: - expect(user, request, failuresDocId); + expectSearch(user, request, failuresDocId); break; default: fail("must cover user: " + user); @@ -659,10 +660,10 @@ public void testFailureStoreAccess() throws Exception { for (var user : users) { switch (user) { case DATA_ACCESS, STAR_READ_ONLY_ACCESS: - expect(user, request); + expectSearch(user, request); break; case ADMIN_USER_NAME, FAILURE_STORE_ACCESS, BOTH_ACCESS: - expect(user, request, failuresDocId); + expectSearch(user, request, failuresDocId); break; default: fail("must cover user: " + user); @@ -674,10 +675,10 @@ public void testFailureStoreAccess() throws Exception { for (var user : users) { switch (user) { case DATA_ACCESS: - expect(user, request); + expectSearch(user, request); break; case ADMIN_USER_NAME, FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: - expect(user, request, failuresDocId); + expectSearch(user, request, failuresDocId); break; default: fail("must cover user: " + user); @@ -689,10 +690,10 @@ public void testFailureStoreAccess() throws Exception { for (var user : users) { switch (user) { case DATA_ACCESS: - expect(user, request, 403); + expectThrows(user, request, 403); break; case ADMIN_USER_NAME, FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: - expect(user, request, failuresDocId); + expectSearch(user, request, failuresDocId); break; default: fail("must cover user: " + user); @@ -704,10 +705,10 @@ public void testFailureStoreAccess() throws Exception { for (var user : users) { switch (user) { case DATA_ACCESS: - expect(user, request); + expectSearch(user, request); break; case ADMIN_USER_NAME, FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: - expect(user, request, failuresDocId); + expectSearch(user, request, failuresDocId); break; default: fail("must cover user: " + user); @@ -719,10 +720,10 @@ public void testFailureStoreAccess() throws Exception { for (var user : users) { switch (user) { case DATA_ACCESS, STAR_READ_ONLY_ACCESS: - expect(user, request, 403); + expectThrows(user, request, 403); break; case ADMIN_USER_NAME, FAILURE_STORE_ACCESS, BOTH_ACCESS: - expect(user, request, 404); + expectThrows(user, request, 404); break; default: fail("must cover user: " + user); @@ -734,7 +735,7 @@ public void testFailureStoreAccess() throws Exception { for (var user : users) { switch (user) { case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, FAILURE_STORE_ACCESS: - expect(user, request); + expectSearch(user, request); break; default: fail("must cover user: " + user); @@ -748,10 +749,10 @@ public void testFailureStoreAccess() throws Exception { for (var user : users) { switch (user) { case DATA_ACCESS, FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS: - expect(user, request, 403); + expectThrows(user, request, 403); break; case ADMIN_USER_NAME, BOTH_ACCESS: - expect(user, request, dataDocId, failuresDocId); + expectSearch(user, request, dataDocId, failuresDocId); break; default: fail("must cover user: " + user); @@ -763,13 +764,13 @@ public void testFailureStoreAccess() throws Exception { for (var user : users) { switch (user) { case DATA_ACCESS, STAR_READ_ONLY_ACCESS: - expect(user, request, dataDocId); + expectSearch(user, request, dataDocId); break; case FAILURE_STORE_ACCESS: - expect(user, request, failuresDocId); + expectSearch(user, request, failuresDocId); break; case ADMIN_USER_NAME, BOTH_ACCESS: - expect(user, request, dataDocId, failuresDocId); + expectSearch(user, request, dataDocId, failuresDocId); break; default: fail("must cover user: " + user); @@ -781,10 +782,10 @@ public void testFailureStoreAccess() throws Exception { for (var user : users) { switch (user) { case DATA_ACCESS, FAILURE_STORE_ACCESS: - expect(user, request, 403); + expectThrows(user, request, 403); break; case ADMIN_USER_NAME, BOTH_ACCESS, STAR_READ_ONLY_ACCESS: - expect(user, request, dataDocId, failuresDocId); + expectSearch(user, request, dataDocId, failuresDocId); break; default: fail("must cover user: " + user); @@ -796,13 +797,13 @@ public void testFailureStoreAccess() throws Exception { for (var user : users) { switch (user) { case DATA_ACCESS: - expect(user, request, dataDocId); + expectSearch(user, request, dataDocId); break; case FAILURE_STORE_ACCESS: - expect(user, request, failuresDocId); + expectSearch(user, request, failuresDocId); break; case ADMIN_USER_NAME, BOTH_ACCESS, STAR_READ_ONLY_ACCESS: - expect(user, request, dataDocId, failuresDocId); + expectSearch(user, request, dataDocId, failuresDocId); break; default: fail("must cover user: " + user); @@ -814,10 +815,10 @@ public void testFailureStoreAccess() throws Exception { for (var user : users) { switch (user) { case DATA_ACCESS, FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS: - expect(user, request, 403); + expectThrows(user, request, 403); break; case ADMIN_USER_NAME, BOTH_ACCESS: - expect(user, request, dataDocId, failuresDocId); + expectSearch(user, request, dataDocId, failuresDocId); break; default: fail("must cover user: " + user); @@ -829,13 +830,13 @@ public void testFailureStoreAccess() throws Exception { for (var user : users) { switch (user) { case DATA_ACCESS, STAR_READ_ONLY_ACCESS: - expect(user, request, dataDocId); + expectSearch(user, request, dataDocId); break; case FAILURE_STORE_ACCESS: - expect(user, request, failuresDocId); + expectSearch(user, request, failuresDocId); break; case ADMIN_USER_NAME, BOTH_ACCESS: - expect(user, request, dataDocId, failuresDocId); + expectSearch(user, request, dataDocId, failuresDocId); break; default: fail("must cover user: " + user); @@ -847,13 +848,13 @@ public void testFailureStoreAccess() throws Exception { for (var user : users) { switch (user) { case FAILURE_STORE_ACCESS: - expect(user, request, 403); + expectThrows(user, request, 403); break; case DATA_ACCESS, STAR_READ_ONLY_ACCESS: - expect(user, request, dataDocId); + expectSearch(user, request, dataDocId); break; case ADMIN_USER_NAME, BOTH_ACCESS: - expect(user, request, dataDocId, failuresDocId); + expectSearch(user, request, dataDocId, failuresDocId); break; default: fail("must cover user: " + user); @@ -865,13 +866,13 @@ public void testFailureStoreAccess() throws Exception { for (var user : users) { switch (user) { case FAILURE_STORE_ACCESS: - expect(user, request, failuresDocId); + expectSearch(user, request, failuresDocId); break; case DATA_ACCESS, STAR_READ_ONLY_ACCESS: - expect(user, request, dataDocId); + expectSearch(user, request, dataDocId); break; case ADMIN_USER_NAME, BOTH_ACCESS: - expect(user, request, dataDocId, failuresDocId); + expectSearch(user, request, dataDocId, failuresDocId); break; default: fail("must cover user: " + user); @@ -883,13 +884,13 @@ public void testFailureStoreAccess() throws Exception { for (var user : users) { switch (user) { case FAILURE_STORE_ACCESS: - expect(user, request, failuresDocId); + expectSearch(user, request, failuresDocId); break; case DATA_ACCESS, STAR_READ_ONLY_ACCESS: - expect(user, request, 403); + expectThrows(user, request, 403); break; case ADMIN_USER_NAME, BOTH_ACCESS: - expect(user, request, dataDocId, failuresDocId); + expectSearch(user, request, dataDocId, failuresDocId); break; default: fail("must cover user: " + user); @@ -901,13 +902,13 @@ public void testFailureStoreAccess() throws Exception { for (var user : users) { switch (user) { case FAILURE_STORE_ACCESS: - expect(user, request, failuresDocId); + expectSearch(user, request, failuresDocId); break; case DATA_ACCESS, STAR_READ_ONLY_ACCESS: - expect(user, request, dataDocId); + expectSearch(user, request, dataDocId); break; case ADMIN_USER_NAME, BOTH_ACCESS: - expect(user, request, dataDocId, failuresDocId); + expectSearch(user, request, dataDocId, failuresDocId); break; default: fail("must cover user: " + user); @@ -919,13 +920,13 @@ public void testFailureStoreAccess() throws Exception { for (var user : users) { switch (user) { case FAILURE_STORE_ACCESS: - expect(user, request, failuresDocId); + expectSearch(user, request, failuresDocId); break; case DATA_ACCESS, STAR_READ_ONLY_ACCESS: - expect(user, request, dataDocId); + expectSearch(user, request, dataDocId); break; case ADMIN_USER_NAME, BOTH_ACCESS: - expect(user, request, dataDocId, failuresDocId); + expectSearch(user, request, dataDocId, failuresDocId); break; default: fail("must cover user: " + user); @@ -1025,10 +1026,10 @@ public void testFailureStoreAccessWithApiKeys() throws Exception { } }"""); - expectUsingApiKey(apiKey, new Search("test1::failures"), failuresDocId); - expectUsingApiKey(apiKey, new Search(failureIndexName), failuresDocId); - expectUsingApiKey(apiKey, new Search(dataIndexName), 403); - expectUsingApiKey(apiKey, new Search("test1"), 403); + expectSearchWithApiKey(apiKey, new Search("test1::failures"), failuresDocId); + expectSearchWithApiKey(apiKey, new Search(failureIndexName), failuresDocId); + expectThrowsWithApiKey(apiKey, new Search(dataIndexName), 403); + expectThrowsWithApiKey(apiKey, new Search("test1"), 403); apiKey = createApiKey(user, """ { @@ -1038,10 +1039,10 @@ public void testFailureStoreAccessWithApiKeys() throws Exception { } }"""); - expectUsingApiKey(apiKey, new Search("test1::failures"), failuresDocId); - expectUsingApiKey(apiKey, new Search(failureIndexName), failuresDocId); - expectUsingApiKey(apiKey, new Search(dataIndexName), 403); - expectUsingApiKey(apiKey, new Search("test1"), 403); + expectSearchWithApiKey(apiKey, new Search("test1::failures"), failuresDocId); + expectSearchWithApiKey(apiKey, new Search(failureIndexName), failuresDocId); + expectThrowsWithApiKey(apiKey, new Search(dataIndexName), 403); + expectThrowsWithApiKey(apiKey, new Search("test1"), 403); apiKey = createApiKey(user, """ { @@ -1051,10 +1052,10 @@ public void testFailureStoreAccessWithApiKeys() throws Exception { } }"""); - expectUsingApiKey(apiKey, new Search("test1::failures"), 403); - expectUsingApiKey(apiKey, new Search(failureIndexName), 403); - expectUsingApiKey(apiKey, new Search(dataIndexName), 403); - expectUsingApiKey(apiKey, new Search("test1"), 403); + expectThrowsWithApiKey(apiKey, new Search("test1::failures"), 403); + expectThrowsWithApiKey(apiKey, new Search(failureIndexName), 403); + expectThrowsWithApiKey(apiKey, new Search(dataIndexName), 403); + expectThrowsWithApiKey(apiKey, new Search("test1"), 403); apiKey = createApiKey(user, """ { @@ -1067,10 +1068,10 @@ public void testFailureStoreAccessWithApiKeys() throws Exception { } }"""); - expectUsingApiKey(apiKey, new Search("test1::failures"), failuresDocId); - expectUsingApiKey(apiKey, new Search(failureIndexName), failuresDocId); - expectUsingApiKey(apiKey, new Search(dataIndexName), 403); - expectUsingApiKey(apiKey, new Search("test1"), 403); + expectSearchWithApiKey(apiKey, new Search("test1::failures"), failuresDocId); + expectSearchWithApiKey(apiKey, new Search(failureIndexName), failuresDocId); + expectThrowsWithApiKey(apiKey, new Search(dataIndexName), 403); + expectThrowsWithApiKey(apiKey, new Search("test1"), 403); apiKey = createApiKey(user, """ { @@ -1082,11 +1083,11 @@ public void testFailureStoreAccessWithApiKeys() throws Exception { } }"""); - expectUsingApiKey(apiKey, new Search("test1::failures"), 403); + expectThrowsWithApiKey(apiKey, new Search("test1::failures"), 403); // funky but correct: assigned role descriptors grant direct access to failure index, limited-by to failure store - expectUsingApiKey(apiKey, new Search(failureIndexName), failuresDocId); - expectUsingApiKey(apiKey, new Search(dataIndexName), 403); - expectUsingApiKey(apiKey, new Search("test1"), 403); + expectSearchWithApiKey(apiKey, new Search(failureIndexName), failuresDocId); + expectThrowsWithApiKey(apiKey, new Search(dataIndexName), 403); + expectThrowsWithApiKey(apiKey, new Search("test1"), 403); upsertRole(""" { @@ -1108,11 +1109,81 @@ public void testFailureStoreAccessWithApiKeys() throws Exception { ] } }"""); - expectUsingApiKey(apiKey, new Search("test1::failures"), 403); + expectThrowsWithApiKey(apiKey, new Search("test1::failures"), 403); // funky but correct: limited-by role descriptors grant direct access to failure index, assigned to failure store - expectUsingApiKey(apiKey, new Search(failureIndexName), failuresDocId); - expectUsingApiKey(apiKey, new Search(dataIndexName), 403); - expectUsingApiKey(apiKey, new Search("test1"), 403); + expectSearchWithApiKey(apiKey, new Search(failureIndexName), failuresDocId); + expectThrowsWithApiKey(apiKey, new Search(dataIndexName), 403); + expectThrowsWithApiKey(apiKey, new Search("test1"), 403); + } + + public void testPit() throws Exception { + List docIds = setupDataStream(); + String dataDocId = "1"; + String failuresDocId = docIds.stream().filter(id -> false == id.equals(dataDocId)).findFirst().get(); + + createUser("user", PASSWORD, "role"); + upsertRole(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["test*"], + "privileges": ["read"] + } + ] + }""", "role"); + + { + expectThrows( + () -> performRequest("user", new Request("POST", Strings.format("/%s/_pit?keep_alive=1m", "test1::failures"))), + 403 + ); + Response pitResponse = performRequest("user", new Request("POST", Strings.format("/%s/_pit?keep_alive=1m", "test1"))); + assertOK(pitResponse); + String pitId = ObjectPath.createFromResponse(pitResponse).evaluate("id"); + assertThat(pitId, notNullValue()); + + var searchRequest = new Request("POST", "/_search"); + searchRequest.setJsonEntity(Strings.format(""" + { + "pit": { + "id": "%s" + } + } + """, pitId)); + Response searchResponse = performRequest("user", searchRequest); + expectSearch(searchResponse, dataDocId); + } + + upsertRole(""" + { + "cluster": ["all"], + "indices": [ + { + "names": ["test*"], + "privileges": ["read_failure_store"] + } + ] + }""", "role"); + + { + expectThrows(() -> performRequest("user", new Request("POST", Strings.format("/%s/_pit?keep_alive=1m", "test1"))), 403); + Response pitResponse = performRequest("user", new Request("POST", Strings.format("/%s/_pit?keep_alive=1m", "test1::failures"))); + assertOK(pitResponse); + String pitId = ObjectPath.createFromResponse(pitResponse).evaluate("id"); + assertThat(pitId, notNullValue()); + + var searchRequest = new Request("POST", "/_search"); + searchRequest.setJsonEntity(Strings.format(""" + { + "pit": { + "id": "%s" + } + } + """, pitId)); + Response searchResponse = performRequest("user", searchRequest); + expectSearch(searchResponse, failuresDocId); + } } public void testDlsFls() throws Exception { @@ -1229,8 +1300,8 @@ public void testDlsFls() throws Exception { ] }""", role); // DLS applies and no docs match the query - expect(user, new Search("test1")); - expect(user, new Search("test1::failures")); + expectSearch(user, new Search("test1")); + expectSearch(user, new Search("test1::failures")); upsertRole(""" { @@ -1244,8 +1315,8 @@ public void testDlsFls() throws Exception { ] }""", role); // DLS applies and doc matches the query - expect(user, new Search("test1"), dataIndexDocId); - expect(user, new Search("test1::failures")); + expectSearch(user, new Search("test1"), dataIndexDocId); + expectSearch(user, new Search("test1::failures")); upsertRole(""" { @@ -1263,7 +1334,7 @@ public void testDlsFls() throws Exception { ] }""", role); // DLS does not apply because there is a section without DLS - expect(user, new Search("test1"), dataIndexDocId); + expectSearch(user, new Search("test1"), dataIndexDocId); } private static void expectThrows(ThrowingRunnable runnable, int statusCode) { @@ -1271,22 +1342,22 @@ private static void expectThrows(ThrowingRunnable runnable, int statusCode) { assertThat(ex.getResponse().getStatusLine().getStatusCode(), equalTo(statusCode)); } - private void expect(String user, Search search, int statusCode) { + private void expectThrows(String user, Search search, int statusCode) { expectThrows(() -> performRequest(user, search.toSearchRequest()), statusCode); expectThrows(() -> performRequest(user, search.toAsyncSearchRequest()), statusCode); } - private void expect(String user, Search search, String... docIds) throws Exception { + private void expectSearch(String user, Search search, String... docIds) throws Exception { expectSearch(performRequestMaybeUsingApiKey(user, search.toSearchRequest()), docIds); expectAsyncSearch(performRequestMaybeUsingApiKey(user, search.toAsyncSearchRequest()), docIds); } - private void expectUsingApiKey(String apiKey, Search search, String... docIds) throws Exception { + private void expectSearchWithApiKey(String apiKey, Search search, String... docIds) throws Exception { expectSearch(performRequestWithApiKey(apiKey, search.toSearchRequest()), docIds); expectAsyncSearch(performRequestWithApiKey(apiKey, search.toAsyncSearchRequest()), docIds); } - private void expectUsingApiKey(String apiKey, Search search, int statusCode) { + private void expectThrowsWithApiKey(String apiKey, Search search, int statusCode) { expectThrows(() -> performRequestWithApiKey(apiKey, search.toSearchRequest()), statusCode); expectThrows(() -> performRequestWithApiKey(apiKey, search.toAsyncSearchRequest()), statusCode); } From f7c5c8e7b1fc45be326b32f548191f5065596c88 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 17 Mar 2025 21:01:34 +0100 Subject: [PATCH 125/131] Changelog --- docs/changelog/123986.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog/123986.yaml b/docs/changelog/123986.yaml index 3166d10fff6eb..61b994f268505 100644 --- a/docs/changelog/123986.yaml +++ b/docs/changelog/123986.yaml @@ -1,5 +1,5 @@ pr: 123986 -summary: Failure store access - selector-aware authorization +summary: Failure Store Access Authorization area: Authorization type: enhancement issues: [] From d05ddcf2dcc37b4d46a3eceb19c22bae256ed2c3 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 19 Mar 2025 11:02:39 +0100 Subject: [PATCH 126/131] Address review comments --- .../security/authz/AuthorizationEngine.java | 37 +++++++++++-------- .../authz/permission/IndicesPermission.java | 14 ++++--- .../authz/privilege/IndexPrivilege.java | 22 ++++++----- .../FailureStoreSecurityRestIT.java | 12 +++--- .../authz/AuthorizedIndicesTests.java | 22 +++-------- 5 files changed, 54 insertions(+), 53 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java index cc173bee44fe8..a718b1dee04ce 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java @@ -13,6 +13,7 @@ import org.elasticsearch.action.IndicesRequest; import org.elasticsearch.action.support.IndexComponentSelector; import org.elasticsearch.action.support.SubscribableListener; +import org.elasticsearch.cluster.metadata.DataStream; import org.elasticsearch.cluster.metadata.IndexAbstraction; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.metadata.ProjectMetadata; @@ -369,21 +370,27 @@ public ActionRequestValidationException validate(ActionRequestValidationExceptio validationException = addValidationError("must specify at least one privilege", validationException); } if (index != null) { - for (RoleDescriptor.IndicesPrivileges indexPrivilege : index) { - if (indexPrivilege.getIndices() != null - && Arrays.stream(indexPrivilege.getIndices()) - // best effort prevent users from attempting to check failure selectors - .anyMatch(idx -> IndexNameExpressionResolver.hasSelector(idx, IndexComponentSelector.FAILURES))) { - validationException = addValidationError( - // TODO adjust message once HasPrivileges check supports checking failure store privileges - "failures selector is not supported in index patterns", - validationException - ); - } - if (indexPrivilege.getPrivileges() != null - && Arrays.stream(indexPrivilege.getPrivileges()) - .anyMatch(p -> "read_failure_store".equals(p) || "manage_failure_store".equals(p))) { - validationException = addValidationError("checking failure store privileges is not supported", validationException); + // no need to validate failure-store related constraints if it's not enabled + if (DataStream.isFailureStoreFeatureFlagEnabled()) { + for (RoleDescriptor.IndicesPrivileges indexPrivilege : index) { + if (indexPrivilege.getIndices() != null + && Arrays.stream(indexPrivilege.getIndices()) + // best effort prevent users from attempting to check failure selectors + .anyMatch(idx -> IndexNameExpressionResolver.hasSelector(idx, IndexComponentSelector.FAILURES))) { + validationException = addValidationError( + // TODO adjust message once HasPrivileges check supports checking failure store privileges + "failures selector is not supported in index patterns", + validationException + ); + } + if (indexPrivilege.getPrivileges() != null + && Arrays.stream(indexPrivilege.getPrivileges()) + .anyMatch(p -> "read_failure_store".equals(p) || "manage_failure_store".equals(p))) { + validationException = addValidationError( + "checking failure store privileges is not supported", + validationException + ); + } } } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 34410c7116201..0a75cdb91c9ba 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -151,20 +151,22 @@ private IsResourceAuthorizedPredicate buildIndexMatcherPredicateForAction(String final boolean isMappingUpdateAction = isMappingUpdateAction(action); for (final Group group : groups) { if (group.actionMatcher.test(action)) { + final List indexList = Arrays.asList(group.indices()); + final boolean dataAccess = group.checkSelector(IndexComponentSelector.DATA); + final boolean failuresAccess = group.checkSelector(IndexComponentSelector.FAILURES); + assert dataAccess || failuresAccess : "group must grant access at least one of [DATA, FAILURES] selectors"; if (group.allowRestrictedIndices) { - List indexList = Arrays.asList(group.indices()); - if (group.checkSelector(IndexComponentSelector.DATA)) { + if (dataAccess) { dataAccessRestrictedIndices.addAll(indexList); } - if (group.checkSelector(IndexComponentSelector.FAILURES)) { + if (failuresAccess) { failuresAccessRestrictedIndices.addAll(indexList); } } else { - List indexList = Arrays.asList(group.indices()); - if (group.checkSelector(IndexComponentSelector.DATA)) { + if (dataAccess) { dataAccessOrdinaryIndices.addAll(indexList); } - if (group.checkSelector(IndexComponentSelector.FAILURES)) { + if (failuresAccess) { failuresAccessOrdinaryIndices.addAll(indexList); } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java index b188ad8fca3b4..f93af05cfd7b1 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java @@ -88,6 +88,7 @@ public final class IndexPrivilege extends Privilege { ResolveIndexAction.NAME, TransportResolveClusterAction.NAME ); + private static final Automaton READ_FAILURE_STORE_AUTOMATON = patterns("indices:data/read/*", ResolveIndexAction.NAME); private static final Automaton READ_CROSS_CLUSTER_AUTOMATON = patterns( "internal:transport/proxy/indices:data/read/*", TransportClusterSearchShardsAction.TYPE.name(), @@ -185,16 +186,6 @@ public final class IndexPrivilege extends Privilege { public static final IndexPrivilege NONE = new IndexPrivilege("none", Automatons.EMPTY); public static final IndexPrivilege ALL = new IndexPrivilege("all", ALL_AUTOMATON, IndexComponentSelectorPredicate.ALL); - public static final IndexPrivilege READ_FAILURE_STORE = new IndexPrivilege( - "read_failure_store", - READ_AUTOMATON, - IndexComponentSelectorPredicate.FAILURES - ); - public static final IndexPrivilege MANAGE_FAILURE_STORE = new IndexPrivilege( - "manage_failure_store", - MANAGE_AUTOMATON, - IndexComponentSelectorPredicate.FAILURES - ); public static final IndexPrivilege READ = new IndexPrivilege("read", READ_AUTOMATON); public static final IndexPrivilege READ_CROSS_CLUSTER = new IndexPrivilege("read_cross_cluster", READ_CROSS_CLUSTER_AUTOMATON); public static final IndexPrivilege CREATE = new IndexPrivilege("create", CREATE_AUTOMATON); @@ -225,6 +216,17 @@ public final class IndexPrivilege extends Privilege { CROSS_CLUSTER_REPLICATION_INTERNAL_AUTOMATON ); + public static final IndexPrivilege READ_FAILURE_STORE = new IndexPrivilege( + "read_failure_store", + READ_FAILURE_STORE_AUTOMATON, + IndexComponentSelectorPredicate.FAILURES + ); + public static final IndexPrivilege MANAGE_FAILURE_STORE = new IndexPrivilege( + "manage_failure_store", + MANAGE_AUTOMATON, + IndexComponentSelectorPredicate.FAILURES + ); + /** * If you are adding a new named index privilege, also add it to the * docs. diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java index 12dab945a54e1..8518f530a0896 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java @@ -1212,7 +1212,7 @@ public void testDlsFls() throws Exception { // FLS applies to regular data stream assertSearchResponseContainsExpectedIndicesAndFields( - performRequest(user, new Search("test1").toSearchRequest()), + performRequest(user, new Search(randomFrom("test1", "test1::data")).toSearchRequest()), Map.of(dataIndexName, Set.of("@timestamp", "age")) ); @@ -1246,7 +1246,7 @@ public void testDlsFls() throws Exception { // FLS applies to regular data stream assertSearchResponseContainsExpectedIndicesAndFields( - performRequest(user, new Search("test1").toSearchRequest()), + performRequest(user, new Search(randomFrom("test1", "test1::data")).toSearchRequest()), Map.of(dataIndexName, Set.of("@timestamp", "age")) ); @@ -1277,7 +1277,7 @@ public void testDlsFls() throws Exception { // since there is a section without FLS, no FLS applies assertSearchResponseContainsExpectedIndicesAndFields( - performRequest(user, new Search("test1").toSearchRequest()), + performRequest(user, new Search(randomFrom("test1", "test1::data")).toSearchRequest()), Map.of(dataIndexName, Set.of("@timestamp", "age", "name", "email")) ); @@ -1300,7 +1300,7 @@ public void testDlsFls() throws Exception { ] }""", role); // DLS applies and no docs match the query - expectSearch(user, new Search("test1")); + expectSearch(user, new Search(randomFrom("test1", "test1::data"))); expectSearch(user, new Search("test1::failures")); upsertRole(""" @@ -1315,7 +1315,7 @@ public void testDlsFls() throws Exception { ] }""", role); // DLS applies and doc matches the query - expectSearch(user, new Search("test1"), dataIndexDocId); + expectSearch(user, new Search(randomFrom("test1", "test1::data")), dataIndexDocId); expectSearch(user, new Search("test1::failures")); upsertRole(""" @@ -1334,7 +1334,7 @@ public void testDlsFls() throws Exception { ] }""", role); // DLS does not apply because there is a section without DLS - expectSearch(user, new Search("test1"), dataIndexDocId); + expectSearch(user, new Search(randomFrom("test1", "test1::data")), dataIndexDocId); } private static void expectThrows(ThrowingRunnable runnable, int statusCode) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java index 5be000bf5f925..3f913b0b84205 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java @@ -275,12 +275,7 @@ public void testDataStreamsAreNotIncludedInAuthorizedIndices() { true ) .put(new IndexMetadata.Builder(backingIndex).settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) - .put( - DataStreamTestHelper.newInstance( - "adatastream1", - List.of(new Index(DataStream.getDefaultBackingIndexName("adatastream1", 1), "_na_")) - ) - ) + .put(DataStreamTestHelper.newInstance("adatastream1", List.of(new Index(backingIndex, "_na_")))) .build(); final PlainActionFuture future = new PlainActionFuture<>(); final Set descriptors = Sets.newHashSet(aStarRole, bRole); @@ -370,8 +365,8 @@ public void testDataStreamsAreNotIncludedInAuthorizedIndicesWithFailuresSelector .put( DataStreamTestHelper.newInstance( "adatastream1", - List.of(new Index(DataStream.getDefaultBackingIndexName("adatastream1", 1), "_na_")), - List.of(new Index(DataStream.getDefaultFailureStoreName("adatastream1", 1, 1), "_na_")) + List.of(new Index(backingIndex, "_na_")), + List.of(new Index(failureIndex, "_na_")) ) ) .build(); @@ -464,8 +459,8 @@ public void testDataStreamsAreIncludedInAuthorizedIndicesWithFailuresSelector() .put( DataStreamTestHelper.newInstance( "adatastream1", - List.of(new Index(DataStream.getDefaultBackingIndexName("adatastream1", 1), "_na_")), - List.of(new Index(DataStream.getDefaultFailureStoreName("adatastream1", 1, 1), "_na_")) + List.of(new Index(backingIndex, "_na_")), + List.of(new Index(failureIndex, "_na_")) ) ) .build(); @@ -552,12 +547,7 @@ public void testDataStreamsAreIncludedInAuthorizedIndices() { true ) .put(new IndexMetadata.Builder(backingIndex).settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) - .put( - DataStreamTestHelper.newInstance( - "adatastream1", - List.of(new Index(DataStream.getDefaultBackingIndexName("adatastream1", 1), "_na_")) - ) - ) + .put(DataStreamTestHelper.newInstance("adatastream1", List.of(new Index(backingIndex, "_na_")))) .build(); final PlainActionFuture future = new PlainActionFuture<>(); final Set descriptors = Sets.newHashSet(aStarRole, bRole); From 6465b7a100e18707586c53f060090eb7d0468dc9 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 19 Mar 2025 11:03:20 +0100 Subject: [PATCH 127/131] Delete docs/changelog/123986.yaml --- docs/changelog/123986.yaml | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 docs/changelog/123986.yaml diff --git a/docs/changelog/123986.yaml b/docs/changelog/123986.yaml deleted file mode 100644 index 61b994f268505..0000000000000 --- a/docs/changelog/123986.yaml +++ /dev/null @@ -1,5 +0,0 @@ -pr: 123986 -summary: Failure Store Access Authorization -area: Authorization -type: enhancement -issues: [] From 5b71fb755c77bb2fe815e104078b55619d0d942f Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 19 Mar 2025 11:03:38 +0100 Subject: [PATCH 128/131] RM changelog --- docs/changelog/123986.yaml | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 docs/changelog/123986.yaml diff --git a/docs/changelog/123986.yaml b/docs/changelog/123986.yaml deleted file mode 100644 index 61b994f268505..0000000000000 --- a/docs/changelog/123986.yaml +++ /dev/null @@ -1,5 +0,0 @@ -pr: 123986 -summary: Failure Store Access Authorization -area: Authorization -type: enhancement -issues: [] From a5b59bb6af2fd9d59f0da8cf11d17f4f7a482cec Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 19 Mar 2025 18:22:46 +0100 Subject: [PATCH 129/131] Fix edge cases --- .../authz/permission/IndicesPermission.java | 27 +- .../FailureStoreSecurityRestIT.java | 320 ++++++++++++++---- .../xpack/security/authz/RBACEngine.java | 15 +- 3 files changed, 267 insertions(+), 95 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 0a75cdb91c9ba..46b84bb34a0dc 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -415,15 +415,6 @@ private IndexResource(String name, @Nullable IndexAbstraction abstraction, @Null assert name != null : "Resource name cannot be null"; assert abstraction == null || abstraction.getName().equals(name) : "Index abstraction has unexpected name [" + abstraction.getName() + "] vs [" + name + "]"; - assert abstraction == null - || selector == null - || IndexComponentSelector.FAILURES.equals(selector) == false - || abstraction.isDataStreamRelated() - : "Invalid index component selector [" - + selector.getKey() - + "] applied to abstraction of type [" - + abstraction.getType() - + "]"; this.name = name; this.indexAbstraction = abstraction; this.selector = selector; @@ -453,13 +444,17 @@ public boolean isPartOfDataStream() { public boolean checkIndex(Group group) { final DataStream ds = indexAbstraction == null ? null : indexAbstraction.getParentDataStream(); if (ds != null) { - // failure indices are special: when accessed directly (not through ::failures on parent data stream) they are accessed - // implicitly as data. However, authz to the parent data stream happens via the failures selector - final IndexComponentSelector selectorToCheck = indexAbstraction.isFailureIndexOfDataStream() - ? IndexComponentSelector.FAILURES - : selector; - if (group.checkSelector(selectorToCheck) && group.checkIndex(ds.getName())) { - return true; + boolean authorizeViaParentDataStream = indexAbstraction.isFailureIndexOfDataStream() + || false == IndexComponentSelector.FAILURES.equals(selector); + if (authorizeViaParentDataStream) { + // failure indices are special: when accessed directly (not through ::failures on parent data stream) they are accessed + // implicitly as data. However, authz to the parent data stream happens via the failures selector + final IndexComponentSelector selectorToCheck = indexAbstraction.isFailureIndexOfDataStream() + ? IndexComponentSelector.FAILURES + : IndexComponentSelector.DATA; + if (group.checkSelector(selectorToCheck) && group.checkIndex(ds.getName())) { + return true; + } } } return group.checkSelector(selector) && group.checkIndex(name); diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java index 8518f530a0896..bfdea27eee98c 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/failurestore/FailureStoreSecurityRestIT.java @@ -77,9 +77,13 @@ protected Settings restAdminSettings() { private static final String ASYNC_SEARCH_TIMEOUT = "30s"; - private static final String ADMIN_USER_NAME = "admin_user"; + private static final String ADMIN_USER = "admin_user"; private static final String DATA_ACCESS = "data_access"; + private static final String BACKING_INDEX_DATA_ACCESS = "backing_index_data_access"; + private static final String BACKING_INDEX_FAILURE_ACCESS = "backing_index_failure_access"; + private static final String FAILURE_INDEX_DATA_ACCESS = "failure_index_data_access"; + private static final String FAILURE_INDEX_FAILURE_ACCESS = "failure_index_failure_access"; private static final String STAR_READ_ONLY_ACCESS = "star_read_only"; private static final String FAILURE_STORE_ACCESS = "failure_store_access"; private static final String BOTH_ACCESS = "both_access"; @@ -278,6 +282,16 @@ public void testRoleWithSelectorInIndexPattern() throws Exception { @SuppressWarnings("unchecked") public void testFailureStoreAccess() throws Exception { + List docIds = setupDataStream(); + assertThat(docIds.size(), equalTo(2)); + assertThat(docIds, hasItem("1")); + String dataDocId = "1"; + String failuresDocId = docIds.stream().filter(id -> false == id.equals(dataDocId)).findFirst().get(); + + Tuple backingIndices = getSingleDataAndFailureIndices("test1"); + String dataIndexName = backingIndices.v1(); + String failureIndexName = backingIndices.v2(); + createUser(DATA_ACCESS, PASSWORD, DATA_ACCESS); upsertRole(Strings.format(""" { @@ -359,15 +373,37 @@ public void testFailureStoreAccess() throws Exception { } """); - List docIds = setupDataStream(); - assertThat(docIds.size(), equalTo(2)); - assertThat(docIds, hasItem("1")); - String dataDocId = "1"; - String failuresDocId = docIds.stream().filter(id -> false == id.equals(dataDocId)).findFirst().get(); + createUser(BACKING_INDEX_DATA_ACCESS, PASSWORD, BACKING_INDEX_DATA_ACCESS); + upsertRole(Strings.format(""" + { + "cluster": ["all"], + "indices": [{"names": ["%s"], "privileges": ["read"]}] + }""", dataIndexName), BACKING_INDEX_DATA_ACCESS); + createAndStoreApiKey(BACKING_INDEX_DATA_ACCESS, null); - Tuple backingIndices = getSingleDataAndFailureIndices("test1"); - String dataIndexName = backingIndices.v1(); - String failureIndexName = backingIndices.v2(); + createUser(BACKING_INDEX_FAILURE_ACCESS, PASSWORD, BACKING_INDEX_FAILURE_ACCESS); + upsertRole(Strings.format(""" + { + "cluster": ["all"], + "indices": [{"names": ["%s"], "privileges": ["read_failure_store"]}] + }""", dataIndexName), BACKING_INDEX_FAILURE_ACCESS); + createAndStoreApiKey(BACKING_INDEX_FAILURE_ACCESS, null); + + createUser(FAILURE_INDEX_DATA_ACCESS, PASSWORD, FAILURE_INDEX_DATA_ACCESS); + upsertRole(Strings.format(""" + { + "cluster": ["all"], + "indices": [{"names": ["%s"], "privileges": ["read"]}] + }""", failureIndexName), FAILURE_INDEX_DATA_ACCESS); + createAndStoreApiKey(FAILURE_INDEX_DATA_ACCESS, null); + + createUser(FAILURE_INDEX_FAILURE_ACCESS, PASSWORD, FAILURE_INDEX_FAILURE_ACCESS); + upsertRole(Strings.format(""" + { + "cluster": ["all"], + "indices": [{"names": ["%s"], "privileges": ["read_failure_store"]}] + }""", failureIndexName), FAILURE_INDEX_FAILURE_ACCESS); + createAndStoreApiKey(FAILURE_INDEX_FAILURE_ACCESS, null); Request aliasRequest = new Request("POST", "/_aliases"); aliasRequest.setJsonEntity(""" @@ -384,17 +420,28 @@ public void testFailureStoreAccess() throws Exception { """); assertOK(adminClient().performRequest(aliasRequest)); - List users = List.of(DATA_ACCESS, FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, ADMIN_USER_NAME); + List users = List.of( + DATA_ACCESS, + FAILURE_STORE_ACCESS, + STAR_READ_ONLY_ACCESS, + BOTH_ACCESS, + ADMIN_USER, + BACKING_INDEX_DATA_ACCESS, + BACKING_INDEX_FAILURE_ACCESS, + FAILURE_INDEX_DATA_ACCESS, + FAILURE_INDEX_FAILURE_ACCESS + ); // search data { var request = new Search(randomFrom("test1::data", "test1")); for (var user : users) { switch (user) { - case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + case ADMIN_USER, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expectSearch(user, request, dataDocId); break; - case FAILURE_STORE_ACCESS: + case FAILURE_STORE_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS, + FAILURE_INDEX_FAILURE_ACCESS: expectThrows(user, request, 403); break; default: @@ -406,10 +453,11 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test1", "?ignore_unavailable=true"); for (var user : users) { switch (user) { - case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + case ADMIN_USER, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expectSearch(user, request, dataDocId); break; - case FAILURE_STORE_ACCESS: + case FAILURE_STORE_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS, + FAILURE_INDEX_FAILURE_ACCESS: expectSearch(user, request); break; default: @@ -421,10 +469,11 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test-alias"); for (var user : users) { switch (user) { - case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + case ADMIN_USER, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expectSearch(user, request, dataDocId); break; - case FAILURE_STORE_ACCESS: + case FAILURE_STORE_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS, + FAILURE_INDEX_FAILURE_ACCESS: expectThrows(user, request, 403); break; default: @@ -436,10 +485,11 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test-alias", "?ignore_unavailable=true"); for (var user : users) { switch (user) { - case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + case ADMIN_USER, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expectSearch(user, request, dataDocId); break; - case FAILURE_STORE_ACCESS: + case FAILURE_STORE_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS, + FAILURE_INDEX_FAILURE_ACCESS: expectSearch(user, request); break; default: @@ -451,10 +501,11 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test*"); for (var user : users) { switch (user) { - case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + case ADMIN_USER, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expectSearch(user, request, dataDocId); break; - case FAILURE_STORE_ACCESS: + case FAILURE_STORE_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS, + FAILURE_INDEX_FAILURE_ACCESS: expectSearch(user, request); break; default: @@ -466,10 +517,11 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("*1"); for (var user : users) { switch (user) { - case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + case ADMIN_USER, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expectSearch(user, request, dataDocId); break; - case FAILURE_STORE_ACCESS: + case FAILURE_STORE_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS, + FAILURE_INDEX_FAILURE_ACCESS: expectSearch(user, request); break; default: @@ -477,13 +529,15 @@ public void testFailureStoreAccess() throws Exception { } } } + // note expand_wildcards does not include hidden here for (var request : List.of(new Search("*"), new Search("_all"), new Search(""))) { for (var user : users) { switch (user) { - case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + case ADMIN_USER, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expectSearch(user, request, dataDocId); break; - case FAILURE_STORE_ACCESS: + case FAILURE_STORE_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS, + BACKING_INDEX_DATA_ACCESS: expectSearch(user, request); break; default: @@ -495,10 +549,10 @@ public void testFailureStoreAccess() throws Exception { var request = new Search(".ds*"); for (var user : users) { switch (user) { - case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + case ADMIN_USER, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, BACKING_INDEX_DATA_ACCESS: expectSearch(user, request, dataDocId); break; - case FAILURE_STORE_ACCESS: + case FAILURE_STORE_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS: expectSearch(user, request); break; default: @@ -510,10 +564,10 @@ public void testFailureStoreAccess() throws Exception { var request = new Search(dataIndexName); for (var user : users) { switch (user) { - case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + case ADMIN_USER, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, BACKING_INDEX_DATA_ACCESS: expectSearch(user, request, dataDocId); break; - case FAILURE_STORE_ACCESS: + case FAILURE_STORE_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS: expectThrows(user, request, 403); break; default: @@ -525,10 +579,10 @@ public void testFailureStoreAccess() throws Exception { var request = new Search(dataIndexName, "?ignore_unavailable=true"); for (var user : users) { switch (user) { - case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + case ADMIN_USER, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, BACKING_INDEX_DATA_ACCESS: expectSearch(user, request, dataDocId); break; - case FAILURE_STORE_ACCESS: + case FAILURE_STORE_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS: expectSearch(user, request); break; default: @@ -540,10 +594,11 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test2"); for (var user : users) { switch (user) { - case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + case ADMIN_USER, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: expectThrows(user, request, 404); break; - case FAILURE_STORE_ACCESS: + case FAILURE_STORE_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_FAILURE_ACCESS, + FAILURE_INDEX_DATA_ACCESS: expectThrows(user, request, 403); break; default: @@ -555,7 +610,8 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test2", "?ignore_unavailable=true"); for (var user : users) { switch (user) { - case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, FAILURE_STORE_ACCESS: + case ADMIN_USER, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, FAILURE_STORE_ACCESS, BACKING_INDEX_DATA_ACCESS, + BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS: expectSearch(user, request); break; default: @@ -569,10 +625,11 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test1::failures"); for (var user : users) { switch (user) { - case DATA_ACCESS, STAR_READ_ONLY_ACCESS: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, + FAILURE_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS: expectThrows(user, request, 403); break; - case ADMIN_USER_NAME, FAILURE_STORE_ACCESS, BOTH_ACCESS: + case ADMIN_USER, FAILURE_STORE_ACCESS, BOTH_ACCESS: expectSearch(user, request, failuresDocId); break; default: @@ -584,10 +641,11 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test1::failures", "?ignore_unavailable=true"); for (var user : users) { switch (user) { - case DATA_ACCESS, STAR_READ_ONLY_ACCESS: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, + FAILURE_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS: expectSearch(user, request); break; - case ADMIN_USER_NAME, FAILURE_STORE_ACCESS, BOTH_ACCESS: + case ADMIN_USER, FAILURE_STORE_ACCESS, BOTH_ACCESS: expectSearch(user, request, failuresDocId); break; default: @@ -599,10 +657,11 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test-alias::failures"); for (var user : users) { switch (user) { - case DATA_ACCESS, STAR_READ_ONLY_ACCESS: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, + FAILURE_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS: expectThrows(user, request, 403); break; - case ADMIN_USER_NAME, FAILURE_STORE_ACCESS, BOTH_ACCESS: + case ADMIN_USER, FAILURE_STORE_ACCESS, BOTH_ACCESS: expectSearch(user, request, failuresDocId); break; default: @@ -614,10 +673,11 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test-alias::failures", "?ignore_unavailable=true"); for (var user : users) { switch (user) { - case DATA_ACCESS, STAR_READ_ONLY_ACCESS: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, + FAILURE_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS: expectSearch(user, request); break; - case ADMIN_USER_NAME, FAILURE_STORE_ACCESS, BOTH_ACCESS: + case ADMIN_USER, FAILURE_STORE_ACCESS, BOTH_ACCESS: expectSearch(user, request, failuresDocId); break; default: @@ -629,10 +689,11 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test*::failures"); for (var user : users) { switch (user) { - case DATA_ACCESS, STAR_READ_ONLY_ACCESS: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, + FAILURE_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS: expectSearch(user, request); break; - case ADMIN_USER_NAME, FAILURE_STORE_ACCESS, BOTH_ACCESS: + case ADMIN_USER, FAILURE_STORE_ACCESS, BOTH_ACCESS: expectSearch(user, request, failuresDocId); break; default: @@ -644,10 +705,11 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("*1::failures"); for (var user : users) { switch (user) { - case DATA_ACCESS, STAR_READ_ONLY_ACCESS: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, + FAILURE_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS: expectSearch(user, request); break; - case ADMIN_USER_NAME, FAILURE_STORE_ACCESS, BOTH_ACCESS: + case ADMIN_USER, FAILURE_STORE_ACCESS, BOTH_ACCESS: expectSearch(user, request, failuresDocId); break; default: @@ -659,10 +721,11 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("*::failures"); for (var user : users) { switch (user) { - case DATA_ACCESS, STAR_READ_ONLY_ACCESS: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, + FAILURE_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS: expectSearch(user, request); break; - case ADMIN_USER_NAME, FAILURE_STORE_ACCESS, BOTH_ACCESS: + case ADMIN_USER, FAILURE_STORE_ACCESS, BOTH_ACCESS: expectSearch(user, request, failuresDocId); break; default: @@ -674,10 +737,10 @@ public void testFailureStoreAccess() throws Exception { var request = new Search(".fs*"); for (var user : users) { switch (user) { - case DATA_ACCESS: + case DATA_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: expectSearch(user, request); break; - case ADMIN_USER_NAME, FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + case ADMIN_USER, FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, FAILURE_INDEX_DATA_ACCESS: expectSearch(user, request, failuresDocId); break; default: @@ -689,10 +752,10 @@ public void testFailureStoreAccess() throws Exception { var request = new Search(failureIndexName); for (var user : users) { switch (user) { - case DATA_ACCESS: + case DATA_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: expectThrows(user, request, 403); break; - case ADMIN_USER_NAME, FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + case ADMIN_USER, FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, FAILURE_INDEX_DATA_ACCESS: expectSearch(user, request, failuresDocId); break; default: @@ -704,10 +767,10 @@ public void testFailureStoreAccess() throws Exception { var request = new Search(failureIndexName, "?ignore_unavailable=true"); for (var user : users) { switch (user) { - case DATA_ACCESS: + case DATA_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: expectSearch(user, request); break; - case ADMIN_USER_NAME, FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS: + case ADMIN_USER, FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, FAILURE_INDEX_DATA_ACCESS: expectSearch(user, request, failuresDocId); break; default: @@ -715,14 +778,99 @@ public void testFailureStoreAccess() throws Exception { } } } + { + var request = new Search(failureIndexName + "::failures"); + for (var user : users) { + switch (user) { + case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, + FAILURE_INDEX_DATA_ACCESS: + expectThrows(user, request, 403); + break; + case FAILURE_STORE_ACCESS, BOTH_ACCESS, ADMIN_USER, FAILURE_INDEX_FAILURE_ACCESS: + expectThrows(user, request, 404); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search(failureIndexName + "::failures", "?ignore_unavailable=true"); + for (var user : users) { + switch (user) { + case DATA_ACCESS, FAILURE_STORE_ACCESS, ADMIN_USER, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, BACKING_INDEX_DATA_ACCESS, + BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: + expectSearch(user, request); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search(dataIndexName + "::failures"); + for (var user : users) { + switch (user) { + case STAR_READ_ONLY_ACCESS, BOTH_ACCESS, DATA_ACCESS, FAILURE_STORE_ACCESS, FAILURE_INDEX_DATA_ACCESS, + FAILURE_INDEX_FAILURE_ACCESS, BACKING_INDEX_DATA_ACCESS: + expectThrows(user, request, 403); + break; + case ADMIN_USER, BACKING_INDEX_FAILURE_ACCESS: + expectThrows(user, request, 404); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search(dataIndexName + "::failures", "?ignore_unavailable=true"); + for (var user : users) { + switch (user) { + case DATA_ACCESS, FAILURE_STORE_ACCESS, ADMIN_USER, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, BACKING_INDEX_DATA_ACCESS, + BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: + expectSearch(user, request); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search(".fs*::failures"); + for (var user : users) { + switch (user) { + case DATA_ACCESS, FAILURE_STORE_ACCESS, ADMIN_USER, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, BACKING_INDEX_DATA_ACCESS, + BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: + expectSearch(user, request); + break; + default: + fail("must cover user: " + user); + } + } + } + { + var request = new Search(".ds*::failures"); + for (var user : users) { + switch (user) { + case DATA_ACCESS, FAILURE_STORE_ACCESS, ADMIN_USER, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, BACKING_INDEX_DATA_ACCESS, + BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: + expectSearch(user, request); + break; + default: + fail("must cover user: " + user); + } + } + } { var request = new Search("test2::failures"); for (var user : users) { switch (user) { - case DATA_ACCESS, STAR_READ_ONLY_ACCESS: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, + FAILURE_INDEX_DATA_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: expectThrows(user, request, 403); break; - case ADMIN_USER_NAME, FAILURE_STORE_ACCESS, BOTH_ACCESS: + case ADMIN_USER, FAILURE_STORE_ACCESS, BOTH_ACCESS: expectThrows(user, request, 404); break; default: @@ -734,7 +882,8 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test2::failures", "?ignore_unavailable=true"); for (var user : users) { switch (user) { - case ADMIN_USER_NAME, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, FAILURE_STORE_ACCESS: + case ADMIN_USER, DATA_ACCESS, STAR_READ_ONLY_ACCESS, BOTH_ACCESS, FAILURE_STORE_ACCESS, BACKING_INDEX_DATA_ACCESS, + BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: expectSearch(user, request); break; default: @@ -748,10 +897,11 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test1,test1::failures"); for (var user : users) { switch (user) { - case DATA_ACCESS, FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS: + case DATA_ACCESS, FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, + FAILURE_INDEX_DATA_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: expectThrows(user, request, 403); break; - case ADMIN_USER_NAME, BOTH_ACCESS: + case ADMIN_USER, BOTH_ACCESS: expectSearch(user, request, dataDocId, failuresDocId); break; default: @@ -769,9 +919,12 @@ public void testFailureStoreAccess() throws Exception { case FAILURE_STORE_ACCESS: expectSearch(user, request, failuresDocId); break; - case ADMIN_USER_NAME, BOTH_ACCESS: + case ADMIN_USER, BOTH_ACCESS: expectSearch(user, request, dataDocId, failuresDocId); break; + case BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: + expectSearch(user, request); + break; default: fail("must cover user: " + user); } @@ -781,10 +934,11 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test1," + failureIndexName); for (var user : users) { switch (user) { - case DATA_ACCESS, FAILURE_STORE_ACCESS: + case DATA_ACCESS, FAILURE_STORE_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, + FAILURE_INDEX_DATA_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: expectThrows(user, request, 403); break; - case ADMIN_USER_NAME, BOTH_ACCESS, STAR_READ_ONLY_ACCESS: + case ADMIN_USER, BOTH_ACCESS, STAR_READ_ONLY_ACCESS: expectSearch(user, request, dataDocId, failuresDocId); break; default: @@ -799,12 +953,15 @@ public void testFailureStoreAccess() throws Exception { case DATA_ACCESS: expectSearch(user, request, dataDocId); break; - case FAILURE_STORE_ACCESS: + case FAILURE_STORE_ACCESS, FAILURE_INDEX_DATA_ACCESS: expectSearch(user, request, failuresDocId); break; - case ADMIN_USER_NAME, BOTH_ACCESS, STAR_READ_ONLY_ACCESS: + case ADMIN_USER, BOTH_ACCESS, STAR_READ_ONLY_ACCESS: expectSearch(user, request, dataDocId, failuresDocId); break; + case BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: + expectSearch(user, request); + break; default: fail("must cover user: " + user); } @@ -814,10 +971,11 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test1::failures," + dataIndexName); for (var user : users) { switch (user) { - case DATA_ACCESS, FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS: + case DATA_ACCESS, FAILURE_STORE_ACCESS, STAR_READ_ONLY_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, + FAILURE_INDEX_DATA_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: expectThrows(user, request, 403); break; - case ADMIN_USER_NAME, BOTH_ACCESS: + case ADMIN_USER, BOTH_ACCESS: expectSearch(user, request, dataDocId, failuresDocId); break; default: @@ -829,15 +987,18 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test1::failures," + dataIndexName, "?ignore_unavailable=true"); for (var user : users) { switch (user) { - case DATA_ACCESS, STAR_READ_ONLY_ACCESS: + case DATA_ACCESS, BACKING_INDEX_DATA_ACCESS, STAR_READ_ONLY_ACCESS: expectSearch(user, request, dataDocId); break; case FAILURE_STORE_ACCESS: expectSearch(user, request, failuresDocId); break; - case ADMIN_USER_NAME, BOTH_ACCESS: + case ADMIN_USER, BOTH_ACCESS: expectSearch(user, request, dataDocId, failuresDocId); break; + case BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: + expectSearch(user, request); + break; default: fail("must cover user: " + user); } @@ -847,13 +1008,14 @@ public void testFailureStoreAccess() throws Exception { var request = new Search("test1,*::failures"); for (var user : users) { switch (user) { - case FAILURE_STORE_ACCESS: + case FAILURE_STORE_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS, + FAILURE_INDEX_FAILURE_ACCESS: expectThrows(user, request, 403); break; case DATA_ACCESS, STAR_READ_ONLY_ACCESS: expectSearch(user, request, dataDocId); break; - case ADMIN_USER_NAME, BOTH_ACCESS: + case ADMIN_USER, BOTH_ACCESS: expectSearch(user, request, dataDocId, failuresDocId); break; default: @@ -871,9 +1033,12 @@ public void testFailureStoreAccess() throws Exception { case DATA_ACCESS, STAR_READ_ONLY_ACCESS: expectSearch(user, request, dataDocId); break; - case ADMIN_USER_NAME, BOTH_ACCESS: + case ADMIN_USER, BOTH_ACCESS: expectSearch(user, request, dataDocId, failuresDocId); break; + case BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: + expectSearch(user, request); + break; default: fail("must cover user: " + user); } @@ -886,10 +1051,11 @@ public void testFailureStoreAccess() throws Exception { case FAILURE_STORE_ACCESS: expectSearch(user, request, failuresDocId); break; - case DATA_ACCESS, STAR_READ_ONLY_ACCESS: + case DATA_ACCESS, STAR_READ_ONLY_ACCESS, BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, + FAILURE_INDEX_DATA_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: expectThrows(user, request, 403); break; - case ADMIN_USER_NAME, BOTH_ACCESS: + case ADMIN_USER, BOTH_ACCESS: expectSearch(user, request, dataDocId, failuresDocId); break; default: @@ -907,9 +1073,12 @@ public void testFailureStoreAccess() throws Exception { case DATA_ACCESS, STAR_READ_ONLY_ACCESS: expectSearch(user, request, dataDocId); break; - case ADMIN_USER_NAME, BOTH_ACCESS: + case ADMIN_USER, BOTH_ACCESS: expectSearch(user, request, dataDocId, failuresDocId); break; + case BACKING_INDEX_DATA_ACCESS, BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_DATA_ACCESS, FAILURE_INDEX_FAILURE_ACCESS: + expectSearch(user, request); + break; default: fail("must cover user: " + user); } @@ -925,9 +1094,12 @@ public void testFailureStoreAccess() throws Exception { case DATA_ACCESS, STAR_READ_ONLY_ACCESS: expectSearch(user, request, dataDocId); break; - case ADMIN_USER_NAME, BOTH_ACCESS: + case ADMIN_USER, BOTH_ACCESS: expectSearch(user, request, dataDocId, failuresDocId); break; + case BACKING_INDEX_FAILURE_ACCESS, FAILURE_INDEX_FAILURE_ACCESS, BACKING_INDEX_DATA_ACCESS, FAILURE_INDEX_DATA_ACCESS: + expectSearch(user, request); + break; default: fail("must cover user: " + user); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java index 7a1fedb0805eb..94f18722e5ac8 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java @@ -941,13 +941,18 @@ static AuthorizedIndices resolveAuthorizedIndicesFromRole( // more efficient than checking the index name first because we recommend grant privileges over data stream // instead of backing indices. if (indexAbstraction.getParentDataStream() != null) { - if (predicate.test( - indexAbstraction.getParentDataStream(), - // access to failure indices is authorized via failures-based selectors on the parent data stream _not_ via data ones - indexAbstraction.isFailureIndexOfDataStream() ? IndexComponentSelector.FAILURES : selector - )) { + boolean authorizeViaParentDataStream = indexAbstraction.isFailureIndexOfDataStream() + || false == IndexComponentSelector.FAILURES.equals(selector); + if (authorizeViaParentDataStream + && predicate.test( + indexAbstraction.getParentDataStream(), + // access to failure indices is authorized via failures-based selectors on the parent data stream _not_ via data + // ones + indexAbstraction.isFailureIndexOfDataStream() ? IndexComponentSelector.FAILURES : IndexComponentSelector.DATA + )) { return true; } + } return predicate.test(indexAbstraction, selector); }); From 91879e712a64b3d73a92c26c5b9ea31a0fc2cb20 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 19 Mar 2025 21:29:21 +0100 Subject: [PATCH 130/131] Clean up --- .../authz/permission/IndicesPermission.java | 15 ++++++------ .../xpack/security/authz/RBACEngine.java | 23 +++++++++---------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 46b84bb34a0dc..3ad3f6fa4f7a6 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -444,18 +444,17 @@ public boolean isPartOfDataStream() { public boolean checkIndex(Group group) { final DataStream ds = indexAbstraction == null ? null : indexAbstraction.getParentDataStream(); if (ds != null) { - boolean authorizeViaParentDataStream = indexAbstraction.isFailureIndexOfDataStream() - || false == IndexComponentSelector.FAILURES.equals(selector); - if (authorizeViaParentDataStream) { + if (indexAbstraction.isFailureIndexOfDataStream()) { // failure indices are special: when accessed directly (not through ::failures on parent data stream) they are accessed // implicitly as data. However, authz to the parent data stream happens via the failures selector - final IndexComponentSelector selectorToCheck = indexAbstraction.isFailureIndexOfDataStream() - ? IndexComponentSelector.FAILURES - : IndexComponentSelector.DATA; - if (group.checkSelector(selectorToCheck) && group.checkIndex(ds.getName())) { + if (group.checkSelector(IndexComponentSelector.FAILURES) && group.checkIndex(ds.getName())) { return true; } - } + } else if (IndexComponentSelector.DATA.equals(selector) || selector == null) { + if (group.checkSelector(IndexComponentSelector.DATA) && group.checkIndex(ds.getName())) { + return true; + } + } // we don't support granting access to a backing index with a failure selector via the parent data stream } return group.checkSelector(selector) && group.checkIndex(name); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java index 94f18722e5ac8..faedcacf21d8c 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java @@ -941,18 +941,17 @@ static AuthorizedIndices resolveAuthorizedIndicesFromRole( // more efficient than checking the index name first because we recommend grant privileges over data stream // instead of backing indices. if (indexAbstraction.getParentDataStream() != null) { - boolean authorizeViaParentDataStream = indexAbstraction.isFailureIndexOfDataStream() - || false == IndexComponentSelector.FAILURES.equals(selector); - if (authorizeViaParentDataStream - && predicate.test( - indexAbstraction.getParentDataStream(), - // access to failure indices is authorized via failures-based selectors on the parent data stream _not_ via data - // ones - indexAbstraction.isFailureIndexOfDataStream() ? IndexComponentSelector.FAILURES : IndexComponentSelector.DATA - )) { - return true; - } - + if (indexAbstraction.isFailureIndexOfDataStream()) { + // access to failure indices is authorized via failures-based selectors on the parent data stream _not_ via data + // ones + if (predicate.test(indexAbstraction.getParentDataStream(), IndexComponentSelector.FAILURES)) { + return true; + } + } else if (IndexComponentSelector.DATA.equals(selector) || selector == null) { + if (predicate.test(indexAbstraction.getParentDataStream(), IndexComponentSelector.DATA)) { + return true; + } + } // we don't support granting access to a backing index with a failure selector via the parent data stream } return predicate.test(indexAbstraction, selector); }); From 998ad6476be2c9b32a4c9cfab385d43b5d2e61f4 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Thu, 20 Mar 2025 08:52:33 +0100 Subject: [PATCH 131/131] Fix tests --- .../authz/AuthorizedIndicesTests.java | 182 ++++++++++++++++-- 1 file changed, 166 insertions(+), 16 deletions(-) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java index 3f913b0b84205..4f712a22eddf7 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java @@ -311,7 +311,7 @@ public void testDataStreamsAreNotIncludedInAuthorizedIndices() { assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, IndexComponentSelector.DATA), is(false)); } - public void testDataStreamsAreNotIncludedInAuthorizedIndicesWithFailuresSelector() { + public void testDataStreamsAreNotIncludedInAuthorizedIndicesWithFailuresSelectorAndAllPrivilege() { assumeTrue("requires failure store", DataStream.isFailureStoreFeatureFlagEnabled()); RoleDescriptor aStarRole = new RoleDescriptor( "a_star", @@ -325,12 +325,6 @@ public void testDataStreamsAreNotIncludedInAuthorizedIndicesWithFailuresSelector new IndicesPrivileges[] { IndicesPrivileges.builder().indices("b").privileges("READ").build() }, null ); - RoleDescriptor cRole = new RoleDescriptor( - "c", - null, - new IndicesPrivileges[] { IndicesPrivileges.builder().indices("b").privileges("READ_FAILURE_STORE").build() }, - null - ); Settings indexSettings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersion.current()).build(); final String internalSecurityIndex = randomFrom( TestRestrictedIndices.INTERNAL_SECURITY_MAIN_INDEX_6, @@ -371,7 +365,7 @@ public void testDataStreamsAreNotIncludedInAuthorizedIndicesWithFailuresSelector ) .build(); final PlainActionFuture future = new PlainActionFuture<>(); - final Set descriptors = Sets.newHashSet(aStarRole, bRole, cRole); + final Set descriptors = Sets.newHashSet(aStarRole, bRole); CompositeRolesStore.buildRoleFromDescriptors( descriptors, new FieldPermissionsCache(Settings.EMPTY), @@ -405,7 +399,7 @@ public void testDataStreamsAreNotIncludedInAuthorizedIndicesWithFailuresSelector assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, IndexComponentSelector.FAILURES), is(false)); } - public void testDataStreamsAreIncludedInAuthorizedIndicesWithFailuresSelector() { + public void testDataStreamsAreIncludedInAuthorizedIndicesWithFailuresSelectorAndAllPrivilege() { assumeTrue("requires failure store", DataStream.isFailureStoreFeatureFlagEnabled()); RoleDescriptor aStarRole = new RoleDescriptor( "a_star", @@ -419,12 +413,6 @@ public void testDataStreamsAreIncludedInAuthorizedIndicesWithFailuresSelector() new IndicesPrivileges[] { IndicesPrivileges.builder().indices("b").privileges("READ").build() }, null ); - RoleDescriptor cRole = new RoleDescriptor( - "b", - null, - new IndicesPrivileges[] { IndicesPrivileges.builder().indices("b").privileges("READ_FAILURE_STORE").build() }, - null - ); Settings indexSettings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersion.current()).build(); final String internalSecurityIndex = randomFrom( TestRestrictedIndices.INTERNAL_SECURITY_MAIN_INDEX_6, @@ -465,7 +453,7 @@ public void testDataStreamsAreIncludedInAuthorizedIndicesWithFailuresSelector() ) .build(); final PlainActionFuture future = new PlainActionFuture<>(); - final Set descriptors = Sets.newHashSet(aStarRole, bRole, cRole); + final Set descriptors = Sets.newHashSet(aStarRole, bRole); CompositeRolesStore.buildRoleFromDescriptors( descriptors, new FieldPermissionsCache(Settings.EMPTY), @@ -505,6 +493,168 @@ public void testDataStreamsAreIncludedInAuthorizedIndicesWithFailuresSelector() assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, IndexComponentSelector.FAILURES), is(false)); } + public void testDataStreamsAreIncludedInAuthorizedIndicesWithFailuresSelector() { + assumeTrue("requires failure store", DataStream.isFailureStoreFeatureFlagEnabled()); + RoleDescriptor aReadFailuresStarRole = new RoleDescriptor( + "a_read_failure_store", + null, + new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a*").privileges("read_failure_store").build() }, + null + ); + RoleDescriptor aReadRole = new RoleDescriptor( + "a_read", + null, + new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a*").privileges("read").build() }, + null + ); + Settings indexSettings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersion.current()).build(); + final String internalSecurityIndex = randomFrom( + TestRestrictedIndices.INTERNAL_SECURITY_MAIN_INDEX_6, + TestRestrictedIndices.INTERNAL_SECURITY_MAIN_INDEX_7 + ); + String backingIndex = DataStream.getDefaultBackingIndexName("adatastream1", 1); + String failureIndex = DataStream.getDefaultFailureStoreName("adatastream1", 1, 1); + Metadata metadata = Metadata.builder() + .put(new IndexMetadata.Builder("a1").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetadata.Builder("a2").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetadata.Builder("aaaaaa").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetadata.Builder("bbbbb").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put( + new IndexMetadata.Builder(internalSecurityIndex).settings(indexSettings) + .numberOfShards(1) + .numberOfReplicas(0) + .putAlias(new AliasMetadata.Builder(SecuritySystemIndices.SECURITY_MAIN_ALIAS).build()) + .build(), + true + ) + .put(new IndexMetadata.Builder(backingIndex).settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetadata.Builder(failureIndex).settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put( + DataStreamTestHelper.newInstance( + "adatastream1", + List.of(new Index(backingIndex, "_na_")), + List.of(new Index(failureIndex, "_na_")) + ) + ) + .build(); + final PlainActionFuture future = new PlainActionFuture<>(); + final Set descriptors = Sets.newHashSet(aReadFailuresStarRole, aReadRole); + CompositeRolesStore.buildRoleFromDescriptors( + descriptors, + new FieldPermissionsCache(Settings.EMPTY), + null, + RESTRICTED_INDICES, + future + ); + Role roles = future.actionGet(); + TransportRequest request = new ResolveIndexAction.Request(new String[] { "a*" }); + AuthorizationEngine.RequestInfo requestInfo = getRequestInfo(request, TransportSearchAction.TYPE.name()); + AuthorizedIndices authorizedIndices = RBACEngine.resolveAuthorizedIndicesFromRole( + roles, + requestInfo, + metadata.getProject().getIndicesLookup(), + () -> ignore -> {} + ); + assertAuthorizedFor( + authorizedIndices, + IndexComponentSelector.DATA, + "a1", + "a2", + "aaaaaa", + "adatastream1", + backingIndex, + failureIndex + ); + assertAuthorizedFor(authorizedIndices, IndexComponentSelector.FAILURES, "adatastream1", failureIndex); + assertThat(authorizedIndices.all(IndexComponentSelector.DATA), not(contains("bbbbb"))); + assertThat(authorizedIndices.check("bbbbb", IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.check("bbbbb", IndexComponentSelector.FAILURES), is(false)); + assertThat(authorizedIndices.check("ba", IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.check("ba", IndexComponentSelector.FAILURES), is(false)); + assertThat(authorizedIndices.check(internalSecurityIndex, IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.check(internalSecurityIndex, IndexComponentSelector.FAILURES), is(false)); + assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, IndexComponentSelector.FAILURES), is(false)); + } + + public void testDataStreamsAreNotIncludedInAuthorizedIndicesWithFailuresSelector() { + assumeTrue("requires failure store", DataStream.isFailureStoreFeatureFlagEnabled()); + RoleDescriptor aReadFailuresStarRole = new RoleDescriptor( + "a_read_failure_store", + null, + new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a*").privileges("read_failure_store").build() }, + null + ); + RoleDescriptor aReadRole = new RoleDescriptor( + "a_read", + null, + new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a*").privileges("read").build() }, + null + ); + Settings indexSettings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersion.current()).build(); + final String internalSecurityIndex = randomFrom( + TestRestrictedIndices.INTERNAL_SECURITY_MAIN_INDEX_6, + TestRestrictedIndices.INTERNAL_SECURITY_MAIN_INDEX_7 + ); + String backingIndex = DataStream.getDefaultBackingIndexName("adatastream1", 1); + String failureIndex = DataStream.getDefaultFailureStoreName("adatastream1", 1, 1); + Metadata metadata = Metadata.builder() + .put(new IndexMetadata.Builder("a1").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetadata.Builder("a2").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetadata.Builder("aaaaaa").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetadata.Builder("bbbbb").settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put( + new IndexMetadata.Builder(internalSecurityIndex).settings(indexSettings) + .numberOfShards(1) + .numberOfReplicas(0) + .putAlias(new AliasMetadata.Builder(SecuritySystemIndices.SECURITY_MAIN_ALIAS).build()) + .build(), + true + ) + .put(new IndexMetadata.Builder(backingIndex).settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put(new IndexMetadata.Builder(failureIndex).settings(indexSettings).numberOfShards(1).numberOfReplicas(0).build(), true) + .put( + DataStreamTestHelper.newInstance( + "adatastream1", + List.of(new Index(backingIndex, "_na_")), + List.of(new Index(failureIndex, "_na_")) + ) + ) + .build(); + final PlainActionFuture future = new PlainActionFuture<>(); + final Set descriptors = Sets.newHashSet(aReadFailuresStarRole, aReadRole); + CompositeRolesStore.buildRoleFromDescriptors( + descriptors, + new FieldPermissionsCache(Settings.EMPTY), + null, + RESTRICTED_INDICES, + future + ); + Role roles = future.actionGet(); + AuthorizedIndices authorizedIndices = RBACEngine.resolveAuthorizedIndicesFromRole( + roles, + getRequestInfo(TransportSearchAction.TYPE.name()), + metadata.getProject().getIndicesLookup(), + () -> ignore -> {} + ); + assertAuthorizedFor(authorizedIndices, IndexComponentSelector.DATA, "a1", "a2", "aaaaaa"); + assertAuthorizedFor(authorizedIndices, IndexComponentSelector.FAILURES); + + assertThat(authorizedIndices.check("bbbbb", IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.check("bbbbb", IndexComponentSelector.FAILURES), is(false)); + + assertThat(authorizedIndices.check("ba", IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.check("ba", IndexComponentSelector.FAILURES), is(false)); + + // data are authorized when explicitly tested (they are not "unavailable" for the Security filter) + assertThat(authorizedIndices.check("adatastream1", IndexComponentSelector.DATA), is(true)); + assertThat(authorizedIndices.check("adatastream1", IndexComponentSelector.FAILURES), is(true)); + + assertThat(authorizedIndices.check(internalSecurityIndex, IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.check(internalSecurityIndex, IndexComponentSelector.FAILURES), is(false)); + assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, IndexComponentSelector.DATA), is(false)); + assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS, IndexComponentSelector.FAILURES), is(false)); + } + public void testDataStreamsAreIncludedInAuthorizedIndices() { RoleDescriptor aStarRole = new RoleDescriptor( "a_star",