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

* Add Project Service Account Auth
This commit is contained in:
Johannes Fredén 2025-05-07 08:13:39 +02:00 committed by GitHub
parent e9fe219067
commit bb9d1d6232
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 388 additions and 132 deletions

View File

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

View File

@ -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.
*

View File

@ -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() + "]");
}
}

View File

@ -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;

View File

@ -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");

View File

@ -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;
}

View File

@ -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;

View File

@ -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;

View File

@ -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) {

View File

@ -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;

View File

@ -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)

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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();

View File

@ -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;

View File

@ -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()

View File

@ -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;

View File

@ -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)
)

View File

@ -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);
}

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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());

View File

@ -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;

View File

@ -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()),

View File

@ -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;

View File

@ -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;

View File

@ -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());

View File

@ -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;