From 938edc6016461c4e20bdc32bca676037465b1db5 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Wed, 17 Mar 2021 16:57:42 +1100 Subject: [PATCH 01/19] basic working auth for service account --- .../xpack/security/Security.java | 7 +- .../security/authc/AuthenticationService.java | 49 ++++--- .../xpack/security/authc/TokenService.java | 90 ++++++------ .../CachingServiceAccountsTokenStore.java | 130 ++++++++++++++++++ .../FileServiceAccountsTokenStore.java | 26 ++-- .../authc/service/FileTokensTool.java | 4 +- .../authc/service/ServiceAccountService.java | 49 ++++--- .../service/ServiceAccountsTokenStore.java | 34 ++++- .../authc/AuthenticationServiceTests.java | 2 +- .../security/authc/TokenServiceTests.java | 85 ++++++++---- ...mpositeServiceAccountsTokenStoreTests.java | 69 ++++++++-- .../FileServiceAccountsTokenStoreTests.java | 2 +- .../service/ServiceAccountServiceTests.java | 28 +++- 13 files changed, 432 insertions(+), 143 deletions(-) create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStore.java diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index dca1547f15890..0cd1aee8ded8a 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -200,6 +200,7 @@ import org.elasticsearch.xpack.security.authc.TokenService; import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; +import org.elasticsearch.xpack.security.authc.service.FileServiceAccountsTokenStore; import org.elasticsearch.xpack.security.authc.service.ServiceAccountService; import org.elasticsearch.xpack.security.authc.service.ServiceAccountsTokenStore.CompositeServiceAccountsTokenStore; import org.elasticsearch.xpack.security.authc.support.SecondaryAuthenticator; @@ -491,8 +492,10 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste clusterService, cacheInvalidatorRegistry, threadPool); components.add(apiKeyService); - final ServiceAccountService serviceAccountService = - new ServiceAccountService(new CompositeServiceAccountsTokenStore(List.of())); + final ServiceAccountService serviceAccountService = new ServiceAccountService( + new CompositeServiceAccountsTokenStore( + List.of(new FileServiceAccountsTokenStore(environment, resourceWatcherService, threadPool)), + threadPool.getThreadContext())); final CompositeRolesStore allRolesStore = new CompositeRolesStore(settings, fileRolesStore, nativeRolesStore, reservedRolesStore, privilegeStore, rolesProviders, threadPool.getThreadContext(), getLicenseState(), fieldPermissionsCache, apiKeyService, diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java index 96f94671f8481..e6eaed1e64771 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java @@ -19,6 +19,7 @@ import org.elasticsearch.common.cache.Cache; import org.elasticsearch.common.cache.CacheBuilder; import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Setting.Property; import org.elasticsearch.common.settings.Settings; @@ -47,6 +48,7 @@ import org.elasticsearch.xpack.security.audit.AuditTrailService; import org.elasticsearch.xpack.security.audit.AuditUtil; import org.elasticsearch.xpack.security.authc.service.ServiceAccountService; +import org.elasticsearch.xpack.security.authc.service.ServiceAccountToken; import org.elasticsearch.xpack.security.authc.support.RealmUserLookup; import org.elasticsearch.xpack.security.operator.OperatorPrivileges.OperatorPrivilegesService; import org.elasticsearch.xpack.security.support.SecurityIndexManager; @@ -333,27 +335,42 @@ private void authenticateAsync() { logger.trace("Found existing authentication [{}] in request [{}]", authentication, request); listener.onResponse(authentication); } else { - tokenService.getAndValidateToken(threadContext, ActionListener.wrap(userToken -> { - if (userToken != null) { - writeAuthToContext(userToken.getAuthentication()); - } else { - checkForApiKey(); - } - }, e -> { - logger.debug(new ParameterizedMessage("Failed to validate token authentication for request [{}]", request), e); - if (e instanceof ElasticsearchSecurityException && - tokenService.isExpiredTokenException((ElasticsearchSecurityException) e) == false) { - // intentionally ignore the returned exception; we call this primarily - // for the auditing as we already have a purpose built exception - request.tamperedRequest(); - } - listener.onFailure(e); - })); + checkForBearerToken(); } }); } } + private void checkForBearerToken() { + final SecureString bearerToken = tokenService.extractBearerTokenFromHeader(threadContext); + serviceAccountService.tryAuthenticateBearerToken(bearerToken, nodeName, ActionListener.wrap(authentication -> { + if (authentication != null) { + this.authenticatedBy = authentication.getAuthenticatedBy(); + writeAuthToContext(authentication); + } else { + tokenService.tryAuthenticateToken(bearerToken, ActionListener.wrap(userToken -> { + if (userToken != null) { + writeAuthToContext(userToken.getAuthentication()); + } else { + checkForApiKey(); + } + }, e -> { + logger.debug(new ParameterizedMessage("Failed to validate token authentication for request [{}]", request), e); + if (e instanceof ElasticsearchSecurityException + && false == tokenService.isExpiredTokenException((ElasticsearchSecurityException) e)) { + // intentionally ignore the returned exception; we call this primarily + // for the auditing as we already have a purpose built exception + request.tamperedRequest(); + } + listener.onFailure(e); + })); + } + }, e -> { + logger.debug("Failed to validate service account token for request [{}]", request); + listener.onFailure(request.exceptionProcessingRequest(e, null)); + })); + } + private void checkForApiKey() { apiKeyService.authenticateWithApiKeyIfPresent(threadContext, ActionListener.wrap(authResult -> { if (authResult.isAuthenticated()) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java index 4a2b9cc6cdf91..8e412d0ddac65 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java @@ -93,6 +93,7 @@ import org.elasticsearch.xpack.core.security.authc.TokenMetadata; import org.elasticsearch.xpack.core.security.authc.support.Hasher; import org.elasticsearch.xpack.core.security.authc.support.TokensInvalidationResult; +import org.elasticsearch.xpack.security.authc.support.SecurityTokenType; import org.elasticsearch.xpack.security.support.FeatureNotEnabledException; import org.elasticsearch.xpack.security.support.FeatureNotEnabledException.Feature; import org.elasticsearch.xpack.security.support.SecurityIndexManager; @@ -200,6 +201,8 @@ public final class TokenService { static final Version VERSION_TOKENS_INDEX_INTRODUCED = Version.V_7_2_0; static final Version VERSION_ACCESS_TOKENS_AS_UUIDS = Version.V_7_2_0; static final Version VERSION_MULTIPLE_CONCURRENT_REFRESHES = Version.V_7_2_0; + static final Version VERSION_TOKEN_TYPE = Version.V_8_0_0; + private static final Logger logger = LogManager.getLogger(TokenService.class); private final SecureRandom secureRandom = new SecureRandom(); @@ -379,30 +382,12 @@ public static String hashTokenString(String accessTokenString) { } /** - * Looks in the context to see if the request provided a header with a user token and if so the - * token is validated, which might include authenticated decryption and verification that the token - * has not been revoked or is expired. + * If the token is non-null, then it is validated, which might include authenticated decryption and + * verification that the token has not been revoked or is expired. */ - void getAndValidateToken(ThreadContext ctx, ActionListener listener) { - if (isEnabled()) { - final String token = getFromHeader(ctx); - if (token == null) { - listener.onResponse(null); - } else { - decodeToken(token, ActionListener.wrap(userToken -> { - if (userToken != null) { - checkIfTokenIsValid(userToken, listener); - } else { - listener.onResponse(null); - } - }, e -> { - if (isShardNotAvailableException(e)) { - listener.onResponse(null); - } else { - listener.onFailure(e); - } - })); - } + void tryAuthenticateToken(SecureString token, ActionListener listener) { + if (isEnabled() && token != null) { + decodeAndValidateToken(token, listener); } else { listener.onResponse(null); } @@ -416,29 +401,13 @@ void getAndValidateToken(ThreadContext ctx, ActionListener listener) * {@code null} authentication object. */ public void authenticateToken(SecureString tokenString, ActionListener listener) { - ensureEnabled(); - decodeToken(tokenString.toString(), ActionListener.wrap(userToken -> { - if (userToken != null) { - checkIfTokenIsValid(userToken, ActionListener.wrap( - token -> { - if (token == null) { - // Typically this means that the index is unavailable, so _probably_ the token is invalid but the only - // this we can say for certain is that we couldn't validate it. The logs will be more explicit. - listener.onFailure(new IllegalArgumentException("Cannot validate access token")); - } else { - listener.onResponse(token.getAuthentication()); - } - }, - listener::onFailure - )); - } else { - listener.onFailure(new IllegalArgumentException("Cannot decode access token")); - } - }, e -> { - if (isShardNotAvailableException(e)) { - listener.onResponse(null); + decodeAndValidateToken(tokenString, listener.map(token -> { + if (token == null) { + // Typically this means that the index is unavailable, so _probably_ the token is invalid but the only + // this we can say for certain is that we couldn't validate it. The logs will be more explicit. + throw new IllegalArgumentException("Cannot validate access token"); } else { - listener.onFailure(e); + return token.getAuthentication(); } })); } @@ -513,6 +482,23 @@ private void getUserTokenFromId(String userTokenId, Version tokenVersion, Action } } + private void decodeAndValidateToken(SecureString tokenString, ActionListener listener) { + ensureEnabled(); + decodeToken(tokenString.toString(), ActionListener.wrap(userToken -> { + if (userToken != null) { + checkIfTokenIsValid(userToken, listener); + } else { + listener.onResponse(null); + } + }, e -> { + if (isShardNotAvailableException(e)) { + listener.onResponse(null); + } else { + listener.onFailure(e); + } + })); + } + /** * If needed, for tokens that were created in a pre {@code #VERSION_ACCESS_TOKENS_UUIDS} cluster, it asynchronously decodes the token to * get the token document id. The process for this is asynchronous as we may need to compute a key, which can be computationally @@ -533,6 +519,14 @@ void decodeToken(String token, ActionListener listener) { listener.onResponse(null); return; } + if (version.onOrAfter(VERSION_TOKEN_TYPE)) { + final SecurityTokenType tokenType = SecurityTokenType.read(in); + if (tokenType != SecurityTokenType.ACCESS_TOKEN) { + logger.trace("token is of type {}, but expected {}", tokenType, SecurityTokenType.ACCESS_TOKEN); + listener.onResponse(null); + return; + } + } final String accessToken = in.readString(); // TODO Remove this conditional after backporting to 7.x if (version.onOrAfter(VERSION_HASHED_TOKENS)) { @@ -1711,11 +1705,13 @@ private void maybeStartTokenRemover() { * Gets the token from the Authorization header if the header begins with * Bearer */ - private String getFromHeader(ThreadContext threadContext) { + public SecureString extractBearerTokenFromHeader(ThreadContext threadContext) { String header = threadContext.getHeader("Authorization"); if (Strings.hasText(header) && header.regionMatches(true, 0, "Bearer ", 0, "Bearer ".length()) && header.length() > "Bearer ".length()) { - return header.substring("Bearer ".length()); + char[] chars = new char[header.length() - "Bearer ".length()]; + header.getChars("Bearer ".length(), header.length(), chars, 0); + return new SecureString(chars); } return null; } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStore.java new file mode 100644 index 0000000000000..dac392ed6e07b --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStore.java @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.authc.service; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.common.cache.Cache; +import org.elasticsearch.common.cache.CacheBuilder; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.util.concurrent.ListenableFuture; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.xpack.core.security.authc.support.Hasher; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; + +public abstract class CachingServiceAccountsTokenStore implements ServiceAccountsTokenStore { + + private static final Logger logger = LogManager.getLogger(CachingServiceAccountsTokenStore.class); + + public static final Setting CACHE_HASH_ALGO_SETTING = Setting.simpleString("xpack.security.authc.service_token.cache.hash_algo", + "ssha256", Setting.Property.NodeScope); + + public static final Setting CACHE_TTL_SETTING = Setting.timeSetting("xpack.security.authc.service_token.cache.ttl", + TimeValue.timeValueMinutes(20), Setting.Property.NodeScope); + public static final Setting CACHE_MAX_TOKENS_SETTING = Setting.intSetting( + "xpack.security.authc.service_token.cache.max_tokens", 100_000, Setting.Property.NodeScope); + + private final ThreadPool threadPool; + private final Cache> cache; + private final Hasher hasher; + + CachingServiceAccountsTokenStore(Settings settings, ThreadPool threadPool) { + this.threadPool = threadPool; + final TimeValue ttl = CACHE_TTL_SETTING.get(settings); + if (ttl.getNanos() > 0) { + cache = CacheBuilder.>builder() + .setExpireAfterWrite(ttl) + .setMaximumWeight(CACHE_MAX_TOKENS_SETTING.get(settings)) + .build(); + } else { + cache = null; + } + hasher = Hasher.resolve(CACHE_HASH_ALGO_SETTING.get(settings)); + } + + @Override + public void authenticate(ServiceAccountToken token, ActionListener listener) { + try { + if (cache == null) { + doAuthenticate(token, listener); + } else { + authenticateWithCache(token, listener); + } + } catch (Exception e) { + listener.onFailure(e); + } + } + + private void authenticateWithCache(ServiceAccountToken token, ActionListener listener) { + assert cache != null; + try { + final AtomicBoolean valueAlreadyInCache = new AtomicBoolean(true); + final ListenableFuture listenableCacheEntry = cache.computeIfAbsent(token.getQualifiedName(), k -> { + valueAlreadyInCache.set(false); + return new ListenableFuture<>(); + }); + if (valueAlreadyInCache.get()) { + listenableCacheEntry.addListener(ActionListener.wrap(result -> { + if (result.success) { + listener.onResponse(result.verify(token)); + } else if (result.verify(token)) { + // same wrong token + listener.onResponse(false); + } else { + cache.invalidate(token.getQualifiedName(), listenableCacheEntry); + authenticateWithCache(token, listener); + } + }, listener::onFailure), threadPool.generic(), threadPool.getThreadContext()); + } else { + doAuthenticate(token, ActionListener.wrap(success -> { + logger.trace("cache service token [{}] authentication result", token.getQualifiedName()); + listenableCacheEntry.onResponse(new CachedResult(hasher, success, token)); + listener.onResponse(success); + }, listener::onFailure)); + } + } catch (final ExecutionException e) { + listener.onFailure(e); + } + } + + public final void invalidate(String qualifiedTokenName) { + if (cache != null) { + logger.trace("invalidating cache for service token [{}]", qualifiedTokenName); + cache.invalidate(qualifiedTokenName); + } + } + + public final void invalidateAll() { + if (cache != null) { + logger.trace("invalidating cache for all service tokens"); + cache.invalidateAll(); + } + } + + abstract void doAuthenticate(ServiceAccountToken token, ActionListener listener); + + private static class CachedResult { + + private final boolean success; + private final char[] hash; + + private CachedResult(Hasher hasher, boolean success, ServiceAccountToken token) { + this.success = success; + this.hash = hasher.hash(token.getSecret()); + } + + private boolean verify(ServiceAccountToken token) { + return hash != null && Hasher.verifyHash(token.getSecret(), hash); + } + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStore.java index 647d6b6f17e7d..ffa56efb606ff 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStore.java @@ -10,9 +10,11 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.action.ActionListener; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.util.Maps; import org.elasticsearch.env.Environment; +import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.watcher.FileWatcher; import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xpack.core.XPackPlugin; @@ -26,19 +28,22 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.HashMap; +import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Optional; import java.util.concurrent.CopyOnWriteArrayList; -public class FileServiceAccountsTokenStore implements ServiceAccountsTokenStore { +public class FileServiceAccountsTokenStore extends CachingServiceAccountsTokenStore { private static final Logger logger = LogManager.getLogger(FileServiceAccountsTokenStore.class); private final Path file; - private final CopyOnWriteArrayList listeners; + private final CopyOnWriteArrayList refreshListeners; private volatile Map tokenHashes; - public FileServiceAccountsTokenStore(Environment env, ResourceWatcherService resourceWatcherService) { + public FileServiceAccountsTokenStore(Environment env, ResourceWatcherService resourceWatcherService, ThreadPool threadPool) { + super(env.settings(), threadPool); file = resolveFile(env); FileWatcher watcher = new FileWatcher(file.getParent()); watcher.addListener(new FileReloadListener(file, this::tryReload)); @@ -52,20 +57,24 @@ public FileServiceAccountsTokenStore(Environment env, ResourceWatcherService res } catch (IOException e) { throw new IllegalStateException("Failed to load service_tokens file [" + file + "]", e); } - listeners = new CopyOnWriteArrayList<>(); + refreshListeners = new CopyOnWriteArrayList<>(List.of(this::invalidateAll)); } @Override - public boolean authenticate(ServiceAccountToken token) { - return false; + public void doAuthenticate(ServiceAccountToken token, ActionListener listener) { + // This is done on the current thread instead of using a dedicated thread pool like API key does + // because it is not expected to have a large number of service tokens. + listener.onResponse(Optional.ofNullable(tokenHashes.get(token.getQualifiedName())) + .map(hash -> Hasher.verifyHash(token.getSecret(), hash)) + .orElse(false)); } public void addListener(Runnable listener) { - listeners.add(listener); + refreshListeners.add(listener); } private void notifyRefresh() { - listeners.forEach(Runnable::run); + refreshListeners.forEach(Runnable::run); } private void tryReload() { @@ -133,4 +142,5 @@ static void writeFile(Path path, Map tokenHashes) { SecurityFiles.writeFileAtomically( path, tokenHashes, e -> String.format(Locale.ROOT, "%s:%s", e.getKey(), new String(e.getValue()))); } + } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileTokensTool.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileTokensTool.java index 8dff080b7cb50..1c5abb816aee6 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileTokensTool.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileTokensTool.java @@ -26,9 +26,9 @@ import org.elasticsearch.xpack.security.support.FileAttributesChecker; import java.nio.file.Path; -import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.TreeMap; public class FileTokensTool extends LoggingAwareMultiCommand { @@ -77,7 +77,7 @@ protected void execute(Terminal terminal, OptionSet options, Environment env) th final Path serviceTokensFile = FileServiceAccountsTokenStore.resolveFile(env); FileAttributesChecker attributesChecker = new FileAttributesChecker(serviceTokensFile); - final Map tokenHashes = new HashMap<>(FileServiceAccountsTokenStore.parseFile(serviceTokensFile, null)); + final Map tokenHashes = new TreeMap<>(FileServiceAccountsTokenStore.parseFile(serviceTokensFile, null)); try (SecureString tokenString = UUIDs.randomBase64UUIDSecureString()) { final ServiceAccountToken token = diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java index 56639b8f87b67..d246b42fda3a9 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java @@ -18,7 +18,6 @@ import org.elasticsearch.common.io.stream.InputStreamStreamInput; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.settings.SecureString; -import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.user.User; @@ -59,10 +58,10 @@ public static Collection getServiceAccountPrincipals() { return ACCOUNTS.keySet(); } - // {@link org.elasticsearch.xpack.security.authc.TokenService#extractBearerTokenFromHeader extracted} from an HTTP authorization header. /** * Parses a token object from the content of a {@link ServiceAccountToken#asBearerString()} bearer string}. * This bearer string would typically be + * {@link org.elasticsearch.xpack.security.authc.TokenService#extractBearerTokenFromHeader extracted} from an HTTP authorization header. * *

* This method does not validate the credential, it simply parses it. @@ -85,37 +84,49 @@ public static ServiceAccountToken tryParseToken(SecureString token) { } } - public void authenticateWithToken(ServiceAccountToken token, ThreadContext threadContext, String nodeName, - ActionListener listener) { + public void tryAuthenticateBearerToken(SecureString bearerToken, String nodeName, ActionListener listener) { + final ServiceAccountToken serviceAccountToken = ServiceAccountService.tryParseToken(bearerToken); + if (serviceAccountToken == null) { + // This should be the only situation where a null is returned to mean the authentication should continue. + // For all other situations, it should be either onResponse(authentication) for success or onFailure for any error. + listener.onResponse(null); + } else { + authenticateToken(serviceAccountToken, nodeName, listener); + } + } - if (ElasticServiceAccounts.NAMESPACE.equals(token.getAccountId().namespace()) == false) { + public void authenticateToken(ServiceAccountToken serviceAccountToken, String nodeName, ActionListener listener) { + + if (ElasticServiceAccounts.NAMESPACE.equals(serviceAccountToken.getAccountId().namespace()) == false) { final ParameterizedMessage message = new ParameterizedMessage( "only [{}] service accounts are supported, but received [{}]", - ElasticServiceAccounts.NAMESPACE, token.getAccountId().asPrincipal()); + ElasticServiceAccounts.NAMESPACE, serviceAccountToken.getAccountId().asPrincipal()); logger.debug(message); listener.onFailure(new ElasticsearchSecurityException(message.getFormattedMessage())); return; } - final ServiceAccount account = ACCOUNTS.get(token.getAccountId().asPrincipal()); + final ServiceAccount account = ACCOUNTS.get(serviceAccountToken.getAccountId().asPrincipal()); if (account == null) { final ParameterizedMessage message = new ParameterizedMessage( - "the [{}] service account does not exist", token.getAccountId().asPrincipal()); + "the [{}] service account does not exist", serviceAccountToken.getAccountId().asPrincipal()); logger.debug(message); listener.onFailure(new ElasticsearchSecurityException(message.getFormattedMessage())); return; } - if (serviceAccountsTokenStore.authenticate(token)) { - listener.onResponse(success(account, token, nodeName)); - } else { - final ParameterizedMessage message = new ParameterizedMessage( - "failed to authenticate service account [{}] with token name [{}]", - token.getAccountId().asPrincipal(), - token.getTokenName()); - logger.debug(message); - listener.onFailure(new ElasticsearchSecurityException(message.getFormattedMessage())); - } + serviceAccountsTokenStore.authenticate(serviceAccountToken, ActionListener.wrap(success -> { + if (success) { + listener.onResponse(createAuthentication(account, serviceAccountToken, nodeName)); + } else { + final ParameterizedMessage message = new ParameterizedMessage( + "failed to authenticate service account [{}] with token name [{}]", + serviceAccountToken.getAccountId().asPrincipal(), + serviceAccountToken.getTokenName()); + logger.debug(message); + listener.onFailure(new ElasticsearchSecurityException(message.getFormattedMessage())); + } + }, listener::onFailure)); } public void getRoleDescriptor(Authentication authentication, ActionListener listener) { @@ -132,7 +143,7 @@ public void getRoleDescriptor(Authentication authentication, ActionListener listener); final class CompositeServiceAccountsTokenStore implements ServiceAccountsTokenStore { + private static final Logger logger = LogManager.getLogger(CompositeServiceAccountsTokenStore.class); + + private final ThreadContext threadContext; private final List stores; - public CompositeServiceAccountsTokenStore(List stores) { + public CompositeServiceAccountsTokenStore( + List stores, ThreadContext threadContext) { this.stores = stores; + this.threadContext = threadContext; } @Override - public boolean authenticate(ServiceAccountToken token) { - return stores.stream().anyMatch(store -> store.authenticate(token)); + public void authenticate(ServiceAccountToken token, ActionListener listener) { + final IteratingActionListener authenticatingListener = + new IteratingActionListener<>( + listener, + (store, successListener) -> store.authenticate(token, successListener), + stores, + threadContext, + Function.identity(), + success -> Boolean.FALSE == success); + try { + authenticatingListener.run(); + } catch (Exception e) { + logger.debug(new ParameterizedMessage("authentication of service token [{}] failed", token.getQualifiedName()), e); + listener.onFailure(e); + } } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java index 875d34aa506f9..077e39698ea31 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java @@ -263,7 +263,7 @@ public void init() throws Exception { mock(CacheInvalidatorRegistry.class), threadPool); tokenService = new TokenService(settings, Clock.systemUTC(), client, licenseState, securityContext, securityIndex, securityIndex, clusterService); - serviceAccountService = new ServiceAccountService(new CompositeServiceAccountsTokenStore(List.of())); + serviceAccountService = new ServiceAccountService(new CompositeServiceAccountsTokenStore(List.of(), threadContext)); operatorPrivilegesService = mock(OperatorPrivileges.OperatorPrivilegesService.class); service = new AuthenticationService(settings, realms, auditTrailService, diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java index 49603d1bf1b4a..2b698f4364869 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java @@ -46,6 +46,7 @@ import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.concurrent.ThreadContext; @@ -239,7 +240,8 @@ public void testAttachAndGetToken() throws Exception { try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) { PlainActionFuture future = new PlainActionFuture<>(); - tokenService.getAndValidateToken(requestContext, future); + final SecureString bearerToken = tokenService.extractBearerTokenFromHeader(requestContext); + tokenService.tryAuthenticateToken(bearerToken, future); UserToken serialized = future.get(); assertAuthentication(authentication, serialized.getAuthentication()); } @@ -249,7 +251,8 @@ public void testAttachAndGetToken() throws Exception { TokenService anotherService = createTokenService(tokenServiceEnabledSettings, systemUTC()); anotherService.refreshMetadata(tokenService.getTokenMetadata()); PlainActionFuture future = new PlainActionFuture<>(); - anotherService.getAndValidateToken(requestContext, future); + final SecureString bearerToken = anotherService.extractBearerTokenFromHeader(requestContext); + anotherService.tryAuthenticateToken(bearerToken, future); UserToken fromOtherService = future.get(); assertAuthentication(authentication, fromOtherService.getAuthentication()); } @@ -264,7 +267,8 @@ public void testInvalidAuthorizationHeader() throws Exception { try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) { PlainActionFuture future = new PlainActionFuture<>(); - tokenService.getAndValidateToken(requestContext, future); + final SecureString bearerToken = tokenService.extractBearerTokenFromHeader(requestContext); + tokenService.tryAuthenticateToken(bearerToken, future); UserToken serialized = future.get(); assertThat(serialized, nullValue()); } @@ -290,7 +294,8 @@ public void testRotateKey() throws Exception { try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) { PlainActionFuture future = new PlainActionFuture<>(); - tokenService.getAndValidateToken(requestContext, future); + final SecureString bearerToken = tokenService.extractBearerTokenFromHeader(requestContext); + tokenService.tryAuthenticateToken(bearerToken, future); UserToken serialized = future.get(); assertAuthentication(authentication, serialized.getAuthentication()); } @@ -298,7 +303,8 @@ public void testRotateKey() throws Exception { try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) { PlainActionFuture future = new PlainActionFuture<>(); - tokenService.getAndValidateToken(requestContext, future); + final SecureString bearerToken = tokenService.extractBearerTokenFromHeader(requestContext); + tokenService.tryAuthenticateToken(bearerToken, future); UserToken serialized = future.get(); assertAuthentication(authentication, serialized.getAuthentication()); } @@ -318,7 +324,8 @@ public void testRotateKey() throws Exception { try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) { PlainActionFuture future = new PlainActionFuture<>(); - tokenService.getAndValidateToken(requestContext, future); + final SecureString bearerToken = tokenService.extractBearerTokenFromHeader(requestContext); + tokenService.tryAuthenticateToken(bearerToken, future); UserToken serialized = future.get(); assertAuthentication(authentication, serialized.getAuthentication()); } @@ -356,7 +363,8 @@ public void testKeyExchange() throws Exception { storeTokenHeader(requestContext, accessToken); try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) { PlainActionFuture future = new PlainActionFuture<>(); - otherTokenService.getAndValidateToken(requestContext, future); + final SecureString bearerToken = otherTokenService.extractBearerTokenFromHeader(requestContext); + otherTokenService.tryAuthenticateToken(bearerToken, future); UserToken serialized = future.get(); assertAuthentication(serialized.getAuthentication(), authentication); } @@ -367,7 +375,8 @@ public void testKeyExchange() throws Exception { try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) { PlainActionFuture future = new PlainActionFuture<>(); - otherTokenService.getAndValidateToken(requestContext, future); + final SecureString bearerToken = otherTokenService.extractBearerTokenFromHeader(requestContext); + otherTokenService.tryAuthenticateToken(bearerToken, future); UserToken serialized = future.get(); assertAuthentication(serialized.getAuthentication(), authentication); } @@ -393,7 +402,8 @@ public void testPruneKeys() throws Exception { try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) { PlainActionFuture future = new PlainActionFuture<>(); - tokenService.getAndValidateToken(requestContext, future); + final SecureString bearerToken = tokenService.extractBearerTokenFromHeader(requestContext); + tokenService.tryAuthenticateToken(bearerToken, future); UserToken serialized = future.get(); assertAuthentication(authentication, serialized.getAuthentication()); } @@ -407,7 +417,8 @@ public void testPruneKeys() throws Exception { try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) { PlainActionFuture future = new PlainActionFuture<>(); - tokenService.getAndValidateToken(requestContext, future); + final SecureString bearerToken = tokenService.extractBearerTokenFromHeader(requestContext); + tokenService.tryAuthenticateToken(bearerToken, future); UserToken serialized = future.get(); assertAuthentication(authentication, serialized.getAuthentication()); } @@ -426,7 +437,8 @@ public void testPruneKeys() throws Exception { try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) { PlainActionFuture future = new PlainActionFuture<>(); - tokenService.getAndValidateToken(requestContext, future); + final SecureString bearerToken = tokenService.extractBearerTokenFromHeader(requestContext); + tokenService.tryAuthenticateToken(bearerToken, future); UserToken serialized = future.get(); assertNull(serialized); } @@ -436,7 +448,8 @@ public void testPruneKeys() throws Exception { mockGetTokenFromId(tokenService, newUserTokenId, authentication, false); try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) { PlainActionFuture future = new PlainActionFuture<>(); - tokenService.getAndValidateToken(requestContext, future); + final SecureString bearerToken = tokenService.extractBearerTokenFromHeader(requestContext); + tokenService.tryAuthenticateToken(bearerToken, future); UserToken serialized = future.get(); assertAuthentication(authentication, serialized.getAuthentication()); } @@ -463,7 +476,8 @@ public void testPassphraseWorks() throws Exception { try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) { PlainActionFuture future = new PlainActionFuture<>(); - tokenService.getAndValidateToken(requestContext, future); + final SecureString bearerToken = tokenService.extractBearerTokenFromHeader(requestContext); + tokenService.tryAuthenticateToken(bearerToken, future); UserToken serialized = future.get(); assertAuthentication(authentication, serialized.getAuthentication()); } @@ -472,7 +486,8 @@ public void testPassphraseWorks() throws Exception { // verify a second separate token service with its own passphrase cannot verify TokenService anotherService = createTokenService(tokenServiceEnabledSettings, systemUTC()); PlainActionFuture future = new PlainActionFuture<>(); - anotherService.getAndValidateToken(requestContext, future); + final SecureString bearerToken = anotherService.extractBearerTokenFromHeader(requestContext); + anotherService.tryAuthenticateToken(bearerToken, future); assertNull(future.get()); } } @@ -516,7 +531,8 @@ public void testInvalidatedToken() throws Exception { try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) { PlainActionFuture future = new PlainActionFuture<>(); - tokenService.getAndValidateToken(requestContext, future); + final SecureString bearerToken = tokenService.extractBearerTokenFromHeader(requestContext); + tokenService.tryAuthenticateToken(bearerToken, future); ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, future::actionGet); final String headerValue = e.getHeader("WWW-Authenticate").get(0); assertThat(headerValue, containsString("Bearer realm=")); @@ -632,7 +648,8 @@ public void testTokenExpiry() throws Exception { try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) { // the clock is still frozen, so the cookie should be valid PlainActionFuture future = new PlainActionFuture<>(); - tokenService.getAndValidateToken(requestContext, future); + final SecureString bearerToken = tokenService.extractBearerTokenFromHeader(requestContext); + tokenService.tryAuthenticateToken(bearerToken, future); assertAuthentication(authentication, future.get().getAuthentication()); } @@ -642,7 +659,8 @@ public void testTokenExpiry() throws Exception { // move the clock forward but don't go to expiry clock.fastForwardSeconds(fastForwardAmount); PlainActionFuture future = new PlainActionFuture<>(); - tokenService.getAndValidateToken(requestContext, future); + final SecureString bearerToken = tokenService.extractBearerTokenFromHeader(requestContext); + tokenService.tryAuthenticateToken(bearerToken, future); assertAuthentication(authentication, future.get().getAuthentication()); } @@ -650,7 +668,8 @@ public void testTokenExpiry() throws Exception { // move to expiry, stripping nanoseconds, as we don't store them in the security-tokens index clock.setTime(userToken.getExpirationTime().truncatedTo(ChronoUnit.MILLIS).atZone(clock.getZone())); PlainActionFuture future = new PlainActionFuture<>(); - tokenService.getAndValidateToken(requestContext, future); + final SecureString bearerToken = tokenService.extractBearerTokenFromHeader(requestContext); + tokenService.tryAuthenticateToken(bearerToken, future); assertAuthentication(authentication, future.get().getAuthentication()); } @@ -658,7 +677,8 @@ public void testTokenExpiry() throws Exception { // move one second past expiry clock.fastForwardSeconds(1); PlainActionFuture future = new PlainActionFuture<>(); - tokenService.getAndValidateToken(requestContext, future); + final SecureString bearerToken = tokenService.extractBearerTokenFromHeader(requestContext); + tokenService.tryAuthenticateToken(bearerToken, future); ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, future::actionGet); final String headerValue = e.getHeader("WWW-Authenticate").get(0); assertThat(headerValue, containsString("Bearer realm=")); @@ -679,7 +699,8 @@ public void testTokenServiceDisabled() throws Exception { assertThat(e.getMetadata(FeatureNotEnabledException.DISABLED_FEATURE_METADATA), contains("security_tokens")); PlainActionFuture future = new PlainActionFuture<>(); - tokenService.getAndValidateToken(null, future); + final SecureString bearerToken = tokenService.extractBearerTokenFromHeader(null); + tokenService.tryAuthenticateToken(bearerToken, future); assertNull(future.get()); PlainActionFuture invalidateFuture = new PlainActionFuture<>(); @@ -725,7 +746,8 @@ public void testMalformedToken() throws Exception { try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) { PlainActionFuture future = new PlainActionFuture<>(); - tokenService.getAndValidateToken(requestContext, future); + final SecureString bearerToken = tokenService.extractBearerTokenFromHeader(requestContext); + tokenService.tryAuthenticateToken(bearerToken, future); assertNull(future.get()); } } @@ -740,7 +762,8 @@ public void testNotValidPre72Tokens() throws Exception { try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) { PlainActionFuture future = new PlainActionFuture<>(); - tokenService.getAndValidateToken(requestContext, future); + final SecureString bearerToken = tokenService.extractBearerTokenFromHeader(requestContext); + tokenService.tryAuthenticateToken(bearerToken, future); assertNull(future.get()); } } @@ -755,7 +778,8 @@ public void testNotValidAfter72Tokens() throws Exception { try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) { PlainActionFuture future = new PlainActionFuture<>(); - tokenService.getAndValidateToken(requestContext, future); + final SecureString bearerToken = tokenService.extractBearerTokenFromHeader(requestContext); + tokenService.tryAuthenticateToken(bearerToken, future); assertNull(future.get()); } } @@ -793,26 +817,30 @@ public void testIndexNotAvailable() throws Exception { } try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) { PlainActionFuture future = new PlainActionFuture<>(); - tokenService.getAndValidateToken(requestContext, future); + final SecureString bearerToken3 = tokenService.extractBearerTokenFromHeader(requestContext); + tokenService.tryAuthenticateToken(bearerToken3, future); assertNull(future.get()); when(tokensIndex.isAvailable()).thenReturn(false); when(tokensIndex.getUnavailableReason()).thenReturn(new UnavailableShardsException(null, "unavailable")); when(tokensIndex.indexExists()).thenReturn(true); future = new PlainActionFuture<>(); - tokenService.getAndValidateToken(requestContext, future); + final SecureString bearerToken2 = tokenService.extractBearerTokenFromHeader(requestContext); + tokenService.tryAuthenticateToken(bearerToken2, future); assertNull(future.get()); when(tokensIndex.indexExists()).thenReturn(false); future = new PlainActionFuture<>(); - tokenService.getAndValidateToken(requestContext, future); + final SecureString bearerToken1 = tokenService.extractBearerTokenFromHeader(requestContext); + tokenService.tryAuthenticateToken(bearerToken1, future); assertNull(future.get()); when(tokensIndex.isAvailable()).thenReturn(true); when(tokensIndex.indexExists()).thenReturn(true); mockGetTokenFromId(tokenService, userTokenId, authentication, false); future = new PlainActionFuture<>(); - tokenService.getAndValidateToken(requestContext, future); + final SecureString bearerToken = tokenService.extractBearerTokenFromHeader(requestContext); + tokenService.tryAuthenticateToken(bearerToken, future); assertAuthentication(future.get().getAuthentication(), authentication); } } @@ -875,7 +903,8 @@ public void testCannotValidateTokenIfLicenseDoesNotAllowTokens() throws Exceptio PlainActionFuture authFuture = new PlainActionFuture<>(); when(licenseState.checkFeature(Feature.SECURITY_TOKEN_SERVICE)).thenReturn(false); - tokenService.getAndValidateToken(threadContext, authFuture); + final SecureString bearerToken = tokenService.extractBearerTokenFromHeader(threadContext); + tokenService.tryAuthenticateToken(bearerToken, authFuture); UserToken authToken = authFuture.actionGet(); assertThat(authToken, Matchers.nullValue()); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountsTokenStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountsTokenStoreTests.java index 788e766782a71..9f3acd97d5781 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountsTokenStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountsTokenStoreTests.java @@ -7,17 +7,34 @@ package org.elasticsearch.xpack.security.authc.service; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.test.ESTestCase; +import org.junit.Before; import java.util.List; +import java.util.concurrent.ExecutionException; import static org.hamcrest.Matchers.is; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; public class CompositeServiceAccountsTokenStoreTests extends ESTestCase { - public void testAuthenticate() { + private ThreadContext threadContext; + + @Before + public void init() { + threadContext = new ThreadContext(Settings.EMPTY); + } + + public void testAuthenticate() throws ExecutionException, InterruptedException { final ServiceAccountToken token = mock(ServiceAccountToken.class); final ServiceAccountsTokenStore store1 = mock(ServiceAccountsTokenStore.class); @@ -28,17 +45,53 @@ public void testAuthenticate() { final boolean store2Success = randomBoolean(); final boolean store3Success = randomBoolean(); - when(store1.authenticate(token)).thenReturn(store1Success); - when(store2.authenticate(token)).thenReturn(store2Success); - when(store3.authenticate(token)).thenReturn(store3Success); + doAnswer(invocationOnMock -> { + @SuppressWarnings("unchecked") + final ActionListener listener = (ActionListener) invocationOnMock.getArguments()[1]; + listener.onResponse(store1Success); + return null; + }).when(store1).authenticate(eq(token), any()); + + doAnswer(invocationOnMock -> { + @SuppressWarnings("unchecked") + final ActionListener listener = (ActionListener) invocationOnMock.getArguments()[1]; + listener.onResponse(store2Success); + return null; + }).when(store2).authenticate(eq(token), any()); + + doAnswer(invocationOnMock -> { + @SuppressWarnings("unchecked") + final ActionListener listener = (ActionListener) invocationOnMock.getArguments()[1]; + listener.onResponse(store3Success); + return null; + }).when(store3).authenticate(eq(token), any()); final ServiceAccountsTokenStore.CompositeServiceAccountsTokenStore compositeStore = - new ServiceAccountsTokenStore.CompositeServiceAccountsTokenStore(List.of(store1, store2, store3)); + new ServiceAccountsTokenStore.CompositeServiceAccountsTokenStore(List.of(store1, store2, store3), threadContext); + final PlainActionFuture future = new PlainActionFuture<>(); + compositeStore.authenticate(token, future); + System.out.println(future.get()); if (store1Success || store2Success || store3Success) { - assertThat(compositeStore.authenticate(token), is(true)); + assertThat(future.get(), is(true)); + if (store1Success) { + verify(store1).authenticate(eq(token), any()); + verifyZeroInteractions(store2); + verifyZeroInteractions(store3); + } else if (store2Success) { + verify(store1).authenticate(eq(token), any()); + verify(store2).authenticate(eq(token), any()); + verifyZeroInteractions(store3); + } else { + verify(store1).authenticate(eq(token), any()); + verify(store2).authenticate(eq(token), any()); + verify(store3).authenticate(eq(token), any()); + } } else { - assertThat(compositeStore.authenticate(token), is(false)); + assertThat(future.get(), is(false)); + verify(store1).authenticate(eq(token), any()); + verify(store2).authenticate(eq(token), any()); + verify(store3).authenticate(eq(token), any()); } } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStoreTests.java index 3c11521a95583..c7aed6940d745 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStoreTests.java @@ -113,7 +113,7 @@ public void testAutoReload() throws Exception { try (ResourceWatcherService watcherService = new ResourceWatcherService(settings, threadPool)) { final CountDownLatch latch = new CountDownLatch(5); - FileServiceAccountsTokenStore store = new FileServiceAccountsTokenStore(env, watcherService); + FileServiceAccountsTokenStore store = new FileServiceAccountsTokenStore(env, watcherService, threadPool); store.addListener(latch::countDown); //Token name shares the hashing algorithm name for convenience String tokenName = settings.get("xpack.security.authc.service_token_hashing.algorithm"); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java index 3e8acd48c0583..129fd27ef03ec 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java @@ -9,6 +9,7 @@ import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.Version; +import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.BytesStreamOutput; @@ -32,8 +33,10 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; public class ServiceAccountServiceTests extends ESTestCase { @@ -126,7 +129,7 @@ public void testAuthenticateWithToken() throws ExecutionException, InterruptedEx final SecureString secret = new SecureString(randomAlphaOfLength(20).toCharArray()); final ServiceAccountToken token1 = new ServiceAccountToken(accountId1, randomAlphaOfLengthBetween(3, 8), secret); final PlainActionFuture future1 = new PlainActionFuture<>(); - serviceAccountService.authenticateWithToken(token1, threadContext, randomAlphaOfLengthBetween(3, 8), future1); + serviceAccountService.authenticateToken(token1, randomAlphaOfLengthBetween(3, 8), future1); final ExecutionException e1 = expectThrows(ExecutionException.class, future1::get); assertThat(e1.getCause().getClass(), is(ElasticsearchSecurityException.class)); assertThat(e1.getMessage(), containsString( @@ -139,7 +142,7 @@ public void testAuthenticateWithToken() throws ExecutionException, InterruptedEx randomValueOtherThan("fleet", () -> randomAlphaOfLengthBetween(3, 8))); final ServiceAccountToken token2 = new ServiceAccountToken(accountId2, randomAlphaOfLengthBetween(3, 8), secret); final PlainActionFuture future2 = new PlainActionFuture<>(); - serviceAccountService.authenticateWithToken(token2, threadContext, randomAlphaOfLengthBetween(3, 8), future2); + serviceAccountService.authenticateToken(token2, randomAlphaOfLengthBetween(3, 8), future2); final ExecutionException e2 = expectThrows(ExecutionException.class, future2::get); assertThat(e2.getCause().getClass(), is(ElasticsearchSecurityException.class)); assertThat(e2.getMessage(), containsString( @@ -151,11 +154,22 @@ public void testAuthenticateWithToken() throws ExecutionException, InterruptedEx final ServiceAccountToken token4 = new ServiceAccountToken(accountId3, randomAlphaOfLengthBetween(3, 8), new SecureString(randomAlphaOfLength(20).toCharArray())); final String nodeName = randomAlphaOfLengthBetween(3, 8); - when(serviceAccountsTokenStore.authenticate(token3)).thenReturn(true); - when(serviceAccountsTokenStore.authenticate(token4)).thenReturn(false); + doAnswer(invocationOnMock -> { + @SuppressWarnings("unchecked") + final ActionListener listener = (ActionListener) invocationOnMock.getArguments()[1]; + listener.onResponse(true); + return null; + }).when(serviceAccountsTokenStore).authenticate(eq(token3), any()); + + doAnswer(invocationOnMock -> { + @SuppressWarnings("unchecked") + final ActionListener listener = (ActionListener) invocationOnMock.getArguments()[1]; + listener.onResponse(false); + return null; + }).when(serviceAccountsTokenStore).authenticate(eq(token4), any()); final PlainActionFuture future3 = new PlainActionFuture<>(); - serviceAccountService.authenticateWithToken(token3, threadContext, nodeName, future3); + serviceAccountService.authenticateToken(token3, nodeName, future3); final Authentication authentication = future3.get(); assertThat(authentication, equalTo(new Authentication( new User("elastic/fleet", Strings.EMPTY_ARRAY, @@ -167,7 +181,7 @@ public void testAuthenticateWithToken() throws ExecutionException, InterruptedEx ))); final PlainActionFuture future4 = new PlainActionFuture<>(); - serviceAccountService.authenticateWithToken(token4, threadContext, nodeName, future4); + serviceAccountService.authenticateToken(token4, nodeName, future4); final ExecutionException e4 = expectThrows(ExecutionException.class, future4::get); assertThat(e4.getCause().getClass(), is(ElasticsearchSecurityException.class)); assertThat(e4.getMessage(), containsString("failed to authenticate service account [" From 104db5249f2f682292daf8b3958ef0780989aa97 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 18 Mar 2021 17:12:04 +1100 Subject: [PATCH 02/19] Working on tests --- .../security/qa/service-account/build.gradle | 44 ++++++ .../authc/service/ServiceAccountIT.java | 134 ++++++++++++++++ .../src/javaRestTest/resources/service_tokens | 1 + .../resources/ssl/README.asciidoc | 37 +++++ .../src/javaRestTest/resources/ssl/ca.crt | 20 +++ .../src/javaRestTest/resources/ssl/ca.key | 30 ++++ .../src/javaRestTest/resources/ssl/ca.p12 | Bin 0 -> 1130 bytes .../src/javaRestTest/resources/ssl/node.crt | 22 +++ .../src/javaRestTest/resources/ssl/node.key | 30 ++++ .../test/SecuritySingleNodeTestCase.java | 9 ++ .../ServiceAccountSingleNodeTests.java | 54 +++++++ .../xpack/security/authc/TokenService.java | 8 +- .../CachingServiceAccountsTokenStore.java | 7 +- .../authc/service/ServiceAccountService.java | 9 +- .../test/SecuritySettingsSource.java | 5 + .../security/authc/TokenServiceTests.java | 47 ++++++ ...CachingServiceAccountsTokenStoreTests.java | 149 ++++++++++++++++++ ...mpositeServiceAccountsTokenStoreTests.java | 1 - 18 files changed, 599 insertions(+), 8 deletions(-) create mode 100644 x-pack/plugin/security/qa/service-account/build.gradle create mode 100644 x-pack/plugin/security/qa/service-account/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountIT.java create mode 100644 x-pack/plugin/security/qa/service-account/src/javaRestTest/resources/service_tokens create mode 100644 x-pack/plugin/security/qa/service-account/src/javaRestTest/resources/ssl/README.asciidoc create mode 100644 x-pack/plugin/security/qa/service-account/src/javaRestTest/resources/ssl/ca.crt create mode 100644 x-pack/plugin/security/qa/service-account/src/javaRestTest/resources/ssl/ca.key create mode 100644 x-pack/plugin/security/qa/service-account/src/javaRestTest/resources/ssl/ca.p12 create mode 100644 x-pack/plugin/security/qa/service-account/src/javaRestTest/resources/ssl/node.crt create mode 100644 x-pack/plugin/security/qa/service-account/src/javaRestTest/resources/ssl/node.key create mode 100644 x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountSingleNodeTests.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStoreTests.java diff --git a/x-pack/plugin/security/qa/service-account/build.gradle b/x-pack/plugin/security/qa/service-account/build.gradle new file mode 100644 index 0000000000000..a2875d6f0bfc9 --- /dev/null +++ b/x-pack/plugin/security/qa/service-account/build.gradle @@ -0,0 +1,44 @@ +apply plugin: 'elasticsearch.java-rest-test' + +dependencies { + javaRestTestImplementation project(':x-pack:plugin:core') + javaRestTestImplementation project(':client:rest-high-level') + javaRestTestImplementation project(':x-pack:plugin:security') + // let the javaRestTest see the classpath of main + javaRestTestImplementation project.sourceSets.main.runtimeClasspath +} + +testClusters.javaRestTest { + testDistribution = 'DEFAULT' + numberOfNodes = 2 + + extraConfigFile 'node.key', file('src/javaRestTest/resources/ssl/node.key') + extraConfigFile 'node.crt', file('src/javaRestTest/resources/ssl/node.crt') + extraConfigFile 'ca.crt', file('src/javaRestTest/resources/ssl/ca.crt') + extraConfigFile 'service_tokens', file('src/javaRestTest/resources/service_tokens') + + setting 'xpack.ml.enabled', 'false' + setting 'xpack.license.self_generated.type', 'trial' + + setting 'xpack.security.enabled', 'true' + setting 'xpack.security.authc.token.enabled', 'true' + setting 'xpack.security.authc.api_key.enabled', 'true' + + setting 'xpack.security.http.ssl.enabled', 'true' + setting 'xpack.security.http.ssl.certificate', 'node.crt' + setting 'xpack.security.http.ssl.key', 'node.key' + setting 'xpack.security.http.ssl.certificate_authorities', 'ca.crt' + + setting 'xpack.security.transport.ssl.enabled', 'true' + setting 'xpack.security.transport.ssl.certificate', 'node.crt' + setting 'xpack.security.transport.ssl.key', 'node.key' + setting 'xpack.security.transport.ssl.certificate_authorities', 'ca.crt' + setting 'xpack.security.transport.ssl.verification_mode', 'certificate' + + keystore 'bootstrap.password', 'x-pack-test-password' + keystore 'xpack.security.transport.ssl.secure_key_passphrase', 'node-password' + keystore 'xpack.security.http.ssl.secure_key_passphrase', 'node-password' + + user username: "test_admin", password: 'x-pack-test-password', role: "superuser" + user username: "elastic/fleet", password: 'x-pack-test-password', role: "superuser" +} diff --git a/x-pack/plugin/security/qa/service-account/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountIT.java b/x-pack/plugin/security/qa/service-account/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountIT.java new file mode 100644 index 0000000000000..ae1854b721eb8 --- /dev/null +++ b/x-pack/plugin/security/qa/service-account/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountIT.java @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.authc.service; + +import org.elasticsearch.client.Request; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.Response; +import org.elasticsearch.client.ResponseException; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.io.PathUtils; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.test.rest.ESRestTestCase; +import org.junit.BeforeClass; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + +import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasEntry; + +public class ServiceAccountIT extends ESRestTestCase { + + private static final String VALID_SERVICE_TOKEN = "46ToAwIHZWxhc3RpYwVmbGVldAZ0b2tlbjEWME1TT0ZobXVRTENIaTNQUGJ4VXQ5ZwAAAAAAAAA"; + private static final String INVALID_SERVICE_TOKEN = "46ToAwIHZWxhc3RpYwVmbGVldAZ0b2tlbjEWQ1MxRXZaQk5SWW1FbndZWlc5T2N3dwAAAAAAAAA"; + private static Path caPath; + + private static final String AUTHENTICATE_RESPONSE = "" + + "{\n" + + " \"username\": \"elastic/fleet\",\n" + + " \"roles\": [],\n" + + " \"full_name\": \"Service account - elastic/fleet\",\n" + + " \"email\": null,\n" + + " \"metadata\": {\n" + + " \"_elastic_service_account\": true\n" + + " },\n" + " \"enabled\": true,\n" + + " \"authentication_realm\": {\n" + + " \"name\": \"service_account\",\n" + + " \"type\": \"service_account\"\n" + + " },\n" + + " \"lookup_realm\": {\n" + + " \"name\": \"service_account\",\n" + + " \"type\": \"service_account\"\n" + + " },\n" + + " \"authentication_type\": \"token\"\n" + + "}\n"; + + @BeforeClass + public static void init() throws URISyntaxException, FileNotFoundException { + URL resource = ServiceAccountIT.class.getResource("/ssl/ca.crt"); + if (resource == null) { + throw new FileNotFoundException("Cannot find classpath resource /ssl/ca.crt"); + } + caPath = PathUtils.get(resource.toURI()); + } + + @Override + protected String getProtocol() { + // Because http.ssl.enabled = true + return "https"; + } + + @Override + protected Settings restClientSettings() { + String token = basicAuthHeaderValue("test_admin", new SecureString("x-pack-test-password".toCharArray())); + return Settings.builder().put(ThreadContext.PREFIX + ".Authorization", token) + .put(CERTIFICATE_AUTHORITIES, caPath) + .build(); + } + + public void testAuthenticate() throws IOException { + final Request request = new Request("GET", "_security/_authenticate"); + request.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", "Bearer " + VALID_SERVICE_TOKEN)); + final Response response = client().performRequest(request); + assertOK(response); + assertThat(responseAsMap(response), + equalTo(XContentHelper.convertToMap(new BytesArray(AUTHENTICATE_RESPONSE), false, XContentType.JSON).v2())); + } + + public void testAuthenticateShouldNotFallThroughInCaseOfFailure() throws IOException { + final Request request = new Request("GET", "_security/_authenticate"); + request.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", "Bearer " + INVALID_SERVICE_TOKEN)); + final ResponseException e = expectThrows(ResponseException.class, () -> client().performRequest(request)); + assertThat(e.getResponse().getStatusLine().getStatusCode(), equalTo(401)); + assertThat(e.getMessage(), containsString("failed to authenticate service account [elastic/fleet] with token name [token1]")); + } + + public void testAuthenticateShouldWorkWithOAuthBearerToken() throws IOException { + final Request oauthTokenRequest = new Request("POST", "_security/oauth2/token"); + oauthTokenRequest.setJsonEntity("{\"grant_type\":\"client_credentials\"}"); + final Response oauthTokenResponse = client().performRequest(oauthTokenRequest); + assertOK(oauthTokenResponse); + final String accessToken = (String) responseAsMap(oauthTokenResponse).get("access_token"); + + final Request request = new Request("GET", "_security/_authenticate"); + request.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", "Bearer " + accessToken)); + final Response response = client().performRequest(request); + assertOK(response); + final Map responseMap = responseAsMap(response); + assertThat(responseMap.get("username"), equalTo("test_admin")); + assertThat(responseMap.get("authentication_type"), equalTo("token")); + } + + public void testAuthenticateShouldDifferentiateBetweenNormalUserAndServiceAccount() throws IOException { + final Request request = new Request("GET", "_security/_authenticate"); + request.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader( + "Authorization", basicAuthHeaderValue("elastic/fleet", new SecureString("x-pack-test-password".toCharArray())) + )); + final Response response = client().performRequest(request); + assertOK(response); + final Map responseMap = responseAsMap(response); + + assertThat(responseMap.get("username"), equalTo("elastic/fleet")); + assertThat(responseMap.get("authentication_type"), equalTo("realm")); + assertThat(responseMap.get("roles"), equalTo(List.of("superuser"))); + Map authRealm = (Map) responseMap.get("authentication_realm"); + assertThat(authRealm, hasEntry("type", "file")); + } +} diff --git a/x-pack/plugin/security/qa/service-account/src/javaRestTest/resources/service_tokens b/x-pack/plugin/security/qa/service-account/src/javaRestTest/resources/service_tokens new file mode 100644 index 0000000000000..72074b39ec97c --- /dev/null +++ b/x-pack/plugin/security/qa/service-account/src/javaRestTest/resources/service_tokens @@ -0,0 +1 @@ +elastic/fleet/token1:{PBKDF2_STRETCH}10000$XHyHETZWckPiHOuplBOnHeHpB41pTO8XkDC5yTujlcw=$691fFB/AwrSnjRhixFR2y9hOhCd5q6/6pDm29c/tsss= diff --git a/x-pack/plugin/security/qa/service-account/src/javaRestTest/resources/ssl/README.asciidoc b/x-pack/plugin/security/qa/service-account/src/javaRestTest/resources/ssl/README.asciidoc new file mode 100644 index 0000000000000..d91e5653cdef9 --- /dev/null +++ b/x-pack/plugin/security/qa/service-account/src/javaRestTest/resources/ssl/README.asciidoc @@ -0,0 +1,37 @@ += Keystore Details +This document details the steps used to create the certificate and keystore files in this directory. + +== Instructions on generating certificates + +The certificates in this directory have been generated using elasticsearch-certutil (8.0.0 SNAPSHOT) + +=== Certificates for security the HTTP server +[source,shell] +----------------------------------------------------------------------------------------------------------- +elasticsearch-certutil ca --pem --out=${PWD}/ca.zip --pass="ca-password" --days=3500 +unzip ca.zip +mv ca/ca.crt ./ca.crt +mv ca/ca.key ./ca.key + +rm ca.zip +rmdir ca +----------------------------------------------------------------------------------------------------------- + +[source,shell] +----------------------------------------------------------------------------------------------------------- +elasticsearch-certutil cert --pem --name=node --out=${PWD}/node.zip --pass="node-password" --days=3500 \ + --ca-cert=${PWD}/ca.crt --ca-key=${PWD}/ca.key --ca-pass="ca-password" \ + --dns=localhost --dns=localhost.localdomain --dns=localhost4 --dns=localhost4.localdomain4 --dns=localhost6 --dns=localhost6.localdomain6 \ + --ip=127.0.0.1 --ip=0:0:0:0:0:0:0:1 + +unzip node.zip +mv node/node.* ./ + +rm node.zip +rmdir node +----------------------------------------------------------------------------------------------------------- + +[source,shell] +----------------------------------------------------------------------------------------------------------- +keytool -importcert -file ca.crt -keystore ca.p12 -storetype PKCS12 -storepass "password" -alias ca +----------------------------------------------------------------------------------------------------------- diff --git a/x-pack/plugin/security/qa/service-account/src/javaRestTest/resources/ssl/ca.crt b/x-pack/plugin/security/qa/service-account/src/javaRestTest/resources/ssl/ca.crt new file mode 100644 index 0000000000000..ccfdadcab6d14 --- /dev/null +++ b/x-pack/plugin/security/qa/service-account/src/javaRestTest/resources/ssl/ca.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDSTCCAjGgAwIBAgIUG4Vi/zqBSBJT7DgRTFDQwh4ShlQwDQYJKoZIhvcNAQEL +BQAwNDEyMDAGA1UEAxMpRWxhc3RpYyBDZXJ0aWZpY2F0ZSBUb29sIEF1dG9nZW5l +cmF0ZWQgQ0EwHhcNMjEwMzE4MDIyNjAyWhcNMzAxMDE3MDIyNjAyWjA0MTIwMAYD +VQQDEylFbGFzdGljIENlcnRpZmljYXRlIFRvb2wgQXV0b2dlbmVyYXRlZCBDQTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIfrBgvsv/i4v6bAtfZTCIBY ++OdhW6d2aF5LSPClruryqmp2vNWhGTEkcqe6EcFe+JRc+E+CnW0nXWslWf6kLxOJ +VR5kjuT7LZ1tGbm70joh5V1t79NXu+BC0B/ET6T/BDzjnrDlt+AsFmR+F348UftY +Y04NZRy+gRh9SxS0Y4riDGj0pWWJkPBK314JXf8rJe1RiYGfNl5OgAljGrs7sHAn +1AO2nEH8Ihad3V55dtMIMXHGQTWkIx+QK25cGpySB78CXR432BmRMieMHZ5z1ELL +A658Kco22HDmbNk4o51r/2AXs1fxcPTVZwK3n5tvC2hABXuILE7ck9A3LyGRZGMC +AwEAAaNTMFEwHQYDVR0OBBYEFNlY6G4x4gG5/lRF8fO6knZaOzzlMB8GA1UdIwQY +MBaAFNlY6G4x4gG5/lRF8fO6knZaOzzlMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI +hvcNAQELBQADggEBAD4e1fOX00AT3bxXHyJd4tT6g40bxAmZhrtFDkoxX86Vp2bp +h+XfUfr54ziVzIbVynYMJ759mdB4BN8oZNTbOpmz/hNbz5skd2wIdAw/oZqAsOiW +l+OZLaaQYVfLesuBUJfxU7JvZeF0rB2F0ODc8BJz0Q6Mjbvj8fyCbSIQS01PjATN +0zeFQYuwJaQgTLVTU9jQYIbNBgCUuVmOW6IDF6QULtbCuH1Wtyr3u2I2nWfpyDhF +u7PY5Qh/O13rRy5o6NJofxaa3nU1PJalQzIA6ExA8ajol4ywiFtAyCVLYuJMKDt9 +HN0WWGAbhCPc/6i5KzNv6vW8EaWAOlAt2t1/7LU= +-----END CERTIFICATE----- diff --git a/x-pack/plugin/security/qa/service-account/src/javaRestTest/resources/ssl/ca.key b/x-pack/plugin/security/qa/service-account/src/javaRestTest/resources/ssl/ca.key new file mode 100644 index 0000000000000..4438c4e59b247 --- /dev/null +++ b/x-pack/plugin/security/qa/service-account/src/javaRestTest/resources/ssl/ca.key @@ -0,0 +1,30 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,AD07A96A73827285800BF6F4C8C37988 + +9F4L3SRxQaSkcmW72PaiPDDPNUW9zdoWy2VvSaKUp7cWCupUpF3gqvIwdpr/sHj5 +Jh4gfWCzASy2yb+Q/OAbeq2Nl5P7p6klDjBtDFVlLXmIearRiXBUgi7i55lic2nB +3zpUzBeXiqWxAiFTl1vhBB0DVexy0Ob7Hf3A7Zp669UQiMquplaGg+KtNVh2IxvJ +vZmV+danHJpTqd4CnC93J4l/4tH3/ZYHPydqe1a7Bhe0BwMurOqtoosuzF0BQMam +BcDVpyeRzg7C+ST1sZq+D/F1OpNvOOCE0hBjHg4NWdqyhiRLLwcbyEUutsyWo5zJ +QCnBiznVzeEobwFdglCLoe+fVFWVNe2fddX541kfcHRXozDvNbRMrkPwqWHzLLBc +bFn9PV3QSYoWE6Pee4/ibX4TYwe8yfxBBg5BpQQV+zjyBaXDQM6NNHMPxSE7YoD1 +TGAjQXwajse4uG0WRwOMgNHU9mzkMBLkv8s03PYmPXbnJkxd2jZSQoZ8FZrHQDXQ +oiMh6zMRDCiQRVrz7NwYN9uS5dwnj7fQDex5uyegIw7He57LuFJ92s7fqYAoaOtO +9QDRD5ky+q9+XN4T/3mOIaHTKNF5/kuN0eXH0vGVGWlNo2h+MBXGn+aA1p/97Cym +tZzmyAqDiXg9DhNMdHJor7DOQa9CCp5YxYYO5rzMa5ElvKIcOEmYkf1MTLq0Al/t +hYC5bL07aQ0sVhA+QW8kfxLkFT+u14rMlp6PJ9/KMLVBRoQWswwBMTBnocSwejkx +lZaGWjzpptQ3VqgSBOtEDjamItSFiZeN2ntwOckauVSRJZDig/q5yLgIlwrqxtDH +Sqh3u6JysIcBCcGg9U1q9AzxzFD8I4P8DwzUd56mbp3eR5iMvGsKcXbwlLvx/dSX +HVs0S7bEUr5WavmSIGwwrHtRO/l3STJNC1W7YxVKhBCxgz46DqADXbHuMvt8ZB13 +Zs94eEDA0vmPQnOilIG200V4OP3Uz8UP9HcNrhkLGuyCe+RIvv6NOwtq/O9YmazR +tmlcyrXEkvb5wubVg0zDlPpMBlYHGBEVl2vnVlNFHbsLkc55WahEbdpynnx3gYid +o4a5I/ywqaMou6ZTtOXZXc+0WuqjsLFAKmytZJtnktScGwJ+3JPWR51pi9j9q9W7 +oTnsyO4/a0nSZTNSGI2hxrmss5Y75bN/ydFuMhwd/GEiupKG40ZF+9hcGrqZRddM +uf0WoRvD5n611Bg8s9nwBMUjN7BFzu+a91s1W8LwwXUTZwkkyhkg/VUCKYbOH329 +Q6lZLb5nvvzEN/1HH/w0Bkl1jKBJSskw/R6zUGyviP1Sr3ZGkvUSvwXhrRHqI8MN +83t5AzZ6hivzy7rzCI/UsKoUx2/ef63TcvgLb/Vf85anuRR08Xcv/XIl775UvibQ +fAA0PE07sbYbO7vwRbv1bLhcPmA3wMsu0v/6Ohcv15uFFgUr/e9zhv5seP0tHdeR +ZKSbqlwfGRgp0smXPWJzIGG3g+lkadrfwTBuzgdjI8V/C+nEMk1eYy8SJd/CmfdG +IgZYMUWhc6GCcaq+eJ9VGVdgFkQU6aGTm4wNpmWPuDk/YDFo7ik48OrMvx67j1kz +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/security/qa/service-account/src/javaRestTest/resources/ssl/ca.p12 b/x-pack/plugin/security/qa/service-account/src/javaRestTest/resources/ssl/ca.p12 new file mode 100644 index 0000000000000000000000000000000000000000..e79ddffd719810f6c0350656187ff7ffed054463 GIT binary patch literal 1130 zcmV-w1eN;Jo}qiO)CCI)8({H`xz`H;(?zfJVmE|Q;eGp);r=u}a?hFBVL1VGI|EM^ ztFmj<+le0l?bzO5>gUM;fjajg`K}`sJ zC2*RAFnWoOn&+_nr0{am5=s2GUWyRW=k)vb3@aICpEI>$%OZIr(Pg|#fWHcL2H?Z} zG|KDDg_Xv2K!r`I8n-FqDv((2+rNdZgNq>h?tR*cW_K?X6VVdaij52K``r%NZ{9Md z|MlOTzvEaM`evpe04Oq2=JQz$UAX&~rKz1A)FOlci+AwDWZ^Uw#|`_KDNY?#;^Wn} z&-;TfCKnwCe{1YjS2T_QdLOJ;#_6K>@E~-pT9^o2L5bp(ghJNr2iAqvn%Ta8p;A~ zpaKC0-;Z46ra7MDDOOAhvpNWQVwVq`?5~8tlt+DlAI3qLTl79^!x5V7ODtL8Y=2tU zFHWWII7h^!z%i1hpEi98OZOG=syx>E88588#%(V_Df6<8Uua{RI0;Tf(JA=z-V>9H zZp613T#7>}0v4$_9td*xnEe5cy42?oF@sCZd!0&6fG|EVAutIB1uG5%0vZJX1QZ_I wc?2M?&~#s@by@d(cYU0Z6?Oy^$7@pH@iy5giV3?nqOwq1mNN6t0s{etp#BdIjQ{`u literal 0 HcmV?d00001 diff --git a/x-pack/plugin/security/qa/service-account/src/javaRestTest/resources/ssl/node.crt b/x-pack/plugin/security/qa/service-account/src/javaRestTest/resources/ssl/node.crt new file mode 100644 index 0000000000000..7b1bc7a5f5586 --- /dev/null +++ b/x-pack/plugin/security/qa/service-account/src/javaRestTest/resources/ssl/node.crt @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDszCCApugAwIBAgIVAO2bFGZI6jJKeo1hea8Yc+RvY1J7MA0GCSqGSIb3DQEB +CwUAMDQxMjAwBgNVBAMTKUVsYXN0aWMgQ2VydGlmaWNhdGUgVG9vbCBBdXRvZ2Vu +ZXJhdGVkIENBMB4XDTIxMDMxODAyMjYzMloXDTMwMTAxNzAyMjYzMlowDzENMAsG +A1UEAxMEbm9kZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKQ7uDRl +d/wKlUkesl1jegzQcFw9po54Mt2O3UTOYBkFWg6amAIyA8Izkavkoh/kQZAR2gqa +O65jqO/rNRrNBlyX2x+IOm0XmDC1ZmHoOBIxaCZUGVqwkeHNxcb5TmVFbYAcRGOJ +b54v42SEarVoqJS9iQaGb7ScKTeQ7XWyPGImReVNwE7SJNWwuABTXMe9c6VtvZpY +xu1SX+gYVk7aWQ0p3ukHKJXrPfXYXSgozF3tKtFQvUrL1VjHEVWqWoBqjIbhl3X8 +eqkzxwC1y+8Zbp3Os9Y8PzHQ4etXG7UAPFRopy5MivlDxZ2u5DpVW/6Yy1B7i6Mp +9Leu2NPNZ7ul/iECAwEAAaOB4DCB3TAdBgNVHQ4EFgQUYVaPvntroOl+zfW5vDFg +Kvmmj1MwHwYDVR0jBBgwFoAU2VjobjHiAbn+VEXx87qSdlo7POUwgY8GA1UdEQSB +hzCBhIIJbG9jYWxob3N0ghdsb2NhbGhvc3Q2LmxvY2FsZG9tYWluNocEfwAAAYcQ +AAAAAAAAAAAAAAAAAAAAAYIKbG9jYWxob3N0NIIKbG9jYWxob3N0NoIVbG9jYWxo +b3N0LmxvY2FsZG9tYWlughdsb2NhbGhvc3Q0LmxvY2FsZG9tYWluNDAJBgNVHRME +AjAAMA0GCSqGSIb3DQEBCwUAA4IBAQAdP/Z/tDOWkM5Eob+6FwIJuM9Pe9+NOwUL ++0qrHNHDt5ITyUf/C/l6yfXgbkvoLRa9QefN0cxy0ru8ew3nUUn7US0EfWF0yrza +M8BwznKKh6cs4AiFUdDliBgyqAzYubcZ6G6Trm3Tdh334bAQKM7M1TOvZa8jwXXb +6T1PUs/2RCWE7nLxBooDTik86phUm65oVtTqoO0c4XbQzzTfRrF7Oy3kmqpKsrzv +UDB4G4TAfGyybdystyEqPPVX3KESV9PDcxpO01R2/BWi49E4YmdL4PitIA/v7iAk +SH0UISQNjDpncRz9mGrt8LrA+O2Canqiq3xXeHJEhU5/KPCPcsrm +-----END CERTIFICATE----- diff --git a/x-pack/plugin/security/qa/service-account/src/javaRestTest/resources/ssl/node.key b/x-pack/plugin/security/qa/service-account/src/javaRestTest/resources/ssl/node.key new file mode 100644 index 0000000000000..3ec434b717a99 --- /dev/null +++ b/x-pack/plugin/security/qa/service-account/src/javaRestTest/resources/ssl/node.key @@ -0,0 +1,30 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,4A5CF28950363F663AA997154AC331F9 + +oHO/8oRnLXHTCljeqW90XBa/pDmLHUwRo82hy6732bSUTrXuBwUopuzcj6r8QzbQ +1ZyCbja0wwWaQ5TuNX3ehseiaBUKWgCLYYjd2IfFsfyFmvVAcPqnltyKMRvpLkFj +NeGyKFXmfxT3rmvzrmId4KkRYCHvH+j3RKfJ0wuhduzv9sH3xfmEe521l2F8Vukq +zVNMRPT9FHlSYhM1h26WpBlzx6Wq7EfP7KdyUtmIZ5/RFJjELG5rUyLgZHDqfKCy +LdNPpOuBdpYuBC+Oy97p2YuaFSLPkkKhiI4MG4MYsOnCmEFBNup9OhF3U/t/ffXh +knTjXh2fX7h8RJ9pH/8czG+O6cZoe5O/1/Ympo+ghS7QYDUtDrNS5M4MI+eP+WiA +X3cev3VkugDw4dDSPq3i3E0oCRZesMpst2W6AtVcpa5EWRM75PVuUws0XY/V/ca0 +CdUO6CPVIAAT3urmJWC1reiNhkEMDrskOL1PnsrseGvOmCLava9xYjiAS6JGawm/ +kWN3unJ6BwlU0NkIEbj8OGHdiKAjNWr0HLR34Xa2gqup5pGVD8EoC20ZPjeDXZ2j +oEfuLo2ZaF5CWDt0CEcdN7v/JtXC9QJjf0BAMHKiULhPzv9rNfqj6xZKkNxgVrW/ +D2/Jpyn5qt6BDiyzG0jaO7AzIk3BTBksdf+5myc5/0NA+kdC9aKZKmeLAazCAK1G +CwtfTs1xF4tMj1P+GRD4DOwypml1OK528BSl+Ydubt2uc37hRsA2EctEEjy+vy2r +pR0akSVs2a4d00p26lWt5RP/h85KJdWwNj/YwRmRxWWMpNd/C4NrGgB5Ehs8CHFk +uQZOaAKXWuy/bPGKG+JdXqEM5CiasNqoJn0UBle2dOpG08Ezb19vHFgNSOuvrxEv +oxkklXzyw+JMyskmD67MxQBsHcxW4g+501OMhIb2J36LNsOMQxzjIpS2jia/P1lh +9R4WohPxKf7Hi0Ui6oQRC7i2USmisuHIlVAmv14AjiISJdhXVOFtu+hVWrCHqHEg +GWRj560G1WwT5EHZr4KN+6IRX6mCKJAO1XjSz5rPfDpet5OQGIr7N+lJwWE03kJs +6Pd8K0OYW+2rbwqFd4YugF18HQlA1T5aok4fj2P+BTOuCNfvf0ZZXFeBn45zgtZI +G/puduRwRRyUzB+XTzhN8o6dfuBjasq6U0/ZFDRKKJnAOAq/fmVxr51+zKvZ0T5B +NSPbD9wUdnABqGCR+y9AL63QP0iVrkLlKzjgUYdlb1lw4TnmLGadmfYaZoOtWH2c +FOucH3VVfinY7Q9EE5/EF5EHeG3pe3I3UHXTbAvcxvuhCByFZd6qe3Vz4AGcQLoT +ProWJzmjeElfziX4e4Ol6tNSAxwL+vhjn4KmvF4mFx6n+QMAyp8lEmPsYgnsT/n9 +pkdnk0VdLGQmp8eKExvvDfiDTagDnh6wr7Nys1VLBADIthsRW4Gdft02q3tFOyae +WpeZent5x28yRPbNgDtoStjqc0yQPdXVFuAsLzA6NT8ujlOhJCnmiPYOurGis0Ch +hQLV+kr5EybbUHGjMB01elqTXy2VTMEqQ/7TQdsy6vIDYeBq5t491t9P/TeeS5Om +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/test/SecuritySingleNodeTestCase.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/test/SecuritySingleNodeTestCase.java index b360325ab34d7..884cfeb798af4 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/test/SecuritySingleNodeTestCase.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/test/SecuritySingleNodeTestCase.java @@ -214,6 +214,10 @@ protected String configOperatorUsers() { return SECURITY_DEFAULT_SETTINGS.configOperatorUsers(); } + protected String configServiceTokens() { + return SECURITY_DEFAULT_SETTINGS.configServiceTokens(); + } + /** * Allows to override the node client username */ @@ -261,6 +265,11 @@ protected String configOperatorUsers() { return SecuritySingleNodeTestCase.this.configOperatorUsers(); } + @Override + protected String configServiceTokens() { + return SecuritySingleNodeTestCase.this.configServiceTokens(); + } + @Override protected String nodeClientUsername() { return SecuritySingleNodeTestCase.this.nodeClientUsername(); diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountSingleNodeTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountSingleNodeTests.java new file mode 100644 index 0000000000000..173e7af62b7a4 --- /dev/null +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountSingleNodeTests.java @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.authc.service; + +import org.elasticsearch.Version; +import org.elasticsearch.client.Client; +import org.elasticsearch.common.Strings; +import org.elasticsearch.node.Node; +import org.elasticsearch.test.SecuritySingleNodeTestCase; +import org.elasticsearch.xpack.core.security.action.user.AuthenticateAction; +import org.elasticsearch.xpack.core.security.action.user.AuthenticateRequest; +import org.elasticsearch.xpack.core.security.action.user.AuthenticateResponse; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.user.User; + +import java.util.Map; + +import static org.hamcrest.Matchers.equalTo; + +public class ServiceAccountSingleNodeTests extends SecuritySingleNodeTestCase { + + private static final String BEARER_TOKEN = "46ToAwIHZWxhc3RpYwVmbGVldAZ0b2tlbjEWME1TT0ZobXVRTENIaTNQUGJ4VXQ5ZwAAAAAAAAA"; + + @Override + protected String configServiceTokens() { + return super.configServiceTokens() + + "elastic/fleet/token1:" + + "{PBKDF2_STRETCH}10000$XHyHETZWckPiHOuplBOnHeHpB41pTO8XkDC5yTujlcw=$691fFB/AwrSnjRhixFR2y9hOhCd5q6/6pDm29c/tsss="; + } + + public void testAuthenticateWithServiceFileToken() { + final AuthenticateRequest authenticateRequest = new AuthenticateRequest("elastic/fleet"); + final AuthenticateResponse authenticateResponse = + createServiceAccountClient().execute(AuthenticateAction.INSTANCE, authenticateRequest).actionGet(); + final String nodeName = node().settings().get(Node.NODE_NAME_SETTING.getKey()); + assertThat(authenticateResponse.authentication(), equalTo( + new Authentication( + new User("elastic/fleet", Strings.EMPTY_ARRAY, "Service account - elastic/fleet", null, + Map.of("_elastic_service_account", true), true), + new Authentication.RealmRef("service_account", "service_account", nodeName), + null, Version.CURRENT, Authentication.AuthenticationType.TOKEN, Map.of("_token_name", "token1") + ) + )); + } + + private Client createServiceAccountClient() { + return client().filterWithHeader(Map.of("Authorization", "Bearer " + BEARER_TOKEN)); + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java index 8e412d0ddac65..be2ca62a2ae8c 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java @@ -194,7 +194,7 @@ public final class TokenService { private static final int TOKEN_LENGTH = 22; private static final String TOKEN_DOC_ID_PREFIX = TOKEN_DOC_TYPE + "_"; static final int LEGACY_MINIMUM_BYTES = VERSION_BYTES + SALT_BYTES + IV_BYTES + 1; - static final int MINIMUM_BYTES = VERSION_BYTES + TOKEN_LENGTH + 1; + static final int MINIMUM_BYTES = VERSION_BYTES + 1 + TOKEN_LENGTH + 1; static final int LEGACY_MINIMUM_BASE64_BYTES = Double.valueOf(Math.ceil((4 * LEGACY_MINIMUM_BYTES) / 3)).intValue(); public static final int MINIMUM_BASE64_BYTES = Double.valueOf(Math.ceil((4 * MINIMUM_BYTES) / 3)).intValue(); static final Version VERSION_HASHED_TOKENS = Version.V_7_2_0; @@ -1723,6 +1723,9 @@ String prependVersionAndEncodeAccessToken(Version version, String accessToken) t StreamOutput out = new OutputStreamStreamOutput(base64)) { out.setVersion(version); Version.writeVersion(version, out); + if (version.onOrAfter(VERSION_TOKEN_TYPE)) { + SecurityTokenType.ACCESS_TOKEN.write(out); + } out.writeString(accessToken); return new String(os.toByteArray(), StandardCharsets.UTF_8); } @@ -1757,6 +1760,9 @@ public static String prependVersionAndEncodeRefreshToken(Version version, String StreamOutput out = new OutputStreamStreamOutput(base64)) { out.setVersion(version); Version.writeVersion(version, out); + if (version.onOrAfter(VERSION_TOKEN_TYPE)) { + SecurityTokenType.REFRESH_TOKEN.write(out); + } out.writeString(payload); return new String(os.toByteArray(), StandardCharsets.UTF_8); } catch (IOException e) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStore.java index dac392ed6e07b..4ef3763eeae2d 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStore.java @@ -113,7 +113,12 @@ public final void invalidateAll() { abstract void doAuthenticate(ServiceAccountToken token, ActionListener listener); - private static class CachedResult { + // package private for testing + Cache> getCache() { + return cache; + } + + static class CachedResult { private final boolean success; private final char[] hash; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java index d246b42fda3a9..4ef38cbff581c 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java @@ -18,6 +18,7 @@ import org.elasticsearch.common.io.stream.InputStreamStreamInput; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.rest.RestStatus; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.user.User; @@ -102,7 +103,7 @@ public void authenticateToken(ServiceAccountToken serviceAccountToken, String no "only [{}] service accounts are supported, but received [{}]", ElasticServiceAccounts.NAMESPACE, serviceAccountToken.getAccountId().asPrincipal()); logger.debug(message); - listener.onFailure(new ElasticsearchSecurityException(message.getFormattedMessage())); + listener.onFailure(new ElasticsearchSecurityException(message.getFormattedMessage(), RestStatus.UNAUTHORIZED)); return; } @@ -111,7 +112,7 @@ public void authenticateToken(ServiceAccountToken serviceAccountToken, String no final ParameterizedMessage message = new ParameterizedMessage( "the [{}] service account does not exist", serviceAccountToken.getAccountId().asPrincipal()); logger.debug(message); - listener.onFailure(new ElasticsearchSecurityException(message.getFormattedMessage())); + listener.onFailure(new ElasticsearchSecurityException(message.getFormattedMessage(), RestStatus.UNAUTHORIZED)); return; } @@ -124,7 +125,7 @@ public void authenticateToken(ServiceAccountToken serviceAccountToken, String no serviceAccountToken.getAccountId().asPrincipal(), serviceAccountToken.getTokenName()); logger.debug(message); - listener.onFailure(new ElasticsearchSecurityException(message.getFormattedMessage())); + listener.onFailure(new ElasticsearchSecurityException(message.getFormattedMessage(), RestStatus.UNAUTHORIZED)); } }, listener::onFailure)); } @@ -150,8 +151,6 @@ private Authentication createAuthentication(ServiceAccount account, ServiceAccou Map.of("_token_name", token.getTokenName())); } - - private static ServiceAccountToken doParseToken(SecureString token) throws IOException { final byte[] bytes = CharArrays.toUtf8Bytes(token.getChars()); logger.trace("parsing token bytes {}", MessageDigests.toHexString(bytes)); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecuritySettingsSource.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecuritySettingsSource.java index a534c09410453..30d3d5b5de74e 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecuritySettingsSource.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecuritySettingsSource.java @@ -131,6 +131,7 @@ public Settings nodeSettings(int nodeOrdinal) { writeFile(xpackConf, "users", configUsers()); writeFile(xpackConf, "users_roles", configUsersRoles()); writeFile(xpackConf, "operator_users.yml", configOperatorUsers()); + writeFile(xpackConf, "service_tokens", configServiceTokens()); Settings.Builder builder = Settings.builder() .put(Environment.PATH_HOME_SETTING.getKey(), home) @@ -186,6 +187,10 @@ protected String configOperatorUsers() { return ""; } + protected String configServiceTokens() { + return ""; + } + protected String nodeClientUsername() { return TEST_USER_NAME; } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java index 2b698f4364869..30a3f770f9d4d 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java @@ -46,6 +46,10 @@ import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.io.stream.InputStreamStreamInput; +import org.elasticsearch.common.io.stream.OutputStreamStreamOutput; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; @@ -90,7 +94,10 @@ import org.junit.BeforeClass; import javax.crypto.SecretKey; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; @@ -1094,4 +1101,44 @@ private String generateAccessToken(TokenService tokenService, Version version) t return tokenService.prependVersionAndEncodeAccessToken(version, accessTokenString); } + public void testStream() throws IOException { + final String token = "YJHOiNRCTW-QHYlTLPgY8A"; + try (ByteArrayOutputStream os = new ByteArrayOutputStream(); + OutputStream base64 = Base64.getEncoder().wrap(os); + StreamOutput out = new OutputStreamStreamOutput(base64)) { + out.setVersion(Version.CURRENT); + Version.writeVersion(Version.CURRENT, out); + out.writeVInt(0); // <-- This is the problem + out.writeString(token); + final String encoded = new String(os.toByteArray(), StandardCharsets.UTF_8); + System.out.println("encoded length = " + encoded.length()); + final byte[] bytes = encoded.getBytes(StandardCharsets.UTF_8); + try (StreamInput in = new InputStreamStreamInput(Base64.getDecoder().wrap(new ByteArrayInputStream(bytes)))) { + final Version version = Version.readVersion(in); + in.setVersion(version); + final int i = in.readVInt(); + final String decoded = in.readString(); + System.out.println(decoded); + } + } + } + + public void testStreamSimple() throws IOException { + final int vint1 = 128; + final int vint2 = 0; + try (ByteArrayOutputStream os = new ByteArrayOutputStream(); + StreamOutput out = new OutputStreamStreamOutput(os)) { + out.writeVInt(vint1); + out.writeVInt(vint2); + out.flush(); + final String encoded = os.toString(StandardCharsets.UTF_8); + System.out.println("encoded length = " + encoded.length()); + final byte[] bytes = encoded.getBytes(StandardCharsets.UTF_8); + try (StreamInput in = new InputStreamStreamInput(new ByteArrayInputStream(bytes))) { + assertEquals(vint1, in.readVInt()); + assertEquals(vint2, in.readVInt()); + } + } + } + } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStoreTests.java new file mode 100644 index 0000000000000..996d21ed5a8d1 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStoreTests.java @@ -0,0 +1,149 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.authc.service; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.common.cache.Cache; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ListenableFuture; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.threadpool.TestThreadPool; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; +import org.junit.After; +import org.junit.Before; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.mockito.Mockito.mock; + +public class CachingServiceAccountsTokenStoreTests extends ESTestCase { + + private Settings globalSettings; + private ThreadPool threadPool; + + @Before + public void init() { + globalSettings = Settings.builder().put("path.home", createTempDir()).build(); + threadPool = new TestThreadPool("test"); + } + + @After + public void stop() { + if (threadPool != null) { + terminate(threadPool); + } + } + + 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 AtomicBoolean doAuthenticateInvoked = new AtomicBoolean(false); + + final CachingServiceAccountsTokenStore store = new CachingServiceAccountsTokenStore(globalSettings, threadPool) { + @Override + void doAuthenticate(ServiceAccountToken token, ActionListener listener) { + doAuthenticateInvoked.set(true); + listener.onResponse(validSecret.equals(token.getSecret())); + } + }; + + final Cache> cache = store.getCache(); + assertThat(cache.count(), equalTo(0)); + + // 1st auth with the right token1 + final PlainActionFuture future1 = new PlainActionFuture<>(); + store.authenticate(token1Valid, future1); + assertThat(future1.get(), is(true)); + assertThat(doAuthenticateInvoked.get(), is(true)); + assertThat(cache.count(), equalTo(1)); + doAuthenticateInvoked.set(false); // reset + + // 2nd auth with the right token1 should use cache + final PlainActionFuture future2 = new PlainActionFuture<>(); + store.authenticate(token1Valid, future2); + assertThat(future2.get(), is(true)); + assertThat(doAuthenticateInvoked.get(), is(false)); + + // 3rd auth with the wrong token1 that has the same qualified name should use cache + final PlainActionFuture future3 = new PlainActionFuture<>(); + store.authenticate(token1Invalid, future3); + assertThat(future3.get(), is(false)); + assertThat(doAuthenticateInvoked.get(), is(false)); + + // 4th auth with the wrong token2 + final PlainActionFuture future4 = new PlainActionFuture<>(); + store.authenticate(token2Invalid, future4); + assertThat(future4.get(), is(false)); + assertThat(doAuthenticateInvoked.get(), is(true)); + assertThat(cache.count(), equalTo(2)); + doAuthenticateInvoked.set(false); // reset + + // 5th auth with the wrong token2 again should use cache + final PlainActionFuture future5 = new PlainActionFuture<>(); + store.authenticate(token2Invalid, future5); + assertThat(future5.get(), is(false)); + assertThat(doAuthenticateInvoked.get(), is(false)); + + // 6th auth with the right token2 + final PlainActionFuture future6 = new PlainActionFuture<>(); + store.authenticate(token2Valid, future6); + assertThat(future6.get(), is(true)); + assertThat(doAuthenticateInvoked.get(), is(true)); + assertThat(cache.count(), equalTo(2)); + doAuthenticateInvoked.set(false); // reset + + // Invalidate token1 in the cache + store.invalidate(token1Valid.getQualifiedName()); + assertThat(cache.count(), equalTo(1)); + + // 7th auth with the right token1 + final PlainActionFuture future7 = new PlainActionFuture<>(); + store.authenticate(token1Valid, future7); + assertThat(future7.get(), is(true)); + assertThat(doAuthenticateInvoked.get(), is(true)); + assertThat(cache.count(), equalTo(2)); + doAuthenticateInvoked.set(false); // reset + + // Invalidate all items in the cache + store.invalidateAll(); + assertThat(cache.count(), equalTo(0)); + } + + public void testCacheCanBeDisabled() throws ExecutionException, InterruptedException { + final Settings settings = Settings.builder() + .put(globalSettings) + .put(CachingServiceAccountsTokenStore.CACHE_TTL_SETTING.getKey(), "0") + .build(); + + final boolean success = randomBoolean(); + + final CachingServiceAccountsTokenStore store = new CachingServiceAccountsTokenStore(settings, threadPool) { + @Override + void doAuthenticate(ServiceAccountToken token, ActionListener listener) { + listener.onResponse(success); + } + }; + assertThat(store.getCache(), nullValue()); + // authenticate should still work + final PlainActionFuture future = new PlainActionFuture<>(); + store.authenticate(mock(ServiceAccountToken.class), future); + assertThat(future.get(), is(success)); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountsTokenStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountsTokenStoreTests.java index 9f3acd97d5781..789fe4d04e6e0 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountsTokenStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountsTokenStoreTests.java @@ -71,7 +71,6 @@ public void testAuthenticate() throws ExecutionException, InterruptedException { final PlainActionFuture future = new PlainActionFuture<>(); compositeStore.authenticate(token, future); - System.out.println(future.get()); if (store1Success || store2Success || store3Success) { assertThat(future.get(), is(true)); if (store1Success) { From 625364544ec12563bc9fa8931c17d6eff049385d Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 18 Mar 2021 20:28:17 +1100 Subject: [PATCH 03/19] fix token service --- .../authc/service/ServiceAccountIT.java | 11 +++- .../xpack/security/authc/TokenService.java | 22 ++++---- .../authc/service/ServiceAccountService.java | 3 +- .../authc/AuthenticationServiceTests.java | 41 ++++++++++++++- .../security/authc/TokenServiceTests.java | 48 ------------------ .../service/ServiceAccountServiceTests.java | 50 +++++++++++++++++++ 6 files changed, 114 insertions(+), 61 deletions(-) diff --git a/x-pack/plugin/security/qa/service-account/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountIT.java b/x-pack/plugin/security/qa/service-account/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountIT.java index ae1854b721eb8..f52353dd4076a 100644 --- a/x-pack/plugin/security/qa/service-account/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountIT.java +++ b/x-pack/plugin/security/qa/service-account/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountIT.java @@ -102,10 +102,11 @@ public void testAuthenticateShouldNotFallThroughInCaseOfFailure() throws IOExcep public void testAuthenticateShouldWorkWithOAuthBearerToken() throws IOException { final Request oauthTokenRequest = new Request("POST", "_security/oauth2/token"); - oauthTokenRequest.setJsonEntity("{\"grant_type\":\"client_credentials\"}"); + oauthTokenRequest.setJsonEntity("{\"grant_type\":\"password\",\"username\":\"test_admin\",\"password\":\"x-pack-test-password\"}"); final Response oauthTokenResponse = client().performRequest(oauthTokenRequest); assertOK(oauthTokenResponse); - final String accessToken = (String) responseAsMap(oauthTokenResponse).get("access_token"); + final Map oauthTokenResponseMap = responseAsMap(oauthTokenResponse); + final String accessToken = (String) oauthTokenResponseMap.get("access_token"); final Request request = new Request("GET", "_security/_authenticate"); request.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", "Bearer " + accessToken)); @@ -114,6 +115,12 @@ public void testAuthenticateShouldWorkWithOAuthBearerToken() throws IOException final Map responseMap = responseAsMap(response); assertThat(responseMap.get("username"), equalTo("test_admin")); assertThat(responseMap.get("authentication_type"), equalTo("token")); + + final String refreshToken = (String) oauthTokenResponseMap.get("refresh_token"); + final Request refreshTokenRequest = new Request("POST", "_security/oauth2/token"); + refreshTokenRequest.setJsonEntity("{\"grant_type\":\"refresh_token\",\"refresh_token\":\"" + refreshToken + "\"}"); + final Response refreshTokenResponse = client().performRequest(refreshTokenRequest); + assertOK(refreshTokenResponse); } public void testAuthenticateShouldDifferentiateBetweenNormalUserAndServiceAccount() throws IOException { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java index be2ca62a2ae8c..55679154e2292 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java @@ -192,9 +192,10 @@ public final class TokenService { private static final int HASHED_TOKEN_LENGTH = 43; // UUIDs are 16 bytes encoded base64 without padding, therefore the length is (16 / 3) * 4 + ((16 % 3) * 8 + 5) / 6 chars private static final int TOKEN_LENGTH = 22; + private static final int TOKEN_TYPE_LENGTH = 1; private static final String TOKEN_DOC_ID_PREFIX = TOKEN_DOC_TYPE + "_"; static final int LEGACY_MINIMUM_BYTES = VERSION_BYTES + SALT_BYTES + IV_BYTES + 1; - static final int MINIMUM_BYTES = VERSION_BYTES + 1 + TOKEN_LENGTH + 1; + static final int MINIMUM_BYTES = VERSION_BYTES + TOKEN_TYPE_LENGTH + TOKEN_LENGTH + 1; static final int LEGACY_MINIMUM_BASE64_BYTES = Double.valueOf(Math.ceil((4 * LEGACY_MINIMUM_BYTES) / 3)).intValue(); public static final int MINIMUM_BASE64_BYTES = Double.valueOf(Math.ceil((4 * MINIMUM_BYTES) / 3)).intValue(); static final Version VERSION_HASHED_TOKENS = Version.V_7_2_0; @@ -1718,16 +1719,14 @@ public SecureString extractBearerTokenFromHeader(ThreadContext threadContext) { String prependVersionAndEncodeAccessToken(Version version, String accessToken) throws IOException, GeneralSecurityException { if (version.onOrAfter(VERSION_ACCESS_TOKENS_AS_UUIDS)) { - try (ByteArrayOutputStream os = new ByteArrayOutputStream(MINIMUM_BASE64_BYTES); - OutputStream base64 = Base64.getEncoder().wrap(os); - StreamOutput out = new OutputStreamStreamOutput(base64)) { + try (BytesStreamOutput out = new BytesStreamOutput(MINIMUM_BASE64_BYTES)) { out.setVersion(version); Version.writeVersion(version, out); if (version.onOrAfter(VERSION_TOKEN_TYPE)) { SecurityTokenType.ACCESS_TOKEN.write(out); } out.writeString(accessToken); - return new String(os.toByteArray(), StandardCharsets.UTF_8); + return Base64.getEncoder().encodeToString(out.bytes().toBytesRef().bytes); } } else { // we know that the minimum length is larger than the default of the ByteArrayOutputStream so set the size to this explicitly @@ -1755,16 +1754,15 @@ String prependVersionAndEncodeAccessToken(Version version, String accessToken) t } public static String prependVersionAndEncodeRefreshToken(Version version, String payload) { - try (ByteArrayOutputStream os = new ByteArrayOutputStream(); - OutputStream base64 = Base64.getEncoder().wrap(os); - StreamOutput out = new OutputStreamStreamOutput(base64)) { + try (BytesStreamOutput out = new BytesStreamOutput()) { out.setVersion(version); Version.writeVersion(version, out); if (version.onOrAfter(VERSION_TOKEN_TYPE)) { SecurityTokenType.REFRESH_TOKEN.write(out); } out.writeString(payload); - return new String(os.toByteArray(), StandardCharsets.UTF_8); + return Base64.getEncoder().encodeToString(out.bytes().toBytesRef().bytes); + } catch (IOException e) { throw new RuntimeException("Unexpected exception when working with small in-memory streams", e); } @@ -1779,6 +1777,12 @@ public static Tuple unpackVersionAndPayload(String encodedPack) try (StreamInput in = new InputStreamStreamInput(Base64.getDecoder().wrap(new ByteArrayInputStream(bytes)), bytes.length)) { final Version version = Version.readVersion(in); in.setVersion(version); + if (version.onOrAfter(VERSION_TOKEN_TYPE)) { + final SecurityTokenType tokenType = SecurityTokenType.read(in); + if (tokenType != SecurityTokenType.REFRESH_TOKEN) { + throw new IllegalArgumentException("expect a token type of [REFRESH_TOKEN], got [" + tokenType.name() + "]"); + } + } final String payload = in.readString(); return new Tuple(version, payload); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java index 4ef38cbff581c..4b32e7e7ced15 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java @@ -96,7 +96,8 @@ public void tryAuthenticateBearerToken(SecureString bearerToken, String nodeName } } - public void authenticateToken(ServiceAccountToken serviceAccountToken, String nodeName, ActionListener listener) { + // package private for testing + void authenticateToken(ServiceAccountToken serviceAccountToken, String nodeName, ActionListener listener) { if (ElasticServiceAccounts.NAMESPACE.equals(serviceAccountToken.getAccountId().namespace()) == false) { final ParameterizedMessage message = new ParameterizedMessage( diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java index 077e39698ea31..cd72677d18af0 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java @@ -95,6 +95,7 @@ import org.elasticsearch.xpack.security.support.SecurityIndexManager; import org.junit.After; import org.junit.Before; +import org.mockito.Mock; import org.mockito.Mockito; import java.io.IOException; @@ -111,6 +112,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; @@ -263,7 +265,13 @@ public void init() throws Exception { mock(CacheInvalidatorRegistry.class), threadPool); tokenService = new TokenService(settings, Clock.systemUTC(), client, licenseState, securityContext, securityIndex, securityIndex, clusterService); - serviceAccountService = new ServiceAccountService(new CompositeServiceAccountsTokenStore(List.of(), threadContext)); + serviceAccountService = mock(ServiceAccountService.class); + doAnswer(invocationOnMock -> { + @SuppressWarnings("unchecked") + final ActionListener listener = (ActionListener) invocationOnMock.getArguments()[2]; + listener.onResponse(null); + return null; + }).when(serviceAccountService).tryAuthenticateBearerToken(any(), any(), any()); operatorPrivilegesService = mock(OperatorPrivileges.OperatorPrivilegesService.class); service = new AuthenticationService(settings, realms, auditTrailService, @@ -1900,6 +1908,37 @@ public void testExpiredApiKey() { } } + public void testCanAuthenticateServiceAccount() throws ExecutionException, InterruptedException { + Mockito.reset(serviceAccountService); + final Authentication authentication = new Authentication( + new User("elastic/fleet"), + new RealmRef("service_account", "service_account", "foo"), null); + doAnswer(invocationOnMock -> { + @SuppressWarnings("unchecked") + final ActionListener listener = (ActionListener) invocationOnMock.getArguments()[2]; + listener.onResponse(authentication); + return null; + }).when(serviceAccountService).tryAuthenticateBearerToken(any(), any(), any()); + final PlainActionFuture future = new PlainActionFuture<>(); + service.authenticate("_action", transportRequest, false, future); + assertThat(future.get(), is(authentication)); + } + + public void testServiceAccountFailureWillNotFallthrough() { + Mockito.reset(serviceAccountService); + final RuntimeException bailOut = new RuntimeException("bail out"); + doAnswer(invocationOnMock -> { + @SuppressWarnings("unchecked") + final ActionListener listener = (ActionListener) invocationOnMock.getArguments()[2]; + listener.onFailure(bailOut); + return null; + }).when(serviceAccountService).tryAuthenticateBearerToken(any(), any(), any()); + final PlainActionFuture future = new PlainActionFuture<>(); + service.authenticate("_action", transportRequest, false, future); + final ExecutionException e = expectThrows(ExecutionException.class, () -> future.get()); + assertThat(e.getCause().getCause(), is(bailOut)); + } + private static class InternalRequest extends TransportRequest { @Override public void writeTo(StreamOutput out) {} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java index 30a3f770f9d4d..2496ae79e5e02 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java @@ -46,10 +46,6 @@ import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.collect.Tuple; -import org.elasticsearch.common.io.stream.InputStreamStreamInput; -import org.elasticsearch.common.io.stream.OutputStreamStreamOutput; -import org.elasticsearch.common.io.stream.StreamInput; -import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; @@ -94,10 +90,7 @@ import org.junit.BeforeClass; import javax.crypto.SecretKey; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.OutputStream; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; @@ -1100,45 +1093,4 @@ private String generateAccessToken(TokenService tokenService, Version version) t } return tokenService.prependVersionAndEncodeAccessToken(version, accessTokenString); } - - public void testStream() throws IOException { - final String token = "YJHOiNRCTW-QHYlTLPgY8A"; - try (ByteArrayOutputStream os = new ByteArrayOutputStream(); - OutputStream base64 = Base64.getEncoder().wrap(os); - StreamOutput out = new OutputStreamStreamOutput(base64)) { - out.setVersion(Version.CURRENT); - Version.writeVersion(Version.CURRENT, out); - out.writeVInt(0); // <-- This is the problem - out.writeString(token); - final String encoded = new String(os.toByteArray(), StandardCharsets.UTF_8); - System.out.println("encoded length = " + encoded.length()); - final byte[] bytes = encoded.getBytes(StandardCharsets.UTF_8); - try (StreamInput in = new InputStreamStreamInput(Base64.getDecoder().wrap(new ByteArrayInputStream(bytes)))) { - final Version version = Version.readVersion(in); - in.setVersion(version); - final int i = in.readVInt(); - final String decoded = in.readString(); - System.out.println(decoded); - } - } - } - - public void testStreamSimple() throws IOException { - final int vint1 = 128; - final int vint2 = 0; - try (ByteArrayOutputStream os = new ByteArrayOutputStream(); - StreamOutput out = new OutputStreamStreamOutput(os)) { - out.writeVInt(vint1); - out.writeVInt(vint2); - out.flush(); - final String encoded = os.toString(StandardCharsets.UTF_8); - System.out.println("encoded length = " + encoded.length()); - final byte[] bytes = encoded.getBytes(StandardCharsets.UTF_8); - try (StreamInput in = new InputStreamStreamInput(new ByteArrayInputStream(bytes))) { - assertEquals(vint1, in.readVInt()); - assertEquals(vint2, in.readVInt()); - } - } - } - } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java index 129fd27ef03ec..ff4e2a14023d5 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java @@ -33,6 +33,7 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doAnswer; @@ -121,6 +122,55 @@ private Authentication.RealmRef randomRealmRef() { randomAlphaOfLengthBetween(3, 8)); } + public void testTryAuthenticateBearerToken() throws ExecutionException, InterruptedException { + // null token + final PlainActionFuture future1 = new PlainActionFuture<>(); + serviceAccountService.tryAuthenticateBearerToken(null, randomAlphaOfLengthBetween(3, 8), future1); + assertThat(future1.get(), nullValue()); + + // garbage token + final PlainActionFuture future2 = new PlainActionFuture<>(); + serviceAccountService.tryAuthenticateBearerToken(new SecureString(randomAlphaOfLength(75).toCharArray()), + randomAlphaOfLengthBetween(3, 8), future2); + assertThat(future2.get(), nullValue()); + + // Wrong version + final PlainActionFuture future3 = new PlainActionFuture<>(); + serviceAccountService.tryAuthenticateBearerToken( + new SecureString("0/uxAwIHZWxhc3RpYwVmbGVldAZ0b2tlbjEWS2xScU9xdDdUSktTNWt3X1hKV0k0QQAAAAAAAAA".toCharArray()), + randomAlphaOfLengthBetween(3, 8), future3); + assertThat(future3.get(), nullValue()); + + // Wrong token type + final PlainActionFuture future4 = new PlainActionFuture<>(); + serviceAccountService.tryAuthenticateBearerToken( + new SecureString("46ToAwAHZWxhc3RpYwVmbGVldAZ0b2tlbjEWUmpXRVp1Q3hTVS1lZTJoMFJoYVFqUQAAAAAAAAA".toCharArray()), + randomAlphaOfLengthBetween(3, 8), future4); + assertThat(future4.get(), nullValue()); + + // Valid token + final PlainActionFuture future5 = new PlainActionFuture<>(); + doAnswer(invocationOnMock -> { + @SuppressWarnings("unchecked") + final ActionListener listener = (ActionListener) invocationOnMock.getArguments()[1]; + listener.onResponse(true); + return null; + }).when(serviceAccountsTokenStore).authenticate(any(), any()); + final String nodeName = randomAlphaOfLengthBetween(3, 8); + serviceAccountService.tryAuthenticateBearerToken( + new SecureString("46ToAwIHZWxhc3RpYwVmbGVldAZ0b2tlbjEWY1hoZExGb2RUZVd4WVU1My02TVBtZwAAAAAAAAA".toCharArray()), + nodeName, future5); + assertThat(future5.get(), equalTo( + new Authentication( + new User("elastic/fleet", Strings.EMPTY_ARRAY, "Service account - elastic/fleet", null, + Map.of("_elastic_service_account", true), true), + new Authentication.RealmRef(ServiceAccountService.REALM_NAME, ServiceAccountService.REALM_TYPE, nodeName), + null, Version.CURRENT, Authentication.AuthenticationType.TOKEN, + Map.of("_token_name", "token1") + ) + )); + } + public void testAuthenticateWithToken() throws ExecutionException, InterruptedException { // Null for non-elastic service account final ServiceAccount.ServiceAccountId accountId1 = new ServiceAccount.ServiceAccountId( From 0107f300679f45e42f36c12488be8ba57a869239 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 18 Mar 2021 20:40:06 +1100 Subject: [PATCH 04/19] checkstyle --- .../xpack/security/authc/AuthenticationService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java index e6eaed1e64771..5c31e5000f44c 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java @@ -48,7 +48,6 @@ import org.elasticsearch.xpack.security.audit.AuditTrailService; import org.elasticsearch.xpack.security.audit.AuditUtil; import org.elasticsearch.xpack.security.authc.service.ServiceAccountService; -import org.elasticsearch.xpack.security.authc.service.ServiceAccountToken; import org.elasticsearch.xpack.security.authc.support.RealmUserLookup; import org.elasticsearch.xpack.security.operator.OperatorPrivileges.OperatorPrivilegesService; import org.elasticsearch.xpack.security.support.SecurityIndexManager; From 289b640e8bfa9d206ba6e5566ad7b27a31f0d9ee Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 18 Mar 2021 20:46:03 +1100 Subject: [PATCH 05/19] checkstyle --- .../xpack/security/authc/AuthenticationServiceTests.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java index cd72677d18af0..c1b147a0035a8 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java @@ -89,13 +89,11 @@ import org.elasticsearch.xpack.security.authc.AuthenticationService.Authenticator; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; import org.elasticsearch.xpack.security.authc.service.ServiceAccountService; -import org.elasticsearch.xpack.security.authc.service.ServiceAccountsTokenStore.CompositeServiceAccountsTokenStore; import org.elasticsearch.xpack.security.operator.OperatorPrivileges; import org.elasticsearch.xpack.security.support.CacheInvalidatorRegistry; import org.elasticsearch.xpack.security.support.SecurityIndexManager; import org.junit.After; import org.junit.Before; -import org.mockito.Mock; import org.mockito.Mockito; import java.io.IOException; From d5b4ecb2ae9d30327a8b74e2c294b33c8fb0cb32 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 18 Mar 2021 21:55:43 +1100 Subject: [PATCH 06/19] fix tests --- .../token/TransportInvalidateTokenActionTests.java | 14 +++++--------- .../xpack/security/authc/TokenServiceTests.java | 3 +-- .../authc/support/SecondaryAuthenticatorTests.java | 11 ++++++++++- .../rest-api-spec/test/token/11_invalidation.yml | 8 ++++---- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/token/TransportInvalidateTokenActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/token/TransportInvalidateTokenActionTests.java index 2f7b94fe3af49..565e507443c3a 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/token/TransportInvalidateTokenActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/token/TransportInvalidateTokenActionTests.java @@ -15,8 +15,7 @@ import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.UUIDs; -import org.elasticsearch.common.io.stream.OutputStreamStreamOutput; -import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.Index; import org.elasticsearch.indices.IndexClosedException; @@ -34,13 +33,11 @@ import org.elasticsearch.xpack.core.security.action.token.InvalidateTokenRequest; import org.elasticsearch.xpack.core.security.action.token.InvalidateTokenResponse; import org.elasticsearch.xpack.security.authc.TokenService; +import org.elasticsearch.xpack.security.authc.support.SecurityTokenType; import org.elasticsearch.xpack.security.support.SecurityIndexManager; import org.junit.After; import org.junit.Before; -import java.io.ByteArrayOutputStream; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; import java.time.Clock; import java.util.Base64; import java.util.Collections; @@ -132,13 +129,12 @@ public void testInvalidateTokensWhenIndexClosed() throws Exception { } private String generateAccessTokenString() throws Exception { - try (ByteArrayOutputStream os = new ByteArrayOutputStream(TokenService.MINIMUM_BASE64_BYTES); - OutputStream base64 = Base64.getEncoder().wrap(os); - StreamOutput out = new OutputStreamStreamOutput(base64)) { + try (BytesStreamOutput out = new BytesStreamOutput(TokenService.MINIMUM_BASE64_BYTES)) { out.setVersion(Version.CURRENT); Version.writeVersion(Version.CURRENT, out); + SecurityTokenType.ACCESS_TOKEN.write(out); out.writeString(UUIDs.randomBase64UUID()); - return new String(os.toByteArray(), StandardCharsets.UTF_8); + return Base64.getEncoder().encodeToString(out.bytes().toBytesRef().bytes); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java index 2496ae79e5e02..07fc8bff93548 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java @@ -699,8 +699,7 @@ public void testTokenServiceDisabled() throws Exception { assertThat(e.getMetadata(FeatureNotEnabledException.DISABLED_FEATURE_METADATA), contains("security_tokens")); PlainActionFuture future = new PlainActionFuture<>(); - final SecureString bearerToken = tokenService.extractBearerTokenFromHeader(null); - tokenService.tryAuthenticateToken(bearerToken, future); + tokenService.tryAuthenticateToken(null, future); assertNull(future.get()); PlainActionFuture invalidateFuture = new PlainActionFuture<>(); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java index 2bb370b95ebfc..3f14060bc1e63 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java @@ -70,6 +70,8 @@ import static org.elasticsearch.xpack.security.authc.support.SecondaryAuthenticator.SECONDARY_AUTH_HEADER_NAME; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.nullValue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -127,8 +129,15 @@ public void setupMocks() throws Exception { final ApiKeyService apiKeyService = new ApiKeyService(settings, clock, client, licenseState, securityIndex, clusterService, mock(CacheInvalidatorRegistry.class),threadPool); + final ServiceAccountService serviceAccountService = mock(ServiceAccountService.class); + doAnswer(invocationOnMock -> { + @SuppressWarnings("unchecked") + final ActionListener listener = (ActionListener) invocationOnMock.getArguments()[2]; + listener.onResponse(null); + return null; + }).when(serviceAccountService).tryAuthenticateBearerToken(any(), any(), any()); authenticationService = new AuthenticationService(settings, realms, auditTrail, failureHandler, threadPool, anonymous, - tokenService, apiKeyService, mock(ServiceAccountService.class), OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); + tokenService, apiKeyService, serviceAccountService, OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); authenticator = new SecondaryAuthenticator(securityContext, authenticationService); } diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/token/11_invalidation.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/token/11_invalidation.yml index d1a8490a834de..f724fc4a3e9d3 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/token/11_invalidation.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/token/11_invalidation.yml @@ -45,7 +45,7 @@ teardown: catch: bad_request security.invalidate_token: body: - token: 46ToAxYzNUdPZWdYOFRQcWhjeHR3NWpvTmVB + token: 46ToAwAWMUVZS0lfWnpSZVdhRkhxSmk0WENQUQAAAAAAAAAAAA== - do: security.get_token: @@ -84,7 +84,7 @@ teardown: catch: missing security.invalidate_token: body: - token: 46ToAxYzNUdPZWdYOFRQcWhjeHR3NWpvTmVB + token: 46ToAwAWMUVZS0lfWnpSZVdhRkhxSmk0WENQUQAAAAAAAAAAAA== - match: { invalidated_tokens: 0 } - match: { previously_invalidated_tokens: 0 } @@ -97,7 +97,7 @@ teardown: catch: bad_request security.invalidate_token: body: - refresh_token: 46ToAxYzNUdPZWdYOFRQcWhjeHR3NWpvTmVB + refresh_token: 46ToAwEWWmIzWTJFMjFRRDJ0czZtVFRyRzB4dwAAAAA= - do: security.get_token: @@ -136,7 +136,7 @@ teardown: catch: missing security.invalidate_token: body: - refresh_token: 46ToAxYzNUdPZWdYOFRQcWhjeHR3NWpvTmVB + refresh_token: 46ToAwEWWmIzWTJFMjFRRDJ0czZtVFRyRzB4dwAAAAA= - match: { invalidated_tokens: 0 } - match: { previously_invalidated_tokens: 0 } From 05997e6c9f76b34427fa9278062486f96ccbddc9 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Sat, 20 Mar 2021 10:12:28 +1100 Subject: [PATCH 07/19] Ensure cache is invalidated on failure --- .../security/authc/AuthenticationService.java | 2 +- .../CachingServiceAccountsTokenStore.java | 25 +++++++++++++++---- .../authc/service/FileTokensTool.java | 6 +---- .../authc/service/ServiceAccountToken.java | 13 +++++++++- 4 files changed, 34 insertions(+), 12 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java index 5c31e5000f44c..033fa45035303 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java @@ -365,7 +365,7 @@ private void checkForBearerToken() { })); } }, e -> { - logger.debug("Failed to validate service account token for request [{}]", request); + logger.debug(new ParameterizedMessage("Failed to validate service account token for request [{}]", request), e); listener.onFailure(request.exceptionProcessingRequest(e, null)); })); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStore.java index 4ef3763eeae2d..c886bfacc48f1 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStore.java @@ -10,6 +10,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.cache.Cache; import org.elasticsearch.common.cache.CacheBuilder; import org.elasticsearch.common.settings.Setting; @@ -18,11 +19,13 @@ import org.elasticsearch.common.util.concurrent.ListenableFuture; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.security.authc.support.Hasher; +import org.elasticsearch.xpack.security.support.CacheInvalidatorRegistry; +import java.util.Collection; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicBoolean; -public abstract class CachingServiceAccountsTokenStore implements ServiceAccountsTokenStore { +public abstract class CachingServiceAccountsTokenStore implements ServiceAccountsTokenStore, CacheInvalidatorRegistry.CacheInvalidator { private static final Logger logger = LogManager.getLogger(CachingServiceAccountsTokenStore.class); @@ -90,20 +93,28 @@ private void authenticateWithCache(ServiceAccountToken token, ActionListener { + // In case of failure, evict the cache entry and notify all listeners + cache.invalidate(token.getQualifiedName(), listenableCacheEntry); + listenableCacheEntry.cancel(true); + listener.onFailure(e); + })); } } catch (final ExecutionException e) { listener.onFailure(e); } } - public final void invalidate(String qualifiedTokenName) { + @Override + public final void invalidate(Collection qualifiedTokenNames) { if (cache != null) { - logger.trace("invalidating cache for service token [{}]", qualifiedTokenName); - cache.invalidate(qualifiedTokenName); + logger.trace("invalidating cache for service token [{}]", + Strings.collectionToCommaDelimitedString(qualifiedTokenNames)); + qualifiedTokenNames.forEach(cache::invalidate); } } + @Override public final void invalidateAll() { if (cache != null) { logger.trace("invalidating cache for all service tokens"); @@ -111,6 +122,10 @@ public final void invalidateAll() { } } + protected ThreadPool getThreadPool() { + return threadPool; + } + abstract void doAuthenticate(ServiceAccountToken token, ActionListener listener); // package private for testing diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileTokensTool.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileTokensTool.java index 1c5abb816aee6..a414e513191c4 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileTokensTool.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileTokensTool.java @@ -15,9 +15,7 @@ import org.elasticsearch.cli.Terminal; import org.elasticsearch.cli.UserException; import org.elasticsearch.common.Strings; -import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.collect.Tuple; -import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; import org.elasticsearch.xpack.core.XPackSettings; @@ -79,9 +77,7 @@ protected void execute(Terminal terminal, OptionSet options, Environment env) th FileAttributesChecker attributesChecker = new FileAttributesChecker(serviceTokensFile); final Map tokenHashes = new TreeMap<>(FileServiceAccountsTokenStore.parseFile(serviceTokensFile, null)); - try (SecureString tokenString = UUIDs.randomBase64UUIDSecureString()) { - final ServiceAccountToken token = - new ServiceAccountToken(ServiceAccountId.fromPrincipal(principal), tokenName, tokenString); + try (ServiceAccountToken token = ServiceAccountToken.of(ServiceAccountId.fromPrincipal(principal), tokenName)) { if (tokenHashes.containsKey(token.getQualifiedName())) { throw new UserException(ExitCodes.CODE_ERROR, "Service token [" + token.getQualifiedName() + "] already exists"); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountToken.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountToken.java index 6ff8981f3a7ea..8c0335e803438 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountToken.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountToken.java @@ -8,11 +8,13 @@ package org.elasticsearch.xpack.security.authc.service; import org.elasticsearch.Version; +import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; import org.elasticsearch.xpack.security.authc.support.SecurityTokenType; +import java.io.Closeable; import java.io.IOException; import java.util.Base64; import java.util.Objects; @@ -26,7 +28,7 @@ *

  • The {@link #getSecret() secret credential} for that token
  • * */ -public class ServiceAccountToken { +public class ServiceAccountToken implements Closeable { private final ServiceAccountId accountId; private final String tokenName; private final SecureString secret; @@ -68,6 +70,11 @@ public SecureString asBearerString() throws IOException { } } + @Override + public void close() throws IOException { + secret.close(); + } + @Override public boolean equals(Object o) { if (this == o) @@ -82,4 +89,8 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(accountId, tokenName, secret); } + + public static ServiceAccountToken of(ServiceAccountId accountId, String tokenName) { + return new ServiceAccountToken(accountId, tokenName, UUIDs.randomBase64UUIDSecureString()); + } } From c32599541eba54c54687145328ae4c6e8531956f Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Sat, 20 Mar 2021 10:27:13 +1100 Subject: [PATCH 08/19] fix test compilation --- .../authc/service/CachingServiceAccountsTokenStoreTests.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStoreTests.java index 996d21ed5a8d1..ad2adea1cfcf5 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStoreTests.java @@ -20,6 +20,7 @@ import org.junit.After; import org.junit.Before; +import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicBoolean; @@ -110,7 +111,7 @@ void doAuthenticate(ServiceAccountToken token, ActionListener listener) doAuthenticateInvoked.set(false); // reset // Invalidate token1 in the cache - store.invalidate(token1Valid.getQualifiedName()); + store.invalidate(List.of(token1Valid.getQualifiedName())); assertThat(cache.count(), equalTo(1)); // 7th auth with the right token1 From f933778fc0711b22495ffcbeccb39b8c6e2e25f5 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Sat, 20 Mar 2021 10:29:17 +1100 Subject: [PATCH 09/19] onFailure instead of cancel --- .../authc/service/CachingServiceAccountsTokenStore.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStore.java index c886bfacc48f1..0d4cc958ec58a 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStore.java @@ -96,7 +96,7 @@ private void authenticateWithCache(ServiceAccountToken token, ActionListener { // In case of failure, evict the cache entry and notify all listeners cache.invalidate(token.getQualifiedName(), listenableCacheEntry); - listenableCacheEntry.cancel(true); + listenableCacheEntry.onFailure(e); listener.onFailure(e); })); } From 3f40476c14f3e7e974ce540370e45b9fa7bb16b8 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Tue, 23 Mar 2021 13:12:00 +1100 Subject: [PATCH 10/19] Update x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountToken.java Co-authored-by: Tim Vernum --- .../xpack/security/authc/service/ServiceAccountToken.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountToken.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountToken.java index 8c0335e803438..4d95672ac9927 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountToken.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountToken.java @@ -90,7 +90,7 @@ public int hashCode() { return Objects.hash(accountId, tokenName, secret); } - public static ServiceAccountToken of(ServiceAccountId accountId, String tokenName) { + public static ServiceAccountToken newToken(ServiceAccountId accountId, String tokenName) { return new ServiceAccountToken(accountId, tokenName, UUIDs.randomBase64UUIDSecureString()); } } From 821d15058bcd25399b8620420f4b28ddb94b5763 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Tue, 23 Mar 2021 14:42:29 +1100 Subject: [PATCH 11/19] Fix compilation --- .../xpack/security/authc/service/FileTokensTool.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileTokensTool.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileTokensTool.java index a414e513191c4..22c165bec89b9 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileTokensTool.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileTokensTool.java @@ -77,7 +77,7 @@ protected void execute(Terminal terminal, OptionSet options, Environment env) th FileAttributesChecker attributesChecker = new FileAttributesChecker(serviceTokensFile); final Map tokenHashes = new TreeMap<>(FileServiceAccountsTokenStore.parseFile(serviceTokensFile, null)); - try (ServiceAccountToken token = ServiceAccountToken.of(ServiceAccountId.fromPrincipal(principal), tokenName)) { + try (ServiceAccountToken token = ServiceAccountToken.newToken(ServiceAccountId.fromPrincipal(principal), tokenName)) { if (tokenHashes.containsKey(token.getQualifiedName())) { throw new UserException(ExitCodes.CODE_ERROR, "Service token [" + token.getQualifiedName() + "] already exists"); } From 56671cc7954b445a969d6c44ac1f573759914ad3 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Tue, 23 Mar 2021 15:03:58 +1100 Subject: [PATCH 12/19] address feedback --- .../security/authc/AuthenticationService.java | 53 ++++++++++--------- .../authc/service/ServiceAccountService.java | 14 +---- .../authc/service/ServiceAccountToken.java | 20 ++++++- .../authc/AuthenticationServiceTests.java | 6 +-- .../service/ServiceAccountServiceTests.java | 50 ++++++----------- .../support/SecondaryAuthenticatorTests.java | 2 +- 6 files changed, 68 insertions(+), 77 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java index 033fa45035303..0e5d05a298378 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java @@ -48,6 +48,7 @@ import org.elasticsearch.xpack.security.audit.AuditTrailService; import org.elasticsearch.xpack.security.audit.AuditUtil; import org.elasticsearch.xpack.security.authc.service.ServiceAccountService; +import org.elasticsearch.xpack.security.authc.service.ServiceAccountToken; import org.elasticsearch.xpack.security.authc.support.RealmUserLookup; import org.elasticsearch.xpack.security.operator.OperatorPrivileges.OperatorPrivilegesService; import org.elasticsearch.xpack.security.support.SecurityIndexManager; @@ -341,33 +342,35 @@ private void authenticateAsync() { } private void checkForBearerToken() { - final SecureString bearerToken = tokenService.extractBearerTokenFromHeader(threadContext); - serviceAccountService.tryAuthenticateBearerToken(bearerToken, nodeName, ActionListener.wrap(authentication -> { - if (authentication != null) { + final SecureString bearerString = tokenService.extractBearerTokenFromHeader(threadContext); + final ServiceAccountToken serviceAccountToken = ServiceAccountService.tryParseToken(bearerString); + if (serviceAccountToken != null) { + serviceAccountService.authenticateToken(serviceAccountToken, nodeName, ActionListener.wrap(authentication -> { + assert authentication != null : "service account authenticate should return either authentication or call onFailure"; this.authenticatedBy = authentication.getAuthenticatedBy(); writeAuthToContext(authentication); - } else { - tokenService.tryAuthenticateToken(bearerToken, ActionListener.wrap(userToken -> { - if (userToken != null) { - writeAuthToContext(userToken.getAuthentication()); - } else { - checkForApiKey(); - } - }, e -> { - logger.debug(new ParameterizedMessage("Failed to validate token authentication for request [{}]", request), e); - if (e instanceof ElasticsearchSecurityException - && false == tokenService.isExpiredTokenException((ElasticsearchSecurityException) e)) { - // intentionally ignore the returned exception; we call this primarily - // for the auditing as we already have a purpose built exception - request.tamperedRequest(); - } - listener.onFailure(e); - })); - } - }, e -> { - logger.debug(new ParameterizedMessage("Failed to validate service account token for request [{}]", request), e); - listener.onFailure(request.exceptionProcessingRequest(e, null)); - })); + }, e -> { + logger.debug(new ParameterizedMessage("Failed to validate service account token for request [{}]", request), e); + listener.onFailure(request.exceptionProcessingRequest(e, serviceAccountToken)); + })); + } else { + tokenService.tryAuthenticateToken(bearerString, ActionListener.wrap(userToken -> { + if (userToken != null) { + writeAuthToContext(userToken.getAuthentication()); + } else { + checkForApiKey(); + } + }, e -> { + logger.debug(new ParameterizedMessage("Failed to validate token authentication for request [{}]", request), e); + if (e instanceof ElasticsearchSecurityException + && false == tokenService.isExpiredTokenException((ElasticsearchSecurityException) e)) { + // intentionally ignore the returned exception; we call this primarily + // for the auditing as we already have a purpose built exception + request.tamperedRequest(); + } + listener.onFailure(e); + })); + } } private void checkForApiKey() { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java index 4b32e7e7ced15..cc398bfe2fc26 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java @@ -85,19 +85,7 @@ public static ServiceAccountToken tryParseToken(SecureString token) { } } - public void tryAuthenticateBearerToken(SecureString bearerToken, String nodeName, ActionListener listener) { - final ServiceAccountToken serviceAccountToken = ServiceAccountService.tryParseToken(bearerToken); - if (serviceAccountToken == null) { - // This should be the only situation where a null is returned to mean the authentication should continue. - // For all other situations, it should be either onResponse(authentication) for success or onFailure for any error. - listener.onResponse(null); - } else { - authenticateToken(serviceAccountToken, nodeName, listener); - } - } - - // package private for testing - void authenticateToken(ServiceAccountToken serviceAccountToken, String nodeName, ActionListener listener) { + public void authenticateToken(ServiceAccountToken serviceAccountToken, String nodeName, ActionListener listener) { if (ElasticServiceAccounts.NAMESPACE.equals(serviceAccountToken.getAccountId().namespace()) == false) { final ParameterizedMessage message = new ParameterizedMessage( diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountToken.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountToken.java index 4d95672ac9927..a1dd314b119f3 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountToken.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountToken.java @@ -11,6 +11,7 @@ import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; import org.elasticsearch.xpack.security.authc.support.SecurityTokenType; @@ -28,7 +29,7 @@ *
  • The {@link #getSecret() secret credential} for that token
  • * */ -public class ServiceAccountToken implements Closeable { +public class ServiceAccountToken implements AuthenticationToken, Closeable { private final ServiceAccountId accountId; private final String tokenName; private final SecureString secret; @@ -71,7 +72,7 @@ public SecureString asBearerString() throws IOException { } @Override - public void close() throws IOException { + public void close() { secret.close(); } @@ -93,4 +94,19 @@ public int hashCode() { public static ServiceAccountToken newToken(ServiceAccountId accountId, String tokenName) { return new ServiceAccountToken(accountId, tokenName, UUIDs.randomBase64UUIDSecureString()); } + + @Override + public String principal() { + return accountId.asPrincipal(); + } + + @Override + public Object credentials() { + return secret; + } + + @Override + public void clearCredentials() { + close(); + } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java index c1b147a0035a8..366a52238d338 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java @@ -269,7 +269,7 @@ public void init() throws Exception { final ActionListener listener = (ActionListener) invocationOnMock.getArguments()[2]; listener.onResponse(null); return null; - }).when(serviceAccountService).tryAuthenticateBearerToken(any(), any(), any()); + }).when(serviceAccountService).authenticateToken(any(), any(), any()); operatorPrivilegesService = mock(OperatorPrivileges.OperatorPrivilegesService.class); service = new AuthenticationService(settings, realms, auditTrailService, @@ -1916,7 +1916,7 @@ public void testCanAuthenticateServiceAccount() throws ExecutionException, Inter final ActionListener listener = (ActionListener) invocationOnMock.getArguments()[2]; listener.onResponse(authentication); return null; - }).when(serviceAccountService).tryAuthenticateBearerToken(any(), any(), any()); + }).when(serviceAccountService).authenticateToken(any(), any(), any()); final PlainActionFuture future = new PlainActionFuture<>(); service.authenticate("_action", transportRequest, false, future); assertThat(future.get(), is(authentication)); @@ -1930,7 +1930,7 @@ public void testServiceAccountFailureWillNotFallthrough() { final ActionListener listener = (ActionListener) invocationOnMock.getArguments()[2]; listener.onFailure(bailOut); return null; - }).when(serviceAccountService).tryAuthenticateBearerToken(any(), any(), any()); + }).when(serviceAccountService).authenticateToken(any(), any(), any()); final PlainActionFuture future = new PlainActionFuture<>(); service.authenticate("_action", transportRequest, false, future); final ExecutionException e = expectThrows(ExecutionException.class, () -> future.get()); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java index ff4e2a14023d5..c35b7749a139a 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java @@ -21,6 +21,7 @@ import org.elasticsearch.xpack.core.security.authc.Authentication; 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 org.elasticsearch.xpack.security.authc.support.SecurityTokenType; import org.junit.Before; @@ -33,7 +34,6 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doAnswer; @@ -82,8 +82,8 @@ public void testTryParseToken() throws IOException { // Null for null assertNull(ServiceAccountService.tryParseToken(null)); - final ServiceAccount.ServiceAccountId accountId = - new ServiceAccount.ServiceAccountId(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8)); + final ServiceAccountId accountId = + new ServiceAccountId(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8)); final String tokenName = randomAlphaOfLengthBetween(3, 8); final SecureString secret = new SecureString(randomAlphaOfLength(20).toCharArray()); @@ -110,6 +110,14 @@ public void testTryParseToken() throws IOException { assertNull(ServiceAccountService.tryParseToken(new SecureString(base64.toCharArray()))); } + // Invalid version again + assertNull(ServiceAccountService.tryParseToken( + new SecureString("0/uxAwIHZWxhc3RpYwVmbGVldAZ0b2tlbjEWS2xScU9xdDdUSktTNWt3X1hKV0k0QQAAAAAAAAA".toCharArray()))); + + // Wrong token type again + assertNull(ServiceAccountService.tryParseToken( + new SecureString("46ToAwAHZWxhc3RpYwVmbGVldAZ0b2tlbjEWUmpXRVp1Q3hTVS1lZTJoMFJoYVFqUQAAAAAAAAA".toCharArray()))); + // Serialise and de-serialise service account token final ServiceAccountToken serviceAccountToken = new ServiceAccountToken(accountId, tokenName, secret); final ServiceAccountToken parsedToken = ServiceAccountService.tryParseToken(serviceAccountToken.asBearerString()); @@ -123,31 +131,6 @@ private Authentication.RealmRef randomRealmRef() { } public void testTryAuthenticateBearerToken() throws ExecutionException, InterruptedException { - // null token - final PlainActionFuture future1 = new PlainActionFuture<>(); - serviceAccountService.tryAuthenticateBearerToken(null, randomAlphaOfLengthBetween(3, 8), future1); - assertThat(future1.get(), nullValue()); - - // garbage token - final PlainActionFuture future2 = new PlainActionFuture<>(); - serviceAccountService.tryAuthenticateBearerToken(new SecureString(randomAlphaOfLength(75).toCharArray()), - randomAlphaOfLengthBetween(3, 8), future2); - assertThat(future2.get(), nullValue()); - - // Wrong version - final PlainActionFuture future3 = new PlainActionFuture<>(); - serviceAccountService.tryAuthenticateBearerToken( - new SecureString("0/uxAwIHZWxhc3RpYwVmbGVldAZ0b2tlbjEWS2xScU9xdDdUSktTNWt3X1hKV0k0QQAAAAAAAAA".toCharArray()), - randomAlphaOfLengthBetween(3, 8), future3); - assertThat(future3.get(), nullValue()); - - // Wrong token type - final PlainActionFuture future4 = new PlainActionFuture<>(); - serviceAccountService.tryAuthenticateBearerToken( - new SecureString("46ToAwAHZWxhc3RpYwVmbGVldAZ0b2tlbjEWUmpXRVp1Q3hTVS1lZTJoMFJoYVFqUQAAAAAAAAA".toCharArray()), - randomAlphaOfLengthBetween(3, 8), future4); - assertThat(future4.get(), nullValue()); - // Valid token final PlainActionFuture future5 = new PlainActionFuture<>(); doAnswer(invocationOnMock -> { @@ -157,8 +140,9 @@ public void testTryAuthenticateBearerToken() throws ExecutionException, Interrup return null; }).when(serviceAccountsTokenStore).authenticate(any(), any()); final String nodeName = randomAlphaOfLengthBetween(3, 8); - serviceAccountService.tryAuthenticateBearerToken( - new SecureString("46ToAwIHZWxhc3RpYwVmbGVldAZ0b2tlbjEWY1hoZExGb2RUZVd4WVU1My02TVBtZwAAAAAAAAA".toCharArray()), + serviceAccountService.authenticateToken( + new ServiceAccountToken(new ServiceAccountId("elastic", "fleet"), "token1", + new SecureString("super-secret-value".toCharArray())), nodeName, future5); assertThat(future5.get(), equalTo( new Authentication( @@ -173,7 +157,7 @@ public void testTryAuthenticateBearerToken() throws ExecutionException, Interrup public void testAuthenticateWithToken() throws ExecutionException, InterruptedException { // Null for non-elastic service account - final ServiceAccount.ServiceAccountId accountId1 = new ServiceAccount.ServiceAccountId( + final ServiceAccountId accountId1 = new ServiceAccountId( randomValueOtherThan(ElasticServiceAccounts.NAMESPACE, () -> randomAlphaOfLengthBetween(3, 8)), randomAlphaOfLengthBetween(3, 8)); final SecureString secret = new SecureString(randomAlphaOfLength(20).toCharArray()); @@ -187,7 +171,7 @@ public void testAuthenticateWithToken() throws ExecutionException, InterruptedEx "but received [" + accountId1.asPrincipal() + "]")); // Null for unknown elastic service name - final ServiceAccount.ServiceAccountId accountId2 = new ServiceAccount.ServiceAccountId( + final ServiceAccountId accountId2 = new ServiceAccountId( ElasticServiceAccounts.NAMESPACE, randomValueOtherThan("fleet", () -> randomAlphaOfLengthBetween(3, 8))); final ServiceAccountToken token2 = new ServiceAccountToken(accountId2, randomAlphaOfLengthBetween(3, 8), secret); @@ -199,7 +183,7 @@ public void testAuthenticateWithToken() throws ExecutionException, InterruptedEx "the [" + accountId2.asPrincipal() + "] service account does not exist")); // Success based on credential store - final ServiceAccount.ServiceAccountId accountId3 = new ServiceAccount.ServiceAccountId(ElasticServiceAccounts.NAMESPACE, "fleet"); + final ServiceAccountId accountId3 = new ServiceAccountId(ElasticServiceAccounts.NAMESPACE, "fleet"); final ServiceAccountToken token3 = new ServiceAccountToken(accountId3, randomAlphaOfLengthBetween(3, 8), secret); final ServiceAccountToken token4 = new ServiceAccountToken(accountId3, randomAlphaOfLengthBetween(3, 8), new SecureString(randomAlphaOfLength(20).toCharArray())); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java index 3f14060bc1e63..7981a205f4979 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java @@ -135,7 +135,7 @@ public void setupMocks() throws Exception { final ActionListener listener = (ActionListener) invocationOnMock.getArguments()[2]; listener.onResponse(null); return null; - }).when(serviceAccountService).tryAuthenticateBearerToken(any(), any(), any()); + }).when(serviceAccountService).authenticateToken(any(), any(), any()); authenticationService = new AuthenticationService(settings, realms, auditTrail, failureHandler, threadPool, anonymous, tokenService, apiKeyService, serviceAccountService, OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); authenticator = new SecondaryAuthenticator(securityContext, authenticationService); From c2c25d6805a87295106b02503083fd53829fd379 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Tue, 23 Mar 2021 15:54:54 +1100 Subject: [PATCH 13/19] fix tests --- .../authc/AuthenticationServiceTests.java | 44 +++++++++++-------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java index 366a52238d338..e276ce09c10b3 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java @@ -1911,30 +1911,36 @@ public void testCanAuthenticateServiceAccount() throws ExecutionException, Inter final Authentication authentication = new Authentication( new User("elastic/fleet"), new RealmRef("service_account", "service_account", "foo"), null); - doAnswer(invocationOnMock -> { - @SuppressWarnings("unchecked") - final ActionListener listener = (ActionListener) invocationOnMock.getArguments()[2]; - listener.onResponse(authentication); - return null; - }).when(serviceAccountService).authenticateToken(any(), any(), any()); - final PlainActionFuture future = new PlainActionFuture<>(); - service.authenticate("_action", transportRequest, false, future); - assertThat(future.get(), is(authentication)); + try (ThreadContext.StoredContext ignored = threadContext.newStoredContext(false)) { + threadContext.putHeader("Authorization", "Bearer 46ToAwIHZWxhc3RpYwVmbGVldAZ0b2tlbjEWME1TT0ZobXVRTENIaTNQUGJ4VXQ5ZwAAAAAAAAA"); + doAnswer(invocationOnMock -> { + @SuppressWarnings("unchecked") + final ActionListener listener = (ActionListener) invocationOnMock.getArguments()[2]; + listener.onResponse(authentication); + return null; + }).when(serviceAccountService).authenticateToken(any(), any(), any()); + final PlainActionFuture future = new PlainActionFuture<>(); + service.authenticate("_action", transportRequest, false, future); + assertThat(future.get(), is(authentication)); + } } public void testServiceAccountFailureWillNotFallthrough() { Mockito.reset(serviceAccountService); final RuntimeException bailOut = new RuntimeException("bail out"); - doAnswer(invocationOnMock -> { - @SuppressWarnings("unchecked") - final ActionListener listener = (ActionListener) invocationOnMock.getArguments()[2]; - listener.onFailure(bailOut); - return null; - }).when(serviceAccountService).authenticateToken(any(), any(), any()); - final PlainActionFuture future = new PlainActionFuture<>(); - service.authenticate("_action", transportRequest, false, future); - final ExecutionException e = expectThrows(ExecutionException.class, () -> future.get()); - assertThat(e.getCause().getCause(), is(bailOut)); + try (ThreadContext.StoredContext ignored = threadContext.newStoredContext(false)) { + threadContext.putHeader("Authorization", "Bearer 46ToAwIHZWxhc3RpYwVmbGVldAZ0b2tlbjEWME1TT0ZobXVRTENIaTNQUGJ4VXQ5ZwAAAAAAAAA"); + doAnswer(invocationOnMock -> { + @SuppressWarnings("unchecked") + final ActionListener listener = (ActionListener) invocationOnMock.getArguments()[2]; + listener.onFailure(bailOut); + return null; + }).when(serviceAccountService).authenticateToken(any(), any(), any()); + final PlainActionFuture future = new PlainActionFuture<>(); + service.authenticate("_action", transportRequest, false, future); + final ExecutionException e = expectThrows(ExecutionException.class, () -> future.get()); + assertThat(e.getCause().getCause(), is(bailOut)); + } } private static class InternalRequest extends TransportRequest { From 586a0fc8300badae4e2e14bcac90c6c5b0c4ae76 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Wed, 24 Mar 2021 17:38:16 +1100 Subject: [PATCH 14/19] Update format and serialisation of service account token --- .../authc/support/UsernamePasswordToken.java | 2 +- .../authc/service/ServiceAccountIT.java | 4 +- .../src/javaRestTest/resources/service_tokens | 2 +- .../ServiceAccountSingleNodeTests.java | 4 +- .../xpack/security/authc/TokenService.java | 21 ------ .../authc/service/ServiceAccountService.java | 47 ++---------- .../authc/service/ServiceAccountToken.java | 72 +++++++++++++++---- .../test/token/11_invalidation.yml | 8 +-- 8 files changed, 75 insertions(+), 85 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/UsernamePasswordToken.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/UsernamePasswordToken.java index 56ca0ddfe82f1..5ace37c4c30f9 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/UsernamePasswordToken.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/UsernamePasswordToken.java @@ -126,7 +126,7 @@ public static void putTokenHeader(ThreadContext context, UsernamePasswordToken t /** * Like String.indexOf for for an array of chars */ - private static int indexOfColon(char[] array) { + public static int indexOfColon(char[] array) { for (int i = 0; (i < array.length); i++) { if (array[i] == ':') { return i; diff --git a/x-pack/plugin/security/qa/service-account/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountIT.java b/x-pack/plugin/security/qa/service-account/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountIT.java index f52353dd4076a..767fa5b0a9fb6 100644 --- a/x-pack/plugin/security/qa/service-account/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountIT.java +++ b/x-pack/plugin/security/qa/service-account/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountIT.java @@ -36,8 +36,8 @@ public class ServiceAccountIT extends ESRestTestCase { - private static final String VALID_SERVICE_TOKEN = "46ToAwIHZWxhc3RpYwVmbGVldAZ0b2tlbjEWME1TT0ZobXVRTENIaTNQUGJ4VXQ5ZwAAAAAAAAA"; - private static final String INVALID_SERVICE_TOKEN = "46ToAwIHZWxhc3RpYwVmbGVldAZ0b2tlbjEWQ1MxRXZaQk5SWW1FbndZWlc5T2N3dwAAAAAAAAA"; + private static final String VALID_SERVICE_TOKEN = "AAEAAWVsYXN0aWMvZmxlZXQvdG9rZW4xOnI1d2RiZGJvUVNlOXZHT0t3YUpHQXc"; + private static final String INVALID_SERVICE_TOKEN = "AAEAAWVsYXN0aWMvZmxlZXQvdG9rZW4xOjNhSkNMYVFXUk4yc1hsT2R0eEEwU1E"; private static Path caPath; private static final String AUTHENTICATE_RESPONSE = "" diff --git a/x-pack/plugin/security/qa/service-account/src/javaRestTest/resources/service_tokens b/x-pack/plugin/security/qa/service-account/src/javaRestTest/resources/service_tokens index 72074b39ec97c..e3bc027f4e6d3 100644 --- a/x-pack/plugin/security/qa/service-account/src/javaRestTest/resources/service_tokens +++ b/x-pack/plugin/security/qa/service-account/src/javaRestTest/resources/service_tokens @@ -1 +1 @@ -elastic/fleet/token1:{PBKDF2_STRETCH}10000$XHyHETZWckPiHOuplBOnHeHpB41pTO8XkDC5yTujlcw=$691fFB/AwrSnjRhixFR2y9hOhCd5q6/6pDm29c/tsss= +elastic/fleet/token1:{PBKDF2_STRETCH}10000$8QN+eThJEaCd18sCP0nfzxJq2D9yhmSZgI20TDooYcE=$+0ELfqW4D2+/SlHvm/885dzv67qO2SMJg32Mv/9epXk= diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountSingleNodeTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountSingleNodeTests.java index 173e7af62b7a4..22dd5ce5d5fca 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountSingleNodeTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountSingleNodeTests.java @@ -24,13 +24,13 @@ public class ServiceAccountSingleNodeTests extends SecuritySingleNodeTestCase { - private static final String BEARER_TOKEN = "46ToAwIHZWxhc3RpYwVmbGVldAZ0b2tlbjEWME1TT0ZobXVRTENIaTNQUGJ4VXQ5ZwAAAAAAAAA"; + private static final String BEARER_TOKEN = "AAEAAWVsYXN0aWMvZmxlZXQvdG9rZW4xOnI1d2RiZGJvUVNlOXZHT0t3YUpHQXc"; @Override protected String configServiceTokens() { return super.configServiceTokens() + "elastic/fleet/token1:" - + "{PBKDF2_STRETCH}10000$XHyHETZWckPiHOuplBOnHeHpB41pTO8XkDC5yTujlcw=$691fFB/AwrSnjRhixFR2y9hOhCd5q6/6pDm29c/tsss="; + + "{PBKDF2_STRETCH}10000$8QN+eThJEaCd18sCP0nfzxJq2D9yhmSZgI20TDooYcE=$+0ELfqW4D2+/SlHvm/885dzv67qO2SMJg32Mv/9epXk="; } public void testAuthenticateWithServiceFileToken() { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java index 55679154e2292..197f2382db9ee 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java @@ -202,7 +202,6 @@ public final class TokenService { static final Version VERSION_TOKENS_INDEX_INTRODUCED = Version.V_7_2_0; static final Version VERSION_ACCESS_TOKENS_AS_UUIDS = Version.V_7_2_0; static final Version VERSION_MULTIPLE_CONCURRENT_REFRESHES = Version.V_7_2_0; - static final Version VERSION_TOKEN_TYPE = Version.V_8_0_0; private static final Logger logger = LogManager.getLogger(TokenService.class); @@ -520,14 +519,6 @@ void decodeToken(String token, ActionListener listener) { listener.onResponse(null); return; } - if (version.onOrAfter(VERSION_TOKEN_TYPE)) { - final SecurityTokenType tokenType = SecurityTokenType.read(in); - if (tokenType != SecurityTokenType.ACCESS_TOKEN) { - logger.trace("token is of type {}, but expected {}", tokenType, SecurityTokenType.ACCESS_TOKEN); - listener.onResponse(null); - return; - } - } final String accessToken = in.readString(); // TODO Remove this conditional after backporting to 7.x if (version.onOrAfter(VERSION_HASHED_TOKENS)) { @@ -1722,9 +1713,6 @@ String prependVersionAndEncodeAccessToken(Version version, String accessToken) t try (BytesStreamOutput out = new BytesStreamOutput(MINIMUM_BASE64_BYTES)) { out.setVersion(version); Version.writeVersion(version, out); - if (version.onOrAfter(VERSION_TOKEN_TYPE)) { - SecurityTokenType.ACCESS_TOKEN.write(out); - } out.writeString(accessToken); return Base64.getEncoder().encodeToString(out.bytes().toBytesRef().bytes); } @@ -1757,9 +1745,6 @@ public static String prependVersionAndEncodeRefreshToken(Version version, String try (BytesStreamOutput out = new BytesStreamOutput()) { out.setVersion(version); Version.writeVersion(version, out); - if (version.onOrAfter(VERSION_TOKEN_TYPE)) { - SecurityTokenType.REFRESH_TOKEN.write(out); - } out.writeString(payload); return Base64.getEncoder().encodeToString(out.bytes().toBytesRef().bytes); @@ -1777,12 +1762,6 @@ public static Tuple unpackVersionAndPayload(String encodedPack) try (StreamInput in = new InputStreamStreamInput(Base64.getDecoder().wrap(new ByteArrayInputStream(bytes)), bytes.length)) { final Version version = Version.readVersion(in); in.setVersion(version); - if (version.onOrAfter(VERSION_TOKEN_TYPE)) { - final SecurityTokenType tokenType = SecurityTokenType.read(in); - if (tokenType != SecurityTokenType.REFRESH_TOKEN) { - throw new IllegalArgumentException("expect a token type of [REFRESH_TOKEN], got [" + tokenType.name() + "]"); - } - } final String payload = in.readString(); return new Tuple(version, payload); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java index cc398bfe2fc26..a47d1270317fc 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java @@ -13,21 +13,12 @@ import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; -import org.elasticsearch.common.CharArrays; -import org.elasticsearch.common.hash.MessageDigests; -import org.elasticsearch.common.io.stream.InputStreamStreamInput; -import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.xpack.core.security.authc.Authentication; 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 org.elasticsearch.xpack.security.authc.support.SecurityTokenType; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.util.Base64; import java.util.Collection; import java.util.Map; @@ -69,24 +60,23 @@ public static Collection getServiceAccountPrincipals() { * There is no guarantee that the {@link ServiceAccountToken#getSecret() secret} is valid, * or even that the {@link ServiceAccountToken#getAccountId() account} exists. *

    - * @param token A raw token string (if this is from an HTTP header, then the "Bearer " prefix must be removed before + * @param bearerString A raw token string (if this is from an HTTP header, then the "Bearer " prefix must be removed before * calling this method. * @return An unvalidated token object. */ - public static ServiceAccountToken tryParseToken(SecureString token) { + public static ServiceAccountToken tryParseToken(SecureString bearerString) { try { - if (token == null) { + if (bearerString == null) { return null; } - return doParseToken(token); - } catch (IOException e) { - logger.debug("Cannot parse possible service account token", e); + return ServiceAccountToken.fromBearerString(bearerString); + } catch (Exception e) { + logger.trace("Cannot parse possible service account token", e); return null; } } public void authenticateToken(ServiceAccountToken serviceAccountToken, String nodeName, ActionListener listener) { - if (ElasticServiceAccounts.NAMESPACE.equals(serviceAccountToken.getAccountId().namespace()) == false) { final ParameterizedMessage message = new ParameterizedMessage( "only [{}] service accounts are supported, but received [{}]", @@ -139,29 +129,4 @@ private Authentication createAuthentication(ServiceAccount account, ServiceAccou return new Authentication(user, authenticatedBy, null, Version.CURRENT, Authentication.AuthenticationType.TOKEN, Map.of("_token_name", token.getTokenName())); } - - private static ServiceAccountToken doParseToken(SecureString token) throws IOException { - final byte[] bytes = CharArrays.toUtf8Bytes(token.getChars()); - logger.trace("parsing token bytes {}", MessageDigests.toHexString(bytes)); - try (StreamInput in = new InputStreamStreamInput(Base64.getDecoder().wrap(new ByteArrayInputStream(bytes)), bytes.length)) { - final Version version = Version.readVersion(in); - in.setVersion(version); - if (version.before(VERSION_MINIMUM)) { - logger.trace("token has version {}, but we need at least {}", version, VERSION_MINIMUM); - return null; - } - final SecurityTokenType tokenType = SecurityTokenType.read(in); - if (tokenType != SecurityTokenType.SERVICE_ACCOUNT) { - logger.trace("token is of type {}, but we only handle {}", tokenType, SecurityTokenType.SERVICE_ACCOUNT); - return null; - } - - final ServiceAccountId account = new ServiceAccountId(in); - final String tokenName = in.readString(); - final SecureString secret = in.readSecureString(); - - return new ServiceAccountToken(account, tokenName, secret); - } - } - } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountToken.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountToken.java index a1dd314b119f3..4ede2439fff6c 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountToken.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountToken.java @@ -7,16 +7,25 @@ package org.elasticsearch.xpack.security.authc.service; -import org.elasticsearch.Version; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.common.CharArrays; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.UUIDs; -import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.hash.MessageDigests; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; +import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; -import org.elasticsearch.xpack.security.authc.support.SecurityTokenType; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.Base64; import java.util.Objects; @@ -30,6 +39,14 @@ * */ public class ServiceAccountToken implements AuthenticationToken, Closeable { + public static final byte MAGIC_BYTE = '\0'; + public static final byte TOKEN_TYPE = '\1'; + public static final byte RESERVED_BYTE = '\0'; + public static final byte FORMAT_VERSION = '\1'; + public static final byte[] PREFIX = new byte[] { MAGIC_BYTE, TOKEN_TYPE, RESERVED_BYTE, FORMAT_VERSION }; + + private static final Logger logger = LogManager.getLogger(ServiceAccountToken.class); + private final ServiceAccountId accountId; private final String tokenName; private final SecureString secret; @@ -40,6 +57,18 @@ public ServiceAccountToken(ServiceAccountId accountId, String tokenName, SecureS this.secret = secret; } + public ServiceAccountToken(String qualifiedName, SecureString secret) { + final String[] split = Strings.delimitedListToStringArray(qualifiedName, "/"); + if (split == null || split.length != 3) { + throw new IllegalArgumentException( + "The qualified name of a service token should take format of 'namespace/service_name/token_name'," + + " got [" + qualifiedName + "]"); + } + this.accountId = new ServiceAccountId(split[0], split[1]); + this.tokenName = split[2]; + this.secret = secret; + } + public ServiceAccountId getAccountId() { return accountId; } @@ -57,20 +86,37 @@ public String getQualifiedName() { } public SecureString asBearerString() throws IOException { - try( - BytesStreamOutput out = new BytesStreamOutput()) { - Version.writeVersion(Version.CURRENT, out); - SecurityTokenType.SERVICE_ACCOUNT.write(out); - accountId.write(out); - out.writeString(tokenName); - out.writeSecureString(secret); - out.flush(); - - final String base64 = Base64.getEncoder().withoutPadding().encodeToString(out.bytes().toBytesRef().bytes); + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + out.writeBytes(PREFIX); + out.write(getQualifiedName().getBytes(StandardCharsets.UTF_8)); + out.write(':'); + out.write(secret.toString().getBytes(StandardCharsets.UTF_8)); + final String base64 = Base64.getEncoder().withoutPadding().encodeToString(out.toByteArray()); return new SecureString(base64.toCharArray()); } } + public static ServiceAccountToken fromBearerString(SecureString bearerString) throws IOException { + final byte[] bytes = CharArrays.toUtf8Bytes(bearerString.getChars()); + logger.trace("parsing token bytes {}", MessageDigests.toHexString(bytes)); + try (InputStream in = Base64.getDecoder().wrap(new ByteArrayInputStream(bytes))) { + final byte[] prefixBytes = in.readNBytes(4); + if (prefixBytes.length != 4 || false == Arrays.equals(prefixBytes, PREFIX)) { + logger.trace(() -> new ParameterizedMessage( + "service account token expects the 4 leading bytes of {}, got {}.", + Arrays.toString(PREFIX), Arrays.toString(prefixBytes))); + return null; + } + final char[] content = CharArrays.utf8BytesToChars(in.readAllBytes()); + final int i = UsernamePasswordToken.indexOfColon(content); + if (i < 0) { + throw new IllegalArgumentException("failed to extract qualified service token name and secret, missing ':'"); + } + return new ServiceAccountToken(new String(Arrays.copyOfRange(content, 0, i)), + new SecureString(Arrays.copyOfRange(content, i + 1, content.length))); + } + } + @Override public void close() { secret.close(); diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/token/11_invalidation.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/token/11_invalidation.yml index f724fc4a3e9d3..d1a8490a834de 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/token/11_invalidation.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/token/11_invalidation.yml @@ -45,7 +45,7 @@ teardown: catch: bad_request security.invalidate_token: body: - token: 46ToAwAWMUVZS0lfWnpSZVdhRkhxSmk0WENQUQAAAAAAAAAAAA== + token: 46ToAxYzNUdPZWdYOFRQcWhjeHR3NWpvTmVB - do: security.get_token: @@ -84,7 +84,7 @@ teardown: catch: missing security.invalidate_token: body: - token: 46ToAwAWMUVZS0lfWnpSZVdhRkhxSmk0WENQUQAAAAAAAAAAAA== + token: 46ToAxYzNUdPZWdYOFRQcWhjeHR3NWpvTmVB - match: { invalidated_tokens: 0 } - match: { previously_invalidated_tokens: 0 } @@ -97,7 +97,7 @@ teardown: catch: bad_request security.invalidate_token: body: - refresh_token: 46ToAwEWWmIzWTJFMjFRRDJ0czZtVFRyRzB4dwAAAAA= + refresh_token: 46ToAxYzNUdPZWdYOFRQcWhjeHR3NWpvTmVB - do: security.get_token: @@ -136,7 +136,7 @@ teardown: catch: missing security.invalidate_token: body: - refresh_token: 46ToAwEWWmIzWTJFMjFRRDJ0czZtVFRyRzB4dwAAAAA= + refresh_token: 46ToAxYzNUdPZWdYOFRQcWhjeHR3NWpvTmVB - match: { invalidated_tokens: 0 } - match: { previously_invalidated_tokens: 0 } From bfbb84847253aa554434863bf7e7dd951d1641de Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Wed, 24 Mar 2021 18:10:50 +1100 Subject: [PATCH 15/19] checkstyle --- .../org/elasticsearch/xpack/security/authc/TokenService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java index 197f2382db9ee..0596616d0169e 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java @@ -93,7 +93,6 @@ import org.elasticsearch.xpack.core.security.authc.TokenMetadata; import org.elasticsearch.xpack.core.security.authc.support.Hasher; import org.elasticsearch.xpack.core.security.authc.support.TokensInvalidationResult; -import org.elasticsearch.xpack.security.authc.support.SecurityTokenType; import org.elasticsearch.xpack.security.support.FeatureNotEnabledException; import org.elasticsearch.xpack.security.support.FeatureNotEnabledException.Feature; import org.elasticsearch.xpack.security.support.SecurityIndexManager; From a615dd7ab71357ef15b41455a5bc180098c4e8db Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Wed, 24 Mar 2021 21:32:55 +1100 Subject: [PATCH 16/19] add token name validation --- .../authc/service/ServiceAccount.java | 2 +- .../authc/service/ServiceAccountToken.java | 36 ++++++++++-- .../service/ServiceAccountTokenTests.java | 57 +++++++++++++++++++ .../security/authc/AuthenticationService.java | 2 +- .../CachingServiceAccountsTokenStore.java | 1 + .../authc/service/ElasticServiceAccounts.java | 1 + .../FileServiceAccountsTokenStore.java | 1 + .../authc/service/FileTokensTool.java | 6 +- .../authc/service/ServiceAccountService.java | 2 + .../service/ServiceAccountsTokenStore.java | 1 + .../authc/AuthenticationServiceTests.java | 4 +- ...CachingServiceAccountsTokenStoreTests.java | 3 +- ...mpositeServiceAccountsTokenStoreTests.java | 1 + .../service/ElasticServiceAccountsTests.java | 1 + .../authc/service/ServiceAccountIdTests.java | 1 + .../service/ServiceAccountServiceTests.java | 3 +- .../authc/service/FileTokensToolTests.java | 24 ++++++-- 17 files changed, 129 insertions(+), 17 deletions(-) rename x-pack/plugin/{security/src/main/java/org/elasticsearch/xpack => core/src/main/java/org/elasticsearch/xpack/core}/security/authc/service/ServiceAccount.java (97%) rename x-pack/plugin/{security/src/main/java/org/elasticsearch/xpack => core/src/main/java/org/elasticsearch/xpack/core}/security/authc/service/ServiceAccountToken.java (80%) create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/service/ServiceAccountTokenTests.java diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccount.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/service/ServiceAccount.java similarity index 97% rename from x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccount.java rename to x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/service/ServiceAccount.java index ba97ac2b8196c..b0ae88810faa6 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccount.java +++ b/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; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountToken.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/service/ServiceAccountToken.java similarity index 80% rename from x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountToken.java rename to x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/service/ServiceAccountToken.java index 4ede2439fff6c..977190ec67182 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountToken.java +++ b/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; @@ -17,7 +17,7 @@ import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; -import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; +import org.elasticsearch.xpack.core.security.authc.service.ServiceAccount.ServiceAccountId; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -28,6 +28,7 @@ import java.util.Arrays; import java.util.Base64; import java.util.Objects; +import java.util.Set; /** * A decoded credential that may be used to authenticate a {@link ServiceAccount}. @@ -39,6 +40,20 @@ * */ public class ServiceAccountToken implements AuthenticationToken, Closeable { + + public static Set VALID_TOKEN_NAME_CHARS = Set.of( + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', + 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', + 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '-', '_' + ); + + public static final String INVALID_TOKEN_NAME_MESSAGE = "service account token name must have at least 1 character " + + "and at most 256 characters that are alphanumeric (A-Z, a-z, 0-9) or hyphen (-) or underscore (_). " + + "It must not begin with an underscore (_)."; + public static final byte MAGIC_BYTE = '\0'; public static final byte TOKEN_TYPE = '\1'; public static final byte RESERVED_BYTE = '\0'; @@ -103,14 +118,15 @@ public static ServiceAccountToken fromBearerString(SecureString bearerString) th final byte[] prefixBytes = in.readNBytes(4); if (prefixBytes.length != 4 || false == Arrays.equals(prefixBytes, PREFIX)) { logger.trace(() -> new ParameterizedMessage( - "service account token expects the 4 leading bytes of {}, got {}.", + "service account token expects the 4 leading bytes to be {}, got {}.", Arrays.toString(PREFIX), Arrays.toString(prefixBytes))); return null; } final char[] content = CharArrays.utf8BytesToChars(in.readAllBytes()); final int i = UsernamePasswordToken.indexOfColon(content); if (i < 0) { - throw new IllegalArgumentException("failed to extract qualified service token name and secret, missing ':'"); + logger.trace("failed to extract qualified service token name and secret, missing ':'"); + return null; } return new ServiceAccountToken(new String(Arrays.copyOfRange(content, 0, i)), new SecureString(Arrays.copyOfRange(content, i + 1, content.length))); @@ -155,4 +171,16 @@ public Object credentials() { public void clearCredentials() { close(); } + + public static boolean isValidTokenName(String name) { + if (Strings.isNullOrEmpty(name) || name.length() > 256 || name.startsWith("_")) { + return false; + } + for (char c: name.toCharArray()) { + if (false == VALID_TOKEN_NAME_CHARS.contains(c)) { + return false; + } + } + return true; + } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/service/ServiceAccountTokenTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/service/ServiceAccountTokenTests.java new file mode 100644 index 0000000000000..dcc92774f2a14 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/service/ServiceAccountTokenTests.java @@ -0,0 +1,57 @@ +/* + * 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.test.ESTestCase; + +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.hamcrest.Matchers.is; + +public class ServiceAccountTokenTests extends ESTestCase { + + private static final Set INVALID_TOKEN_NAME_CHARS = Set.of( + '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '.', '/', ':', ';', '<', '=', '>', '?', '@', '[', + '\\', ']', '^', '`', '{', '|', '}', '~', ' ', '\t', '\n', '\r'); + + public void testIsValidTokenName() { + final String tokenName1 = randomTokenName(); + assertThat(ServiceAccountToken.isValidTokenName(tokenName1), is(true)); + + final String tokenName2 = "_" + randomTokenName().substring(1); + assertThat(ServiceAccountToken.isValidTokenName(tokenName2), is(false)); + + final String tokenName3 = randomInvalidTokenName(); + assertThat(ServiceAccountToken.isValidTokenName(tokenName3), is(false)); + } + + public static String randomTokenName() { + final Character[] chars = randomArray( + 1, + 256, + Character[]::new, + () -> randomFrom(ServiceAccountToken.VALID_TOKEN_NAME_CHARS)); + final String name = Arrays.stream(chars).map(String::valueOf).collect(Collectors.joining()); + return name.startsWith("_") ? "-" + name.substring(1) : name; + } + + public static String randomInvalidTokenName() { + if (randomBoolean()) { + final String tokenName = randomTokenName(); + final char[] chars = tokenName.toCharArray(); + IntStream.rangeClosed(1, randomIntBetween(1, chars.length)) + .forEach(i -> chars[randomIntBetween(0, chars.length - 1)] = randomFrom(INVALID_TOKEN_NAME_CHARS)); + return new String(chars); + } else { + return randomFrom("", " ", randomAlphaOfLength(257), null); + } + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java index 0e5d05a298378..73afb289665d3 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java @@ -48,7 +48,7 @@ import org.elasticsearch.xpack.security.audit.AuditTrailService; import org.elasticsearch.xpack.security.audit.AuditUtil; import org.elasticsearch.xpack.security.authc.service.ServiceAccountService; -import org.elasticsearch.xpack.security.authc.service.ServiceAccountToken; +import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountToken; import org.elasticsearch.xpack.security.authc.support.RealmUserLookup; import org.elasticsearch.xpack.security.operator.OperatorPrivileges.OperatorPrivilegesService; import org.elasticsearch.xpack.security.support.SecurityIndexManager; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStore.java index 0d4cc958ec58a..1f2c5fd85b0d4 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStore.java @@ -18,6 +18,7 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.concurrent.ListenableFuture; import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountToken; import org.elasticsearch.xpack.core.security.authc.support.Hasher; import org.elasticsearch.xpack.security.support.CacheInvalidatorRegistry; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccounts.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccounts.java index b96265aac2824..fdb40a1477d41 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccounts.java +++ b/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.user.User; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStore.java index ffa56efb606ff..415d302ad16f8 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStore.java @@ -18,6 +18,7 @@ import org.elasticsearch.watcher.FileWatcher; import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xpack.core.XPackPlugin; +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.support.FileLineParser; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileTokensTool.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileTokensTool.java index 22c165bec89b9..a3576abbcc6de 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileTokensTool.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileTokensTool.java @@ -19,8 +19,9 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; import org.elasticsearch.xpack.core.XPackSettings; +import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountToken; import org.elasticsearch.xpack.core.security.authc.support.Hasher; -import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; +import org.elasticsearch.xpack.core.security.authc.service.ServiceAccount.ServiceAccountId; import org.elasticsearch.xpack.security.support.FileAttributesChecker; import java.nio.file.Path; @@ -71,6 +72,9 @@ protected void execute(Terminal terminal, OptionSet options, Environment env) th throw new UserException(ExitCodes.NO_USER, "Unknown service account principal: [" + principal + "]. Must be one of [" + Strings.collectionToDelimitedString(ServiceAccountService.getServiceAccountPrincipals(), ",") + "]"); } + if (false == ServiceAccountToken.isValidTokenName(tokenName)) { + throw new UserException(ExitCodes.CODE_ERROR, ServiceAccountToken.INVALID_TOKEN_NAME_MESSAGE); + } final Hasher hasher = Hasher.resolve(XPackSettings.SERVICE_TOKEN_HASHING_ALGORITHM.get(env.settings())); final Path serviceTokensFile = FileServiceAccountsTokenStore.resolveFile(env); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java index a47d1270317fc..f88728432d800 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java @@ -16,6 +16,8 @@ import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.rest.RestStatus; 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.ServiceAccountToken; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.user.User; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountsTokenStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountsTokenStore.java index 4252cfed8abec..26cddd311d2fe 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountsTokenStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountsTokenStore.java @@ -13,6 +13,7 @@ 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 java.util.List; import java.util.function.Function; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java index e276ce09c10b3..25e0e9e053735 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java @@ -1912,7 +1912,7 @@ public void testCanAuthenticateServiceAccount() throws ExecutionException, Inter new User("elastic/fleet"), new RealmRef("service_account", "service_account", "foo"), null); try (ThreadContext.StoredContext ignored = threadContext.newStoredContext(false)) { - threadContext.putHeader("Authorization", "Bearer 46ToAwIHZWxhc3RpYwVmbGVldAZ0b2tlbjEWME1TT0ZobXVRTENIaTNQUGJ4VXQ5ZwAAAAAAAAA"); + threadContext.putHeader("Authorization", "Bearer AAEAAWVsYXN0aWMvZmxlZXQvdG9rZW4xOnI1d2RiZGJvUVNlOXZHT0t3YUpHQXc"); doAnswer(invocationOnMock -> { @SuppressWarnings("unchecked") final ActionListener listener = (ActionListener) invocationOnMock.getArguments()[2]; @@ -1929,7 +1929,7 @@ public void testServiceAccountFailureWillNotFallthrough() { Mockito.reset(serviceAccountService); final RuntimeException bailOut = new RuntimeException("bail out"); try (ThreadContext.StoredContext ignored = threadContext.newStoredContext(false)) { - threadContext.putHeader("Authorization", "Bearer 46ToAwIHZWxhc3RpYwVmbGVldAZ0b2tlbjEWME1TT0ZobXVRTENIaTNQUGJ4VXQ5ZwAAAAAAAAA"); + threadContext.putHeader("Authorization", "Bearer AAEAAWVsYXN0aWMvZmxlZXQvdG9rZW4xOnI1d2RiZGJvUVNlOXZHT0t3YUpHQXc"); doAnswer(invocationOnMock -> { @SuppressWarnings("unchecked") final ActionListener listener = (ActionListener) invocationOnMock.getArguments()[2]; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStoreTests.java index ad2adea1cfcf5..225fb678f86d1 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStoreTests.java @@ -16,7 +16,8 @@ import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.threadpool.ThreadPool; -import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; +import org.elasticsearch.xpack.core.security.authc.service.ServiceAccount.ServiceAccountId; +import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountToken; import org.junit.After; import org.junit.Before; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountsTokenStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountsTokenStoreTests.java index 789fe4d04e6e0..1e6658e5e3c21 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountsTokenStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountsTokenStoreTests.java @@ -12,6 +12,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountToken; import org.junit.Before; import java.util.List; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccountsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccountsTests.java index 60ab161643354..d85e56a6fb1c7 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccountsTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccountsTests.java @@ -16,6 +16,7 @@ import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyAction; import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyRequest; import org.elasticsearch.xpack.core.security.authc.Authentication; +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.Role; import org.elasticsearch.xpack.core.security.user.User; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountIdTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountIdTests.java index 2665a0b0d1e7f..850dc687f231d 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountIdTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountIdTests.java @@ -10,6 +10,7 @@ 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; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java index c35b7749a139a..8f26443c790b0 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java @@ -19,9 +19,10 @@ import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.VersionUtils; import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountToken; 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 org.elasticsearch.xpack.core.security.authc.service.ServiceAccount.ServiceAccountId; import org.elasticsearch.xpack.security.authc.support.SecurityTokenType; import org.junit.Before; diff --git a/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/authc/service/FileTokensToolTests.java b/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/authc/service/FileTokensToolTests.java index 4bd670912c3da..ebf700a22f4ae 100644 --- a/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/authc/service/FileTokensToolTests.java +++ b/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/authc/service/FileTokensToolTests.java @@ -19,6 +19,8 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.env.Environment; +import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountToken; +import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountTokenTests; import org.elasticsearch.xpack.core.security.authc.support.Hasher; import org.elasticsearch.xpack.security.authc.service.FileTokensTool.CreateFileTokenCommand; import org.junit.AfterClass; @@ -128,13 +130,23 @@ public void testParsePrincipalAndTokenName() throws UserException { } public void testCreateToken() throws Exception { - execute("create", pathHomeParameter, "elastic/fleet", "server_42"); - assertServiceTokenExists("elastic/fleet/server_42"); - execute("create", pathHomeParameter, "elastic/fleet", "server_43"); - assertServiceTokenExists("elastic/fleet/server_43"); + final String tokenName1 = ServiceAccountTokenTests.randomTokenName(); + execute("create", pathHomeParameter, "elastic/fleet", tokenName1); + assertServiceTokenExists("elastic/fleet/" + tokenName1); + final String tokenName2 = ServiceAccountTokenTests.randomTokenName(); + execute("create", pathHomeParameter, "elastic/fleet", tokenName2); + assertServiceTokenExists("elastic/fleet/" + tokenName2); final String output = terminal.getOutput(); - assertThat(output, containsString("SERVICE_TOKEN elastic/fleet/server_42 = ")); - assertThat(output, containsString("SERVICE_TOKEN elastic/fleet/server_43 = ")); + assertThat(output, containsString("SERVICE_TOKEN elastic/fleet/" + tokenName1 + " = ")); + assertThat(output, containsString("SERVICE_TOKEN elastic/fleet/" + tokenName2 + " = ")); + } + + public void testCreateTokenWithInvalidTokenName() throws Exception { + final String tokenName = ServiceAccountTokenTests.randomInvalidTokenName(); + final UserException e = expectThrows(UserException.class, + () -> execute("create", pathHomeParameter, "elastic/fleet", tokenName)); + assertServiceTokenNotExists("elastic/fleet/" + tokenName); + assertThat(e.getMessage(), containsString(ServiceAccountToken.INVALID_TOKEN_NAME_MESSAGE)); } public void testCreateTokenWithInvalidServiceAccount() throws Exception { From 367b5e4e98a0e13414b3c193d9499a4e5c7df178 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Wed, 24 Mar 2021 22:13:37 +1100 Subject: [PATCH 17/19] better name check --- .../authc/service/ServiceAccountToken.java | 23 ++++--------------- .../service/ServiceAccountTokenTests.java | 13 ++++++++++- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/service/ServiceAccountToken.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/service/ServiceAccountToken.java index 977190ec67182..cc0cd9797c833 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/service/ServiceAccountToken.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/service/ServiceAccountToken.java @@ -28,7 +28,7 @@ import java.util.Arrays; import java.util.Base64; import java.util.Objects; -import java.util.Set; +import java.util.regex.Pattern; /** * A decoded credential that may be used to authenticate a {@link ServiceAccount}. @@ -41,19 +41,12 @@ */ public class ServiceAccountToken implements AuthenticationToken, Closeable { - public static Set VALID_TOKEN_NAME_CHARS = Set.of( - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', - 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', - 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', - 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', - 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', - '-', '_' - ); - public static final String INVALID_TOKEN_NAME_MESSAGE = "service account token name must have at least 1 character " + "and at most 256 characters that are alphanumeric (A-Z, a-z, 0-9) or hyphen (-) or underscore (_). " + "It must not begin with an underscore (_)."; + private static final Pattern VALID_TOKEN_NAME = Pattern.compile("^[a-zA-Z0-9-][a-zA-Z0-9_-]{0,255}$"); + public static final byte MAGIC_BYTE = '\0'; public static final byte TOKEN_TYPE = '\1'; public static final byte RESERVED_BYTE = '\0'; @@ -173,14 +166,6 @@ public void clearCredentials() { } public static boolean isValidTokenName(String name) { - if (Strings.isNullOrEmpty(name) || name.length() > 256 || name.startsWith("_")) { - return false; - } - for (char c: name.toCharArray()) { - if (false == VALID_TOKEN_NAME_CHARS.contains(c)) { - return false; - } - } - return true; + return name != null && VALID_TOKEN_NAME.matcher(name).matches(); } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/service/ServiceAccountTokenTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/service/ServiceAccountTokenTests.java index dcc92774f2a14..fc9164a5aa4ff 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/service/ServiceAccountTokenTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/service/ServiceAccountTokenTests.java @@ -18,6 +18,15 @@ public class ServiceAccountTokenTests extends ESTestCase { + private static final Set VALID_TOKEN_NAME_CHARS = Set.of( + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', + 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', + 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '-', '_' + ); + private static final Set INVALID_TOKEN_NAME_CHARS = Set.of( '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '.', '/', ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '^', '`', '{', '|', '}', '~', ' ', '\t', '\n', '\r'); @@ -29,6 +38,8 @@ public void testIsValidTokenName() { final String tokenName2 = "_" + randomTokenName().substring(1); assertThat(ServiceAccountToken.isValidTokenName(tokenName2), is(false)); + assertThat(ServiceAccountToken.isValidTokenName(null), is(false)); + final String tokenName3 = randomInvalidTokenName(); assertThat(ServiceAccountToken.isValidTokenName(tokenName3), is(false)); } @@ -38,7 +49,7 @@ public static String randomTokenName() { 1, 256, Character[]::new, - () -> randomFrom(ServiceAccountToken.VALID_TOKEN_NAME_CHARS)); + () -> randomFrom(VALID_TOKEN_NAME_CHARS)); final String name = Arrays.stream(chars).map(String::valueOf).collect(Collectors.joining()); return name.startsWith("_") ? "-" + name.substring(1) : name; } From d3791d0d9bd53a10d6dab0e6d34502eaa19fe644 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 25 Mar 2021 23:34:06 +1100 Subject: [PATCH 18/19] address feedback --- .../service/ServiceAccountTokenTests.java | 68 --------- .../security/authc/AuthenticationService.java | 2 +- .../xpack/security/authc/TokenService.java | 3 +- .../CachingServiceAccountsTokenStore.java | 1 - .../authc/service/ElasticServiceAccounts.java | 1 - .../FileServiceAccountsTokenStore.java | 1 - .../authc/service/FileTokensTool.java | 3 +- .../authc/service/ServiceAccount.java | 2 +- .../authc/service/ServiceAccountService.java | 36 +++-- .../authc/service/ServiceAccountToken.java | 35 +++-- .../service/ServiceAccountsTokenStore.java | 1 - .../authc/support/SecurityTokenType.java | 33 ----- .../TransportInvalidateTokenActionTests.java | 2 - ...CachingServiceAccountsTokenStoreTests.java | 3 +- ...mpositeServiceAccountsTokenStoreTests.java | 1 - .../service/ElasticServiceAccountsTests.java | 1 - .../authc/service/ServiceAccountIdTests.java | 1 - .../service/ServiceAccountServiceTests.java | 134 ++++++++++++------ .../service/ServiceAccountTokenTests.java | 118 +++++++++++++++ .../authc/service/FileTokensToolTests.java | 2 - 20 files changed, 248 insertions(+), 200 deletions(-) delete mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/service/ServiceAccountTokenTests.java rename x-pack/plugin/{core/src/main/java/org/elasticsearch/xpack/core => security/src/main/java/org/elasticsearch/xpack}/security/authc/service/ServiceAccount.java (97%) rename x-pack/plugin/{core/src/main/java/org/elasticsearch/xpack/core => security/src/main/java/org/elasticsearch/xpack}/security/authc/service/ServiceAccountToken.java (83%) delete mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/SecurityTokenType.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountTokenTests.java diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/service/ServiceAccountTokenTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/service/ServiceAccountTokenTests.java deleted file mode 100644 index fc9164a5aa4ff..0000000000000 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/service/ServiceAccountTokenTests.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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.test.ESTestCase; - -import java.util.Arrays; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -import static org.hamcrest.Matchers.is; - -public class ServiceAccountTokenTests extends ESTestCase { - - private static final Set VALID_TOKEN_NAME_CHARS = Set.of( - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', - 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', - 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', - 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', - 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', - '-', '_' - ); - - private static final Set INVALID_TOKEN_NAME_CHARS = Set.of( - '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '.', '/', ':', ';', '<', '=', '>', '?', '@', '[', - '\\', ']', '^', '`', '{', '|', '}', '~', ' ', '\t', '\n', '\r'); - - public void testIsValidTokenName() { - final String tokenName1 = randomTokenName(); - assertThat(ServiceAccountToken.isValidTokenName(tokenName1), is(true)); - - final String tokenName2 = "_" + randomTokenName().substring(1); - assertThat(ServiceAccountToken.isValidTokenName(tokenName2), is(false)); - - assertThat(ServiceAccountToken.isValidTokenName(null), is(false)); - - final String tokenName3 = randomInvalidTokenName(); - assertThat(ServiceAccountToken.isValidTokenName(tokenName3), is(false)); - } - - public static String randomTokenName() { - final Character[] chars = randomArray( - 1, - 256, - Character[]::new, - () -> randomFrom(VALID_TOKEN_NAME_CHARS)); - final String name = Arrays.stream(chars).map(String::valueOf).collect(Collectors.joining()); - return name.startsWith("_") ? "-" + name.substring(1) : name; - } - - public static String randomInvalidTokenName() { - if (randomBoolean()) { - final String tokenName = randomTokenName(); - final char[] chars = tokenName.toCharArray(); - IntStream.rangeClosed(1, randomIntBetween(1, chars.length)) - .forEach(i -> chars[randomIntBetween(0, chars.length - 1)] = randomFrom(INVALID_TOKEN_NAME_CHARS)); - return new String(chars); - } else { - return randomFrom("", " ", randomAlphaOfLength(257), null); - } - } -} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java index 73afb289665d3..0e5d05a298378 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java @@ -48,7 +48,7 @@ import org.elasticsearch.xpack.security.audit.AuditTrailService; import org.elasticsearch.xpack.security.audit.AuditUtil; import org.elasticsearch.xpack.security.authc.service.ServiceAccountService; -import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountToken; +import org.elasticsearch.xpack.security.authc.service.ServiceAccountToken; import org.elasticsearch.xpack.security.authc.support.RealmUserLookup; import org.elasticsearch.xpack.security.operator.OperatorPrivileges.OperatorPrivilegesService; import org.elasticsearch.xpack.security.support.SecurityIndexManager; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java index 0596616d0169e..13f403fc5113a 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java @@ -191,10 +191,9 @@ public final class TokenService { private static final int HASHED_TOKEN_LENGTH = 43; // UUIDs are 16 bytes encoded base64 without padding, therefore the length is (16 / 3) * 4 + ((16 % 3) * 8 + 5) / 6 chars private static final int TOKEN_LENGTH = 22; - private static final int TOKEN_TYPE_LENGTH = 1; private static final String TOKEN_DOC_ID_PREFIX = TOKEN_DOC_TYPE + "_"; static final int LEGACY_MINIMUM_BYTES = VERSION_BYTES + SALT_BYTES + IV_BYTES + 1; - static final int MINIMUM_BYTES = VERSION_BYTES + TOKEN_TYPE_LENGTH + TOKEN_LENGTH + 1; + static final int MINIMUM_BYTES = VERSION_BYTES + TOKEN_LENGTH + 1; static final int LEGACY_MINIMUM_BASE64_BYTES = Double.valueOf(Math.ceil((4 * LEGACY_MINIMUM_BYTES) / 3)).intValue(); public static final int MINIMUM_BASE64_BYTES = Double.valueOf(Math.ceil((4 * MINIMUM_BYTES) / 3)).intValue(); static final Version VERSION_HASHED_TOKENS = Version.V_7_2_0; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStore.java index 1f2c5fd85b0d4..0d4cc958ec58a 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStore.java @@ -18,7 +18,6 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.concurrent.ListenableFuture; import org.elasticsearch.threadpool.ThreadPool; -import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountToken; import org.elasticsearch.xpack.core.security.authc.support.Hasher; import org.elasticsearch.xpack.security.support.CacheInvalidatorRegistry; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccounts.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccounts.java index fdb40a1477d41..b96265aac2824 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccounts.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccounts.java @@ -8,7 +8,6 @@ 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.user.User; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStore.java index 415d302ad16f8..ffa56efb606ff 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStore.java @@ -18,7 +18,6 @@ import org.elasticsearch.watcher.FileWatcher; import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xpack.core.XPackPlugin; -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.support.FileLineParser; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileTokensTool.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileTokensTool.java index a3576abbcc6de..07abb3d3f22a2 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileTokensTool.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileTokensTool.java @@ -19,9 +19,8 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; import org.elasticsearch.xpack.core.XPackSettings; -import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountToken; import org.elasticsearch.xpack.core.security.authc.support.Hasher; -import org.elasticsearch.xpack.core.security.authc.service.ServiceAccount.ServiceAccountId; +import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; import org.elasticsearch.xpack.security.support.FileAttributesChecker; import java.nio.file.Path; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/service/ServiceAccount.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccount.java similarity index 97% rename from x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/service/ServiceAccount.java rename to x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccount.java index b0ae88810faa6..ba97ac2b8196c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/service/ServiceAccount.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccount.java @@ -5,7 +5,7 @@ * 2.0. */ -package org.elasticsearch.xpack.core.security.authc.service; +package org.elasticsearch.xpack.security.authc.service; import org.apache.logging.log4j.util.Strings; import org.elasticsearch.common.io.stream.StreamInput; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java index f88728432d800..c5373fde6626a 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java @@ -9,15 +9,12 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.message.ParameterizedMessage; import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.rest.RestStatus; 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.ServiceAccountToken; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.user.User; @@ -30,7 +27,6 @@ public class ServiceAccountService { public static final String REALM_TYPE = "service_account"; public static final String REALM_NAME = "service_account"; - public static final Version VERSION_MINIMUM = Version.V_8_0_0; private static final Logger logger = LogManager.getLogger(ServiceAccountService.class); @@ -79,21 +75,18 @@ public static ServiceAccountToken tryParseToken(SecureString bearerString) { } public void authenticateToken(ServiceAccountToken serviceAccountToken, String nodeName, ActionListener listener) { + logger.trace("attempt to authenticate service account token [{}]", serviceAccountToken.getQualifiedName()); if (ElasticServiceAccounts.NAMESPACE.equals(serviceAccountToken.getAccountId().namespace()) == false) { - final ParameterizedMessage message = new ParameterizedMessage( - "only [{}] service accounts are supported, but received [{}]", + logger.debug("only [{}] service accounts are supported, but received [{}]", ElasticServiceAccounts.NAMESPACE, serviceAccountToken.getAccountId().asPrincipal()); - logger.debug(message); - listener.onFailure(new ElasticsearchSecurityException(message.getFormattedMessage(), RestStatus.UNAUTHORIZED)); + listener.onFailure(createAuthenticationException(serviceAccountToken)); return; } final ServiceAccount account = ACCOUNTS.get(serviceAccountToken.getAccountId().asPrincipal()); if (account == null) { - final ParameterizedMessage message = new ParameterizedMessage( - "the [{}] service account does not exist", serviceAccountToken.getAccountId().asPrincipal()); - logger.debug(message); - listener.onFailure(new ElasticsearchSecurityException(message.getFormattedMessage(), RestStatus.UNAUTHORIZED)); + logger.debug("the [{}] service account does not exist", serviceAccountToken.getAccountId().asPrincipal()); + listener.onFailure(createAuthenticationException(serviceAccountToken)); return; } @@ -101,12 +94,9 @@ public void authenticateToken(ServiceAccountToken serviceAccountToken, String no if (success) { listener.onResponse(createAuthentication(account, serviceAccountToken, nodeName)); } else { - final ParameterizedMessage message = new ParameterizedMessage( - "failed to authenticate service account [{}] with token name [{}]", - serviceAccountToken.getAccountId().asPrincipal(), - serviceAccountToken.getTokenName()); - logger.debug(message); - listener.onFailure(new ElasticsearchSecurityException(message.getFormattedMessage(), RestStatus.UNAUTHORIZED)); + final ElasticsearchSecurityException e = createAuthenticationException(serviceAccountToken); + logger.debug(e.getMessage()); + listener.onFailure(e); } }, listener::onFailure)); } @@ -118,8 +108,7 @@ public void getRoleDescriptor(Authentication authentication, ActionListenerAuthorization: Bearer {value} header. - */ -public enum SecurityTokenType { - - // There enum values are written to streams. They cannot be reordered - ACCESS_TOKEN, - REFRESH_TOKEN, - SERVICE_ACCOUNT; - - public void write(StreamOutput out) throws IOException { - out.writeEnum(this); - } - - public static SecurityTokenType read(StreamInput in) throws IOException { - return in.readEnum(SecurityTokenType.class); - } -} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/token/TransportInvalidateTokenActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/token/TransportInvalidateTokenActionTests.java index 565e507443c3a..d7d062744bc3c 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/token/TransportInvalidateTokenActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/token/TransportInvalidateTokenActionTests.java @@ -33,7 +33,6 @@ import org.elasticsearch.xpack.core.security.action.token.InvalidateTokenRequest; import org.elasticsearch.xpack.core.security.action.token.InvalidateTokenResponse; import org.elasticsearch.xpack.security.authc.TokenService; -import org.elasticsearch.xpack.security.authc.support.SecurityTokenType; import org.elasticsearch.xpack.security.support.SecurityIndexManager; import org.junit.After; import org.junit.Before; @@ -132,7 +131,6 @@ private String generateAccessTokenString() throws Exception { try (BytesStreamOutput out = new BytesStreamOutput(TokenService.MINIMUM_BASE64_BYTES)) { out.setVersion(Version.CURRENT); Version.writeVersion(Version.CURRENT, out); - SecurityTokenType.ACCESS_TOKEN.write(out); out.writeString(UUIDs.randomBase64UUID()); return Base64.getEncoder().encodeToString(out.bytes().toBytesRef().bytes); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStoreTests.java index 225fb678f86d1..ad2adea1cfcf5 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStoreTests.java @@ -16,8 +16,7 @@ import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.threadpool.ThreadPool; -import org.elasticsearch.xpack.core.security.authc.service.ServiceAccount.ServiceAccountId; -import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountToken; +import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; import org.junit.After; import org.junit.Before; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountsTokenStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountsTokenStoreTests.java index 1e6658e5e3c21..789fe4d04e6e0 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountsTokenStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountsTokenStoreTests.java @@ -12,7 +12,6 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountToken; import org.junit.Before; import java.util.List; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccountsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccountsTests.java index d85e56a6fb1c7..60ab161643354 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccountsTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccountsTests.java @@ -16,7 +16,6 @@ import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyAction; import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyRequest; import org.elasticsearch.xpack.core.security.authc.Authentication; -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.Role; import org.elasticsearch.xpack.core.security.user.User; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountIdTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountIdTests.java index 850dc687f231d..2665a0b0d1e7f 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountIdTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountIdTests.java @@ -10,7 +10,6 @@ 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; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java index 8f26443c790b0..e4f91b81021cc 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java @@ -12,22 +12,22 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.common.Strings; -import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.test.VersionUtils; import org.elasticsearch.xpack.core.security.authc.Authentication; -import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountToken; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.user.User; -import org.elasticsearch.xpack.core.security.authc.service.ServiceAccount.ServiceAccountId; -import org.elasticsearch.xpack.security.authc.support.SecurityTokenType; +import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; import org.junit.Before; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.Base64; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutionException; @@ -83,46 +83,88 @@ public void testTryParseToken() throws IOException { // Null for null assertNull(ServiceAccountService.tryParseToken(null)); - final ServiceAccountId accountId = - new ServiceAccountId(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8)); - final String tokenName = randomAlphaOfLengthBetween(3, 8); - final SecureString secret = new SecureString(randomAlphaOfLength(20).toCharArray()); + final byte[] magicBytes = { 0, 1, 0, 1 }; + // Less than 4 bytes + final SecureString bearerString0 = createBearerString(List.of(Arrays.copyOfRange(magicBytes, 0, randomIntBetween(0, 3)))); + assertNull(ServiceAccountService.tryParseToken(bearerString0)); - // Invalid version or token type - try (BytesStreamOutput out = new BytesStreamOutput()) { - if (randomBoolean()) { - final Version invalidVersion = VersionUtils.randomVersionBetween(random(), - Version.V_7_0_0, - VersionUtils.getPreviousVersion(ServiceAccountService.VERSION_MINIMUM)); - Version.writeVersion(invalidVersion, out); - out.writeEnum(SecurityTokenType.SERVICE_ACCOUNT); - } else { - Version.writeVersion( - VersionUtils.randomVersionBetween(random(), ServiceAccountService.VERSION_MINIMUM, Version.CURRENT), - out); - out.writeEnum(randomFrom(SecurityTokenType.ACCESS_TOKEN, SecurityTokenType.REFRESH_TOKEN)); - } - accountId.write(out); - out.writeString(tokenName); - out.writeSecureString(secret); - out.flush(); + // Prefix mismatch + final SecureString bearerString1 = createBearerString(List.of( + new byte[] { randomValueOtherThan((byte) 0, ESTestCase::randomByte) }, + randomByteArrayOfLength(randomIntBetween(30, 50)))); + assertNull(ServiceAccountService.tryParseToken(bearerString1)); + + // No colon + final SecureString bearerString2 = createBearerString(List.of( + magicBytes, + randomAlphaOfLengthBetween(30, 50).getBytes(StandardCharsets.UTF_8))); + assertNull(ServiceAccountService.tryParseToken(bearerString2)); - final String base64 = Base64.getEncoder().withoutPadding().encodeToString(out.bytes().toBytesRef().bytes); - assertNull(ServiceAccountService.tryParseToken(new SecureString(base64.toCharArray()))); + // Invalid delimiter for qualified name + if (randomBoolean()) { + final SecureString bearerString3 = createBearerString(List.of( + magicBytes, + (randomAlphaOfLengthBetween(10, 20) + ":" + randomAlphaOfLengthBetween(10, 20)).getBytes(StandardCharsets.UTF_8) + )); + assertNull(ServiceAccountService.tryParseToken(bearerString3)); + } else { + final SecureString bearerString3 = createBearerString(List.of( + magicBytes, + (randomAlphaOfLengthBetween(3, 8) + "/" + randomAlphaOfLengthBetween(3, 8) + + ":" + randomAlphaOfLengthBetween(10, 20)).getBytes(StandardCharsets.UTF_8) + )); + assertNull(ServiceAccountService.tryParseToken(bearerString3)); } - // Invalid version again + // Invalid token name + final SecureString bearerString4 = createBearerString(List.of( + magicBytes, + (randomAlphaOfLengthBetween(3, 8) + "/" + randomAlphaOfLengthBetween(3, 8) + + "/" + ServiceAccountTokenTests.randomInvalidTokenName() + + ":" + randomAlphaOfLengthBetween(10, 20)).getBytes(StandardCharsets.UTF_8) + )); + assertNull(ServiceAccountService.tryParseToken(bearerString4)); + + // Everything is good + final String namespace = randomAlphaOfLengthBetween(3, 8); + final String serviceName = randomAlphaOfLengthBetween(3, 8); + final String tokenName = ServiceAccountTokenTests.randomTokenName(); + final ServiceAccountId accountId = new ServiceAccountId(namespace, serviceName); + final String secret = randomAlphaOfLengthBetween(10, 20); + final SecureString bearerString5 = createBearerString(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)); + + // Serialise and de-serialise service account token + final ServiceAccountToken parsedToken = ServiceAccountService.tryParseToken(serviceAccountToken2.asBearerString()); + assertThat(parsedToken, equalTo(serviceAccountToken2)); + + // Invalid magic byte assertNull(ServiceAccountService.tryParseToken( - new SecureString("0/uxAwIHZWxhc3RpYwVmbGVldAZ0b2tlbjEWS2xScU9xdDdUSktTNWt3X1hKV0k0QQAAAAAAAAA".toCharArray()))); + new SecureString("AQEAAWVsYXN0aWMvZmxlZXQvdG9rZW4xOnN1cGVyc2VjcmV0".toCharArray()))); - // Wrong token type again + // No colon assertNull(ServiceAccountService.tryParseToken( - new SecureString("46ToAwAHZWxhc3RpYwVmbGVldAZ0b2tlbjEWUmpXRVp1Q3hTVS1lZTJoMFJoYVFqUQAAAAAAAAA".toCharArray()))); + new SecureString("AQEAAWVsYXN0aWMvZmxlZXQvdG9rZW4xX3N1cGVyc2VjcmV0".toCharArray()))); - // Serialise and de-serialise service account token - final ServiceAccountToken serviceAccountToken = new ServiceAccountToken(accountId, tokenName, secret); - final ServiceAccountToken parsedToken = ServiceAccountService.tryParseToken(serviceAccountToken.asBearerString()); - assertThat(parsedToken, equalTo(serviceAccountToken)); + // Invalid qualified name + assertNull(ServiceAccountService.tryParseToken( + new SecureString("AQEAAWVsYXN0aWMvZmxlZXRfdG9rZW4xOnN1cGVyc2VjcmV0".toCharArray()))); + + // Invalid token name + assertNull(ServiceAccountService.tryParseToken( + new SecureString("AAEAAWVsYXN0aWMvZmxlZXQvdG9rZW4hOnN1cGVyc2VjcmV0".toCharArray()))); + + // everything is fine + assertThat(ServiceAccountService.tryParseToken( + new SecureString("AAEAAWVsYXN0aWMvZmxlZXQvdG9rZW4xOnN1cGVyc2VjcmV0".toCharArray())), + equalTo(new ServiceAccountToken(new ServiceAccountId("elastic", "fleet"), "token1", + new SecureString("supersecret".toCharArray())))); } private Authentication.RealmRef randomRealmRef() { @@ -167,9 +209,8 @@ public void testAuthenticateWithToken() throws ExecutionException, InterruptedEx serviceAccountService.authenticateToken(token1, randomAlphaOfLengthBetween(3, 8), future1); final ExecutionException e1 = expectThrows(ExecutionException.class, future1::get); assertThat(e1.getCause().getClass(), is(ElasticsearchSecurityException.class)); - assertThat(e1.getMessage(), containsString( - "only [" + ElasticServiceAccounts.NAMESPACE + "] service accounts are supported, " + - "but received [" + accountId1.asPrincipal() + "]")); + assertThat(e1.getMessage(), containsString("failed to authenticate service account [" + + token1.getAccountId().asPrincipal() + "] with token name [" + token1.getTokenName() + "]")); // Null for unknown elastic service name final ServiceAccountId accountId2 = new ServiceAccountId( @@ -180,8 +221,8 @@ public void testAuthenticateWithToken() throws ExecutionException, InterruptedEx serviceAccountService.authenticateToken(token2, randomAlphaOfLengthBetween(3, 8), future2); final ExecutionException e2 = expectThrows(ExecutionException.class, future2::get); assertThat(e2.getCause().getClass(), is(ElasticsearchSecurityException.class)); - assertThat(e2.getMessage(), containsString( - "the [" + accountId2.asPrincipal() + "] service account does not exist")); + assertThat(e2.getMessage(), containsString("failed to authenticate service account [" + + token2.getAccountId().asPrincipal() + "] with token name [" + token2.getTokenName() + "]")); // Success based on credential store final ServiceAccountId accountId3 = new ServiceAccountId(ElasticServiceAccounts.NAMESPACE, "fleet"); @@ -262,4 +303,13 @@ ServiceAccountService.REALM_NAME, ServiceAccountService.REALM_TYPE, randomAlphaO assertThat(e.getMessage(), containsString( "cannot load role for service account [" + username + "] - no such service account")); } + + private SecureString createBearerString(List bytesList) throws IOException { + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + for (byte[] bytes : bytesList) { + out.write(bytes); + } + return new SecureString(Base64.getEncoder().withoutPadding().encodeToString(out.toByteArray()).toCharArray()); + } + } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountTokenTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountTokenTests.java new file mode 100644 index 0000000000000..ded83e0dea0a4 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountTokenTests.java @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.authc.service; + +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +public class ServiceAccountTokenTests extends ESTestCase { + + private static final Set VALID_TOKEN_NAME_CHARS = Set.of( + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', + 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', + 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '-', '_' + ); + + private static final Set INVALID_TOKEN_NAME_CHARS = Set.of( + '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '.', '/', ':', ';', '<', '=', '>', '?', '@', '[', + '\\', ']', '^', '`', '{', '|', '}', '~', ' ', '\t', '\n', '\r'); + + public void testIsValidTokenName() { + final String tokenName1 = randomTokenName(); + assertThat(ServiceAccountToken.isValidTokenName(tokenName1), is(true)); + + final String tokenName2 = "_" + randomTokenName().substring(1); + assertThat(ServiceAccountToken.isValidTokenName(tokenName2), is(false)); + + assertThat(ServiceAccountToken.isValidTokenName(null), is(false)); + + final String tokenName3 = randomInvalidTokenName(); + assertThat(ServiceAccountToken.isValidTokenName(tokenName3), is(false)); + } + + public void testNewToken() { + final ServiceAccountId accountId = new ServiceAccountId(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8)); + ServiceAccountToken.newToken(accountId, randomTokenName()); + + final IllegalArgumentException e1 = + expectThrows(IllegalArgumentException.class, () -> ServiceAccountToken.newToken(accountId, randomInvalidTokenName())); + assertThat(e1.getMessage(), containsString(ServiceAccountToken.INVALID_TOKEN_NAME_MESSAGE)); + + final NullPointerException e2 = + expectThrows(NullPointerException.class, () -> ServiceAccountToken.newToken(null, randomTokenName())); + assertThat(e2.getMessage(), containsString("service account ID cannot be null")); + } + + public void testServiceAccountTokenNew() { + final ServiceAccountId accountId = new ServiceAccountId(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8)); + final SecureString secret = new SecureString(randomAlphaOfLength(20).toCharArray()); + new ServiceAccountToken(accountId, randomTokenName(), secret); + + final NullPointerException e1 = + expectThrows(NullPointerException.class, () -> new ServiceAccountToken(null, randomTokenName(), secret)); + assertThat(e1.getMessage(), containsString("service account ID cannot be null")); + + final IllegalArgumentException e2 = + expectThrows(IllegalArgumentException.class, () -> new ServiceAccountToken(accountId, randomInvalidTokenName(), secret)); + assertThat(e2.getMessage(), containsString(ServiceAccountToken.INVALID_TOKEN_NAME_MESSAGE)); + + final NullPointerException e3 = + expectThrows(NullPointerException.class, () -> new ServiceAccountToken(accountId, randomTokenName(), null)); + assertThat(e3.getMessage(), containsString("service account token secret cannot be null")); + } + + public void testBearerString() throws IOException { + final ServiceAccountToken serviceAccountToken = + new ServiceAccountToken(new ServiceAccountId("elastic", "fleet"), + "token1", new SecureString("supersecret".toCharArray())); + assertThat(serviceAccountToken.asBearerString(), equalTo("AAEAAWVsYXN0aWMvZmxlZXQvdG9rZW4xOnN1cGVyc2VjcmV0")); + + assertThat(ServiceAccountToken.fromBearerString(new SecureString("AAEAAWVsYXN0aWMvZmxlZXQvdG9rZW4xOnN1cGVyc2VjcmV0".toCharArray())), + equalTo(serviceAccountToken)); + + final ServiceAccountId accountId = new ServiceAccountId(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8)); + final ServiceAccountToken serviceAccountToken1 = ServiceAccountToken.newToken(accountId, randomTokenName()); + assertThat(ServiceAccountToken.fromBearerString(serviceAccountToken1.asBearerString()), equalTo(serviceAccountToken1)); + } + + public static String randomTokenName() { + final Character[] chars = randomArray( + 1, + 256, + Character[]::new, + () -> randomFrom(VALID_TOKEN_NAME_CHARS)); + final String name = Arrays.stream(chars).map(String::valueOf).collect(Collectors.joining()); + return name.startsWith("_") ? "-" + name.substring(1) : name; + } + + public static String randomInvalidTokenName() { + if (randomBoolean()) { + final String tokenName = randomTokenName(); + final char[] chars = tokenName.toCharArray(); + IntStream.rangeClosed(1, randomIntBetween(1, chars.length)) + .forEach(i -> chars[randomIntBetween(0, chars.length - 1)] = randomFrom(INVALID_TOKEN_NAME_CHARS)); + return new String(chars); + } else { + return randomFrom("", " ", randomAlphaOfLength(257), null); + } + } +} diff --git a/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/authc/service/FileTokensToolTests.java b/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/authc/service/FileTokensToolTests.java index ebf700a22f4ae..ae4ad44cf0784 100644 --- a/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/authc/service/FileTokensToolTests.java +++ b/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/authc/service/FileTokensToolTests.java @@ -19,8 +19,6 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.env.Environment; -import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountToken; -import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountTokenTests; import org.elasticsearch.xpack.core.security.authc.support.Hasher; import org.elasticsearch.xpack.security.authc.service.FileTokensTool.CreateFileTokenCommand; import org.junit.AfterClass; From 737caba2af3541a2946dd84fe2b1e0853f381216 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Fri, 26 Mar 2021 09:28:27 +1100 Subject: [PATCH 19/19] fix tests and add logger appender tests --- .../service/ServiceAccountServiceTests.java | 374 +++++++++++------- .../service/ServiceAccountTokenTests.java | 2 +- 2 files changed, 237 insertions(+), 139 deletions(-) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java index e4f91b81021cc..e0c5a32e7e264 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java @@ -7,15 +7,20 @@ package org.elasticsearch.xpack.security.authc.service; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.MockLogAppender; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.user.User; @@ -79,92 +84,156 @@ public void testGetServiceAccountPrincipals() { assertThat(ServiceAccountService.getServiceAccountPrincipals(), equalTo(Set.of("elastic/fleet"))); } - public void testTryParseToken() throws IOException { + public void testTryParseToken() throws IOException, IllegalAccessException { // Null for null assertNull(ServiceAccountService.tryParseToken(null)); final byte[] magicBytes = { 0, 1, 0, 1 }; - // Less than 4 bytes - final SecureString bearerString0 = createBearerString(List.of(Arrays.copyOfRange(magicBytes, 0, randomIntBetween(0, 3)))); - assertNull(ServiceAccountService.tryParseToken(bearerString0)); - - // Prefix mismatch - final SecureString bearerString1 = createBearerString(List.of( - new byte[] { randomValueOtherThan((byte) 0, ESTestCase::randomByte) }, - randomByteArrayOfLength(randomIntBetween(30, 50)))); - assertNull(ServiceAccountService.tryParseToken(bearerString1)); - - // No colon - final SecureString bearerString2 = createBearerString(List.of( - magicBytes, - randomAlphaOfLengthBetween(30, 50).getBytes(StandardCharsets.UTF_8))); - assertNull(ServiceAccountService.tryParseToken(bearerString2)); - - // Invalid delimiter for qualified name - if (randomBoolean()) { - final SecureString bearerString3 = createBearerString(List.of( + + final Logger satLogger = LogManager.getLogger(ServiceAccountToken.class); + Loggers.setLevel(satLogger, Level.TRACE); + final Logger sasLogger = LogManager.getLogger(ServiceAccountService.class); + Loggers.setLevel(sasLogger, Level.TRACE); + + final MockLogAppender appender = new MockLogAppender(); + Loggers.addAppender(satLogger, appender); + Loggers.addAppender(sasLogger, appender); + appender.start(); + + try { + // Less than 4 bytes + appender.addExpectation(new MockLogAppender.SeenEventExpectation( + "less than 4 bytes", ServiceAccountToken.class.getName(), Level.TRACE, + "service account token expects the 4 leading bytes") + ); + final SecureString bearerString0 = createBearerString(List.of(Arrays.copyOfRange(magicBytes, 0, randomIntBetween(0, 3)))); + assertNull(ServiceAccountService.tryParseToken(bearerString0)); + appender.assertAllExpectationsMatched(); + + // Prefix mismatch + appender.addExpectation(new MockLogAppender.SeenEventExpectation( + "prefix mismatch", ServiceAccountToken.class.getName(), Level.TRACE, + "service account token expects the 4 leading bytes" + )); + final SecureString bearerString1 = createBearerString(List.of( + new byte[] { randomValueOtherThan((byte) 0, ESTestCase::randomByte) }, + randomByteArrayOfLength(randomIntBetween(30, 50)))); + assertNull(ServiceAccountService.tryParseToken(bearerString1)); + appender.assertAllExpectationsMatched(); + + // No colon + appender.addExpectation(new MockLogAppender.SeenEventExpectation( + "no colon", ServiceAccountToken.class.getName(), Level.TRACE, + "failed to extract qualified service token name and secret, missing ':'" + )); + final SecureString bearerString2 = createBearerString(List.of( magicBytes, - (randomAlphaOfLengthBetween(10, 20) + ":" + randomAlphaOfLengthBetween(10, 20)).getBytes(StandardCharsets.UTF_8) + randomAlphaOfLengthBetween(30, 50).getBytes(StandardCharsets.UTF_8))); + assertNull(ServiceAccountService.tryParseToken(bearerString2)); + appender.assertAllExpectationsMatched(); + + // Invalid delimiter for qualified name + appender.addExpectation(new MockLogAppender.SeenEventExpectation( + "invalid delimiter for qualified name", ServiceAccountToken.class.getName(), Level.TRACE, + "The qualified name of a service token should take format of 'namespace/service_name/token_name'" )); - assertNull(ServiceAccountService.tryParseToken(bearerString3)); - } else { - final SecureString bearerString3 = createBearerString(List.of( + if (randomBoolean()) { + final SecureString bearerString3 = createBearerString(List.of( + magicBytes, + (randomAlphaOfLengthBetween(10, 20) + ":" + randomAlphaOfLengthBetween(10, 20)).getBytes(StandardCharsets.UTF_8) + )); + assertNull(ServiceAccountService.tryParseToken(bearerString3)); + } else { + final SecureString bearerString3 = createBearerString(List.of( + magicBytes, + (randomAlphaOfLengthBetween(3, 8) + "/" + randomAlphaOfLengthBetween(3, 8) + + ":" + randomAlphaOfLengthBetween(10, 20)).getBytes(StandardCharsets.UTF_8) + )); + assertNull(ServiceAccountService.tryParseToken(bearerString3)); + } + appender.assertAllExpectationsMatched(); + + // Invalid token name + appender.addExpectation(new MockLogAppender.SeenEventExpectation( + "invalid token name", ServiceAccountService.class.getName(), Level.TRACE, + "Cannot parse possible service account token" + )); + final SecureString bearerString4 = createBearerString(List.of( magicBytes, (randomAlphaOfLengthBetween(3, 8) + "/" + randomAlphaOfLengthBetween(3, 8) + + "/" + ServiceAccountTokenTests.randomInvalidTokenName() + ":" + randomAlphaOfLengthBetween(10, 20)).getBytes(StandardCharsets.UTF_8) )); - assertNull(ServiceAccountService.tryParseToken(bearerString3)); + assertNull(ServiceAccountService.tryParseToken(bearerString4)); + appender.assertAllExpectationsMatched(); + + // Everything is good + final String namespace = randomAlphaOfLengthBetween(3, 8); + final String serviceName = randomAlphaOfLengthBetween(3, 8); + final String tokenName = ServiceAccountTokenTests.randomTokenName(); + final ServiceAccountId accountId = new ServiceAccountId(namespace, serviceName); + final String secret = randomAlphaOfLengthBetween(10, 20); + final SecureString bearerString5 = createBearerString(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)); + + // Serialise and de-serialise service account token + final ServiceAccountToken parsedToken = ServiceAccountService.tryParseToken(serviceAccountToken2.asBearerString()); + assertThat(parsedToken, equalTo(serviceAccountToken2)); + + // Invalid magic byte + appender.addExpectation(new MockLogAppender.SeenEventExpectation( + "invalid magic byte again", ServiceAccountToken.class.getName(), Level.TRACE, + "service account token expects the 4 leading bytes" + )); + assertNull(ServiceAccountService.tryParseToken( + new SecureString("AQEAAWVsYXN0aWMvZmxlZXQvdG9rZW4xOnN1cGVyc2VjcmV0".toCharArray()))); + appender.assertAllExpectationsMatched(); + + // No colon + appender.addExpectation(new MockLogAppender.SeenEventExpectation( + "no colon again", ServiceAccountToken.class.getName(), Level.TRACE, + "failed to extract qualified service token name and secret, missing ':'" + )); + assertNull(ServiceAccountService.tryParseToken( + new SecureString("AAEAAWVsYXN0aWMvZmxlZXQvdG9rZW4xX3N1cGVyc2VjcmV0".toCharArray()))); + appender.assertAllExpectationsMatched(); + + // Invalid qualified name + appender.addExpectation(new MockLogAppender.SeenEventExpectation( + "invalid delimiter for qualified name again", ServiceAccountToken.class.getName(), Level.TRACE, + "The qualified name of a service token should take format of 'namespace/service_name/token_name'" + )); + assertNull(ServiceAccountService.tryParseToken( + new SecureString("AAEAAWVsYXN0aWMvZmxlZXRfdG9rZW4xOnN1cGVyc2VjcmV0".toCharArray()))); + appender.assertAllExpectationsMatched(); + + // Invalid token name + appender.addExpectation(new MockLogAppender.SeenEventExpectation( + "invalid token name again", ServiceAccountService.class.getName(), Level.TRACE, + "Cannot parse possible service account token" + )); + assertNull(ServiceAccountService.tryParseToken( + new SecureString("AAEAAWVsYXN0aWMvZmxlZXQvdG9rZW4hOnN1cGVyc2VjcmV0".toCharArray()))); + appender.assertAllExpectationsMatched(); + + // everything is fine + assertThat(ServiceAccountService.tryParseToken( + new SecureString("AAEAAWVsYXN0aWMvZmxlZXQvdG9rZW4xOnN1cGVyc2VjcmV0".toCharArray())), + equalTo(new ServiceAccountToken(new ServiceAccountId("elastic", "fleet"), "token1", + new SecureString("supersecret".toCharArray())))); + } finally { + appender.stop(); + Loggers.setLevel(satLogger, Level.INFO); + Loggers.setLevel(sasLogger, Level.INFO); + Loggers.removeAppender(satLogger, appender); + Loggers.removeAppender(sasLogger, appender); } - - // Invalid token name - final SecureString bearerString4 = createBearerString(List.of( - magicBytes, - (randomAlphaOfLengthBetween(3, 8) + "/" + randomAlphaOfLengthBetween(3, 8) - + "/" + ServiceAccountTokenTests.randomInvalidTokenName() - + ":" + randomAlphaOfLengthBetween(10, 20)).getBytes(StandardCharsets.UTF_8) - )); - assertNull(ServiceAccountService.tryParseToken(bearerString4)); - - // Everything is good - final String namespace = randomAlphaOfLengthBetween(3, 8); - final String serviceName = randomAlphaOfLengthBetween(3, 8); - final String tokenName = ServiceAccountTokenTests.randomTokenName(); - final ServiceAccountId accountId = new ServiceAccountId(namespace, serviceName); - final String secret = randomAlphaOfLengthBetween(10, 20); - final SecureString bearerString5 = createBearerString(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)); - - // Serialise and de-serialise service account token - final ServiceAccountToken parsedToken = ServiceAccountService.tryParseToken(serviceAccountToken2.asBearerString()); - assertThat(parsedToken, equalTo(serviceAccountToken2)); - - // Invalid magic byte - assertNull(ServiceAccountService.tryParseToken( - new SecureString("AQEAAWVsYXN0aWMvZmxlZXQvdG9rZW4xOnN1cGVyc2VjcmV0".toCharArray()))); - - // No colon - assertNull(ServiceAccountService.tryParseToken( - new SecureString("AQEAAWVsYXN0aWMvZmxlZXQvdG9rZW4xX3N1cGVyc2VjcmV0".toCharArray()))); - - // Invalid qualified name - assertNull(ServiceAccountService.tryParseToken( - new SecureString("AQEAAWVsYXN0aWMvZmxlZXRfdG9rZW4xOnN1cGVyc2VjcmV0".toCharArray()))); - - // Invalid token name - assertNull(ServiceAccountService.tryParseToken( - new SecureString("AAEAAWVsYXN0aWMvZmxlZXQvdG9rZW4hOnN1cGVyc2VjcmV0".toCharArray()))); - - // everything is fine - assertThat(ServiceAccountService.tryParseToken( - new SecureString("AAEAAWVsYXN0aWMvZmxlZXQvdG9rZW4xOnN1cGVyc2VjcmV0".toCharArray())), - equalTo(new ServiceAccountToken(new ServiceAccountId("elastic", "fleet"), "token1", - new SecureString("supersecret".toCharArray())))); } private Authentication.RealmRef randomRealmRef() { @@ -198,70 +267,99 @@ public void testTryAuthenticateBearerToken() throws ExecutionException, Interrup )); } - public void testAuthenticateWithToken() throws ExecutionException, InterruptedException { - // Null for non-elastic service account - final ServiceAccountId accountId1 = new ServiceAccountId( - randomValueOtherThan(ElasticServiceAccounts.NAMESPACE, () -> randomAlphaOfLengthBetween(3, 8)), - randomAlphaOfLengthBetween(3, 8)); - final SecureString secret = new SecureString(randomAlphaOfLength(20).toCharArray()); - final ServiceAccountToken token1 = new ServiceAccountToken(accountId1, randomAlphaOfLengthBetween(3, 8), secret); - final PlainActionFuture future1 = new PlainActionFuture<>(); - serviceAccountService.authenticateToken(token1, randomAlphaOfLengthBetween(3, 8), future1); - final ExecutionException e1 = expectThrows(ExecutionException.class, future1::get); - assertThat(e1.getCause().getClass(), is(ElasticsearchSecurityException.class)); - assertThat(e1.getMessage(), containsString("failed to authenticate service account [" - + token1.getAccountId().asPrincipal() + "] with token name [" + token1.getTokenName() + "]")); - - // Null for unknown elastic service name - final ServiceAccountId accountId2 = new ServiceAccountId( - ElasticServiceAccounts.NAMESPACE, - randomValueOtherThan("fleet", () -> randomAlphaOfLengthBetween(3, 8))); - final ServiceAccountToken token2 = new ServiceAccountToken(accountId2, randomAlphaOfLengthBetween(3, 8), secret); - final PlainActionFuture future2 = new PlainActionFuture<>(); - serviceAccountService.authenticateToken(token2, randomAlphaOfLengthBetween(3, 8), future2); - final ExecutionException e2 = expectThrows(ExecutionException.class, future2::get); - assertThat(e2.getCause().getClass(), is(ElasticsearchSecurityException.class)); - assertThat(e2.getMessage(), containsString("failed to authenticate service account [" - + token2.getAccountId().asPrincipal() + "] with token name [" + token2.getTokenName() + "]")); - - // Success based on credential store - final ServiceAccountId accountId3 = new ServiceAccountId(ElasticServiceAccounts.NAMESPACE, "fleet"); - final ServiceAccountToken token3 = new ServiceAccountToken(accountId3, randomAlphaOfLengthBetween(3, 8), secret); - final ServiceAccountToken token4 = new ServiceAccountToken(accountId3, randomAlphaOfLengthBetween(3, 8), - new SecureString(randomAlphaOfLength(20).toCharArray())); - final String nodeName = randomAlphaOfLengthBetween(3, 8); - doAnswer(invocationOnMock -> { - @SuppressWarnings("unchecked") - final ActionListener listener = (ActionListener) invocationOnMock.getArguments()[1]; - listener.onResponse(true); - return null; - }).when(serviceAccountsTokenStore).authenticate(eq(token3), any()); + public void testAuthenticateWithToken() throws ExecutionException, InterruptedException, IllegalAccessException { + final Logger sasLogger = LogManager.getLogger(ServiceAccountService.class); + Loggers.setLevel(sasLogger, Level.TRACE); - doAnswer(invocationOnMock -> { - @SuppressWarnings("unchecked") - final ActionListener listener = (ActionListener) invocationOnMock.getArguments()[1]; - listener.onResponse(false); - return null; - }).when(serviceAccountsTokenStore).authenticate(eq(token4), any()); - - final PlainActionFuture future3 = new PlainActionFuture<>(); - serviceAccountService.authenticateToken(token3, nodeName, future3); - final Authentication authentication = future3.get(); - assertThat(authentication, equalTo(new Authentication( - new User("elastic/fleet", Strings.EMPTY_ARRAY, - "Service account - elastic/fleet", null, Map.of("_elastic_service_account", true), - true), - new Authentication.RealmRef(ServiceAccountService.REALM_NAME, ServiceAccountService.REALM_TYPE, nodeName), - null, Version.CURRENT, Authentication.AuthenticationType.TOKEN, - Map.of("_token_name", token3.getTokenName()) - ))); - - final PlainActionFuture future4 = new PlainActionFuture<>(); - serviceAccountService.authenticateToken(token4, nodeName, future4); - final ExecutionException e4 = expectThrows(ExecutionException.class, future4::get); - assertThat(e4.getCause().getClass(), is(ElasticsearchSecurityException.class)); - assertThat(e4.getMessage(), containsString("failed to authenticate service account [" - + token4.getAccountId().asPrincipal() + "] with token name [" + token4.getTokenName() + "]")); + final MockLogAppender appender = new MockLogAppender(); + Loggers.addAppender(sasLogger, appender); + appender.start(); + + try { + // non-elastic service account + final ServiceAccountId accountId1 = new ServiceAccountId( + randomValueOtherThan(ElasticServiceAccounts.NAMESPACE, () -> randomAlphaOfLengthBetween(3, 8)), + randomAlphaOfLengthBetween(3, 8)); + appender.addExpectation(new MockLogAppender.SeenEventExpectation( + "non-elastic service account", ServiceAccountService.class.getName(), Level.DEBUG, + "only [elastic] service accounts are supported, but received [" + accountId1.asPrincipal() + "]" + )); + final SecureString secret = new SecureString(randomAlphaOfLength(20).toCharArray()); + final ServiceAccountToken token1 = new ServiceAccountToken(accountId1, randomAlphaOfLengthBetween(3, 8), secret); + final PlainActionFuture future1 = new PlainActionFuture<>(); + serviceAccountService.authenticateToken(token1, randomAlphaOfLengthBetween(3, 8), future1); + final ExecutionException e1 = expectThrows(ExecutionException.class, future1::get); + assertThat(e1.getCause().getClass(), is(ElasticsearchSecurityException.class)); + assertThat(e1.getMessage(), containsString("failed to authenticate service account [" + + token1.getAccountId().asPrincipal() + "] with token name [" + token1.getTokenName() + "]")); + appender.assertAllExpectationsMatched(); + + // Unknown elastic service name + final ServiceAccountId accountId2 = new ServiceAccountId( + ElasticServiceAccounts.NAMESPACE, + randomValueOtherThan("fleet", () -> randomAlphaOfLengthBetween(3, 8))); + appender.addExpectation(new MockLogAppender.SeenEventExpectation( + "non-elastic service account", ServiceAccountService.class.getName(), Level.DEBUG, + "the [" + accountId2.asPrincipal() + "] service account does not exist" + )); + final ServiceAccountToken token2 = new ServiceAccountToken(accountId2, randomAlphaOfLengthBetween(3, 8), secret); + final PlainActionFuture future2 = new PlainActionFuture<>(); + serviceAccountService.authenticateToken(token2, randomAlphaOfLengthBetween(3, 8), future2); + final ExecutionException e2 = expectThrows(ExecutionException.class, future2::get); + assertThat(e2.getCause().getClass(), is(ElasticsearchSecurityException.class)); + assertThat(e2.getMessage(), containsString("failed to authenticate service account [" + + token2.getAccountId().asPrincipal() + "] with token name [" + token2.getTokenName() + "]")); + appender.assertAllExpectationsMatched(); + + // Success based on credential store + final ServiceAccountId accountId3 = new ServiceAccountId(ElasticServiceAccounts.NAMESPACE, "fleet"); + final ServiceAccountToken token3 = new ServiceAccountToken(accountId3, randomAlphaOfLengthBetween(3, 8), secret); + final ServiceAccountToken token4 = new ServiceAccountToken(accountId3, randomAlphaOfLengthBetween(3, 8), + new SecureString(randomAlphaOfLength(20).toCharArray())); + final String nodeName = randomAlphaOfLengthBetween(3, 8); + doAnswer(invocationOnMock -> { + @SuppressWarnings("unchecked") + final ActionListener listener = (ActionListener) invocationOnMock.getArguments()[1]; + listener.onResponse(true); + return null; + }).when(serviceAccountsTokenStore).authenticate(eq(token3), any()); + + doAnswer(invocationOnMock -> { + @SuppressWarnings("unchecked") + final ActionListener listener = (ActionListener) invocationOnMock.getArguments()[1]; + listener.onResponse(false); + return null; + }).when(serviceAccountsTokenStore).authenticate(eq(token4), any()); + + final PlainActionFuture future3 = new PlainActionFuture<>(); + serviceAccountService.authenticateToken(token3, nodeName, future3); + final Authentication authentication = future3.get(); + assertThat(authentication, equalTo(new Authentication( + new User("elastic/fleet", Strings.EMPTY_ARRAY, + "Service account - elastic/fleet", null, Map.of("_elastic_service_account", true), + true), + new Authentication.RealmRef(ServiceAccountService.REALM_NAME, ServiceAccountService.REALM_TYPE, nodeName), + null, Version.CURRENT, Authentication.AuthenticationType.TOKEN, + Map.of("_token_name", token3.getTokenName()) + ))); + + appender.addExpectation(new MockLogAppender.SeenEventExpectation( + "non-elastic service account", ServiceAccountService.class.getName(), Level.DEBUG, + "failed to authenticate service account [" + token4.getAccountId().asPrincipal() + + "] with token name [" + token4.getTokenName() + "]" + )); + final PlainActionFuture future4 = new PlainActionFuture<>(); + serviceAccountService.authenticateToken(token4, nodeName, future4); + final ExecutionException e4 = expectThrows(ExecutionException.class, future4::get); + assertThat(e4.getCause().getClass(), is(ElasticsearchSecurityException.class)); + assertThat(e4.getMessage(), containsString("failed to authenticate service account [" + + token4.getAccountId().asPrincipal() + "] with token name [" + token4.getTokenName() + "]")); + appender.assertAllExpectationsMatched(); + } finally { + appender.stop(); + Loggers.setLevel(sasLogger, Level.INFO); + Loggers.removeAppender(sasLogger, appender); + } } public void testGetRoleDescriptor() throws ExecutionException, InterruptedException { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountTokenTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountTokenTests.java index ded83e0dea0a4..716c64c3405b6 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountTokenTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountTokenTests.java @@ -112,7 +112,7 @@ public static String randomInvalidTokenName() { .forEach(i -> chars[randomIntBetween(0, chars.length - 1)] = randomFrom(INVALID_TOKEN_NAME_CHARS)); return new String(chars); } else { - return randomFrom("", " ", randomAlphaOfLength(257), null); + return randomFrom("", " ", randomAlphaOfLength(257)); } } }