Sfoglia il codice sorgente

Add Support for Providing a custom ServiceAccountTokenStore through SecurityExtensions (#126612)

* Add Project Service Account Auth
Johannes Fredén 5 mesi fa
parent
commit
bb9d1d6232
33 ha cambiato i file con 388 aggiunte e 132 eliminazioni
  1. 5 0
      docs/changelog/126612.yaml
  2. 14 0
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java
  3. 18 0
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/service/NodeLocalServiceAccountTokenStore.java
  4. 1 1
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/service/ServiceAccount.java
  5. 2 3
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/service/ServiceAccountToken.java
  6. 14 2
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/service/ServiceAccountTokenStore.java
  7. 2 2
      x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/service/ServiceAccountTokenTests.java
  8. 1 0
      x-pack/plugin/security/src/main/java/module-info.java
  9. 96 30
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java
  10. 1 1
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountAction.java
  11. 6 6
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountNodesCredentialsAction.java
  12. 1 1
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java
  13. 1 1
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ServiceAccountAuthenticator.java
  14. 4 2
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountTokenStore.java
  15. 3 1
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountTokenStore.java
  16. 1 0
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccounts.java
  17. 9 5
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountTokenStore.java
  18. 3 2
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileTokensTool.java
  19. 8 6
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountTokenStore.java
  20. 23 10
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java
  21. 86 2
      x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityTests.java
  22. 2 2
      x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java
  23. 1 1
      x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java
  24. 1 1
      x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticatorChainTests.java
  25. 2 2
      x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ServiceAccountAuthenticatorTests.java
  26. 19 9
      x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountTokenStoreTests.java
  27. 6 4
      x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountTokenStoreTests.java
  28. 1 0
      x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccountsTests.java
  29. 2 2
      x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountTokenStoreTests.java
  30. 3 2
      x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountTokenStoreTests.java
  31. 1 0
      x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountIdTests.java
  32. 50 33
      x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java
  33. 1 1
      x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/authc/service/FileTokensToolTests.java

+ 5 - 0
docs/changelog/126612.yaml

@@ -0,0 +1,5 @@
+pr: 126612
+summary: Add Support for Providing a custom `ServiceAccountTokenStore` through `SecurityExtensions`
+area: Authentication
+type: enhancement
+issues: []

+ 14 - 0
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java

@@ -16,6 +16,8 @@ import org.elasticsearch.threadpool.ThreadPool;
 import org.elasticsearch.watcher.ResourceWatcherService;
 import org.elasticsearch.xpack.core.security.authc.AuthenticationFailureHandler;
 import org.elasticsearch.xpack.core.security.authc.Realm;
+import org.elasticsearch.xpack.core.security.authc.service.NodeLocalServiceAccountTokenStore;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountTokenStore;
 import org.elasticsearch.xpack.core.security.authc.support.UserRoleMapper;
 import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine;
 import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
@@ -114,6 +116,18 @@ public interface SecurityExtension {
         return Collections.emptyList();
     }
 
+    /**
+     * Returns a {@link NodeLocalServiceAccountTokenStore} used to authenticate service account tokens.
+     * If {@code null} is returned, the default service account token stores will be used.
+     *
+     * Providing a custom {@link NodeLocalServiceAccountTokenStore} here overrides the default implementation.
+     *
+     * @param components Access to components that can be used to authenticate service account tokens
+     */
+    default ServiceAccountTokenStore getServiceAccountTokenStore(SecurityComponents components) {
+        return null;
+    }
+
     /**
      * Returns a authorization engine for authorizing requests, or null to use the default authorization mechanism.
      *

+ 18 - 0
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/service/NodeLocalServiceAccountTokenStore.java

@@ -0,0 +1,18 @@
+/*
+ * 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.authc.service;
+
+import org.elasticsearch.xpack.core.security.action.service.TokenInfo;
+
+import java.util.List;
+
+public interface NodeLocalServiceAccountTokenStore extends ServiceAccountTokenStore {
+    default List<TokenInfo> findNodeLocalTokensFor(ServiceAccount.ServiceAccountId accountId) {
+        throw new IllegalStateException("Find node local tokens not supported by [" + this.getClass() + "]");
+    }
+}

+ 1 - 1
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccount.java → x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/service/ServiceAccount.java

@@ -5,7 +5,7 @@
  * 2.0.
  */
 
-package org.elasticsearch.xpack.security.authc.service;
+package org.elasticsearch.xpack.core.security.authc.service;
 
 import org.apache.logging.log4j.util.Strings;
 import org.elasticsearch.common.io.stream.StreamInput;

+ 2 - 3
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountToken.java → x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/service/ServiceAccountToken.java

@@ -5,7 +5,7 @@
  * 2.0.
  */
 
-package org.elasticsearch.xpack.security.authc.service;
+package org.elasticsearch.xpack.core.security.authc.service;
 
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
@@ -14,9 +14,9 @@ import org.elasticsearch.common.UUIDs;
 import org.elasticsearch.common.settings.SecureString;
 import org.elasticsearch.core.CharArrays;
 import org.elasticsearch.xpack.core.security.authc.AuthenticationToken;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccount.ServiceAccountId;
 import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken;
 import org.elasticsearch.xpack.core.security.support.Validation;
-import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
@@ -51,7 +51,6 @@ public class ServiceAccountToken implements AuthenticationToken, Closeable {
     private final ServiceAccountTokenId tokenId;
     private final SecureString secret;
 
-    // pkg private for testing
     ServiceAccountToken(ServiceAccountId accountId, String tokenName, SecureString secret) {
         tokenId = new ServiceAccountTokenId(accountId, tokenName);
         this.secret = Objects.requireNonNull(secret, "service account token secret cannot be null");

+ 14 - 2
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountTokenStore.java → x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/service/ServiceAccountTokenStore.java

@@ -5,7 +5,7 @@
  * 2.0.
  */
 
-package org.elasticsearch.xpack.security.authc.service;
+package org.elasticsearch.xpack.core.security.authc.service;
 
 import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.xpack.core.security.action.service.TokenInfo.TokenSource;
@@ -24,11 +24,23 @@ public interface ServiceAccountTokenStore {
         private final boolean success;
         private final TokenSource tokenSource;
 
-        public StoreAuthenticationResult(boolean success, TokenSource tokenSource) {
+        private StoreAuthenticationResult(TokenSource tokenSource, boolean success) {
             this.success = success;
             this.tokenSource = tokenSource;
         }
 
+        public static StoreAuthenticationResult successful(TokenSource tokenSource) {
+            return new StoreAuthenticationResult(tokenSource, true);
+        }
+
+        public static StoreAuthenticationResult failed(TokenSource tokenSource) {
+            return new StoreAuthenticationResult(tokenSource, false);
+        }
+
+        public static StoreAuthenticationResult fromBooleanResult(TokenSource tokenSource, boolean result) {
+            return result ? successful(tokenSource) : failed(tokenSource);
+        }
+
         public boolean isSuccess() {
             return success;
         }

+ 2 - 2
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountTokenTests.java → x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/service/ServiceAccountTokenTests.java

@@ -5,13 +5,13 @@
  * 2.0.
  */
 
-package org.elasticsearch.xpack.security.authc.service;
+package org.elasticsearch.xpack.core.security.authc.service;
 
 import org.elasticsearch.common.settings.SecureString;
 import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccount.ServiceAccountId;
 import org.elasticsearch.xpack.core.security.support.Validation;
 import org.elasticsearch.xpack.core.security.support.ValidationTests;
-import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId;
 
 import java.io.IOException;
 

+ 1 - 0
x-pack/plugin/security/src/main/java/module-info.java

@@ -74,6 +74,7 @@ module org.elasticsearch.security {
     exports org.elasticsearch.xpack.security.rest.action.apikey to org.elasticsearch.internal.security;
     exports org.elasticsearch.xpack.security.support to org.elasticsearch.internal.security;
     exports org.elasticsearch.xpack.security.authz.store to org.elasticsearch.internal.security;
+    exports org.elasticsearch.xpack.security.authc.service;
 
     provides org.elasticsearch.index.SlowLogFieldProvider with org.elasticsearch.xpack.security.slowlog.SecuritySlowLogFieldProvider;
 

+ 96 - 30
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java

@@ -208,6 +208,8 @@ import org.elasticsearch.xpack.core.security.authc.Realm;
 import org.elasticsearch.xpack.core.security.authc.RealmConfig;
 import org.elasticsearch.xpack.core.security.authc.RealmSettings;
 import org.elasticsearch.xpack.core.security.authc.Subject;
+import org.elasticsearch.xpack.core.security.authc.service.NodeLocalServiceAccountTokenStore;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountTokenStore;
 import org.elasticsearch.xpack.core.security.authc.support.UserRoleMapper;
 import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken;
 import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine;
@@ -310,6 +312,7 @@ import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore;
 import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm;
 import org.elasticsearch.xpack.security.authc.jwt.JwtRealm;
 import org.elasticsearch.xpack.security.authc.service.CachingServiceAccountTokenStore;
+import org.elasticsearch.xpack.security.authc.service.CompositeServiceAccountTokenStore;
 import org.elasticsearch.xpack.security.authc.service.FileServiceAccountTokenStore;
 import org.elasticsearch.xpack.security.authc.service.IndexServiceAccountTokenStore;
 import org.elasticsearch.xpack.security.authc.service.ServiceAccountService;
@@ -915,12 +918,34 @@ public class Security extends Plugin
         this.realms.set(realms);
 
         systemIndices.getMainIndexManager().addStateListener(nativeRoleMappingStore::onSecurityIndexStateChange);
-
         final CacheInvalidatorRegistry cacheInvalidatorRegistry = new CacheInvalidatorRegistry();
-        cacheInvalidatorRegistry.registerAlias("service", Set.of("file_service_account_token", "index_service_account_token"));
         components.add(cacheInvalidatorRegistry);
-        systemIndices.getMainIndexManager().addStateListener(cacheInvalidatorRegistry::onSecurityIndexStateChange);
 
+        ServiceAccountService serviceAccountService = createServiceAccountService(
+            components,
+            cacheInvalidatorRegistry,
+            extensionComponents,
+            () -> new IndexServiceAccountTokenStore(
+                settings,
+                threadPool,
+                getClock(),
+                client,
+                systemIndices.getMainIndexManager(),
+                clusterService,
+                cacheInvalidatorRegistry
+            ),
+            () -> new FileServiceAccountTokenStore(
+                environment,
+                resourceWatcherService,
+                threadPool,
+                clusterService,
+                cacheInvalidatorRegistry
+            )
+        );
+
+        components.add(serviceAccountService);
+
+        systemIndices.getMainIndexManager().addStateListener(cacheInvalidatorRegistry::onSecurityIndexStateChange);
         final NativePrivilegeStore privilegeStore = new NativePrivilegeStore(
             settings,
             client,
@@ -1004,33 +1029,6 @@ public class Security extends Plugin
         );
         components.add(apiKeyService);
 
-        final IndexServiceAccountTokenStore indexServiceAccountTokenStore = new IndexServiceAccountTokenStore(
-            settings,
-            threadPool,
-            getClock(),
-            client,
-            systemIndices.getMainIndexManager(),
-            clusterService,
-            cacheInvalidatorRegistry
-        );
-        components.add(indexServiceAccountTokenStore);
-
-        final FileServiceAccountTokenStore fileServiceAccountTokenStore = new FileServiceAccountTokenStore(
-            environment,
-            resourceWatcherService,
-            threadPool,
-            clusterService,
-            cacheInvalidatorRegistry
-        );
-        components.add(fileServiceAccountTokenStore);
-
-        final ServiceAccountService serviceAccountService = new ServiceAccountService(
-            client,
-            fileServiceAccountTokenStore,
-            indexServiceAccountTokenStore
-        );
-        components.add(serviceAccountService);
-
         final RoleProviders roleProviders = new RoleProviders(
             reservedRolesStore,
             fileRolesStore.get(),
@@ -1250,6 +1248,74 @@ public class Security extends Plugin
         return components;
     }
 
+    private ServiceAccountService createServiceAccountService(
+        List<Object> components,
+        CacheInvalidatorRegistry cacheInvalidatorRegistry,
+        SecurityExtension.SecurityComponents extensionComponents,
+        Supplier<IndexServiceAccountTokenStore> indexServiceAccountTokenStoreSupplier,
+        Supplier<FileServiceAccountTokenStore> fileServiceAccountTokenStoreSupplier
+    ) {
+        Map<String, ServiceAccountTokenStore> accountTokenStoreByExtension = new HashMap<>();
+
+        for (var extension : securityExtensions) {
+            var serviceAccountTokenStore = extension.getServiceAccountTokenStore(extensionComponents);
+            if (serviceAccountTokenStore != null) {
+                if (isInternalExtension(extension) == false) {
+                    throw new IllegalStateException(
+                        "The ["
+                            + extension.getClass().getName()
+                            + "] extension tried to install a custom ServiceAccountTokenStore. This functionality is not available to "
+                            + "external extensions."
+                    );
+                }
+                accountTokenStoreByExtension.put(extension.extensionName(), serviceAccountTokenStore);
+            }
+        }
+
+        if (accountTokenStoreByExtension.size() > 1) {
+            throw new IllegalStateException(
+                "More than one extension provided a ServiceAccountTokenStore override: " + accountTokenStoreByExtension.keySet()
+            );
+        }
+
+        if (accountTokenStoreByExtension.isEmpty()) {
+            var fileServiceAccountTokenStore = fileServiceAccountTokenStoreSupplier.get();
+            var indexServiceAccountTokenStore = indexServiceAccountTokenStoreSupplier.get();
+
+            components.add(new PluginComponentBinding<>(NodeLocalServiceAccountTokenStore.class, fileServiceAccountTokenStore));
+            components.add(fileServiceAccountTokenStore);
+            components.add(indexServiceAccountTokenStore);
+            cacheInvalidatorRegistry.registerAlias("service", Set.of("file_service_account_token", "index_service_account_token"));
+
+            return new ServiceAccountService(
+                client.get(),
+                new CompositeServiceAccountTokenStore(
+                    List.of(fileServiceAccountTokenStore, indexServiceAccountTokenStore),
+                    client.get().threadPool().getThreadContext()
+                ),
+                indexServiceAccountTokenStore
+            );
+        }
+        // Completely handover service account token management to the extension if provided,
+        // this will disable the index managed
+        // service account tokens managed through the service account token API
+        var extensionStore = accountTokenStoreByExtension.values().stream().findFirst();
+        components.add(new PluginComponentBinding<>(NodeLocalServiceAccountTokenStore.class, (token, listener) -> {
+            throw new IllegalStateException("Node local config not supported by [" + extensionStore.get().getClass() + "]");
+        }));
+        components.add(extensionStore);
+        logger.debug("Service account authentication handled by extension, disabling file and index token stores");
+        return new ServiceAccountService(client.get(), extensionStore.get());
+    }
+
+    private static boolean isInternalExtension(SecurityExtension extension) {
+        final String canonicalName = extension.getClass().getCanonicalName();
+        if (canonicalName == null) {
+            return false;
+        }
+        return canonicalName.startsWith("org.elasticsearch.xpack.") || canonicalName.startsWith("co.elastic.elasticsearch.");
+    }
+
     @FixForMultiProject
     // TODO : The migration task needs to be project aware
     private void applyPendingSecurityMigrations(ProjectId projectId, SecurityIndexManager.IndexState newState) {

+ 1 - 1
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountAction.java

@@ -19,7 +19,7 @@ import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountAct
 import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountRequest;
 import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountResponse;
 import org.elasticsearch.xpack.core.security.action.service.ServiceAccountInfo;
-import org.elasticsearch.xpack.security.authc.service.ServiceAccount;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccount;
 import org.elasticsearch.xpack.security.authc.service.ServiceAccountService;
 
 import java.util.function.Predicate;

+ 6 - 6
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountNodesCredentialsAction.java

@@ -21,8 +21,8 @@ import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountCre
 import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountCredentialsNodesResponse;
 import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountNodesCredentialsAction;
 import org.elasticsearch.xpack.core.security.action.service.TokenInfo;
-import org.elasticsearch.xpack.security.authc.service.FileServiceAccountTokenStore;
-import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId;
+import org.elasticsearch.xpack.core.security.authc.service.NodeLocalServiceAccountTokenStore;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccount.ServiceAccountId;
 
 import java.io.IOException;
 import java.util.List;
@@ -38,7 +38,7 @@ public class TransportGetServiceAccountNodesCredentialsAction extends TransportN
     GetServiceAccountCredentialsNodesResponse.Node,
     Void> {
 
-    private final FileServiceAccountTokenStore fileServiceAccountTokenStore;
+    private final NodeLocalServiceAccountTokenStore readOnlyServiceAccountTokenStore;
 
     @Inject
     public TransportGetServiceAccountNodesCredentialsAction(
@@ -46,7 +46,7 @@ public class TransportGetServiceAccountNodesCredentialsAction extends TransportN
         ClusterService clusterService,
         TransportService transportService,
         ActionFilters actionFilters,
-        FileServiceAccountTokenStore fileServiceAccountTokenStore
+        NodeLocalServiceAccountTokenStore readOnlyServiceAccountTokenStore
     ) {
         super(
             GetServiceAccountNodesCredentialsAction.NAME,
@@ -56,7 +56,7 @@ public class TransportGetServiceAccountNodesCredentialsAction extends TransportN
             GetServiceAccountCredentialsNodesRequest.Node::new,
             threadPool.executor(ThreadPool.Names.GENERIC)
         );
-        this.fileServiceAccountTokenStore = fileServiceAccountTokenStore;
+        this.readOnlyServiceAccountTokenStore = readOnlyServiceAccountTokenStore;
     }
 
     @Override
@@ -84,7 +84,7 @@ public class TransportGetServiceAccountNodesCredentialsAction extends TransportN
         Task task
     ) {
         final ServiceAccountId accountId = new ServiceAccountId(request.getNamespace(), request.getServiceName());
-        final List<TokenInfo> tokenInfos = fileServiceAccountTokenStore.findTokensFor(accountId);
+        final List<TokenInfo> tokenInfos = readOnlyServiceAccountTokenStore.findNodeLocalTokensFor(accountId);
         return new GetServiceAccountCredentialsNodesResponse.Node(
             clusterService.localNode(),
             tokenInfos.stream().map(TokenInfo::getName).toArray(String[]::new)

+ 1 - 1
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java

@@ -97,6 +97,7 @@ import org.elasticsearch.xpack.core.security.authc.Authentication;
 import org.elasticsearch.xpack.core.security.authc.AuthenticationField;
 import org.elasticsearch.xpack.core.security.authc.AuthenticationToken;
 import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountToken;
 import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo;
 import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
 import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivileges;
@@ -110,7 +111,6 @@ import org.elasticsearch.xpack.security.audit.AuditLevel;
 import org.elasticsearch.xpack.security.audit.AuditTrail;
 import org.elasticsearch.xpack.security.audit.AuditUtil;
 import org.elasticsearch.xpack.security.authc.ApiKeyService;
-import org.elasticsearch.xpack.security.authc.service.ServiceAccountToken;
 import org.elasticsearch.xpack.security.rest.RemoteHostHeader;
 import org.elasticsearch.xpack.security.transport.filter.IPFilter;
 import org.elasticsearch.xpack.security.transport.filter.SecurityIpFilterRule;

+ 1 - 1
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ServiceAccountAuthenticator.java

@@ -15,8 +15,8 @@ import org.elasticsearch.telemetry.metric.MeterRegistry;
 import org.elasticsearch.xpack.core.security.authc.Authentication;
 import org.elasticsearch.xpack.core.security.authc.AuthenticationResult;
 import org.elasticsearch.xpack.core.security.authc.AuthenticationToken;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountToken;
 import org.elasticsearch.xpack.security.authc.service.ServiceAccountService;
-import org.elasticsearch.xpack.security.authc.service.ServiceAccountToken;
 import org.elasticsearch.xpack.security.metric.InstrumentedSecurityActionListener;
 import org.elasticsearch.xpack.security.metric.SecurityMetricType;
 import org.elasticsearch.xpack.security.metric.SecurityMetrics;

+ 4 - 2
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountTokenStore.java

@@ -19,6 +19,8 @@ import org.elasticsearch.common.util.concurrent.ListenableFuture;
 import org.elasticsearch.core.TimeValue;
 import org.elasticsearch.threadpool.ThreadPool;
 import org.elasticsearch.xpack.core.security.action.service.TokenInfo.TokenSource;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountToken;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountTokenStore;
 import org.elasticsearch.xpack.core.security.authc.support.Hasher;
 import org.elasticsearch.xpack.security.support.CacheInvalidatorRegistry;
 
@@ -97,10 +99,10 @@ public abstract class CachingServiceAccountTokenStore implements ServiceAccountT
             if (valueAlreadyInCache.get()) {
                 listenableCacheEntry.addListener(listener.delegateFailureAndWrap((l, result) -> {
                     if (result.success) {
-                        l.onResponse(new StoreAuthenticationResult(result.verify(token), getTokenSource()));
+                        l.onResponse(StoreAuthenticationResult.fromBooleanResult(getTokenSource(), result.verify(token)));
                     } else if (result.verify(token)) {
                         // same wrong token
-                        l.onResponse(new StoreAuthenticationResult(false, getTokenSource()));
+                        l.onResponse(StoreAuthenticationResult.failed(getTokenSource()));
                     } else {
                         cache.invalidate(token.getQualifiedName(), listenableCacheEntry);
                         authenticateWithCache(token, l);

+ 3 - 1
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountTokenStore.java

@@ -12,6 +12,8 @@ import org.apache.logging.log4j.Logger;
 import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.common.util.concurrent.ThreadContext;
 import org.elasticsearch.xpack.core.common.IteratingActionListener;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountToken;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountTokenStore;
 
 import java.util.List;
 import java.util.function.Function;
@@ -38,7 +40,7 @@ public final class CompositeServiceAccountTokenStore implements ServiceAccountTo
                 stores,
                 threadContext,
                 Function.identity(),
-                storeAuthenticationResult -> false == storeAuthenticationResult.isSuccess()
+                storeAuthenticationResult -> storeAuthenticationResult.isSuccess() == false
             );
         try {
             authenticatingListener.run();

+ 1 - 0
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccounts.java

@@ -8,6 +8,7 @@
 package org.elasticsearch.xpack.security.authc.service;
 
 import org.elasticsearch.common.Strings;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccount;
 import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
 import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore;
 import org.elasticsearch.xpack.core.security.user.User;

+ 9 - 5
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountTokenStore.java

@@ -22,10 +22,12 @@ import org.elasticsearch.watcher.ResourceWatcherService;
 import org.elasticsearch.xpack.core.XPackPlugin;
 import org.elasticsearch.xpack.core.security.action.service.TokenInfo;
 import org.elasticsearch.xpack.core.security.action.service.TokenInfo.TokenSource;
+import org.elasticsearch.xpack.core.security.authc.service.NodeLocalServiceAccountTokenStore;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccount.ServiceAccountId;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountToken;
 import org.elasticsearch.xpack.core.security.authc.support.Hasher;
 import org.elasticsearch.xpack.core.security.support.NoOpLogger;
 import org.elasticsearch.xpack.security.PrivilegedFileWatcher;
-import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId;
 import org.elasticsearch.xpack.security.support.CacheInvalidatorRegistry;
 import org.elasticsearch.xpack.security.support.FileLineParser;
 import org.elasticsearch.xpack.security.support.FileReloadListener;
@@ -41,7 +43,7 @@ import java.util.Map;
 import java.util.Optional;
 import java.util.concurrent.CopyOnWriteArrayList;
 
-public class FileServiceAccountTokenStore extends CachingServiceAccountTokenStore {
+public class FileServiceAccountTokenStore extends CachingServiceAccountTokenStore implements NodeLocalServiceAccountTokenStore {
 
     private static final Logger logger = LogManager.getLogger(FileServiceAccountTokenStore.class);
 
@@ -50,6 +52,7 @@ public class FileServiceAccountTokenStore extends CachingServiceAccountTokenStor
     private final CopyOnWriteArrayList<Runnable> refreshListeners;
     private volatile Map<String, char[]> tokenHashes;
 
+    @SuppressWarnings("this-escape")
     public FileServiceAccountTokenStore(
         Environment env,
         ResourceWatcherService resourceWatcherService,
@@ -82,8 +85,8 @@ public class FileServiceAccountTokenStore extends CachingServiceAccountTokenStor
         // because it is not expected to have a large number of service tokens.
         listener.onResponse(
             Optional.ofNullable(tokenHashes.get(token.getQualifiedName()))
-                .map(hash -> new StoreAuthenticationResult(Hasher.verifyHash(token.getSecret(), hash), getTokenSource()))
-                .orElse(new StoreAuthenticationResult(false, getTokenSource()))
+                .map(hash -> StoreAuthenticationResult.fromBooleanResult(getTokenSource(), Hasher.verifyHash(token.getSecret(), hash)))
+                .orElse(StoreAuthenticationResult.failed(getTokenSource()))
         );
     }
 
@@ -92,7 +95,8 @@ public class FileServiceAccountTokenStore extends CachingServiceAccountTokenStor
         return TokenSource.FILE;
     }
 
-    public List<TokenInfo> findTokensFor(ServiceAccountId accountId) {
+    @Override
+    public List<TokenInfo> findNodeLocalTokensFor(ServiceAccountId accountId) {
         final String principal = accountId.asPrincipal();
         return tokenHashes.keySet()
             .stream()

+ 3 - 2
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileTokensTool.java

@@ -21,10 +21,11 @@ import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.core.Predicates;
 import org.elasticsearch.env.Environment;
 import org.elasticsearch.xpack.core.XPackSettings;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccount.ServiceAccountId;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountToken;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountToken.ServiceAccountTokenId;
 import org.elasticsearch.xpack.core.security.authc.support.Hasher;
 import org.elasticsearch.xpack.core.security.support.Validation;
-import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId;
-import org.elasticsearch.xpack.security.authc.service.ServiceAccountToken.ServiceAccountTokenId;
 import org.elasticsearch.xpack.security.support.FileAttributesChecker;
 
 import java.nio.file.Path;

+ 8 - 6
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountTokenStore.java

@@ -47,9 +47,10 @@ import org.elasticsearch.xpack.core.security.action.service.TokenInfo;
 import org.elasticsearch.xpack.core.security.action.service.TokenInfo.TokenSource;
 import org.elasticsearch.xpack.core.security.authc.Authentication;
 import org.elasticsearch.xpack.core.security.authc.Subject;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccount.ServiceAccountId;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountToken;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountToken.ServiceAccountTokenId;
 import org.elasticsearch.xpack.core.security.authc.support.Hasher;
-import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId;
-import org.elasticsearch.xpack.security.authc.service.ServiceAccountToken.ServiceAccountTokenId;
 import org.elasticsearch.xpack.security.support.CacheInvalidatorRegistry;
 import org.elasticsearch.xpack.security.support.SecurityIndexManager;
 import org.elasticsearch.xpack.security.support.SecurityIndexManager.IndexState;
@@ -80,6 +81,7 @@ public class IndexServiceAccountTokenStore extends CachingServiceAccountTokenSto
     private final ClusterService clusterService;
     private final Hasher hasher;
 
+    @SuppressWarnings("this-escape")
     public IndexServiceAccountTokenStore(
         Settings settings,
         ThreadPool threadPool,
@@ -116,14 +118,14 @@ public class IndexServiceAccountTokenStore extends CachingServiceAccountTokenSto
                             final String tokenHash = (String) response.getSource().get("password");
                             assert tokenHash != null : "service account token hash cannot be null";
                             listener.onResponse(
-                                new StoreAuthenticationResult(
-                                    Hasher.verifyHash(token.getSecret(), tokenHash.toCharArray()),
-                                    getTokenSource()
+                                StoreAuthenticationResult.fromBooleanResult(
+                                    getTokenSource(),
+                                    Hasher.verifyHash(token.getSecret(), tokenHash.toCharArray())
                                 )
                             );
                         } else {
                             logger.trace("service account token [{}] not found in index", token.getQualifiedName());
-                            listener.onResponse(new StoreAuthenticationResult(false, getTokenSource()));
+                            listener.onResponse(StoreAuthenticationResult.failed(getTokenSource()));
                         }
                     }, listener::onFailure)
                 )

+ 23 - 10
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java

@@ -13,6 +13,7 @@ import org.elasticsearch.ElasticsearchSecurityException;
 import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.client.internal.Client;
 import org.elasticsearch.common.settings.SecureString;
+import org.elasticsearch.core.Nullable;
 import org.elasticsearch.rest.RestStatus;
 import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenRequest;
 import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenResponse;
@@ -24,12 +25,14 @@ import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountNod
 import org.elasticsearch.xpack.core.security.action.service.TokenInfo;
 import org.elasticsearch.xpack.core.security.action.service.TokenInfo.TokenSource;
 import org.elasticsearch.xpack.core.security.authc.Authentication;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccount;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccount.ServiceAccountId;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountToken;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountTokenStore;
 import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
 import org.elasticsearch.xpack.core.security.user.User;
-import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId;
 
 import java.util.Collection;
-import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 
@@ -46,19 +49,20 @@ public class ServiceAccountService {
 
     private final Client client;
     private final IndexServiceAccountTokenStore indexServiceAccountTokenStore;
-    private final CompositeServiceAccountTokenStore compositeServiceAccountTokenStore;
+    private final ServiceAccountTokenStore readOnlyServiceAccountTokenStore;
+
+    public ServiceAccountService(Client client, ServiceAccountTokenStore readOnlyServiceAccountTokenStore) {
+        this(client, readOnlyServiceAccountTokenStore, null);
+    }
 
     public ServiceAccountService(
         Client client,
-        FileServiceAccountTokenStore fileServiceAccountTokenStore,
-        IndexServiceAccountTokenStore indexServiceAccountTokenStore
+        ServiceAccountTokenStore readOnlyServiceAccountTokenStore,
+        @Nullable IndexServiceAccountTokenStore indexServiceAccountTokenStore
     ) {
         this.client = client;
+        this.readOnlyServiceAccountTokenStore = readOnlyServiceAccountTokenStore;
         this.indexServiceAccountTokenStore = indexServiceAccountTokenStore;
-        this.compositeServiceAccountTokenStore = new CompositeServiceAccountTokenStore(
-            List.of(fileServiceAccountTokenStore, indexServiceAccountTokenStore),
-            client.threadPool().getThreadContext()
-        );
     }
 
     public static boolean isServiceAccountPrincipal(String principal) {
@@ -131,7 +135,7 @@ public class ServiceAccountService {
             return;
         }
 
-        compositeServiceAccountTokenStore.authenticate(serviceAccountToken, ActionListener.wrap(storeAuthenticationResult -> {
+        readOnlyServiceAccountTokenStore.authenticate(serviceAccountToken, ActionListener.wrap(storeAuthenticationResult -> {
             if (storeAuthenticationResult.isSuccess()) {
                 listener.onResponse(
                     createAuthentication(account, serviceAccountToken, storeAuthenticationResult.getTokenSource(), nodeName)
@@ -149,14 +153,23 @@ public class ServiceAccountService {
         CreateServiceAccountTokenRequest request,
         ActionListener<CreateServiceAccountTokenResponse> listener
     ) {
+        if (indexServiceAccountTokenStore == null) {
+            throw new IllegalStateException("Can't create token because index service account token store not configured");
+        }
         indexServiceAccountTokenStore.createToken(authentication, request, listener);
     }
 
     public void deleteIndexToken(DeleteServiceAccountTokenRequest request, ActionListener<Boolean> listener) {
+        if (indexServiceAccountTokenStore == null) {
+            throw new IllegalStateException("Can't delete token because index service account token store not configured");
+        }
         indexServiceAccountTokenStore.deleteToken(request, listener);
     }
 
     public void findTokensFor(GetServiceAccountCredentialsRequest request, ActionListener<GetServiceAccountCredentialsResponse> listener) {
+        if (indexServiceAccountTokenStore == null) {
+            throw new IllegalStateException("Can't find tokens because index service account token store not configured");
+        }
         final ServiceAccountId accountId = new ServiceAccountId(request.getNamespace(), request.getServiceName());
         findIndexTokens(accountId, listener);
     }

+ 86 - 2
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityTests.java

@@ -15,6 +15,7 @@ import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.action.ActionModule;
 import org.elasticsearch.action.ActionResponse;
 import org.elasticsearch.action.bulk.IncrementalBulkService;
+import org.elasticsearch.action.support.PlainActionFuture;
 import org.elasticsearch.client.internal.Client;
 import org.elasticsearch.cluster.ClusterName;
 import org.elasticsearch.cluster.ClusterState;
@@ -32,6 +33,7 @@ import org.elasticsearch.common.settings.Setting;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.settings.SettingsModule;
 import org.elasticsearch.common.util.concurrent.ThreadContext;
+import org.elasticsearch.core.Nullable;
 import org.elasticsearch.core.TimeValue;
 import org.elasticsearch.env.Environment;
 import org.elasticsearch.env.TestEnvironment;
@@ -77,12 +79,15 @@ import org.elasticsearch.xpack.core.security.SecurityContext;
 import org.elasticsearch.xpack.core.security.SecurityExtension;
 import org.elasticsearch.xpack.core.security.SecurityField;
 import org.elasticsearch.xpack.core.security.action.ActionTypes;
+import org.elasticsearch.xpack.core.security.action.service.TokenInfo;
 import org.elasticsearch.xpack.core.security.authc.Authentication;
 import org.elasticsearch.xpack.core.security.authc.AuthenticationTestHelper;
 import org.elasticsearch.xpack.core.security.authc.Realm;
 import org.elasticsearch.xpack.core.security.authc.RealmConfig;
 import org.elasticsearch.xpack.core.security.authc.RealmSettings;
 import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountToken;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountTokenStore;
 import org.elasticsearch.xpack.core.security.authc.support.CachingUsernamePasswordRealmSettings;
 import org.elasticsearch.xpack.core.security.authc.support.Hasher;
 import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl;
@@ -99,6 +104,9 @@ import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore;
 import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm;
 import org.elasticsearch.xpack.security.authc.jwt.JwtRealm;
 import org.elasticsearch.xpack.security.authc.service.CachingServiceAccountTokenStore;
+import org.elasticsearch.xpack.security.authc.service.FileServiceAccountTokenStore;
+import org.elasticsearch.xpack.security.authc.service.IndexServiceAccountTokenStore;
+import org.elasticsearch.xpack.security.authc.service.ServiceAccountService;
 import org.elasticsearch.xpack.security.operator.DefaultOperatorOnlyRegistry;
 import org.elasticsearch.xpack.security.operator.OperatorOnlyRegistry;
 import org.elasticsearch.xpack.security.operator.OperatorPrivileges;
@@ -157,16 +165,34 @@ public class SecurityTests extends ESTestCase {
     private TestUtils.UpdatableLicenseState licenseState;
 
     public static class DummyExtension implements SecurityExtension {
-        private String realmType;
+        private final String realmType;
+        private final ServiceAccountTokenStore serviceAccountTokenStore;
+        private final String extensionName;
 
         DummyExtension(String realmType) {
+            this(realmType, "DummyExtension", null);
+        }
+
+        DummyExtension(String realmType, String extensionName, @Nullable ServiceAccountTokenStore serviceAccountTokenStore) {
             this.realmType = realmType;
+            this.extensionName = extensionName;
+            this.serviceAccountTokenStore = serviceAccountTokenStore;
+        }
+
+        @Override
+        public String extensionName() {
+            return extensionName;
         }
 
         @Override
         public Map<String, Realm.Factory> getRealms(SecurityComponents components) {
             return Collections.singletonMap(realmType, config -> null);
         }
+
+        @Override
+        public ServiceAccountTokenStore getServiceAccountTokenStore(SecurityComponents components) {
+            return serviceAccountTokenStore;
+        }
     }
 
     public static class DummyOperatorOnlyRegistry implements OperatorOnlyRegistry {
@@ -266,7 +292,7 @@ public class SecurityTests extends ESTestCase {
         assertNotNull(realms.realmFactory("myrealm"));
     }
 
-    public void testCustomRealmExtensionConflict() throws Exception {
+    public void testCustomRealmExtensionConflict() {
         IllegalArgumentException e = expectThrows(
             IllegalArgumentException.class,
             () -> createComponents(Settings.EMPTY, new DummyExtension(FileRealmSettings.TYPE))
@@ -274,6 +300,64 @@ public class SecurityTests extends ESTestCase {
         assertEquals("Realm type [" + FileRealmSettings.TYPE + "] is already registered", e.getMessage());
     }
 
+    public void testServiceAccountTokenStoreExtensionSuccess() throws Exception {
+        Collection<Object> components = createComponents(
+            Settings.EMPTY,
+            new DummyExtension(
+                "test_realm",
+                "DummyExtension",
+                (token, listener) -> listener.onResponse(
+                    ServiceAccountTokenStore.StoreAuthenticationResult.successful(TokenInfo.TokenSource.FILE)
+                )
+            )
+        );
+        ServiceAccountService serviceAccountService = findComponent(ServiceAccountService.class, components);
+        assertNotNull(serviceAccountService);
+        FileServiceAccountTokenStore fileServiceAccountTokenStore = findComponent(FileServiceAccountTokenStore.class, components);
+        assertNull(fileServiceAccountTokenStore);
+        IndexServiceAccountTokenStore indexServiceAccountTokenStore = findComponent(IndexServiceAccountTokenStore.class, components);
+        assertNull(indexServiceAccountTokenStore);
+        var account = randomFrom(ServiceAccountService.getServiceAccounts().values());
+        assertThrows(IllegalStateException.class, () -> serviceAccountService.createIndexToken(null, null, null));
+        var future = new PlainActionFuture<Authentication>();
+        serviceAccountService.authenticateToken(ServiceAccountToken.newToken(account.id(), "test"), "test", future);
+        assertTrue(future.get().isServiceAccount());
+    }
+
+    public void testSeveralServiceAccountTokenStoreExtensionFail() {
+        IllegalStateException exception = assertThrows(
+            IllegalStateException.class,
+            () -> createComponents(
+                Settings.EMPTY,
+                new DummyExtension(
+                    "test_realm_1",
+                    "DummyExtension1",
+                    (token, listener) -> listener.onResponse(
+                        ServiceAccountTokenStore.StoreAuthenticationResult.successful(TokenInfo.TokenSource.FILE)
+                    )
+                ),
+                new DummyExtension(
+                    "test_realm_2",
+                    "DummyExtension2",
+                    (token, listener) -> listener.onResponse(
+                        ServiceAccountTokenStore.StoreAuthenticationResult.successful(TokenInfo.TokenSource.FILE)
+                    )
+                )
+            )
+        );
+        assertThat(exception.getMessage(), containsString("More than one extension provided a ServiceAccountTokenStore override: "));
+    }
+
+    public void testNoServiceAccountTokenStoreExtension() throws Exception {
+        Collection<Object> components = createComponents(Settings.EMPTY);
+        ServiceAccountService serviceAccountService = findComponent(ServiceAccountService.class, components);
+        assertNotNull(serviceAccountService);
+        FileServiceAccountTokenStore fileServiceAccountTokenStore = findComponent(FileServiceAccountTokenStore.class, components);
+        assertNotNull(fileServiceAccountTokenStore);
+        IndexServiceAccountTokenStore indexServiceAccountTokenStore = findComponent(IndexServiceAccountTokenStore.class, components);
+        assertNotNull(indexServiceAccountTokenStore);
+    }
+
     public void testAuditEnabled() throws Exception {
         Settings settings = Settings.builder().put(XPackSettings.AUDIT_ENABLED.getKey(), true).build();
         Collection<Object> components = createComponents(settings);

+ 2 - 2
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java

@@ -102,7 +102,9 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationField;
 import org.elasticsearch.xpack.core.security.authc.AuthenticationTestHelper;
 import org.elasticsearch.xpack.core.security.authc.AuthenticationToken;
 import org.elasticsearch.xpack.core.security.authc.CrossClusterAccessSubjectInfo;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccount.ServiceAccountId;
 import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountToken;
 import org.elasticsearch.xpack.core.security.authc.support.mapper.TemplateRoleName;
 import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.ExpressionModel;
 import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.RoleMapperExpression;
@@ -124,8 +126,6 @@ import org.elasticsearch.xpack.security.audit.AuditLevel;
 import org.elasticsearch.xpack.security.audit.AuditTrail;
 import org.elasticsearch.xpack.security.audit.AuditUtil;
 import org.elasticsearch.xpack.security.authc.ApiKeyService;
-import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId;
-import org.elasticsearch.xpack.security.authc.service.ServiceAccountToken;
 import org.elasticsearch.xpack.security.rest.RemoteHostHeader;
 import org.elasticsearch.xpack.security.support.CacheInvalidatorRegistry;
 import org.elasticsearch.xpack.security.support.SecurityIndexManager;

+ 1 - 1
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java

@@ -81,6 +81,7 @@ import org.elasticsearch.xpack.core.security.authc.RealmConfig;
 import org.elasticsearch.xpack.core.security.authc.RealmDomain;
 import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings;
 import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountToken;
 import org.elasticsearch.xpack.core.security.authc.support.AuthenticationContextSerializer;
 import org.elasticsearch.xpack.core.security.authc.support.Hasher;
 import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken;
@@ -98,7 +99,6 @@ import org.elasticsearch.xpack.security.authc.esnative.NativeRealm;
 import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm;
 import org.elasticsearch.xpack.security.authc.file.FileRealm;
 import org.elasticsearch.xpack.security.authc.service.ServiceAccountService;
-import org.elasticsearch.xpack.security.authc.service.ServiceAccountToken;
 import org.elasticsearch.xpack.security.operator.OperatorPrivileges;
 import org.elasticsearch.xpack.security.support.CacheInvalidatorRegistry;
 import org.elasticsearch.xpack.security.support.SecurityIndexManager;

+ 1 - 1
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticatorChainTests.java

@@ -28,12 +28,12 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationServiceField;
 import org.elasticsearch.xpack.core.security.authc.AuthenticationTestHelper;
 import org.elasticsearch.xpack.core.security.authc.AuthenticationToken;
 import org.elasticsearch.xpack.core.security.authc.Realm;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountToken;
 import org.elasticsearch.xpack.core.security.authc.support.AuthenticationContextSerializer;
 import org.elasticsearch.xpack.core.security.authc.support.BearerToken;
 import org.elasticsearch.xpack.core.security.user.AnonymousUser;
 import org.elasticsearch.xpack.core.security.user.User;
 import org.elasticsearch.xpack.security.authc.ApiKeyService.ApiKeyCredentials;
-import org.elasticsearch.xpack.security.authc.service.ServiceAccountToken;
 import org.elasticsearch.xpack.security.operator.OperatorPrivileges.OperatorPrivilegesService;
 import org.junit.Before;
 

+ 2 - 2
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ServiceAccountAuthenticatorTests.java

@@ -16,10 +16,10 @@ import org.elasticsearch.rest.RestStatus;
 import org.elasticsearch.telemetry.TestTelemetryPlugin;
 import org.elasticsearch.xpack.core.security.authc.Authentication;
 import org.elasticsearch.xpack.core.security.authc.AuthenticationResult;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccount.ServiceAccountId;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountToken;
 import org.elasticsearch.xpack.core.security.user.User;
-import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId;
 import org.elasticsearch.xpack.security.authc.service.ServiceAccountService;
-import org.elasticsearch.xpack.security.authc.service.ServiceAccountToken;
 import org.elasticsearch.xpack.security.metric.SecurityMetricType;
 
 import java.util.Map;

+ 19 - 9
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountTokenStoreTests.java

@@ -17,9 +17,10 @@ import org.elasticsearch.test.ESTestCase;
 import org.elasticsearch.threadpool.TestThreadPool;
 import org.elasticsearch.threadpool.ThreadPool;
 import org.elasticsearch.xpack.core.security.action.service.TokenInfo.TokenSource;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccount.ServiceAccountId;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountToken;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountTokenStore.StoreAuthenticationResult;
 import org.elasticsearch.xpack.core.security.support.ValidationTests;
-import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId;
-import org.elasticsearch.xpack.security.authc.service.ServiceAccountTokenStore.StoreAuthenticationResult;
 import org.junit.After;
 import org.junit.Before;
 
@@ -34,6 +35,7 @@ import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.nullValue;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 public class CachingServiceAccountTokenStoreTests extends ESTestCase {
 
@@ -53,14 +55,22 @@ public class CachingServiceAccountTokenStoreTests extends ESTestCase {
         }
     }
 
+    private ServiceAccountToken newMockServiceAccountToken(ServiceAccountId accountId, String tokenName, SecureString secret) {
+        ServiceAccountToken serviceAccountToken = mock(ServiceAccountToken.class);
+        var serviceAccountTokenId = new ServiceAccountToken.ServiceAccountTokenId(accountId, tokenName);
+        when(serviceAccountToken.getQualifiedName()).thenReturn(serviceAccountTokenId.getQualifiedName());
+        when(serviceAccountToken.getSecret()).thenReturn(secret);
+        return serviceAccountToken;
+    }
+
     public void testCache() throws ExecutionException, InterruptedException {
         final ServiceAccountId accountId = new ServiceAccountId(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8));
         final SecureString validSecret = new SecureString("super-secret-value".toCharArray());
         final SecureString invalidSecret = new SecureString("some-fishy-value".toCharArray());
-        final ServiceAccountToken token1Valid = new ServiceAccountToken(accountId, "token1", validSecret);
-        final ServiceAccountToken token1Invalid = new ServiceAccountToken(accountId, "token1", invalidSecret);
-        final ServiceAccountToken token2Valid = new ServiceAccountToken(accountId, "token2", validSecret);
-        final ServiceAccountToken token2Invalid = new ServiceAccountToken(accountId, "token2", invalidSecret);
+        final ServiceAccountToken token1Valid = newMockServiceAccountToken(accountId, "token1", validSecret);
+        final ServiceAccountToken token1Invalid = newMockServiceAccountToken(accountId, "token1", invalidSecret);
+        final ServiceAccountToken token2Valid = newMockServiceAccountToken(accountId, "token2", validSecret);
+        final ServiceAccountToken token2Invalid = newMockServiceAccountToken(accountId, "token2", invalidSecret);
         final AtomicBoolean doAuthenticateInvoked = new AtomicBoolean(false);
         final TokenSource tokenSource = randomFrom(TokenSource.values());
 
@@ -68,7 +78,7 @@ public class CachingServiceAccountTokenStoreTests extends ESTestCase {
             @Override
             void doAuthenticate(ServiceAccountToken token, ActionListener<StoreAuthenticationResult> listener) {
                 doAuthenticateInvoked.set(true);
-                listener.onResponse(new StoreAuthenticationResult(validSecret.equals(token.getSecret()), getTokenSource()));
+                listener.onResponse(StoreAuthenticationResult.fromBooleanResult(getTokenSource(), validSecret.equals(token.getSecret())));
             }
 
             @Override
@@ -160,7 +170,7 @@ public class CachingServiceAccountTokenStoreTests extends ESTestCase {
         final CachingServiceAccountTokenStore store = new CachingServiceAccountTokenStore(settings, threadPool) {
             @Override
             void doAuthenticate(ServiceAccountToken token, ActionListener<StoreAuthenticationResult> listener) {
-                listener.onResponse(new StoreAuthenticationResult(success, getTokenSource()));
+                listener.onResponse(StoreAuthenticationResult.fromBooleanResult(getTokenSource(), success));
             }
 
             @Override
@@ -181,7 +191,7 @@ public class CachingServiceAccountTokenStoreTests extends ESTestCase {
         final CachingServiceAccountTokenStore store = new CachingServiceAccountTokenStore(globalSettings, threadPool) {
             @Override
             void doAuthenticate(ServiceAccountToken token, ActionListener<StoreAuthenticationResult> listener) {
-                listener.onResponse(new StoreAuthenticationResult(true, getTokenSource()));
+                listener.onResponse(StoreAuthenticationResult.successful(getTokenSource()));
             }
 
             @Override

+ 6 - 4
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountTokenStoreTests.java

@@ -13,7 +13,9 @@ import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.util.concurrent.ThreadContext;
 import org.elasticsearch.test.ESTestCase;
 import org.elasticsearch.xpack.core.security.action.service.TokenInfo.TokenSource;
-import org.elasticsearch.xpack.security.authc.service.ServiceAccountTokenStore.StoreAuthenticationResult;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountToken;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountTokenStore;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountTokenStore.StoreAuthenticationResult;
 import org.junit.Before;
 import org.mockito.Mockito;
 
@@ -58,7 +60,7 @@ public class CompositeServiceAccountTokenStoreTests extends ESTestCase {
             @SuppressWarnings("unchecked")
             final ActionListener<StoreAuthenticationResult> listener = (ActionListener<StoreAuthenticationResult>) invocationOnMock
                 .getArguments()[1];
-            listener.onResponse(new StoreAuthenticationResult(store1Success, tokenSource));
+            listener.onResponse(StoreAuthenticationResult.fromBooleanResult(tokenSource, store1Success));
             return null;
         }).when(store1).authenticate(eq(token), any());
 
@@ -66,7 +68,7 @@ public class CompositeServiceAccountTokenStoreTests extends ESTestCase {
             @SuppressWarnings("unchecked")
             final ActionListener<StoreAuthenticationResult> listener = (ActionListener<StoreAuthenticationResult>) invocationOnMock
                 .getArguments()[1];
-            listener.onResponse(new StoreAuthenticationResult(store2Success, tokenSource));
+            listener.onResponse(StoreAuthenticationResult.fromBooleanResult(tokenSource, store2Success));
             return null;
         }).when(store2).authenticate(eq(token), any());
 
@@ -74,7 +76,7 @@ public class CompositeServiceAccountTokenStoreTests extends ESTestCase {
             @SuppressWarnings("unchecked")
             final ActionListener<StoreAuthenticationResult> listener = (ActionListener<StoreAuthenticationResult>) invocationOnMock
                 .getArguments()[1];
-            listener.onResponse(new StoreAuthenticationResult(store3Success, tokenSource));
+            listener.onResponse(StoreAuthenticationResult.fromBooleanResult(tokenSource, store3Success));
             return null;
         }).when(store3).authenticate(eq(token), any());
 

+ 1 - 0
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccountsTests.java

@@ -56,6 +56,7 @@ import org.elasticsearch.xpack.core.security.action.role.PutRoleAction;
 import org.elasticsearch.xpack.core.security.action.user.PutUserAction;
 import org.elasticsearch.xpack.core.security.authc.Authentication;
 import org.elasticsearch.xpack.core.security.authc.AuthenticationTestHelper;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccount;
 import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
 import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsCache;
 import org.elasticsearch.xpack.core.security.authz.permission.Role;

+ 2 - 2
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountTokenStoreTests.java

@@ -21,8 +21,8 @@ import org.elasticsearch.threadpool.ThreadPool;
 import org.elasticsearch.watcher.ResourceWatcherService;
 import org.elasticsearch.xpack.core.security.action.service.TokenInfo;
 import org.elasticsearch.xpack.core.security.audit.logfile.CapturingLogger;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccount.ServiceAccountId;
 import org.elasticsearch.xpack.core.security.authc.support.Hasher;
-import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId;
 import org.elasticsearch.xpack.security.support.CacheInvalidatorRegistry;
 import org.junit.After;
 import org.junit.Before;
@@ -238,7 +238,7 @@ public class FileServiceAccountTokenStoreTests extends ESTestCase {
         );
 
         final ServiceAccountId accountId = new ServiceAccountId("elastic", "fleet-server");
-        final List<TokenInfo> tokenInfos = store.findTokensFor(accountId);
+        final List<TokenInfo> tokenInfos = store.findNodeLocalTokensFor(accountId);
         assertThat(tokenInfos, hasSize(5));
         assertThat(
             tokenInfos.stream().map(TokenInfo::getName).collect(Collectors.toUnmodifiableSet()),

+ 3 - 2
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountTokenStoreTests.java

@@ -55,10 +55,11 @@ import org.elasticsearch.xpack.core.security.action.service.TokenInfo;
 import org.elasticsearch.xpack.core.security.action.service.TokenInfo.TokenSource;
 import org.elasticsearch.xpack.core.security.authc.Authentication;
 import org.elasticsearch.xpack.core.security.authc.AuthenticationTestHelper;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccount.ServiceAccountId;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountToken;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountTokenStore.StoreAuthenticationResult;
 import org.elasticsearch.xpack.core.security.authc.support.Hasher;
 import org.elasticsearch.xpack.core.security.support.ValidationTests;
-import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId;
-import org.elasticsearch.xpack.security.authc.service.ServiceAccountTokenStore.StoreAuthenticationResult;
 import org.elasticsearch.xpack.security.support.CacheInvalidatorRegistry;
 import org.elasticsearch.xpack.security.support.SecurityIndexManager;
 import org.junit.Before;

+ 1 - 0
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountIdTests.java

@@ -10,6 +10,7 @@ package org.elasticsearch.xpack.security.authc.service;
 import org.elasticsearch.common.io.stream.BytesStreamOutput;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccount;
 
 import java.io.IOException;
 

+ 50 - 33
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java

@@ -33,10 +33,12 @@ import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountNod
 import org.elasticsearch.xpack.core.security.action.service.TokenInfo;
 import org.elasticsearch.xpack.core.security.authc.Authentication;
 import org.elasticsearch.xpack.core.security.authc.AuthenticationTestHelper;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccount.ServiceAccountId;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountToken;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountTokenStore;
 import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
 import org.elasticsearch.xpack.core.security.support.ValidationTests;
 import org.elasticsearch.xpack.core.security.user.User;
-import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId;
 import org.junit.After;
 import org.junit.Before;
 
@@ -83,7 +85,14 @@ public class ServiceAccountServiceTests extends ESTestCase {
         when(indexServiceAccountTokenStore.getTokenSource()).thenReturn(TokenInfo.TokenSource.INDEX);
         client = mock(Client.class);
         when(client.threadPool()).thenReturn(threadPool);
-        serviceAccountService = new ServiceAccountService(client, fileServiceAccountTokenStore, indexServiceAccountTokenStore);
+        serviceAccountService = new ServiceAccountService(
+            client,
+            new CompositeServiceAccountTokenStore(
+                List.of(fileServiceAccountTokenStore, indexServiceAccountTokenStore),
+                threadPool.getThreadContext()
+            ),
+            indexServiceAccountTokenStore
+        );
     }
 
     @After
@@ -228,16 +237,15 @@ public class ServiceAccountServiceTests extends ESTestCase {
                 List.of(magicBytes, (namespace + "/" + serviceName + "/" + tokenName + ":" + secret).getBytes(StandardCharsets.UTF_8))
             );
             final ServiceAccountToken serviceAccountToken1 = ServiceAccountService.tryParseToken(bearerString5);
-            final ServiceAccountToken serviceAccountToken2 = new ServiceAccountToken(
-                accountId,
-                tokenName,
-                new SecureString(secret.toCharArray())
-            );
-            assertThat(serviceAccountToken1, equalTo(serviceAccountToken2));
+
+            assertNotNull(serviceAccountToken1);
+            assertThat(serviceAccountToken1.getAccountId(), equalTo(accountId));
+            assertThat(serviceAccountToken1.getTokenName(), equalTo(tokenName));
+            assertThat(serviceAccountToken1.getSecret(), equalTo(new SecureString(secret.toCharArray())));
 
             // Serialise and de-serialise service account token
-            final ServiceAccountToken parsedToken = ServiceAccountService.tryParseToken(serviceAccountToken2.asBearerString());
-            assertThat(parsedToken, equalTo(serviceAccountToken2));
+            final ServiceAccountToken parsedToken = ServiceAccountService.tryParseToken(serviceAccountToken1.asBearerString());
+            assertThat(parsedToken, equalTo(serviceAccountToken1));
 
             // Invalid magic byte
             satMockLog.addExpectation(
@@ -295,25 +303,31 @@ public class ServiceAccountServiceTests extends ESTestCase {
             );
             sasMockLog.assertAllExpectationsMatched();
 
-            // everything is fine
-            assertThat(
-                ServiceAccountService.tryParseToken(
-                    new SecureString("AAEAAWVsYXN0aWMvZmxlZXQtc2VydmVyL3Rva2VuMTpzdXBlcnNlY3JldA".toCharArray())
-                ),
-                equalTo(
-                    new ServiceAccountToken(
-                        new ServiceAccountId("elastic", "fleet-server"),
-                        "token1",
-                        new SecureString("supersecret".toCharArray())
-                    )
-                )
+            ServiceAccountToken parsedServiceAccountToken = ServiceAccountService.tryParseToken(
+                new SecureString("AAEAAWVsYXN0aWMvZmxlZXQtc2VydmVyL3Rva2VuMTpzdXBlcnNlY3JldA".toCharArray())
             );
+
+            // everything is fine
+            assertNotNull(parsedServiceAccountToken);
+            assertThat(parsedServiceAccountToken.getAccountId(), equalTo(new ServiceAccountId("elastic", "fleet-server")));
+            assertThat(parsedServiceAccountToken.getTokenName(), equalTo("token1"));
+            assertThat(parsedServiceAccountToken.getSecret(), equalTo(new SecureString("supersecret".toCharArray())));
         } finally {
             Loggers.setLevel(satLogger, Level.INFO);
             Loggers.setLevel(sasLogger, Level.INFO);
         }
     }
 
+    private ServiceAccountToken newMockServiceAccountToken(ServiceAccountId accountId, String tokenName, SecureString secret) {
+        ServiceAccountToken serviceAccountToken = mock(ServiceAccountToken.class);
+        var serviceAccountTokenId = new ServiceAccountToken.ServiceAccountTokenId(accountId, tokenName);
+        when(serviceAccountToken.getQualifiedName()).thenReturn(serviceAccountTokenId.getQualifiedName());
+        when(serviceAccountToken.getSecret()).thenReturn(secret);
+        when(serviceAccountToken.getAccountId()).thenReturn(accountId);
+        when(serviceAccountToken.getTokenName()).thenReturn(tokenName);
+        return serviceAccountToken;
+    }
+
     public void testTryAuthenticateBearerToken() throws ExecutionException, InterruptedException {
         // Valid token
         final PlainActionFuture<Authentication> future5 = new PlainActionFuture<>();
@@ -325,7 +339,10 @@ public class ServiceAccountServiceTests extends ESTestCase {
                 final ActionListener<ServiceAccountTokenStore.StoreAuthenticationResult> listener = (ActionListener<
                     ServiceAccountTokenStore.StoreAuthenticationResult>) invocationOnMock.getArguments()[1];
                 listener.onResponse(
-                    new ServiceAccountTokenStore.StoreAuthenticationResult(store == authenticatingStore, store.getTokenSource())
+                    ServiceAccountTokenStore.StoreAuthenticationResult.fromBooleanResult(
+                        store.getTokenSource(),
+                        store == authenticatingStore
+                    )
                 );
                 return null;
             }).when(store).authenticate(any(), any());
@@ -333,7 +350,7 @@ public class ServiceAccountServiceTests extends ESTestCase {
 
         final String nodeName = randomAlphaOfLengthBetween(3, 8);
         serviceAccountService.authenticateToken(
-            new ServiceAccountToken(
+            newMockServiceAccountToken(
                 new ServiceAccountId("elastic", "fleet-server"),
                 "token1",
                 new SecureString("super-secret-value".toCharArray())
@@ -379,7 +396,7 @@ public class ServiceAccountServiceTests extends ESTestCase {
                 )
             );
             final SecureString secret = new SecureString(randomAlphaOfLength(20).toCharArray());
-            final ServiceAccountToken token1 = new ServiceAccountToken(accountId1, randomAlphaOfLengthBetween(3, 8), secret);
+            final ServiceAccountToken token1 = newMockServiceAccountToken(accountId1, randomAlphaOfLengthBetween(3, 8), secret);
             final PlainActionFuture<Authentication> future1 = new PlainActionFuture<>();
             serviceAccountService.authenticateToken(token1, randomAlphaOfLengthBetween(3, 8), future1);
             final ExecutionException e1 = expectThrows(ExecutionException.class, future1::get);
@@ -409,7 +426,7 @@ public class ServiceAccountServiceTests extends ESTestCase {
                     "the [" + accountId2.asPrincipal() + "] service account does not exist"
                 )
             );
-            final ServiceAccountToken token2 = new ServiceAccountToken(accountId2, randomAlphaOfLengthBetween(3, 8), secret);
+            final ServiceAccountToken token2 = newMockServiceAccountToken(accountId2, randomAlphaOfLengthBetween(3, 8), secret);
             final PlainActionFuture<Authentication> future2 = new PlainActionFuture<>();
             serviceAccountService.authenticateToken(token2, randomAlphaOfLengthBetween(3, 8), future2);
             final ExecutionException e2 = expectThrows(ExecutionException.class, future2::get);
@@ -429,7 +446,7 @@ public class ServiceAccountServiceTests extends ESTestCase {
             // Length of secret value is too short
             final ServiceAccountId accountId3 = new ServiceAccountId(ElasticServiceAccounts.NAMESPACE, "fleet-server");
             final SecureString secret3 = new SecureString(randomAlphaOfLengthBetween(1, 9).toCharArray());
-            final ServiceAccountToken token3 = new ServiceAccountToken(accountId3, randomAlphaOfLengthBetween(3, 8), secret3);
+            final ServiceAccountToken token3 = newMockServiceAccountToken(accountId3, randomAlphaOfLengthBetween(3, 8), secret3);
             mockLog.addExpectation(
                 new MockLog.SeenEventExpectation(
                     "secret value too short",
@@ -456,7 +473,7 @@ public class ServiceAccountServiceTests extends ESTestCase {
             );
             mockLog.assertAllExpectationsMatched();
 
-            final TokenInfo.TokenSource tokenSource = randomFrom(TokenInfo.TokenSource.values());
+            final TokenInfo.TokenSource tokenSource = randomFrom(TokenInfo.TokenSource.FILE, TokenInfo.TokenSource.INDEX);
             final CachingServiceAccountTokenStore store;
             final CachingServiceAccountTokenStore otherStore;
             if (tokenSource == TokenInfo.TokenSource.FILE) {
@@ -469,8 +486,8 @@ public class ServiceAccountServiceTests extends ESTestCase {
 
             // Success based on credential store
             final ServiceAccountId accountId4 = new ServiceAccountId(ElasticServiceAccounts.NAMESPACE, "fleet-server");
-            final ServiceAccountToken token4 = new ServiceAccountToken(accountId4, randomAlphaOfLengthBetween(3, 8), secret);
-            final ServiceAccountToken token5 = new ServiceAccountToken(
+            final ServiceAccountToken token4 = newMockServiceAccountToken(accountId4, randomAlphaOfLengthBetween(3, 8), secret);
+            final ServiceAccountToken token5 = newMockServiceAccountToken(
                 accountId4,
                 randomAlphaOfLengthBetween(3, 8),
                 new SecureString(randomAlphaOfLength(20).toCharArray())
@@ -480,7 +497,7 @@ public class ServiceAccountServiceTests extends ESTestCase {
                 @SuppressWarnings("unchecked")
                 final ActionListener<ServiceAccountTokenStore.StoreAuthenticationResult> listener = (ActionListener<
                     ServiceAccountTokenStore.StoreAuthenticationResult>) invocationOnMock.getArguments()[1];
-                listener.onResponse(new ServiceAccountTokenStore.StoreAuthenticationResult(true, store.getTokenSource()));
+                listener.onResponse(ServiceAccountTokenStore.StoreAuthenticationResult.successful(store.getTokenSource()));
                 return null;
             }).when(store).authenticate(eq(token4), any());
 
@@ -488,7 +505,7 @@ public class ServiceAccountServiceTests extends ESTestCase {
                 @SuppressWarnings("unchecked")
                 final ActionListener<ServiceAccountTokenStore.StoreAuthenticationResult> listener = (ActionListener<
                     ServiceAccountTokenStore.StoreAuthenticationResult>) invocationOnMock.getArguments()[1];
-                listener.onResponse(new ServiceAccountTokenStore.StoreAuthenticationResult(false, store.getTokenSource()));
+                listener.onResponse(ServiceAccountTokenStore.StoreAuthenticationResult.failed(store.getTokenSource()));
                 return null;
             }).when(store).authenticate(eq(token5), any());
 
@@ -496,7 +513,7 @@ public class ServiceAccountServiceTests extends ESTestCase {
                 @SuppressWarnings("unchecked")
                 final ActionListener<ServiceAccountTokenStore.StoreAuthenticationResult> listener = (ActionListener<
                     ServiceAccountTokenStore.StoreAuthenticationResult>) invocationOnMock.getArguments()[1];
-                listener.onResponse(new ServiceAccountTokenStore.StoreAuthenticationResult(false, otherStore.getTokenSource()));
+                listener.onResponse(ServiceAccountTokenStore.StoreAuthenticationResult.failed(otherStore.getTokenSource()));
                 return null;
             }).when(otherStore).authenticate(any(), any());
 

+ 1 - 1
x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/authc/service/FileTokensToolTests.java

@@ -22,10 +22,10 @@ import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.core.IOUtils;
 import org.elasticsearch.core.PathUtilsForTesting;
 import org.elasticsearch.env.Environment;
+import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountToken.ServiceAccountTokenId;
 import org.elasticsearch.xpack.core.security.authc.support.Hasher;
 import org.elasticsearch.xpack.core.security.support.Validation;
 import org.elasticsearch.xpack.core.security.support.ValidationTests;
-import org.elasticsearch.xpack.security.authc.service.ServiceAccountToken.ServiceAccountTokenId;
 import org.junit.AfterClass;
 import org.junit.Before;
 import org.junit.BeforeClass;