1010import org .apache .logging .log4j .Logger ;
1111import org .elasticsearch .ElasticsearchException ;
1212import org .elasticsearch .action .ActionListener ;
13+ import org .elasticsearch .action .ActionRunnable ;
1314import org .elasticsearch .common .Strings ;
1415import org .elasticsearch .common .bytes .BytesReference ;
1516import org .elasticsearch .common .cache .Cache ;
6566import java .util .Objects ;
6667import java .util .Optional ;
6768import java .util .Set ;
69+ import java .util .concurrent .Executor ;
6870import java .util .concurrent .atomic .AtomicLong ;
6971import java .util .function .Consumer ;
7072import java .util .stream .Collectors ;
@@ -91,6 +93,11 @@ public class CompositeRolesStore {
9193 Property .NodeScope
9294 );
9395 private static final Logger logger = LogManager .getLogger (CompositeRolesStore .class );
96+ /**
97+ * See {@link #shouldForkRoleBuilding(Set)}
98+ */
99+ private static final int ROLE_DESCRIPTOR_FORK_THRESHOLD = 100 ;
100+ private static final int INDEX_PRIVILEGE_FORK_THRESHOLD = 1000 ;
94101
95102 private final RoleProviders roleProviders ;
96103 private final NativePrivilegeStore privilegeStore ;
@@ -106,6 +113,7 @@ public class CompositeRolesStore {
106113 private final Map <String , Role > internalUserRoles ;
107114 private final RestrictedIndices restrictedIndices ;
108115 private final ThreadContext threadContext ;
116+ private final Executor roleBuildingExecutor ;
109117
110118 public CompositeRolesStore (
111119 Settings settings ,
@@ -118,6 +126,7 @@ public CompositeRolesStore(
118126 ServiceAccountService serviceAccountService ,
119127 DocumentSubsetBitsetCache dlsBitsetCache ,
120128 RestrictedIndices restrictedIndices ,
129+ Executor roleBuildingExecutor ,
121130 Consumer <Collection <RoleDescriptor >> effectiveRoleDescriptorsConsumer
122131 ) {
123132 this .roleProviders = roleProviders ;
@@ -179,6 +188,7 @@ public void providersChanged() {
179188 );
180189 this .anonymousUser = new AnonymousUser (settings );
181190 this .threadContext = threadContext ;
191+ this .roleBuildingExecutor = roleBuildingExecutor ;
182192 }
183193
184194 public void getRoles (Authentication authentication , ActionListener <Tuple <Role , Role >> roleActionListener ) {
@@ -276,21 +286,70 @@ public void buildRoleFromRoleReference(RoleReference roleReference, ActionListen
276286 } else if (RolesRetrievalResult .SUPERUSER == rolesRetrievalResult ) {
277287 roleActionListener .onResponse (superuserRole );
278288 } else {
279- buildThenMaybeCacheRole (
280- roleKey ,
281- rolesRetrievalResult .getRoleDescriptors (),
282- rolesRetrievalResult .getMissingRoles (),
283- rolesRetrievalResult .isSuccess (),
284- invalidationCounter ,
285- ActionListener .wrap (roleActionListener ::onResponse , failureHandler )
286- );
289+ final ActionListener <Role > wrapped = ActionListener .wrap (roleActionListener ::onResponse , failureHandler );
290+ if (shouldForkRoleBuilding (rolesRetrievalResult .getRoleDescriptors ())) {
291+ roleBuildingExecutor .execute (
292+ ActionRunnable .wrap (
293+ wrapped ,
294+ l -> buildThenMaybeCacheRole (
295+ roleKey ,
296+ rolesRetrievalResult .getRoleDescriptors (),
297+ rolesRetrievalResult .getMissingRoles (),
298+ rolesRetrievalResult .isSuccess (),
299+ invalidationCounter ,
300+ l
301+ )
302+ )
303+ );
304+ } else {
305+ buildThenMaybeCacheRole (
306+ roleKey ,
307+ rolesRetrievalResult .getRoleDescriptors (),
308+ rolesRetrievalResult .getMissingRoles (),
309+ rolesRetrievalResult .isSuccess (),
310+ invalidationCounter ,
311+ wrapped
312+ );
313+ }
287314 }
288315 }, failureHandler ));
289316 } else {
290317 roleActionListener .onResponse (existing );
291318 }
292319 }
293320
321+ /**
322+ * Uses heuristics such as presence of application privileges to determine if role building will be expensive
323+ * and therefore warrants forking.
324+ * Package-private for testing.
325+ */
326+ boolean shouldForkRoleBuilding (Set <RoleDescriptor > roleDescriptors ) {
327+ // A role with many role descriptors is likely expensive to build
328+ if (roleDescriptors .size () > ROLE_DESCRIPTOR_FORK_THRESHOLD ) {
329+ return true ;
330+ }
331+ int totalIndexPrivileges = 0 ;
332+ int totalRemoteIndexPrivileges = 0 ;
333+ for (RoleDescriptor roleDescriptor : roleDescriptors ) {
334+ // Application privileges can also result in big automata; it's difficult to determine how big application privileges
335+ // are so err on the side of caution
336+ if (roleDescriptor .hasApplicationPrivileges ()) {
337+ return true ;
338+ }
339+ // Index privilege names or remote index privilege names can result in big and complex automata
340+ totalIndexPrivileges += roleDescriptor .getIndicesPrivileges ().length ;
341+ totalRemoteIndexPrivileges += roleDescriptor .getRemoteIndicesPrivileges ().length ;
342+ if (totalIndexPrivileges > INDEX_PRIVILEGE_FORK_THRESHOLD || totalRemoteIndexPrivileges > INDEX_PRIVILEGE_FORK_THRESHOLD ) {
343+ return true ;
344+ }
345+ // Likewise for FLS/DLS
346+ if (roleDescriptor .isUsingDocumentOrFieldLevelSecurity ()) {
347+ return true ;
348+ }
349+ }
350+ return false ;
351+ }
352+
294353 private static boolean includesSuperuserRole (RoleReference roleReference ) {
295354 if (roleReference instanceof RoleReference .NamedRoleReference namedRoles ) {
296355 return Arrays .asList (namedRoles .getRoleNames ()).contains (ReservedRolesStore .SUPERUSER_ROLE_DESCRIPTOR .getName ());
@@ -313,10 +372,11 @@ private void buildThenMaybeCacheRole(
313372 ActionListener <Role > listener
314373 ) {
315374 logger .trace (
316- "Building role from descriptors [{}] for names [{}] from source [{}]" ,
375+ "Building role from descriptors [{}] for names [{}] from source [{}] on [{}] " ,
317376 roleDescriptors ,
318377 roleKey .getNames (),
319- roleKey .getSource ()
378+ roleKey .getSource (),
379+ Thread .currentThread ().getName ()
320380 );
321381 buildRoleFromDescriptors (
322382 roleDescriptors ,
0 commit comments