From 7162eddd8cb186bdf8692f2214015a0b74335688 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 3 Mar 2026 17:04:02 -0500 Subject: [PATCH 1/4] Add test to test out optimizations to RoleBasedActionPrivileges Signed-off-by: Craig Perkins --- .../RoleBasedActionPrivilegesTest.java | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java index 1fc0ef35c6..df0a95948d 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java @@ -24,6 +24,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -37,12 +38,15 @@ import org.opensearch.common.settings.Settings; import org.opensearch.core.common.unit.ByteSizeUnit; import org.opensearch.core.common.unit.ByteSizeValue; +import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; import org.opensearch.security.resolver.IndexResolverReplacer; +import org.opensearch.security.securityconf.DynamicConfigFactory; import org.opensearch.security.securityconf.FlattenedActionGroups; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.securityconf.impl.v7.ActionGroupsV7; import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.user.User; import org.opensearch.security.util.MockIndexMetadataBuilder; @@ -279,6 +283,118 @@ public void hasAny_wildcard() throws Exception { ); } + /** + * Tests that ClusterPrivileges construction is optimized when many roles share the same permission patterns. + * The pattern cache should ensure each unique pattern is only processed once. + *

+ * This test simulates a realistic scenario with: + * - 1000 roles, each with a unique index pattern + shared patterns + * - DLS queries using user attribute substitution (${attr.internal.should_hide}) + * - 100 actual indices to test against + */ + @Test + public void constructionPerformance_sharedPatterns() throws Exception { + // Shared cluster permission patterns - 3 are wildcards that should be cached + List sharedClusterPatterns = Arrays.asList( + "cluster:admin/tasks/cancel", + "cluster:monitor/task", + "cluster:monitor/task/get", + "indices:admin/aliases/get", + "indices:admin/exists", + "indices:admin/get", + "indices:admin/mappings/get", + "indices:admin/mapping/put", + "indices:admin/refresh*", // wildcard pattern 1 + "indices:data*", // wildcard pattern 2 + "indices:admin/flush*", // wildcard pattern 3 + "indices:admin/forcemerge", + "indices:monitor/stats" + ); + + // Shared index patterns that all roles have + List sharedIndexPatterns = Arrays.asList("logs-*", "metrics-*"); + + // DLS query with user attribute substitution + String dlsQuery = "{\"bool\": {\"must\": {\"match\": {\"should_hide\": \"${attr.internal.should_hide}\"}}}}"; + + // Create 1000 roles, each with: + // - shared cluster permissions + // - unique index pattern (role_N_index_*) + shared index patterns + // - DLS query with user attribute substitution + // - First 100 roles also have alias-* pattern to test alias expansion + Map rolesMap = new HashMap<>(); + for (int i = 0; i < 1000; i++) { + List indexPatterns = new ArrayList<>(sharedIndexPatterns); + indexPatterns.add("role_" + i + "_index_*"); // unique pattern per role + if (i < 100) { + indexPatterns.add("alias-*"); // test alias expansion for first 100 roles + } + + Map indexPermission = new HashMap<>(); + indexPermission.put("index_patterns", indexPatterns); + indexPermission.put("dls", dlsQuery); + indexPermission.put("allowed_actions", Arrays.asList("indices_all")); + + rolesMap.put( + "role_" + i, + ImmutableMap.of("cluster_permissions", sharedClusterPatterns, "index_permissions", Arrays.asList(indexPermission)) + ); + } + + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromMap(rolesMap, CType.ROLES); + + // Create 100 actual indices and 1000 aliases + String[] indexNames = new String[10000]; + for (int i = 0; i < 10000; i++) { + indexNames[i] = "logs-" + i; + } + MockIndexMetadataBuilder builder = indices(indexNames); + // Create 1000 aliases, each pointing to a subset of indices + for (int i = 0; i < 5000; i++) { + builder.alias("alias-" + i).of("logs-" + i); + } + Metadata indexMetadata = builder.build(); + + // Load static action groups (includes indices_all -> indices:*) + JsonNode staticActionGroupsJsonNode = DefaultObjectMapper.YAML_MAPPER.readTree( + DynamicConfigFactory.class.getResourceAsStream("/static_config/static_action_groups.yml") + ); + SecurityDynamicConfiguration actionGroupsConfig = SecurityDynamicConfiguration.fromNode( + staticActionGroupsJsonNode, + CType.ACTIONGROUPS, + 2, + 0, + 0 + ); + FlattenedActionGroups actionGroups = new FlattenedActionGroups(actionGroupsConfig); + + // Build RoleBasedActionPrivileges and measure construction time + long start = System.nanoTime(); + RoleBasedActionPrivileges subject = new RoleBasedActionPrivileges(roles, actionGroups, Settings.EMPTY); + long constructionMs = java.util.concurrent.TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); + + long startStateful = System.nanoTime(); + subject.updateStatefulIndexPrivileges(indexMetadata.getIndicesLookup(), 1); + long statefulMs = java.util.concurrent.TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startStateful); + + System.out.println("[constructionPerformance_sharedPatterns] RoleBasedActionPrivileges construction: " + constructionMs + "ms"); + System.out.println("[constructionPerformance_sharedPatterns] StatefulIndexPrivileges update: " + statefulMs + "ms"); + System.out.println("[constructionPerformance_sharedPatterns] Total: " + (constructionMs + statefulMs) + "ms"); + + // Verify correctness - cluster privileges + assertThat(subject.hasClusterPrivilege(ctx().roles("role_0").get(), "cluster:monitor/task/get"), isAllowed()); + assertThat(subject.hasClusterPrivilege(ctx().roles("role_999").get(), "cluster:monitor/task/get"), isAllowed()); + assertThat(subject.hasClusterPrivilege(ctx().roles("role_500").get(), "indices:data/read/search"), isAllowed()); + + // Verify correctness - index privileges with user attributes + PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege( + ctx().roles("role_0").attr("attr.internal.should_hide", "false").indexMetadata(indexMetadata).get(), + ImmutableSet.of("indices:data/read/search"), + IndexResolverReplacer.Resolved.ofIndex("logs-0") + ); + assertThat(result, isAllowed()); + } + } /** From 5b1a5a8a2b8a1cf3b457d3678f8b9749e79d62ef Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 3 Mar 2026 17:33:57 -0500 Subject: [PATCH 2/4] WIP on optimization --- .../RoleBasedActionPrivileges.java | 46 ++++++++++++++----- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivileges.java index 3734f340ab..7b23af740a 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivileges.java @@ -651,6 +651,11 @@ static class StatefulIndexPrivileges extends RuntimeOptimizedActionPrivileges.St ByteSizeValue statefulIndexMaxHeapSize ) { long startTime = System.currentTimeMillis(); + long matchingTime = 0; + long innerLoopTime = 0; + long aliasTime = 0; + int aliasHits = 0; + int totalIterations = 0; Map< String, @@ -672,7 +677,7 @@ static class StatefulIndexPrivileges extends RuntimeOptimizedActionPrivileges.St // and m is the number of matched indices. This formula does not take the loop through matchedActions in // account, as this is bound by a constant number and thus does not need to be considered in the O() notation. - top: for (Map.Entry entry : roles.getCEntries().entrySet()) { + for (Map.Entry entry : roles.getCEntries().entrySet()) { try { String roleName = entry.getKey(); RoleV7 role = entry.getValue(); @@ -688,6 +693,7 @@ static class StatefulIndexPrivileges extends RuntimeOptimizedActionPrivileges.St continue; } + long t0 = System.nanoTime(); WildcardMatcher indexMatcher = IndexPattern.from(indexPermissions.getIndex_patterns()).getStaticPattern(); if (indexMatcher == WildcardMatcher.NONE) { @@ -697,6 +703,7 @@ static class StatefulIndexPrivileges extends RuntimeOptimizedActionPrivileges.St } List matchingIndices = indexMatcher.matching(indices.values(), IndexAbstraction::getName); + matchingTime += System.nanoTime() - t0; if (matchingIndices.isEmpty()) { continue; } @@ -708,8 +715,10 @@ static class StatefulIndexPrivileges extends RuntimeOptimizedActionPrivileges.St Collectors.toList() ); + long t1 = System.nanoTime(); for (IndexAbstraction index : matchingIndices) { for (String action : matchedActions) { + totalIterations++; CompactMapGroupBuilder.MapBuilder< String, DeduplicatingCompactSubSetBuilder.SubSetBuilder> indexToRoles = actionToIndexToRoles @@ -718,8 +727,10 @@ static class StatefulIndexPrivileges extends RuntimeOptimizedActionPrivileges.St indexToRoles.get(index.getName()).add(roleName); if (index instanceof IndexAbstraction.Alias) { + long t2 = System.nanoTime(); // For aliases we additionally add the sub-indices to the privilege map for (IndexMetadata subIndex : index.getIndices()) { + aliasHits++; String subIndexName = subIndex.getIndex().getName(); // We need to check whether the subIndex is part of the global indices // metadata map because that map has been filtered by relevantOnly(). @@ -738,22 +749,23 @@ static class StatefulIndexPrivileges extends RuntimeOptimizedActionPrivileges.St ); } } - } - - if (roleSetBuilder.getEstimatedByteSize() + indexMapBuilder - .getEstimatedByteSize() > statefulIndexMaxHeapSize.getBytes()) { - log.info( - "Size of precomputed index privileges exceeds configured limit ({}). Using capped data structure." - + "This might lead to slightly lower performance during privilege evaluation. Consider raising {}.", - statefulIndexMaxHeapSize, - PRECOMPUTED_PRIVILEGES_MAX_HEAP_SIZE.getKey() - ); - break top; + aliasTime += System.nanoTime() - t2; } } } + innerLoopTime += System.nanoTime() - t1; } } + if (roleSetBuilder.getEstimatedByteSize() + indexMapBuilder + .getEstimatedByteSize() > statefulIndexMaxHeapSize.getBytes()) { + log.info( + "Size of precomputed index privileges exceeds configured limit ({}). Using capped data structure." + + "This might lead to slightly lower performance during privilege evaluation. Consider raising {}.", + statefulIndexMaxHeapSize, + PRECOMPUTED_PRIVILEGES_MAX_HEAP_SIZE.getKey() + ); + break; + } } catch (Exception e) { log.error("Unexpected exception while processing role: {}\nIgnoring role.", entry.getKey(), e); } @@ -778,6 +790,16 @@ static class StatefulIndexPrivileges extends RuntimeOptimizedActionPrivileges.St long duration = System.currentTimeMillis() - startTime; + log.warn( + "StatefulIndexPrivileges timing breakdown: total={}ms, matching={}ms, innerLoop={}ms, alias={}ms, aliasHits={}, iterations={}", + duration, + matchingTime / 1_000_000, + innerLoopTime / 1_000_000, + aliasTime / 1_000_000, + aliasHits, + totalIterations + ); + if (duration > 30000) { log.warn("Creation of StatefulIndexPrivileges took {} ms", duration); } else { From 5d90683d6bdd1678add494285731bcdab966759e Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Wed, 4 Mar 2026 13:50:14 -0500 Subject: [PATCH 3/4] Optimize index matching Signed-off-by: Craig Perkins --- .../RoleBasedActionPrivilegesTest.java | 113 +++++++++++------- .../RoleBasedActionPrivileges.java | 90 +++++++++----- 2 files changed, 128 insertions(+), 75 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java index df0a95948d..9de6738005 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java @@ -284,51 +284,50 @@ public void hasAny_wildcard() throws Exception { } /** - * Tests that ClusterPrivileges construction is optimized when many roles share the same permission patterns. - * The pattern cache should ensure each unique pattern is only processed once. + * Tests StatefulIndexPrivileges construction performance *

- * This test simulates a realistic scenario with: - * - 1000 roles, each with a unique index pattern + shared patterns + * This test simulates: + * - 3000 roles + * - 6000 indices + 9000 aliases + * - Each role has a unique pattern "role_N*" matching ~3 indices + shared "test-index" * - DLS queries using user attribute substitution (${attr.internal.should_hide}) - * - 100 actual indices to test against */ @Test public void constructionPerformance_sharedPatterns() throws Exception { - // Shared cluster permission patterns - 3 are wildcards that should be cached - List sharedClusterPatterns = Arrays.asList( + final int NUM_ROLES = 3000; + final int NUM_INDICES = 6000; + final int NUM_ALIASES = 9000; + final int INDICES_PER_ROLE = 3; // Each role_N* pattern matches ~3 indices + + // Cluster permissions matching production role config + List clusterPatterns = Arrays.asList( "cluster:admin/tasks/cancel", "cluster:monitor/task", "cluster:monitor/task/get", + "cluster:monitor/tasks/list", + "cluster:monitor/stats", "indices:admin/aliases/get", "indices:admin/exists", "indices:admin/get", "indices:admin/mappings/get", "indices:admin/mapping/put", - "indices:admin/refresh*", // wildcard pattern 1 - "indices:data*", // wildcard pattern 2 - "indices:admin/flush*", // wildcard pattern 3 + "indices:admin/refresh*", + "indices:data*", + "indices:admin/flush*", "indices:admin/forcemerge", "indices:monitor/stats" ); - // Shared index patterns that all roles have - List sharedIndexPatterns = Arrays.asList("logs-*", "metrics-*"); - - // DLS query with user attribute substitution + // DLS query with user attribute substitution (like production) String dlsQuery = "{\"bool\": {\"must\": {\"match\": {\"should_hide\": \"${attr.internal.should_hide}\"}}}}"; - // Create 1000 roles, each with: - // - shared cluster permissions - // - unique index pattern (role_N_index_*) + shared index patterns - // - DLS query with user attribute substitution - // - First 100 roles also have alias-* pattern to test alias expansion + // Create roles: each has pattern "role_N*" + shared "test-index" Map rolesMap = new HashMap<>(); - for (int i = 0; i < 1000; i++) { - List indexPatterns = new ArrayList<>(sharedIndexPatterns); - indexPatterns.add("role_" + i + "_index_*"); // unique pattern per role - if (i < 100) { - indexPatterns.add("alias-*"); // test alias expansion for first 100 roles - } + for (int i = 0; i < NUM_ROLES; i++) { + List indexPatterns = Arrays.asList( + "role_" + i + "*", // unique pattern matching ~3 indices per role + "test-index" // shared index across all roles + ); Map indexPermission = new HashMap<>(); indexPermission.put("index_patterns", indexPatterns); @@ -336,23 +335,36 @@ public void constructionPerformance_sharedPatterns() throws Exception { indexPermission.put("allowed_actions", Arrays.asList("indices_all")); rolesMap.put( - "role_" + i, - ImmutableMap.of("cluster_permissions", sharedClusterPatterns, "index_permissions", Arrays.asList(indexPermission)) + "role_" + i + "_user", + ImmutableMap.of("cluster_permissions", clusterPatterns, "index_permissions", Arrays.asList(indexPermission)) ); } - SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromMap(rolesMap, CType.ROLES); - // Create 100 actual indices and 1000 aliases - String[] indexNames = new String[10000]; - for (int i = 0; i < 10000; i++) { - indexNames[i] = "logs-" + i; + // Create indices: for each role, create INDICES_PER_ROLE indices that match "role_N*" + // e.g., role_0_data, role_0_logs, role_0_metrics + MockIndexMetadataBuilder builder = indices(); + String[] suffixes = { "_data", "_logs", "_metrics" }; + for (int i = 0; i < NUM_ROLES; i++) { + for (int j = 0; j < INDICES_PER_ROLE; j++) { + builder.index("role_" + i + suffixes[j % suffixes.length]); + } + } + // Add the shared index + builder.index("test-index"); + + // Fill remaining indices to reach NUM_INDICES + int createdIndices = NUM_ROLES * INDICES_PER_ROLE + 1; + for (int i = createdIndices; i < NUM_INDICES; i++) { + builder.index("other_index_" + i); } - MockIndexMetadataBuilder builder = indices(indexNames); - // Create 1000 aliases, each pointing to a subset of indices - for (int i = 0; i < 5000; i++) { - builder.alias("alias-" + i).of("logs-" + i); + + // Create aliases - each pointing to one index + for (int i = 0; i < NUM_ALIASES; i++) { + int indexNum = i % NUM_ROLES; + builder.alias("alias_" + i).of("role_" + indexNum + suffixes[0]); } + Metadata indexMetadata = builder.build(); // Load static action groups (includes indices_all -> indices:*) @@ -377,20 +389,29 @@ public void constructionPerformance_sharedPatterns() throws Exception { subject.updateStatefulIndexPrivileges(indexMetadata.getIndicesLookup(), 1); long statefulMs = java.util.concurrent.TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startStateful); - System.out.println("[constructionPerformance_sharedPatterns] RoleBasedActionPrivileges construction: " + constructionMs + "ms"); - System.out.println("[constructionPerformance_sharedPatterns] StatefulIndexPrivileges update: " + statefulMs + "ms"); - System.out.println("[constructionPerformance_sharedPatterns] Total: " + (constructionMs + statefulMs) + "ms"); + System.out.println( + "[constructionPerformance_productionScenario] Configuration: " + + NUM_ROLES + + " roles, " + + NUM_INDICES + + " indices, " + + NUM_ALIASES + + " aliases" + ); + System.out.println( + "[constructionPerformance_productionScenario] RoleBasedActionPrivileges construction: " + constructionMs + "ms" + ); + System.out.println("[constructionPerformance_productionScenario] StatefulIndexPrivileges update: " + statefulMs + "ms"); + System.out.println("[constructionPerformance_productionScenario] Total: " + (constructionMs + statefulMs) + "ms"); - // Verify correctness - cluster privileges - assertThat(subject.hasClusterPrivilege(ctx().roles("role_0").get(), "cluster:monitor/task/get"), isAllowed()); - assertThat(subject.hasClusterPrivilege(ctx().roles("role_999").get(), "cluster:monitor/task/get"), isAllowed()); - assertThat(subject.hasClusterPrivilege(ctx().roles("role_500").get(), "indices:data/read/search"), isAllowed()); + // Verify correctness + assertThat(subject.hasClusterPrivilege(ctx().roles("role_0_user").get(), "cluster:monitor/task/get"), isAllowed()); + assertThat(subject.hasClusterPrivilege(ctx().roles("role_2999_user").get(), "indices:data/read/search"), isAllowed()); - // Verify correctness - index privileges with user attributes PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege( - ctx().roles("role_0").attr("attr.internal.should_hide", "false").indexMetadata(indexMetadata).get(), + ctx().roles("role_0_user").attr("attr.internal.project_id", "123").indexMetadata(indexMetadata).get(), ImmutableSet.of("indices:data/read/search"), - IndexResolverReplacer.Resolved.ofIndex("logs-0") + IndexResolverReplacer.Resolved.ofIndex("role_0_data") ); assertThat(result, isAllowed()); } diff --git a/src/main/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivileges.java index 7b23af740a..688900c10f 100644 --- a/src/main/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivileges.java @@ -12,6 +12,7 @@ package org.opensearch.security.privileges.actionlevel; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -667,6 +668,10 @@ static class StatefulIndexPrivileges extends RuntimeOptimizedActionPrivileges.St CompactMapGroupBuilder> indexMapBuilder = new CompactMapGroupBuilder<>(indices.keySet(), (k2) -> roleSetBuilder.createSubSetBuilder()); + // Pre-sort index names once for efficient prefix matching + String[] sortedIndexNames = indices.keySet().toArray(new String[0]); + Arrays.sort(sortedIndexNames); + // We iterate here through the present RoleV7 instances and nested through their "index_permissions" sections. // During the loop, the actionToIndexToRoles map is being built. // For that, action patterns from the role will be matched against the "well-known actions" to build @@ -694,15 +699,8 @@ static class StatefulIndexPrivileges extends RuntimeOptimizedActionPrivileges.St } long t0 = System.nanoTime(); - WildcardMatcher indexMatcher = IndexPattern.from(indexPermissions.getIndex_patterns()).getStaticPattern(); - - if (indexMatcher == WildcardMatcher.NONE) { - // The pattern is likely blank because there are only templated patterns. - // Index patterns with templates are not handled here, but in the static IndexPermissions object - continue; - } - - List matchingIndices = indexMatcher.matching(indices.values(), IndexAbstraction::getName); + List indexPatterns = indexPermissions.getIndex_patterns(); + List matchingIndices = matchIndicesOptimized(indexPatterns, indices, sortedIndexNames); matchingTime += System.nanoTime() - t0; if (matchingIndices.isEmpty()) { continue; @@ -756,13 +754,13 @@ static class StatefulIndexPrivileges extends RuntimeOptimizedActionPrivileges.St innerLoopTime += System.nanoTime() - t1; } } - if (roleSetBuilder.getEstimatedByteSize() + indexMapBuilder - .getEstimatedByteSize() > statefulIndexMaxHeapSize.getBytes()) { + if (roleSetBuilder.getEstimatedByteSize() + indexMapBuilder.getEstimatedByteSize() > statefulIndexMaxHeapSize + .getBytes()) { log.info( - "Size of precomputed index privileges exceeds configured limit ({}). Using capped data structure." - + "This might lead to slightly lower performance during privilege evaluation. Consider raising {}.", - statefulIndexMaxHeapSize, - PRECOMPUTED_PRIVILEGES_MAX_HEAP_SIZE.getKey() + "Size of precomputed index privileges exceeds configured limit ({}). Using capped data structure." + + "This might lead to slightly lower performance during privilege evaluation. Consider raising {}.", + statefulIndexMaxHeapSize, + PRECOMPUTED_PRIVILEGES_MAX_HEAP_SIZE.getKey() ); break; } @@ -787,24 +785,58 @@ static class StatefulIndexPrivileges extends RuntimeOptimizedActionPrivileges.St this.indices = ImmutableMap.copyOf(indices); this.metadataVersion = metadataVersion; + } - long duration = System.currentTimeMillis() - startTime; + /** + * Optimized index matching using pattern categorization: + * - Exact matches: O(1) map lookup + * - Prefix patterns (ending with *): O(log n) binary search + linear scan of matches + * - Complex patterns: O(n) linear scan fallback + */ + private static List matchIndicesOptimized( + List patterns, + Map indices, + String[] sortedIndexNames + ) { + List result = new ArrayList<>(); + List complexPatterns = new ArrayList<>(); - log.warn( - "StatefulIndexPrivileges timing breakdown: total={}ms, matching={}ms, innerLoop={}ms, alias={}ms, aliasHits={}, iterations={}", - duration, - matchingTime / 1_000_000, - innerLoopTime / 1_000_000, - aliasTime / 1_000_000, - aliasHits, - totalIterations - ); + for (String pattern : patterns) { + if (pattern.contains("${")) { + // Templated pattern - skip, handled by static IndexPermissions + continue; + } + if (!pattern.contains("*") && !pattern.contains("?")) { + // Exact match - O(1) + IndexAbstraction idx = indices.get(pattern); + if (idx != null) { + result.add(idx); + } + } else if (pattern.endsWith("*") && !pattern.substring(0, pattern.length() - 1).contains("*") && !pattern.contains("?")) { + // Simple prefix pattern like "role_123*" - use binary search on pre-sorted array + String prefix = pattern.substring(0, pattern.length() - 1); + int idx = Arrays.binarySearch(sortedIndexNames, prefix); + int start = idx >= 0 ? idx : -(idx + 1); + for (int i = start; i < sortedIndexNames.length && sortedIndexNames[i].startsWith(prefix); i++) { + result.add(indices.get(sortedIndexNames[i])); + } + } else { + // Complex pattern - collect for batch processing + complexPatterns.add(pattern); + } + } - if (duration > 30000) { - log.warn("Creation of StatefulIndexPrivileges took {} ms", duration); - } else { - log.debug("Creation of StatefulIndexPrivileges took {} ms", duration); + // Process complex patterns with linear scan + if (!complexPatterns.isEmpty()) { + WildcardMatcher matcher = WildcardMatcher.from(complexPatterns); + for (IndexAbstraction idx : indices.values()) { + if (matcher.test(idx.getName()) && !result.contains(idx)) { + result.add(idx); + } + } } + + return result; } /** From ebec9685cac71ac8fc458dd4cfb428ab236e3da9 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Wed, 4 Mar 2026 13:51:50 -0500 Subject: [PATCH 4/4] Fix typo Signed-off-by: Craig Perkins --- .../actionlevel/RoleBasedActionPrivilegesTest.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java index 9de6738005..4d5784b06b 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/actionlevel/RoleBasedActionPrivilegesTest.java @@ -299,7 +299,6 @@ public void constructionPerformance_sharedPatterns() throws Exception { final int NUM_ALIASES = 9000; final int INDICES_PER_ROLE = 3; // Each role_N* pattern matches ~3 indices - // Cluster permissions matching production role config List clusterPatterns = Arrays.asList( "cluster:admin/tasks/cancel", "cluster:monitor/task", @@ -318,7 +317,7 @@ public void constructionPerformance_sharedPatterns() throws Exception { "indices:monitor/stats" ); - // DLS query with user attribute substitution (like production) + // DLS query with user attribute substitution String dlsQuery = "{\"bool\": {\"must\": {\"match\": {\"should_hide\": \"${attr.internal.should_hide}\"}}}}"; // Create roles: each has pattern "role_N*" + shared "test-index" @@ -390,7 +389,7 @@ public void constructionPerformance_sharedPatterns() throws Exception { long statefulMs = java.util.concurrent.TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startStateful); System.out.println( - "[constructionPerformance_productionScenario] Configuration: " + "[constructionPerformance_sharedPatterns] Configuration: " + NUM_ROLES + " roles, " + NUM_INDICES @@ -399,10 +398,10 @@ public void constructionPerformance_sharedPatterns() throws Exception { + " aliases" ); System.out.println( - "[constructionPerformance_productionScenario] RoleBasedActionPrivileges construction: " + constructionMs + "ms" + "[constructionPerformance_sharedPatterns] RoleBasedActionPrivileges construction: " + constructionMs + "ms" ); - System.out.println("[constructionPerformance_productionScenario] StatefulIndexPrivileges update: " + statefulMs + "ms"); - System.out.println("[constructionPerformance_productionScenario] Total: " + (constructionMs + statefulMs) + "ms"); + System.out.println("[constructionPerformance_sharedPatterns] StatefulIndexPrivileges update: " + statefulMs + "ms"); + System.out.println("[constructionPerformance_sharedPatterns] Total: " + (constructionMs + statefulMs) + "ms"); // Verify correctness assertThat(subject.hasClusterPrivilege(ctx().roles("role_0_user").get(), "cluster:monitor/task/get"), isAllowed());