From 77bdebe7615a86baf9fc4dbba4b1a1c4be1faac9 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Wed, 26 Feb 2020 23:12:13 +1100 Subject: [PATCH 1/4] Add exception metadata for disabled features This change adds a new exception with consistent metadata for when security features are not enabled. This allows clients to be able to tell that an API failed due to a configuration option, and respond accordingly. Relates: kibana#55255 Resolves: #52311 --- .../xpack/security/authc/ApiKeyService.java | 4 +- .../xpack/security/authc/TokenService.java | 461 +++++++++--------- .../support/FeatureNotEnabledException.java | 41 ++ .../security/authc/ApiKeyServiceTests.java | 17 + .../security/authc/TokenServiceTests.java | 24 +- 5 files changed, 309 insertions(+), 238 deletions(-) create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/FeatureNotEnabledException.java diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java index 11ba9a6b23b0d..fae48287ef59b 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java @@ -69,6 +69,8 @@ import org.elasticsearch.xpack.core.security.authc.support.Hasher; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.security.support.FeatureNotEnabledException; +import org.elasticsearch.xpack.security.support.FeatureNotEnabledException.Feature; import org.elasticsearch.xpack.security.support.SecurityIndexManager; import javax.crypto.SecretKeyFactory; @@ -577,7 +579,7 @@ private void ensureEnabled() { throw LicenseUtils.newComplianceException("api keys"); } if (enabled == false) { - throw new IllegalStateException("api keys are not enabled"); + throw new FeatureNotEnabledException(Feature.API_KEY_SERVICE, "api keys are not enabled"); } } 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 3680afb98c774..b83f8d63d75ca 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 @@ -89,6 +89,8 @@ 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.support.FeatureNotEnabledException; +import org.elasticsearch.xpack.security.support.FeatureNotEnabledException.Feature; import org.elasticsearch.xpack.security.support.SecurityIndexManager; import javax.crypto.Cipher; @@ -168,18 +170,18 @@ public final class TokenService { private static final int VERSION_BYTES = 4; private static final String ENCRYPTION_CIPHER = "AES/GCM/NoPadding"; private static final String EXPIRED_TOKEN_WWW_AUTH_VALUE = "Bearer realm=\"" + XPackField.SECURITY + - "\", error=\"invalid_token\", error_description=\"The access token expired\""; + "\", error=\"invalid_token\", error_description=\"The access token expired\""; private static final String MALFORMED_TOKEN_WWW_AUTH_VALUE = "Bearer realm=\"" + XPackField.SECURITY + - "\", error=\"invalid_token\", error_description=\"The access token is malformed\""; + "\", error=\"invalid_token\", error_description=\"The access token is malformed\""; private static final BackoffPolicy DEFAULT_BACKOFF = BackoffPolicy.exponentialBackoff(); public static final String THREAD_POOL_NAME = XPackField.SECURITY + "-token-key"; public static final Setting TOKEN_EXPIRATION = Setting.timeSetting("xpack.security.authc.token.timeout", - TimeValue.timeValueMinutes(20L), TimeValue.timeValueSeconds(1L), TimeValue.timeValueHours(1L), Property.NodeScope); + TimeValue.timeValueMinutes(20L), TimeValue.timeValueSeconds(1L), TimeValue.timeValueHours(1L), Property.NodeScope); public static final Setting DELETE_INTERVAL = Setting.timeSetting("xpack.security.authc.token.delete.interval", - TimeValue.timeValueMinutes(30L), Property.NodeScope); + TimeValue.timeValueMinutes(30L), Property.NodeScope); public static final Setting DELETE_TIMEOUT = Setting.timeSetting("xpack.security.authc.token.delete.timeout", - TimeValue.MINUS_ONE, Property.NodeScope); + TimeValue.MINUS_ONE, Property.NodeScope); static final String TOKEN_DOC_TYPE = "token"; private static final int HASHED_TOKEN_LENGTH = 43; @@ -236,7 +238,7 @@ public TokenService(Settings settings, Clock clock, Client client, XPackLicenseS this.expiredTokenRemover = new ExpiredTokenRemover(settings, client, this.securityMainIndex, securityTokensIndex); ensureEncryptionCiphersSupported(); KeyAndCache keyAndCache = new KeyAndCache(new KeyAndTimestamp(tokenPassphrase, createdTimeStamps.incrementAndGet()), - new BytesKey(saltArr)); + new BytesKey(saltArr)); keyCache = new TokenKeys(Collections.singletonMap(keyAndCache.getKeyHash(), keyAndCache), keyAndCache.getKeyHash()); this.clusterService = clusterService; initialize(clusterService); @@ -268,7 +270,7 @@ public void createOAuth2Tokens(Authentication authentication, Authentication ori //public for testing public void createOAuth2Tokens(String accessToken, String refreshToken, Authentication authentication, Authentication originatingClientAuth, - Map metadata, ActionListener> listener) { + Map metadata, ActionListener> listener) { // the created token is compatible with the oldest node version in the cluster final Version tokenVersion = getTokenVersionCompatibility(); // tokens moved to a separate index in newer versions @@ -280,29 +282,29 @@ public void createOAuth2Tokens(String accessToken, String refreshToken, Authenti * Create an access token and optionally a refresh token as well from predefined values, based on the provided authentication and * metadata. * - * @param accessToken The predefined seed value for the access token. This will then be - *
    - *
  • Encrypted before stored for versions before {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • - *
  • Hashed before stored for versions after {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • - *
  • Stored in the security index for versions up to {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • - *
  • Stored in a specific security tokens index for versions after {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • - *
  • Prepended with a version ID and encoded with Base64 before returned to the caller of the APIs
  • - *
- * @param refreshToken The predefined seed value for the access token. This will then be - *
    - *
  • Hashed before stored for versions after {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • - *
  • Stored in the security index for versions up to {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • - *
  • Stored in a specific security tokens index for versions after {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • - *
  • Prepended with a version ID and encoded with Base64 before returned to the caller of the APIs for - * versions after {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • - *
- * @param tokenVersion The version of the nodes with which these tokens will be compatible. - * @param tokensIndex The security tokens index - * @param authentication The authentication object representing the user for which the tokens are created + * @param accessToken The predefined seed value for the access token. This will then be + *
    + *
  • Encrypted before stored for versions before {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • + *
  • Hashed before stored for versions after {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • + *
  • Stored in the security index for versions up to {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • + *
  • Stored in a specific security tokens index for versions after {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • + *
  • Prepended with a version ID and encoded with Base64 before returned to the caller of the APIs
  • + *
+ * @param refreshToken The predefined seed value for the access token. This will then be + *
    + *
  • Hashed before stored for versions after {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • + *
  • Stored in the security index for versions up to {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • + *
  • Stored in a specific security tokens index for versions after {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • + *
  • Prepended with a version ID and encoded with Base64 before returned to the caller of the APIs for + * versions after {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • + *
+ * @param tokenVersion The version of the nodes with which these tokens will be compatible. + * @param tokensIndex The security tokens index + * @param authentication The authentication object representing the user for which the tokens are created * @param originatingClientAuth The authentication object representing the client that called the related API - * @param metadata A map with metadata to be stored in the token document - * @param listener The listener to call upon completion with a {@link Tuple} containing the - * serialized access token and serialized refresh token as these will be returned to the client + * @param metadata A map with metadata to be stored in the token document + * @param listener The listener to call upon completion with a {@link Tuple} containing the + * serialized access token and serialized refresh token as these will be returned to the client */ private void createOAuth2Tokens(String accessToken, String refreshToken, Version tokenVersion, SecurityIndexManager tokensIndex, Authentication authentication, Authentication originatingClientAuth, Map metadata, @@ -332,32 +334,32 @@ private void createOAuth2Tokens(String accessToken, String refreshToken, Version final String documentId = getTokenDocumentId(storedAccessToken); final IndexRequest indexTokenRequest = client.prepareIndex(tokensIndex.aliasName()).setId(documentId) - .setOpType(OpType.CREATE) - .setSource(tokenDocument, XContentType.JSON) - .setRefreshPolicy(RefreshPolicy.WAIT_UNTIL) - .request(); + .setOpType(OpType.CREATE) + .setSource(tokenDocument, XContentType.JSON) + .setRefreshPolicy(RefreshPolicy.WAIT_UNTIL) + .request(); tokensIndex.prepareIndexIfNeededThenExecute( - ex -> listener.onFailure(traceLog("prepare tokens index [" + tokensIndex.aliasName() + "]", documentId, ex)), - () -> executeAsyncWithOrigin(client, SECURITY_ORIGIN, IndexAction.INSTANCE, indexTokenRequest, - ActionListener.wrap(indexResponse -> { - if (indexResponse.getResult() == Result.CREATED) { - final String versionedAccessToken = prependVersionAndEncodeAccessToken(tokenVersion, accessToken); - if (tokenVersion.onOrAfter(VERSION_TOKENS_INDEX_INTRODUCED)) { - final String versionedRefreshToken = refreshToken != null - ? prependVersionAndEncodeRefreshToken(tokenVersion, refreshToken) - : null; - listener.onResponse(new Tuple<>(versionedAccessToken, versionedRefreshToken)); - } else { - // prior versions of the refresh token are not version-prepended, as nodes on those - // versions don't expect it. - // Such nodes might exist in a mixed cluster during a rolling upgrade. - listener.onResponse(new Tuple<>(versionedAccessToken, refreshToken)); - } - } else { - listener.onFailure(traceLog("create token", - new ElasticsearchException("failed to create token document [{}]", indexResponse))); - } - }, listener::onFailure))); + ex -> listener.onFailure(traceLog("prepare tokens index [" + tokensIndex.aliasName() + "]", documentId, ex)), + () -> executeAsyncWithOrigin(client, SECURITY_ORIGIN, IndexAction.INSTANCE, indexTokenRequest, + ActionListener.wrap(indexResponse -> { + if (indexResponse.getResult() == Result.CREATED) { + final String versionedAccessToken = prependVersionAndEncodeAccessToken(tokenVersion, accessToken); + if (tokenVersion.onOrAfter(VERSION_TOKENS_INDEX_INTRODUCED)) { + final String versionedRefreshToken = refreshToken != null + ? prependVersionAndEncodeRefreshToken(tokenVersion, refreshToken) + : null; + listener.onResponse(new Tuple<>(versionedAccessToken, versionedRefreshToken)); + } else { + // prior versions of the refresh token are not version-prepended, as nodes on those + // versions don't expect it. + // Such nodes might exist in a mixed cluster during a rolling upgrade. + listener.onResponse(new Tuple<>(versionedAccessToken, refreshToken)); + } + } else { + listener.onFailure(traceLog("create token", + new ElasticsearchException("failed to create token document [{}]", indexResponse))); + } + }, listener::onFailure))); } } @@ -400,14 +402,14 @@ void getAndValidateToken(ThreadContext ctx, ActionListener listener) */ public void getAuthenticationAndMetaData(String token, ActionListener>> listener) { decodeToken(token, ActionListener.wrap( - userToken -> { - if (userToken == null) { - listener.onFailure(new ElasticsearchSecurityException("supplied token is not valid")); - } else { - listener.onResponse(new Tuple<>(userToken.getAuthentication(), userToken.getMetadata())); - } - }, - listener::onFailure + userToken -> { + if (userToken == null) { + listener.onFailure(new ElasticsearchSecurityException("supplied token is not valid")); + } else { + listener.onResponse(new Tuple<>(userToken.getAuthentication(), userToken.getMetadata())); + } + }, + listener::onFailure )); } @@ -422,45 +424,45 @@ private void getUserTokenFromId(String userTokenId, Version tokenVersion, Action listener.onResponse(null); } else { final GetRequest getRequest = client.prepareGet(tokensIndex.aliasName(), - getTokenDocumentId(userTokenId)).request(); + getTokenDocumentId(userTokenId)).request(); final Consumer onFailure = ex -> listener.onFailure(traceLog("get token from id", userTokenId, ex)); tokensIndex.checkIndexVersionThenExecute( - ex -> listener.onFailure(traceLog("prepare tokens index [" + tokensIndex.aliasName() +"]", userTokenId, ex)), + ex -> listener.onFailure(traceLog("prepare tokens index [" + tokensIndex.aliasName() + "]", userTokenId, ex)), () -> executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN, getRequest, - ActionListener.wrap(response -> { - if (response.isExists()) { - Map accessTokenSource = - (Map) response.getSource().get("access_token"); - if (accessTokenSource == null) { - onFailure.accept(new IllegalStateException( - "token document is missing the access_token field")); - } else if (accessTokenSource.containsKey("user_token") == false) { - onFailure.accept(new IllegalStateException( - "token document is missing the user_token field")); - } else { - Map userTokenSource = - (Map) accessTokenSource.get("user_token"); - listener.onResponse(UserToken.fromSourceMap(userTokenSource)); - } - } else { - // The chances of a random token string decoding to something that we can read is minimal, so - // we assume that this was a token we have created but is now expired/revoked and deleted - logger.trace("The access token [{}] is expired and already deleted", userTokenId); - listener.onResponse(null); - } - }, e -> { - // if the index or the shard is not there / available we assume that - // the token is not valid - if (isShardNotAvailableException(e)) { - logger.warn("failed to get access token [{}] because index [{}] is not available", userTokenId, - tokensIndex.aliasName()); - listener.onResponse(null); + ActionListener.wrap(response -> { + if (response.isExists()) { + Map accessTokenSource = + (Map) response.getSource().get("access_token"); + if (accessTokenSource == null) { + onFailure.accept(new IllegalStateException( + "token document is missing the access_token field")); + } else if (accessTokenSource.containsKey("user_token") == false) { + onFailure.accept(new IllegalStateException( + "token document is missing the user_token field")); } else { - logger.error(new ParameterizedMessage("failed to get access token [{}]", userTokenId), e); - listener.onFailure(e); + Map userTokenSource = + (Map) accessTokenSource.get("user_token"); + listener.onResponse(UserToken.fromSourceMap(userTokenSource)); } - }), client::get) - ); + } else { + // The chances of a random token string decoding to something that we can read is minimal, so + // we assume that this was a token we have created but is now expired/revoked and deleted + logger.trace("The access token [{}] is expired and already deleted", userTokenId); + listener.onResponse(null); + } + }, e -> { + // if the index or the shard is not there / available we assume that + // the token is not valid + if (isShardNotAvailableException(e)) { + logger.warn("failed to get access token [{}] because index [{}] is not available", userTokenId, + tokensIndex.aliasName()); + listener.onResponse(null); + } else { + logger.error(new ParameterizedMessage("failed to get access token [{}]", userTokenId), e); + listener.onFailure(e); + } + }), client::get) + ); } } @@ -531,7 +533,7 @@ void decodeToken(String token, ActionListener listener) { } catch (Exception e) { // could happen with a token that is not ours if (logger.isDebugEnabled()) { - logger.debug("built in token service unable to decode token", e); + logger.debug("built in token service unable to decode token", e); } else { logger.warn("built in token service unable to decode token"); } @@ -581,7 +583,7 @@ public void invalidateAccessToken(UserToken userToken, ActionListenerrefresh_token.invalidated field to true * * @param refreshToken The string representation of the refresh token - * @param listener the listener to notify upon completion + * @param listener the listener to notify upon completion */ public void invalidateRefreshToken(String refreshToken, ActionListener listener) { ensureEnabled(); @@ -604,7 +606,7 @@ public void invalidateRefreshToken(String refreshToken, ActionListener userTokens, ActionListener listener) { maybeStartTokenRemover(); @@ -653,15 +655,15 @@ private void invalidateAllTokens(Collection userTokens, ActionListene // access tokens while we invalidate the access tokens we currently know about final Iterator backoff = DEFAULT_BACKOFF.iterator(); indexInvalidation(userTokens, backoff, "refresh_token", null, ActionListener.wrap(result -> - indexInvalidation(userTokens, backoff, "access_token", result, listener), - listener::onFailure)); + indexInvalidation(userTokens, backoff, "access_token", result, listener), + listener::onFailure)); } /** * Invalidates access and/or refresh tokens associated to a user token (coexisting in the same token document) */ private void indexInvalidation(Collection userTokens, Iterator backoff, String srcPrefix, - @Nullable TokensInvalidationResult previousResult, ActionListener listener) { + @Nullable TokensInvalidationResult previousResult, ActionListener listener) { final Set idsOfRecentTokens = new HashSet<>(); final Set idsOfOlderTokens = new HashSet<>(); for (UserToken userToken : userTokens) { @@ -690,14 +692,14 @@ private void indexInvalidation(Collection userTokens, Iterator tokenIds, SecurityIndexManager tokensIndexManager, Iterator backoff, String srcPrefix, @Nullable TokensInvalidationResult previousResult, @@ -709,10 +711,10 @@ private void indexInvalidation(Collection tokenIds, SecurityIndexManager BulkRequestBuilder bulkRequestBuilder = client.prepareBulk(); for (String tokenId : tokenIds) { UpdateRequest request = client - .prepareUpdate(tokensIndexManager.aliasName(), getTokenDocumentId(tokenId)) - .setDoc(srcPrefix, Collections.singletonMap("invalidated", true)) - .setFetchSource(srcPrefix, null) - .request(); + .prepareUpdate(tokensIndexManager.aliasName(), getTokenDocumentId(tokenId)) + .setDoc(srcPrefix, Collections.singletonMap("invalidated", true)) + .setFetchSource(srcPrefix, null) + .request(); bulkRequestBuilder.add(request); } bulkRequestBuilder.setRefreshPolicy(RefreshPolicy.WAIT_UNTIL); @@ -743,7 +745,7 @@ private void indexInvalidation(Collection tokenIds, SecurityIndexManager UpdateResponse updateResponse = bulkItemResponse.getResponse(); if (updateResponse.getResult() == DocWriteResponse.Result.UPDATED) { logger.debug(() -> new ParameterizedMessage("Invalidated [{}] for doc [{}]", - srcPrefix, updateResponse.getGetResult().getId())); + srcPrefix, updateResponse.getGetResult().getId())); invalidated.add(updateResponse.getGetResult().getId()); } else if (updateResponse.getResult() == DocWriteResponse.Result.NOOP) { previouslyInvalidated.add(updateResponse.getGetResult().getId()); @@ -752,25 +754,25 @@ private void indexInvalidation(Collection tokenIds, SecurityIndexManager } if (retryTokenDocIds.isEmpty() == false && backoff.hasNext()) { logger.debug("failed to invalidate [{}] tokens out of [{}], retrying to invalidate these too", - retryTokenDocIds.size(), tokenIds.size()); + retryTokenDocIds.size(), tokenIds.size()); final TokensInvalidationResult incompleteResult = new TokensInvalidationResult(invalidated, - previouslyInvalidated, failedRequestResponses); + previouslyInvalidated, failedRequestResponses); final Runnable retryWithContextRunnable = client.threadPool().getThreadContext() - .preserveContext(() -> indexInvalidation(retryTokenDocIds, tokensIndexManager, backoff, - srcPrefix, incompleteResult, listener)); + .preserveContext(() -> indexInvalidation(retryTokenDocIds, tokensIndexManager, backoff, + srcPrefix, incompleteResult, listener)); client.threadPool().schedule(retryWithContextRunnable, backoff.next(), GENERIC); } else { if (retryTokenDocIds.isEmpty() == false) { logger.warn("failed to invalidate [{}] tokens out of [{}] after all retries", retryTokenDocIds.size(), - tokenIds.size()); + tokenIds.size()); for (String retryTokenDocId : retryTokenDocIds) { failedRequestResponses.add( - new ElasticsearchException("Error invalidating [{}] with doc id [{}] after retries exhausted", - srcPrefix, retryTokenDocId)); + new ElasticsearchException("Error invalidating [{}] with doc id [{}] after retries exhausted", + srcPrefix, retryTokenDocId)); } } final TokensInvalidationResult result = new TokensInvalidationResult(invalidated, previouslyInvalidated, - failedRequestResponses); + failedRequestResponses); listener.onResponse(result); } }, e -> { @@ -779,8 +781,8 @@ private void indexInvalidation(Collection tokenIds, SecurityIndexManager if (isShardNotAvailableException(cause) && backoff.hasNext()) { logger.debug("failed to invalidate tokens, retrying "); final Runnable retryWithContextRunnable = client.threadPool().getThreadContext() - .preserveContext(() -> indexInvalidation(tokenIds, tokensIndexManager, backoff, srcPrefix, - previousResult, listener)); + .preserveContext(() -> indexInvalidation(tokenIds, tokensIndexManager, backoff, srcPrefix, + previousResult, listener)); client.threadPool().schedule(retryWithContextRunnable, backoff.next(), GENERIC); } else { listener.onFailure(e); @@ -793,8 +795,8 @@ private void indexInvalidation(Collection tokenIds, SecurityIndexManager * Called by the transport action in order to start the process of refreshing a token. * * @param refreshToken The refresh token as provided by the client - * @param listener The listener to call upon completion with a {@link Tuple} containing the - * serialized access token and serialized refresh token as these will be returned to the client + * @param listener The listener to call upon completion with a {@link Tuple} containing the + * serialized access token and serialized refresh token as these will be returned to the client */ public void refreshToken(String refreshToken, ActionListener> listener) { ensureEnabled(); @@ -864,7 +866,7 @@ private void findTokenFromRefreshToken(String refreshToken, SecurityIndexManager final TimeValue backofTimeValue = backoff.next(); logger.debug("retrying after [{}] back off", backofTimeValue); final Runnable retryWithContextRunnable = client.threadPool().getThreadContext() - .preserveContext(() -> findTokenFromRefreshToken(refreshToken, tokensIndexManager, backoff, listener)); + .preserveContext(() -> findTokenFromRefreshToken(refreshToken, tokensIndexManager, backoff, listener)); client.threadPool().schedule(retryWithContextRunnable, backofTimeValue, GENERIC); } else { logger.warn("failed to find token from refresh token after all retries"); @@ -880,11 +882,11 @@ private void findTokenFromRefreshToken(String refreshToken, SecurityIndexManager maybeRetryOnFailure.accept(invalidGrantException("could not refresh the requested token")); } else { final SearchRequest request = client.prepareSearch(tokensIndexManager.aliasName()) - .setQuery(QueryBuilders.boolQuery() - .filter(QueryBuilders.termQuery("doc_type", TOKEN_DOC_TYPE)) - .filter(QueryBuilders.termQuery("refresh_token.token", refreshToken))) - .seqNoAndPrimaryTerm(true) - .request(); + .setQuery(QueryBuilders.boolQuery() + .filter(QueryBuilders.termQuery("doc_type", TOKEN_DOC_TYPE)) + .filter(QueryBuilders.termQuery("refresh_token.token", refreshToken))) + .seqNoAndPrimaryTerm(true) + .request(); tokensIndexManager.checkIndexVersionThenExecute(listener::onFailure, () -> executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN, request, ActionListener.wrap(searchResponse -> { @@ -966,19 +968,19 @@ private void innerRefresh(String refreshToken, String tokenDocId, Map listener.onFailure(traceLog("prepare index [" + refreshedTokenIndex.aliasName() + "]", ex)), - () -> executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN, updateRequest.request(), + ex -> listener.onFailure(traceLog("prepare index [" + refreshedTokenIndex.aliasName() + "]", ex)), + () -> executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN, updateRequest.request(), ActionListener.wrap(updateResponse -> { if (updateResponse.getResult() == DocWriteResponse.Result.UPDATED) { logger.debug(() -> new ParameterizedMessage("updated the original token document to {}", - updateResponse.getGetResult().sourceAsMap())); + updateResponse.getGetResult().sourceAsMap())); final Tuple parsedTokens = parseTokensFromDocument(source, null); final UserToken toRefreshUserToken = parsedTokens.v1(); createOAuth2Tokens(newAccessTokenString, newRefreshTokenString, newTokenVersion, @@ -986,14 +988,14 @@ private void innerRefresh(String refreshToken, String tokenDocId, Map innerRefresh(refreshToken, tokenDocId, source, seqNo, primaryTerm, clientAuth, backoff, refreshRequested, listener)); client.threadPool().schedule(retryWithContextRunnable, backoff.next(), GENERIC); } else { logger.info("failed to update the original token document [{}] after all retries, the update result was [{}]. ", - tokenDocId, updateResponse.getResult()); + tokenDocId, updateResponse.getResult()); listener.onFailure(invalidGrantException("could not refresh the requested token")); } }, e -> { @@ -1019,7 +1021,7 @@ public void onFailure(Exception e) { if (backoff.hasNext()) { logger.info("could not get token document [{}] for refresh, retrying", tokenDocId); final Runnable retryWithContextRunnable = client.threadPool().getThreadContext() - .preserveContext(() -> getTokenDocAsync(tokenDocId, refreshedTokenIndex, this)); + .preserveContext(() -> getTokenDocAsync(tokenDocId, refreshedTokenIndex, this)); client.threadPool().schedule(retryWithContextRunnable, backoff.next(), GENERIC); } else { logger.warn("could not get token document [{}] for refresh after all retries", tokenDocId); @@ -1052,11 +1054,11 @@ public void onFailure(Exception e) { * Decrypts the values of the superseding access token and the refresh token, using a key derived from the superseded refresh token. It * encodes the version and serializes the tokens before calling the listener, in the same manner as {@link #createOAuth2Tokens } does. * - * @param refreshToken The refresh token that the user sent in the request, used to derive the decryption key + * @param refreshToken The refresh token that the user sent in the request, used to derive the decryption key * @param refreshTokenStatus The {@link RefreshTokenStatus} containing information about the superseding tokens as retrieved from the - * index - * @param listener The listener to call upon completion with a {@link Tuple} containing the - * serialized access token and serialized refresh token as these will be returned to the client + * index + * @param listener The listener to call upon completion with a {@link Tuple} containing the + * serialized access token and serialized refresh token as these will be returned to the client */ void decryptAndReturnSupersedingTokens(String refreshToken, RefreshTokenStatus refreshTokenStatus, ActionListener> listener) { @@ -1094,8 +1096,8 @@ String encryptSupersedingTokens(String supersedingAccessToken, String supersedin private void getTokenDocAsync(String tokenDocId, SecurityIndexManager tokensIndex, ActionListener listener) { final GetRequest getRequest = client.prepareGet(tokensIndex.aliasName(), tokenDocId).request(); tokensIndex.checkIndexVersionThenExecute( - ex -> listener.onFailure(traceLog("prepare tokens index [" + tokensIndex.aliasName() + "]", tokenDocId, ex)), - () -> executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN, getRequest, listener, client::get)); + ex -> listener.onFailure(traceLog("prepare tokens index [" + tokensIndex.aliasName() + "]", tokenDocId, ex)), + () -> executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN, getRequest, listener, client::get)); } Version getTokenVersionCompatibility() { @@ -1154,12 +1156,12 @@ private static Optional checkClientCanRefresh(Re Authentication clientAuthentication) { if (clientAuthentication.getUser().principal().equals(refreshToken.getAssociatedUser()) == false) { logger.warn("Token was originally created by [{}] but [{}] attempted to refresh it", refreshToken.getAssociatedUser(), - clientAuthentication.getUser().principal()); + clientAuthentication.getUser().principal()); return Optional.of(invalidGrantException("tokens must be refreshed by the creating client")); } else if (clientAuthentication.getAuthenticatedBy().getName().equals(refreshToken.getAssociatedRealm()) == false) { logger.warn("[{}] created the refresh token while authenticated by [{}] but is now authenticated by [{}]", - refreshToken.getAssociatedUser(), refreshToken.getAssociatedRealm(), - clientAuthentication.getAuthenticatedBy().getName()); + refreshToken.getAssociatedUser(), refreshToken.getAssociatedRealm(), + clientAuthentication.getAuthenticatedBy().getName()); return Optional.of(invalidGrantException("tokens must be refreshed by the creating client")); } else { return Optional.empty(); @@ -1191,7 +1193,7 @@ private static Map getUserTokenSourceMap(Map sou * short span of time (30 s). * * @return An {@code Optional} containing the exception in case this refresh token cannot be reused, or an empty Optional if - * refreshing is allowed. + * refreshing is allowed. */ private static Optional checkMultipleRefreshes(Instant refreshRequested, RefreshTokenStatus refreshTokenStatus) { @@ -1202,7 +1204,7 @@ private static Optional checkMultipleRefreshes(I } if (refreshRequested.isBefore(refreshTokenStatus.getRefreshInstant().minus(30L, ChronoUnit.SECONDS))) { return Optional - .of(invalidGrantException("token has been refreshed more than 30 seconds in the future, clock skew too great")); + .of(invalidGrantException("token has been refreshed more than 30 seconds in the future, clock skew too great")); } } else { return Optional.of(invalidGrantException("token has already been refreshed")); @@ -1232,30 +1234,30 @@ public void findActiveTokensForRealm(String realmName, @Nullable Predicate supplier = client.threadPool().getThreadContext().newRestorableContext(false); try (ThreadContext.StoredContext ignore = client.threadPool().getThreadContext().stashWithOrigin(SECURITY_ORIGIN)) { final SearchRequest request = client.prepareSearch(indicesWithTokens.toArray(new String[0])) - .setScroll(DEFAULT_KEEPALIVE_SETTING.get(settings)) - .setQuery(boolQuery) - .setVersion(false) - .setSize(1000) - .setFetchSource(true) - .request(); + .setScroll(DEFAULT_KEEPALIVE_SETTING.get(settings)) + .setQuery(boolQuery) + .setVersion(false) + .setSize(1000) + .setFetchSource(true) + .request(); ScrollHelper.fetchAllByEntity(client, request, new ContextPreservingActionListener<>(supplier, listener), - (SearchHit hit) -> filterAndParseHit(hit, filter)); + (SearchHit hit) -> filterAndParseHit(hit, filter)); } } }, listener::onFailure)); @@ -1280,29 +1282,29 @@ public void findActiveTokensForUser(String username, ActionListener supplier = client.threadPool().getThreadContext().newRestorableContext(false); try (ThreadContext.StoredContext ignore = client.threadPool().getThreadContext().stashWithOrigin(SECURITY_ORIGIN)) { final SearchRequest request = client.prepareSearch(indicesWithTokens.toArray(new String[0])) - .setScroll(DEFAULT_KEEPALIVE_SETTING.get(settings)) - .setQuery(boolQuery) - .setVersion(false) - .setSize(1000) - .setFetchSource(true) - .request(); + .setScroll(DEFAULT_KEEPALIVE_SETTING.get(settings)) + .setQuery(boolQuery) + .setVersion(false) + .setSize(1000) + .setFetchSource(true) + .request(); ScrollHelper.fetchAllByEntity(client, request, new ContextPreservingActionListener<>(supplier, listener), - (SearchHit hit) -> filterAndParseHit(hit, isOfUser(username))); + (SearchHit hit) -> filterAndParseHit(hit, isOfUser(username))); } } }, listener::onFailure)); @@ -1329,8 +1331,8 @@ private void sourceIndicesWithTokensAndRun(ActionListener> listener } if (false == frozenTokensIndex.isIndexUpToDate()) { listener.onFailure(new IllegalStateException( - "Index [" + frozenTokensIndex.aliasName() + "] is not on the current version. Features relying on the index" - + " will not be available until the upgrade API is run on the index")); + "Index [" + frozenTokensIndex.aliasName() + "] is not on the current version. Features relying on the index" + + " will not be available until the upgrade API is run on the index")); return; } indicesWithTokens.add(frozenTokensIndex.aliasName()); @@ -1339,15 +1341,15 @@ private void sourceIndicesWithTokensAndRun(ActionListener> listener if (frozenMainIndex.indexExists()) { // main security index _might_ contain tokens if the tokens index has been created recently if (false == frozenTokensIndex.indexExists() || frozenTokensIndex.getCreationTime() - .isAfter(clock.instant().minus(ExpiredTokenRemover.MAXIMUM_TOKEN_LIFETIME_HOURS, ChronoUnit.HOURS))) { + .isAfter(clock.instant().minus(ExpiredTokenRemover.MAXIMUM_TOKEN_LIFETIME_HOURS, ChronoUnit.HOURS))) { if (false == frozenMainIndex.isAvailable()) { listener.onFailure(frozenMainIndex.getUnavailableReason()); return; } if (false == frozenMainIndex.isIndexUpToDate()) { listener.onFailure(new IllegalStateException( - "Index [" + frozenMainIndex.aliasName() + "] is not on the current version. Features relying on the index" - + " will not be available until the upgrade API is run on the index")); + "Index [" + frozenMainIndex.aliasName() + "] is not on the current version. Features relying on the index" + + " will not be available until the upgrade API is run on the index")); return; } indicesWithTokens.add(frozenMainIndex.aliasName()); @@ -1370,17 +1372,17 @@ private BytesReference createTokenDocument(UserToken userToken, @Nullable String .field("invalidated", false) .field("refreshed", false) .startObject("client") - .field("type", "unassociated_client") - .field("user", originatingClientAuth.getUser().principal()) - .field("realm", originatingClientAuth.getAuthenticatedBy().getName()) + .field("type", "unassociated_client") + .field("user", originatingClientAuth.getUser().principal()) + .field("realm", originatingClientAuth.getAuthenticatedBy().getName()) .endObject() .endObject(); } builder.startObject("access_token") - .field("invalidated", false) - .field("user_token", userToken) - .field("realm", userToken.getAuthentication().getAuthenticatedBy().getName()) - .endObject(); + .field("invalidated", false) + .field("user_token", userToken) + .field("realm", userToken.getAuthentication().getAuthenticatedBy().getName()) + .endObject(); builder.endObject(); return BytesReference.bytes(builder); } catch (IOException e) { @@ -1404,7 +1406,7 @@ private static Predicate> isOfUser(String username) { } private Tuple filterAndParseHit(SearchHit hit, @Nullable Predicate> filter) - throws IllegalStateException, DateTimeException { + throws IllegalStateException, DateTimeException { final Map source = hit.getSourceAsMap(); if (source == null) { throw new IllegalStateException("token document did not have source but source should have been fetched"); @@ -1421,7 +1423,7 @@ private Tuple filterAndParseHit(SearchHit hit, @Nullable Pred * satisfy it */ private Tuple parseTokensFromDocument(Map source, @Nullable Predicate> filter) - throws IllegalStateException, DateTimeException { + throws IllegalStateException, DateTimeException { final String hashedRefreshToken = (String) ((Map) source.get("refresh_token")).get("token"); final Map userTokenSource = (Map) ((Map) source.get("access_token")).get("user_token"); @@ -1457,7 +1459,7 @@ private void ensureEnabled() { throw LicenseUtils.newComplianceException("security tokens"); } if (enabled == false) { - throw new IllegalStateException("security tokens are not enabled"); + throw new FeatureNotEnabledException(Feature.TOKEN_SERVICE, "security tokens are not enabled"); } } @@ -1491,7 +1493,7 @@ private void checkIfTokenIsValid(UserToken userToken, ActionListener listener.onResponse(null); } else { final GetRequest getRequest = client - .prepareGet(tokensIndex.aliasName(), getTokenDocumentId(userToken)).request(); + .prepareGet(tokensIndex.aliasName(), getTokenDocumentId(userToken)).request(); Consumer onFailure = ex -> listener.onFailure(traceLog("check token state", userToken.getId(), ex)); tokensIndex.checkIndexVersionThenExecute(listener::onFailure, () -> { executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN, getRequest, @@ -1598,8 +1600,8 @@ String prependVersionAndEncodeAccessToken(Version version, String accessToken) t static String prependVersionAndEncodeRefreshToken(Version version, String payload) { try (ByteArrayOutputStream os = new ByteArrayOutputStream(); - OutputStream base64 = Base64.getEncoder().wrap(os); - StreamOutput out = new OutputStreamStreamOutput(base64)) { + OutputStream base64 = Base64.getEncoder().wrap(os); + StreamOutput out = new OutputStreamStreamOutput(base64)) { out.setVersion(version); Version.writeVersion(version, out); out.writeString(payload); @@ -1610,6 +1612,7 @@ static String prependVersionAndEncodeRefreshToken(Version version, String payloa } // public for testing + /** * Unpacks a base64 encoded pair of a version tag and String payload. */ @@ -1664,14 +1667,14 @@ private void getKeyAsync(BytesKey decodedSalt, KeyAndCache keyAndCache, ActionLi * some additional latency. */ client.threadPool().executor(THREAD_POOL_NAME) - .submit(new KeyComputingRunnable(decodedSalt, keyAndCache, listener)); + .submit(new KeyComputingRunnable(decodedSalt, keyAndCache, listener)); } } private static String decryptTokenId(byte[] encryptedTokenId, Cipher cipher, Version version) throws IOException { try (ByteArrayInputStream bais = new ByteArrayInputStream(encryptedTokenId); - CipherInputStream cis = new CipherInputStream(bais, cipher); - StreamInput decryptedInput = new InputStreamStreamInput(cis)) { + CipherInputStream cis = new CipherInputStream(bais, cipher); + StreamInput decryptedInput = new InputStreamStreamInput(cis)) { decryptedInput.setVersion(version); return decryptedInput.readString(); } @@ -1733,7 +1736,7 @@ private static ElasticsearchSecurityException expiredTokenException() { */ private static ElasticsearchSecurityException malformedTokenException() { ElasticsearchSecurityException e = - new ElasticsearchSecurityException("token malformed", RestStatus.UNAUTHORIZED); + new ElasticsearchSecurityException("token malformed", RestStatus.UNAUTHORIZED); e.addHeader("WWW-Authenticate", MALFORMED_TOKEN_WWW_AUTH_VALUE); return e; } @@ -1968,14 +1971,14 @@ void rotateKeysOnMaster(ActionListener listener) { clusterService.submitStateUpdateTask("publish next key to prepare key rotation", new TokenMetadataPublishAction( tokenMetaData, ActionListener.wrap((res) -> { - if (res.isAcknowledged()) { - TokenMetaData metaData = rotateToSpareKey(); - clusterService.submitStateUpdateTask("publish next key to prepare key rotation", - new TokenMetadataPublishAction(metaData, listener)); - } else { - listener.onFailure(new IllegalStateException("not acked")); - } - }, listener::onFailure))); + if (res.isAcknowledged()) { + TokenMetaData metaData = rotateToSpareKey(); + clusterService.submitStateUpdateTask("publish next key to prepare key rotation", + new TokenMetadataPublishAction(metaData, listener)); + } else { + listener.onFailure(new IllegalStateException("not acked")); + } + }, listener::onFailure))); } private final class TokenMetadataPublishAction extends AckedClusterStateUpdateTask { @@ -2180,7 +2183,8 @@ static final class RefreshTokenStatus { private final String associatedUser; private final String associatedRealm; private final boolean refreshed; - @Nullable private final Instant refreshInstant; + @Nullable + private final Instant refreshInstant; @Nullable private final String supersedingTokens; @Nullable @@ -2218,7 +2222,8 @@ boolean isRefreshed() { return refreshed; } - @Nullable Instant getRefreshInstant() { + @Nullable + Instant getRefreshInstant() { return refreshInstant; } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/FeatureNotEnabledException.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/FeatureNotEnabledException.java new file mode 100644 index 0000000000000..752995c77ec52 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/FeatureNotEnabledException.java @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.security.support; + +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.ElasticsearchStatusException; +import org.elasticsearch.rest.RestStatus; + +public class FeatureNotEnabledException extends ElasticsearchException { + + public static final String DISABLED_FEATURE_METADATA = "es.disabled.feature"; + + /** + * The features names here are constants that form part of our API contract. + * Callers (e.g. Kibana) may be dependent on these strings. Do not change them without consideration of BWC. + */ + public enum Feature { + TOKEN_SERVICE("security_tokens"), + API_KEY_SERVICE("api_keys"); + + private final String featureName; + + Feature(String featureName) { + this.featureName = featureName; + } + } + + public FeatureNotEnabledException(Feature feature, String message, Object... args) { + super(message, args); + addMetadata(DISABLED_FEATURE_METADATA, feature.featureName); + } + + @Override + public RestStatus status() { + return RestStatus.BAD_REQUEST; + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java index e1f0c02e504b3..ce1339658b601 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java @@ -6,6 +6,7 @@ package org.elasticsearch.xpack.security.authc; +import org.elasticsearch.ElasticsearchException; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.PlainActionFuture; @@ -23,6 +24,7 @@ import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.test.ClusterServiceUtils; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.TestMatchers; import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.XPackSettings; @@ -38,6 +40,7 @@ import org.elasticsearch.xpack.security.authc.ApiKeyService.ApiKeyRoleDescriptors; import org.elasticsearch.xpack.security.authc.ApiKeyService.CachedApiKeyHashResult; import org.elasticsearch.xpack.security.authz.store.NativePrivilegeStore; +import org.elasticsearch.xpack.security.support.FeatureNotEnabledException; import org.elasticsearch.xpack.security.support.SecurityIndexManager; import org.elasticsearch.xpack.security.test.SecurityMocks; import org.junit.After; @@ -59,8 +62,10 @@ import java.util.concurrent.Semaphore; import java.util.concurrent.atomic.AtomicInteger; +import static org.elasticsearch.test.TestMatchers.throwableWithMessage; import static org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR; import static org.hamcrest.Matchers.arrayContaining; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; @@ -435,6 +440,18 @@ public void testGetRolesForApiKey() throws Exception { } } + public void testApiKeyServiceDisabled() throws Exception { + final Settings settings = Settings.builder().put(XPackSettings.API_KEY_SERVICE_ENABLED_SETTING.getKey(), false).build(); + final ApiKeyService service = createApiKeyService(settings); + + ElasticsearchException e = expectThrows(ElasticsearchException.class, + () -> service.getApiKeys(randomAlphaOfLength(6), randomAlphaOfLength(8), null, null, new PlainActionFuture<>())); + + assertThat(e, instanceOf(FeatureNotEnabledException.class)); + assertThat(e, throwableWithMessage("api keys are not enabled")); + assertThat(e.getMetadata(FeatureNotEnabledException.DISABLED_FEATURE_METADATA), contains("api_keys")); + } + public void testApiKeyCache() { final String apiKey = randomAlphaOfLength(16); Hasher hasher = randomFrom(Hasher.PBKDF2, Hasher.BCRYPT4, Hasher.BCRYPT); 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 4e8d270b89d57..660377b2e8fb9 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 @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.security.authc; +import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; @@ -43,6 +44,7 @@ import org.elasticsearch.test.ClusterServiceUtils; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.EqualsHashCodeTestUtils; +import org.elasticsearch.test.TestMatchers; import org.elasticsearch.threadpool.FixedExecutorBuilder; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.XPackSettings; @@ -54,6 +56,7 @@ import org.elasticsearch.xpack.core.security.authc.support.TokensInvalidationResult; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.watcher.watch.ClockMock; +import org.elasticsearch.xpack.security.support.FeatureNotEnabledException; import org.elasticsearch.xpack.security.support.SecurityIndexManager; import org.elasticsearch.xpack.security.test.SecurityMocks; import org.hamcrest.Matchers; @@ -63,7 +66,6 @@ import org.junit.BeforeClass; import javax.crypto.SecretKey; - import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; @@ -79,8 +81,11 @@ import static java.time.Clock.systemUTC; import static org.elasticsearch.repositories.blobstore.ESBlobStoreRepositoryIntegTestCase.randomBytes; import static org.elasticsearch.test.ClusterServiceUtils.setState; +import static org.elasticsearch.test.TestMatchers.throwableWithMessage; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.mockito.Matchers.any; @@ -560,20 +565,21 @@ public void testTokenServiceDisabled() throws Exception { .put(XPackSettings.TOKEN_SERVICE_ENABLED_SETTING.getKey(), false) .build(), Clock.systemUTC(), client, licenseState, securityContext, securityMainIndex, securityTokensIndex, clusterService); - IllegalStateException e = expectThrows(IllegalStateException.class, + ElasticsearchException e = expectThrows(ElasticsearchException.class, () -> tokenService.createOAuth2Tokens(null, null, null, true, null)); - assertEquals("security tokens are not enabled", e.getMessage()); + assertThat(e, throwableWithMessage("security tokens are not enabled")); + assertThat(e, instanceOf(FeatureNotEnabledException.class)); + assertThat(e.getMetadata(FeatureNotEnabledException.DISABLED_FEATURE_METADATA), contains("security_tokens")); PlainActionFuture future = new PlainActionFuture<>(); tokenService.getAndValidateToken(null, future); assertNull(future.get()); - e = expectThrows(IllegalStateException.class, () -> { - PlainActionFuture invalidateFuture = new PlainActionFuture<>(); - tokenService.invalidateAccessToken((String) null, invalidateFuture); - invalidateFuture.actionGet(); - }); - assertEquals("security tokens are not enabled", e.getMessage()); + PlainActionFuture invalidateFuture = new PlainActionFuture<>(); + e = expectThrows(ElasticsearchException.class, () -> tokenService.invalidateAccessToken((String) null, invalidateFuture)); + assertThat(e, throwableWithMessage("security tokens are not enabled")); + assertThat(e, instanceOf(FeatureNotEnabledException.class)); + assertThat(e.getMetadata(FeatureNotEnabledException.DISABLED_FEATURE_METADATA), contains("security_tokens")); } public void testBytesKeyEqualsHashCode() { From 0c39f54ae39454c0ef31a51e06f4306186194dab Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Wed, 26 Feb 2020 23:36:55 +1100 Subject: [PATCH 2/4] Revert format changes --- .../xpack/security/authc/TokenService.java | 457 +++++++++--------- 1 file changed, 227 insertions(+), 230 deletions(-) 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 b83f8d63d75ca..24b40ba2922ae 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 @@ -170,18 +170,18 @@ public final class TokenService { private static final int VERSION_BYTES = 4; private static final String ENCRYPTION_CIPHER = "AES/GCM/NoPadding"; private static final String EXPIRED_TOKEN_WWW_AUTH_VALUE = "Bearer realm=\"" + XPackField.SECURITY + - "\", error=\"invalid_token\", error_description=\"The access token expired\""; + "\", error=\"invalid_token\", error_description=\"The access token expired\""; private static final String MALFORMED_TOKEN_WWW_AUTH_VALUE = "Bearer realm=\"" + XPackField.SECURITY + - "\", error=\"invalid_token\", error_description=\"The access token is malformed\""; + "\", error=\"invalid_token\", error_description=\"The access token is malformed\""; private static final BackoffPolicy DEFAULT_BACKOFF = BackoffPolicy.exponentialBackoff(); public static final String THREAD_POOL_NAME = XPackField.SECURITY + "-token-key"; public static final Setting TOKEN_EXPIRATION = Setting.timeSetting("xpack.security.authc.token.timeout", - TimeValue.timeValueMinutes(20L), TimeValue.timeValueSeconds(1L), TimeValue.timeValueHours(1L), Property.NodeScope); + TimeValue.timeValueMinutes(20L), TimeValue.timeValueSeconds(1L), TimeValue.timeValueHours(1L), Property.NodeScope); public static final Setting DELETE_INTERVAL = Setting.timeSetting("xpack.security.authc.token.delete.interval", - TimeValue.timeValueMinutes(30L), Property.NodeScope); + TimeValue.timeValueMinutes(30L), Property.NodeScope); public static final Setting DELETE_TIMEOUT = Setting.timeSetting("xpack.security.authc.token.delete.timeout", - TimeValue.MINUS_ONE, Property.NodeScope); + TimeValue.MINUS_ONE, Property.NodeScope); static final String TOKEN_DOC_TYPE = "token"; private static final int HASHED_TOKEN_LENGTH = 43; @@ -238,7 +238,7 @@ public TokenService(Settings settings, Clock clock, Client client, XPackLicenseS this.expiredTokenRemover = new ExpiredTokenRemover(settings, client, this.securityMainIndex, securityTokensIndex); ensureEncryptionCiphersSupported(); KeyAndCache keyAndCache = new KeyAndCache(new KeyAndTimestamp(tokenPassphrase, createdTimeStamps.incrementAndGet()), - new BytesKey(saltArr)); + new BytesKey(saltArr)); keyCache = new TokenKeys(Collections.singletonMap(keyAndCache.getKeyHash(), keyAndCache), keyAndCache.getKeyHash()); this.clusterService = clusterService; initialize(clusterService); @@ -270,7 +270,7 @@ public void createOAuth2Tokens(Authentication authentication, Authentication ori //public for testing public void createOAuth2Tokens(String accessToken, String refreshToken, Authentication authentication, Authentication originatingClientAuth, - Map metadata, ActionListener> listener) { + Map metadata, ActionListener> listener) { // the created token is compatible with the oldest node version in the cluster final Version tokenVersion = getTokenVersionCompatibility(); // tokens moved to a separate index in newer versions @@ -282,29 +282,29 @@ public void createOAuth2Tokens(String accessToken, String refreshToken, Authenti * Create an access token and optionally a refresh token as well from predefined values, based on the provided authentication and * metadata. * - * @param accessToken The predefined seed value for the access token. This will then be - *
    - *
  • Encrypted before stored for versions before {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • - *
  • Hashed before stored for versions after {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • - *
  • Stored in the security index for versions up to {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • - *
  • Stored in a specific security tokens index for versions after {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • - *
  • Prepended with a version ID and encoded with Base64 before returned to the caller of the APIs
  • - *
- * @param refreshToken The predefined seed value for the access token. This will then be - *
    - *
  • Hashed before stored for versions after {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • - *
  • Stored in the security index for versions up to {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • - *
  • Stored in a specific security tokens index for versions after {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • - *
  • Prepended with a version ID and encoded with Base64 before returned to the caller of the APIs for - * versions after {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • - *
- * @param tokenVersion The version of the nodes with which these tokens will be compatible. - * @param tokensIndex The security tokens index - * @param authentication The authentication object representing the user for which the tokens are created + * @param accessToken The predefined seed value for the access token. This will then be + *
    + *
  • Encrypted before stored for versions before {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • + *
  • Hashed before stored for versions after {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • + *
  • Stored in the security index for versions up to {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • + *
  • Stored in a specific security tokens index for versions after {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • + *
  • Prepended with a version ID and encoded with Base64 before returned to the caller of the APIs
  • + *
+ * @param refreshToken The predefined seed value for the access token. This will then be + *
    + *
  • Hashed before stored for versions after {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • + *
  • Stored in the security index for versions up to {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • + *
  • Stored in a specific security tokens index for versions after {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • + *
  • Prepended with a version ID and encoded with Base64 before returned to the caller of the APIs for + * versions after {@link #VERSION_TOKENS_INDEX_INTRODUCED}
  • + *
+ * @param tokenVersion The version of the nodes with which these tokens will be compatible. + * @param tokensIndex The security tokens index + * @param authentication The authentication object representing the user for which the tokens are created * @param originatingClientAuth The authentication object representing the client that called the related API - * @param metadata A map with metadata to be stored in the token document - * @param listener The listener to call upon completion with a {@link Tuple} containing the - * serialized access token and serialized refresh token as these will be returned to the client + * @param metadata A map with metadata to be stored in the token document + * @param listener The listener to call upon completion with a {@link Tuple} containing the + * serialized access token and serialized refresh token as these will be returned to the client */ private void createOAuth2Tokens(String accessToken, String refreshToken, Version tokenVersion, SecurityIndexManager tokensIndex, Authentication authentication, Authentication originatingClientAuth, Map metadata, @@ -334,32 +334,32 @@ private void createOAuth2Tokens(String accessToken, String refreshToken, Version final String documentId = getTokenDocumentId(storedAccessToken); final IndexRequest indexTokenRequest = client.prepareIndex(tokensIndex.aliasName()).setId(documentId) - .setOpType(OpType.CREATE) - .setSource(tokenDocument, XContentType.JSON) - .setRefreshPolicy(RefreshPolicy.WAIT_UNTIL) - .request(); + .setOpType(OpType.CREATE) + .setSource(tokenDocument, XContentType.JSON) + .setRefreshPolicy(RefreshPolicy.WAIT_UNTIL) + .request(); tokensIndex.prepareIndexIfNeededThenExecute( - ex -> listener.onFailure(traceLog("prepare tokens index [" + tokensIndex.aliasName() + "]", documentId, ex)), - () -> executeAsyncWithOrigin(client, SECURITY_ORIGIN, IndexAction.INSTANCE, indexTokenRequest, - ActionListener.wrap(indexResponse -> { - if (indexResponse.getResult() == Result.CREATED) { - final String versionedAccessToken = prependVersionAndEncodeAccessToken(tokenVersion, accessToken); - if (tokenVersion.onOrAfter(VERSION_TOKENS_INDEX_INTRODUCED)) { - final String versionedRefreshToken = refreshToken != null - ? prependVersionAndEncodeRefreshToken(tokenVersion, refreshToken) - : null; - listener.onResponse(new Tuple<>(versionedAccessToken, versionedRefreshToken)); - } else { - // prior versions of the refresh token are not version-prepended, as nodes on those - // versions don't expect it. - // Such nodes might exist in a mixed cluster during a rolling upgrade. - listener.onResponse(new Tuple<>(versionedAccessToken, refreshToken)); - } - } else { - listener.onFailure(traceLog("create token", - new ElasticsearchException("failed to create token document [{}]", indexResponse))); - } - }, listener::onFailure))); + ex -> listener.onFailure(traceLog("prepare tokens index [" + tokensIndex.aliasName() + "]", documentId, ex)), + () -> executeAsyncWithOrigin(client, SECURITY_ORIGIN, IndexAction.INSTANCE, indexTokenRequest, + ActionListener.wrap(indexResponse -> { + if (indexResponse.getResult() == Result.CREATED) { + final String versionedAccessToken = prependVersionAndEncodeAccessToken(tokenVersion, accessToken); + if (tokenVersion.onOrAfter(VERSION_TOKENS_INDEX_INTRODUCED)) { + final String versionedRefreshToken = refreshToken != null + ? prependVersionAndEncodeRefreshToken(tokenVersion, refreshToken) + : null; + listener.onResponse(new Tuple<>(versionedAccessToken, versionedRefreshToken)); + } else { + // prior versions of the refresh token are not version-prepended, as nodes on those + // versions don't expect it. + // Such nodes might exist in a mixed cluster during a rolling upgrade. + listener.onResponse(new Tuple<>(versionedAccessToken, refreshToken)); + } + } else { + listener.onFailure(traceLog("create token", + new ElasticsearchException("failed to create token document [{}]", indexResponse))); + } + }, listener::onFailure))); } } @@ -402,14 +402,14 @@ void getAndValidateToken(ThreadContext ctx, ActionListener listener) */ public void getAuthenticationAndMetaData(String token, ActionListener>> listener) { decodeToken(token, ActionListener.wrap( - userToken -> { - if (userToken == null) { - listener.onFailure(new ElasticsearchSecurityException("supplied token is not valid")); - } else { - listener.onResponse(new Tuple<>(userToken.getAuthentication(), userToken.getMetadata())); - } - }, - listener::onFailure + userToken -> { + if (userToken == null) { + listener.onFailure(new ElasticsearchSecurityException("supplied token is not valid")); + } else { + listener.onResponse(new Tuple<>(userToken.getAuthentication(), userToken.getMetadata())); + } + }, + listener::onFailure )); } @@ -424,45 +424,45 @@ private void getUserTokenFromId(String userTokenId, Version tokenVersion, Action listener.onResponse(null); } else { final GetRequest getRequest = client.prepareGet(tokensIndex.aliasName(), - getTokenDocumentId(userTokenId)).request(); + getTokenDocumentId(userTokenId)).request(); final Consumer onFailure = ex -> listener.onFailure(traceLog("get token from id", userTokenId, ex)); tokensIndex.checkIndexVersionThenExecute( - ex -> listener.onFailure(traceLog("prepare tokens index [" + tokensIndex.aliasName() + "]", userTokenId, ex)), + ex -> listener.onFailure(traceLog("prepare tokens index [" + tokensIndex.aliasName() +"]", userTokenId, ex)), () -> executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN, getRequest, - ActionListener.wrap(response -> { - if (response.isExists()) { - Map accessTokenSource = - (Map) response.getSource().get("access_token"); - if (accessTokenSource == null) { - onFailure.accept(new IllegalStateException( - "token document is missing the access_token field")); - } else if (accessTokenSource.containsKey("user_token") == false) { - onFailure.accept(new IllegalStateException( - "token document is missing the user_token field")); + ActionListener.wrap(response -> { + if (response.isExists()) { + Map accessTokenSource = + (Map) response.getSource().get("access_token"); + if (accessTokenSource == null) { + onFailure.accept(new IllegalStateException( + "token document is missing the access_token field")); + } else if (accessTokenSource.containsKey("user_token") == false) { + onFailure.accept(new IllegalStateException( + "token document is missing the user_token field")); + } else { + Map userTokenSource = + (Map) accessTokenSource.get("user_token"); + listener.onResponse(UserToken.fromSourceMap(userTokenSource)); + } } else { - Map userTokenSource = - (Map) accessTokenSource.get("user_token"); - listener.onResponse(UserToken.fromSourceMap(userTokenSource)); + // The chances of a random token string decoding to something that we can read is minimal, so + // we assume that this was a token we have created but is now expired/revoked and deleted + logger.trace("The access token [{}] is expired and already deleted", userTokenId); + listener.onResponse(null); } - } else { - // The chances of a random token string decoding to something that we can read is minimal, so - // we assume that this was a token we have created but is now expired/revoked and deleted - logger.trace("The access token [{}] is expired and already deleted", userTokenId); - listener.onResponse(null); - } - }, e -> { - // if the index or the shard is not there / available we assume that - // the token is not valid - if (isShardNotAvailableException(e)) { - logger.warn("failed to get access token [{}] because index [{}] is not available", userTokenId, - tokensIndex.aliasName()); - listener.onResponse(null); - } else { - logger.error(new ParameterizedMessage("failed to get access token [{}]", userTokenId), e); - listener.onFailure(e); - } - }), client::get) - ); + }, e -> { + // if the index or the shard is not there / available we assume that + // the token is not valid + if (isShardNotAvailableException(e)) { + logger.warn("failed to get access token [{}] because index [{}] is not available", userTokenId, + tokensIndex.aliasName()); + listener.onResponse(null); + } else { + logger.error(new ParameterizedMessage("failed to get access token [{}]", userTokenId), e); + listener.onFailure(e); + } + }), client::get) + ); } } @@ -533,7 +533,7 @@ void decodeToken(String token, ActionListener listener) { } catch (Exception e) { // could happen with a token that is not ours if (logger.isDebugEnabled()) { - logger.debug("built in token service unable to decode token", e); + logger.debug("built in token service unable to decode token", e); } else { logger.warn("built in token service unable to decode token"); } @@ -583,7 +583,7 @@ public void invalidateAccessToken(UserToken userToken, ActionListenerrefresh_token.invalidated field to true * * @param refreshToken The string representation of the refresh token - * @param listener the listener to notify upon completion + * @param listener the listener to notify upon completion */ public void invalidateRefreshToken(String refreshToken, ActionListener listener) { ensureEnabled(); @@ -606,7 +606,7 @@ public void invalidateRefreshToken(String refreshToken, ActionListener userTokens, ActionListener listener) { maybeStartTokenRemover(); @@ -655,15 +655,15 @@ private void invalidateAllTokens(Collection userTokens, ActionListene // access tokens while we invalidate the access tokens we currently know about final Iterator backoff = DEFAULT_BACKOFF.iterator(); indexInvalidation(userTokens, backoff, "refresh_token", null, ActionListener.wrap(result -> - indexInvalidation(userTokens, backoff, "access_token", result, listener), - listener::onFailure)); + indexInvalidation(userTokens, backoff, "access_token", result, listener), + listener::onFailure)); } /** * Invalidates access and/or refresh tokens associated to a user token (coexisting in the same token document) */ private void indexInvalidation(Collection userTokens, Iterator backoff, String srcPrefix, - @Nullable TokensInvalidationResult previousResult, ActionListener listener) { + @Nullable TokensInvalidationResult previousResult, ActionListener listener) { final Set idsOfRecentTokens = new HashSet<>(); final Set idsOfOlderTokens = new HashSet<>(); for (UserToken userToken : userTokens) { @@ -692,14 +692,14 @@ private void indexInvalidation(Collection userTokens, Iterator tokenIds, SecurityIndexManager tokensIndexManager, Iterator backoff, String srcPrefix, @Nullable TokensInvalidationResult previousResult, @@ -711,10 +711,10 @@ private void indexInvalidation(Collection tokenIds, SecurityIndexManager BulkRequestBuilder bulkRequestBuilder = client.prepareBulk(); for (String tokenId : tokenIds) { UpdateRequest request = client - .prepareUpdate(tokensIndexManager.aliasName(), getTokenDocumentId(tokenId)) - .setDoc(srcPrefix, Collections.singletonMap("invalidated", true)) - .setFetchSource(srcPrefix, null) - .request(); + .prepareUpdate(tokensIndexManager.aliasName(), getTokenDocumentId(tokenId)) + .setDoc(srcPrefix, Collections.singletonMap("invalidated", true)) + .setFetchSource(srcPrefix, null) + .request(); bulkRequestBuilder.add(request); } bulkRequestBuilder.setRefreshPolicy(RefreshPolicy.WAIT_UNTIL); @@ -745,7 +745,7 @@ private void indexInvalidation(Collection tokenIds, SecurityIndexManager UpdateResponse updateResponse = bulkItemResponse.getResponse(); if (updateResponse.getResult() == DocWriteResponse.Result.UPDATED) { logger.debug(() -> new ParameterizedMessage("Invalidated [{}] for doc [{}]", - srcPrefix, updateResponse.getGetResult().getId())); + srcPrefix, updateResponse.getGetResult().getId())); invalidated.add(updateResponse.getGetResult().getId()); } else if (updateResponse.getResult() == DocWriteResponse.Result.NOOP) { previouslyInvalidated.add(updateResponse.getGetResult().getId()); @@ -754,25 +754,25 @@ private void indexInvalidation(Collection tokenIds, SecurityIndexManager } if (retryTokenDocIds.isEmpty() == false && backoff.hasNext()) { logger.debug("failed to invalidate [{}] tokens out of [{}], retrying to invalidate these too", - retryTokenDocIds.size(), tokenIds.size()); + retryTokenDocIds.size(), tokenIds.size()); final TokensInvalidationResult incompleteResult = new TokensInvalidationResult(invalidated, - previouslyInvalidated, failedRequestResponses); + previouslyInvalidated, failedRequestResponses); final Runnable retryWithContextRunnable = client.threadPool().getThreadContext() - .preserveContext(() -> indexInvalidation(retryTokenDocIds, tokensIndexManager, backoff, - srcPrefix, incompleteResult, listener)); + .preserveContext(() -> indexInvalidation(retryTokenDocIds, tokensIndexManager, backoff, + srcPrefix, incompleteResult, listener)); client.threadPool().schedule(retryWithContextRunnable, backoff.next(), GENERIC); } else { if (retryTokenDocIds.isEmpty() == false) { logger.warn("failed to invalidate [{}] tokens out of [{}] after all retries", retryTokenDocIds.size(), - tokenIds.size()); + tokenIds.size()); for (String retryTokenDocId : retryTokenDocIds) { failedRequestResponses.add( - new ElasticsearchException("Error invalidating [{}] with doc id [{}] after retries exhausted", - srcPrefix, retryTokenDocId)); + new ElasticsearchException("Error invalidating [{}] with doc id [{}] after retries exhausted", + srcPrefix, retryTokenDocId)); } } final TokensInvalidationResult result = new TokensInvalidationResult(invalidated, previouslyInvalidated, - failedRequestResponses); + failedRequestResponses); listener.onResponse(result); } }, e -> { @@ -781,8 +781,8 @@ private void indexInvalidation(Collection tokenIds, SecurityIndexManager if (isShardNotAvailableException(cause) && backoff.hasNext()) { logger.debug("failed to invalidate tokens, retrying "); final Runnable retryWithContextRunnable = client.threadPool().getThreadContext() - .preserveContext(() -> indexInvalidation(tokenIds, tokensIndexManager, backoff, srcPrefix, - previousResult, listener)); + .preserveContext(() -> indexInvalidation(tokenIds, tokensIndexManager, backoff, srcPrefix, + previousResult, listener)); client.threadPool().schedule(retryWithContextRunnable, backoff.next(), GENERIC); } else { listener.onFailure(e); @@ -795,8 +795,8 @@ private void indexInvalidation(Collection tokenIds, SecurityIndexManager * Called by the transport action in order to start the process of refreshing a token. * * @param refreshToken The refresh token as provided by the client - * @param listener The listener to call upon completion with a {@link Tuple} containing the - * serialized access token and serialized refresh token as these will be returned to the client + * @param listener The listener to call upon completion with a {@link Tuple} containing the + * serialized access token and serialized refresh token as these will be returned to the client */ public void refreshToken(String refreshToken, ActionListener> listener) { ensureEnabled(); @@ -866,7 +866,7 @@ private void findTokenFromRefreshToken(String refreshToken, SecurityIndexManager final TimeValue backofTimeValue = backoff.next(); logger.debug("retrying after [{}] back off", backofTimeValue); final Runnable retryWithContextRunnable = client.threadPool().getThreadContext() - .preserveContext(() -> findTokenFromRefreshToken(refreshToken, tokensIndexManager, backoff, listener)); + .preserveContext(() -> findTokenFromRefreshToken(refreshToken, tokensIndexManager, backoff, listener)); client.threadPool().schedule(retryWithContextRunnable, backofTimeValue, GENERIC); } else { logger.warn("failed to find token from refresh token after all retries"); @@ -882,11 +882,11 @@ private void findTokenFromRefreshToken(String refreshToken, SecurityIndexManager maybeRetryOnFailure.accept(invalidGrantException("could not refresh the requested token")); } else { final SearchRequest request = client.prepareSearch(tokensIndexManager.aliasName()) - .setQuery(QueryBuilders.boolQuery() - .filter(QueryBuilders.termQuery("doc_type", TOKEN_DOC_TYPE)) - .filter(QueryBuilders.termQuery("refresh_token.token", refreshToken))) - .seqNoAndPrimaryTerm(true) - .request(); + .setQuery(QueryBuilders.boolQuery() + .filter(QueryBuilders.termQuery("doc_type", TOKEN_DOC_TYPE)) + .filter(QueryBuilders.termQuery("refresh_token.token", refreshToken))) + .seqNoAndPrimaryTerm(true) + .request(); tokensIndexManager.checkIndexVersionThenExecute(listener::onFailure, () -> executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN, request, ActionListener.wrap(searchResponse -> { @@ -968,19 +968,19 @@ private void innerRefresh(String refreshToken, String tokenDocId, Map listener.onFailure(traceLog("prepare index [" + refreshedTokenIndex.aliasName() + "]", ex)), - () -> executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN, updateRequest.request(), + ex -> listener.onFailure(traceLog("prepare index [" + refreshedTokenIndex.aliasName() + "]", ex)), + () -> executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN, updateRequest.request(), ActionListener.wrap(updateResponse -> { if (updateResponse.getResult() == DocWriteResponse.Result.UPDATED) { logger.debug(() -> new ParameterizedMessage("updated the original token document to {}", - updateResponse.getGetResult().sourceAsMap())); + updateResponse.getGetResult().sourceAsMap())); final Tuple parsedTokens = parseTokensFromDocument(source, null); final UserToken toRefreshUserToken = parsedTokens.v1(); createOAuth2Tokens(newAccessTokenString, newRefreshTokenString, newTokenVersion, @@ -988,14 +988,14 @@ private void innerRefresh(String refreshToken, String tokenDocId, Map innerRefresh(refreshToken, tokenDocId, source, seqNo, primaryTerm, clientAuth, backoff, refreshRequested, listener)); client.threadPool().schedule(retryWithContextRunnable, backoff.next(), GENERIC); } else { logger.info("failed to update the original token document [{}] after all retries, the update result was [{}]. ", - tokenDocId, updateResponse.getResult()); + tokenDocId, updateResponse.getResult()); listener.onFailure(invalidGrantException("could not refresh the requested token")); } }, e -> { @@ -1021,7 +1021,7 @@ public void onFailure(Exception e) { if (backoff.hasNext()) { logger.info("could not get token document [{}] for refresh, retrying", tokenDocId); final Runnable retryWithContextRunnable = client.threadPool().getThreadContext() - .preserveContext(() -> getTokenDocAsync(tokenDocId, refreshedTokenIndex, this)); + .preserveContext(() -> getTokenDocAsync(tokenDocId, refreshedTokenIndex, this)); client.threadPool().schedule(retryWithContextRunnable, backoff.next(), GENERIC); } else { logger.warn("could not get token document [{}] for refresh after all retries", tokenDocId); @@ -1054,11 +1054,11 @@ public void onFailure(Exception e) { * Decrypts the values of the superseding access token and the refresh token, using a key derived from the superseded refresh token. It * encodes the version and serializes the tokens before calling the listener, in the same manner as {@link #createOAuth2Tokens } does. * - * @param refreshToken The refresh token that the user sent in the request, used to derive the decryption key + * @param refreshToken The refresh token that the user sent in the request, used to derive the decryption key * @param refreshTokenStatus The {@link RefreshTokenStatus} containing information about the superseding tokens as retrieved from the - * index - * @param listener The listener to call upon completion with a {@link Tuple} containing the - * serialized access token and serialized refresh token as these will be returned to the client + * index + * @param listener The listener to call upon completion with a {@link Tuple} containing the + * serialized access token and serialized refresh token as these will be returned to the client */ void decryptAndReturnSupersedingTokens(String refreshToken, RefreshTokenStatus refreshTokenStatus, ActionListener> listener) { @@ -1096,8 +1096,8 @@ String encryptSupersedingTokens(String supersedingAccessToken, String supersedin private void getTokenDocAsync(String tokenDocId, SecurityIndexManager tokensIndex, ActionListener listener) { final GetRequest getRequest = client.prepareGet(tokensIndex.aliasName(), tokenDocId).request(); tokensIndex.checkIndexVersionThenExecute( - ex -> listener.onFailure(traceLog("prepare tokens index [" + tokensIndex.aliasName() + "]", tokenDocId, ex)), - () -> executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN, getRequest, listener, client::get)); + ex -> listener.onFailure(traceLog("prepare tokens index [" + tokensIndex.aliasName() + "]", tokenDocId, ex)), + () -> executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN, getRequest, listener, client::get)); } Version getTokenVersionCompatibility() { @@ -1156,12 +1156,12 @@ private static Optional checkClientCanRefresh(Re Authentication clientAuthentication) { if (clientAuthentication.getUser().principal().equals(refreshToken.getAssociatedUser()) == false) { logger.warn("Token was originally created by [{}] but [{}] attempted to refresh it", refreshToken.getAssociatedUser(), - clientAuthentication.getUser().principal()); + clientAuthentication.getUser().principal()); return Optional.of(invalidGrantException("tokens must be refreshed by the creating client")); } else if (clientAuthentication.getAuthenticatedBy().getName().equals(refreshToken.getAssociatedRealm()) == false) { logger.warn("[{}] created the refresh token while authenticated by [{}] but is now authenticated by [{}]", - refreshToken.getAssociatedUser(), refreshToken.getAssociatedRealm(), - clientAuthentication.getAuthenticatedBy().getName()); + refreshToken.getAssociatedUser(), refreshToken.getAssociatedRealm(), + clientAuthentication.getAuthenticatedBy().getName()); return Optional.of(invalidGrantException("tokens must be refreshed by the creating client")); } else { return Optional.empty(); @@ -1193,7 +1193,7 @@ private static Map getUserTokenSourceMap(Map sou * short span of time (30 s). * * @return An {@code Optional} containing the exception in case this refresh token cannot be reused, or an empty Optional if - * refreshing is allowed. + * refreshing is allowed. */ private static Optional checkMultipleRefreshes(Instant refreshRequested, RefreshTokenStatus refreshTokenStatus) { @@ -1204,7 +1204,7 @@ private static Optional checkMultipleRefreshes(I } if (refreshRequested.isBefore(refreshTokenStatus.getRefreshInstant().minus(30L, ChronoUnit.SECONDS))) { return Optional - .of(invalidGrantException("token has been refreshed more than 30 seconds in the future, clock skew too great")); + .of(invalidGrantException("token has been refreshed more than 30 seconds in the future, clock skew too great")); } } else { return Optional.of(invalidGrantException("token has already been refreshed")); @@ -1234,30 +1234,30 @@ public void findActiveTokensForRealm(String realmName, @Nullable Predicate supplier = client.threadPool().getThreadContext().newRestorableContext(false); try (ThreadContext.StoredContext ignore = client.threadPool().getThreadContext().stashWithOrigin(SECURITY_ORIGIN)) { final SearchRequest request = client.prepareSearch(indicesWithTokens.toArray(new String[0])) - .setScroll(DEFAULT_KEEPALIVE_SETTING.get(settings)) - .setQuery(boolQuery) - .setVersion(false) - .setSize(1000) - .setFetchSource(true) - .request(); + .setScroll(DEFAULT_KEEPALIVE_SETTING.get(settings)) + .setQuery(boolQuery) + .setVersion(false) + .setSize(1000) + .setFetchSource(true) + .request(); ScrollHelper.fetchAllByEntity(client, request, new ContextPreservingActionListener<>(supplier, listener), - (SearchHit hit) -> filterAndParseHit(hit, filter)); + (SearchHit hit) -> filterAndParseHit(hit, filter)); } } }, listener::onFailure)); @@ -1282,29 +1282,29 @@ public void findActiveTokensForUser(String username, ActionListener supplier = client.threadPool().getThreadContext().newRestorableContext(false); try (ThreadContext.StoredContext ignore = client.threadPool().getThreadContext().stashWithOrigin(SECURITY_ORIGIN)) { final SearchRequest request = client.prepareSearch(indicesWithTokens.toArray(new String[0])) - .setScroll(DEFAULT_KEEPALIVE_SETTING.get(settings)) - .setQuery(boolQuery) - .setVersion(false) - .setSize(1000) - .setFetchSource(true) - .request(); + .setScroll(DEFAULT_KEEPALIVE_SETTING.get(settings)) + .setQuery(boolQuery) + .setVersion(false) + .setSize(1000) + .setFetchSource(true) + .request(); ScrollHelper.fetchAllByEntity(client, request, new ContextPreservingActionListener<>(supplier, listener), - (SearchHit hit) -> filterAndParseHit(hit, isOfUser(username))); + (SearchHit hit) -> filterAndParseHit(hit, isOfUser(username))); } } }, listener::onFailure)); @@ -1331,8 +1331,8 @@ private void sourceIndicesWithTokensAndRun(ActionListener> listener } if (false == frozenTokensIndex.isIndexUpToDate()) { listener.onFailure(new IllegalStateException( - "Index [" + frozenTokensIndex.aliasName() + "] is not on the current version. Features relying on the index" - + " will not be available until the upgrade API is run on the index")); + "Index [" + frozenTokensIndex.aliasName() + "] is not on the current version. Features relying on the index" + + " will not be available until the upgrade API is run on the index")); return; } indicesWithTokens.add(frozenTokensIndex.aliasName()); @@ -1341,15 +1341,15 @@ private void sourceIndicesWithTokensAndRun(ActionListener> listener if (frozenMainIndex.indexExists()) { // main security index _might_ contain tokens if the tokens index has been created recently if (false == frozenTokensIndex.indexExists() || frozenTokensIndex.getCreationTime() - .isAfter(clock.instant().minus(ExpiredTokenRemover.MAXIMUM_TOKEN_LIFETIME_HOURS, ChronoUnit.HOURS))) { + .isAfter(clock.instant().minus(ExpiredTokenRemover.MAXIMUM_TOKEN_LIFETIME_HOURS, ChronoUnit.HOURS))) { if (false == frozenMainIndex.isAvailable()) { listener.onFailure(frozenMainIndex.getUnavailableReason()); return; } if (false == frozenMainIndex.isIndexUpToDate()) { listener.onFailure(new IllegalStateException( - "Index [" + frozenMainIndex.aliasName() + "] is not on the current version. Features relying on the index" - + " will not be available until the upgrade API is run on the index")); + "Index [" + frozenMainIndex.aliasName() + "] is not on the current version. Features relying on the index" + + " will not be available until the upgrade API is run on the index")); return; } indicesWithTokens.add(frozenMainIndex.aliasName()); @@ -1372,17 +1372,17 @@ private BytesReference createTokenDocument(UserToken userToken, @Nullable String .field("invalidated", false) .field("refreshed", false) .startObject("client") - .field("type", "unassociated_client") - .field("user", originatingClientAuth.getUser().principal()) - .field("realm", originatingClientAuth.getAuthenticatedBy().getName()) + .field("type", "unassociated_client") + .field("user", originatingClientAuth.getUser().principal()) + .field("realm", originatingClientAuth.getAuthenticatedBy().getName()) .endObject() .endObject(); } builder.startObject("access_token") - .field("invalidated", false) - .field("user_token", userToken) - .field("realm", userToken.getAuthentication().getAuthenticatedBy().getName()) - .endObject(); + .field("invalidated", false) + .field("user_token", userToken) + .field("realm", userToken.getAuthentication().getAuthenticatedBy().getName()) + .endObject(); builder.endObject(); return BytesReference.bytes(builder); } catch (IOException e) { @@ -1406,7 +1406,7 @@ private static Predicate> isOfUser(String username) { } private Tuple filterAndParseHit(SearchHit hit, @Nullable Predicate> filter) - throws IllegalStateException, DateTimeException { + throws IllegalStateException, DateTimeException { final Map source = hit.getSourceAsMap(); if (source == null) { throw new IllegalStateException("token document did not have source but source should have been fetched"); @@ -1423,7 +1423,7 @@ private Tuple filterAndParseHit(SearchHit hit, @Nullable Pred * satisfy it */ private Tuple parseTokensFromDocument(Map source, @Nullable Predicate> filter) - throws IllegalStateException, DateTimeException { + throws IllegalStateException, DateTimeException { final String hashedRefreshToken = (String) ((Map) source.get("refresh_token")).get("token"); final Map userTokenSource = (Map) ((Map) source.get("access_token")).get("user_token"); @@ -1493,7 +1493,7 @@ private void checkIfTokenIsValid(UserToken userToken, ActionListener listener.onResponse(null); } else { final GetRequest getRequest = client - .prepareGet(tokensIndex.aliasName(), getTokenDocumentId(userToken)).request(); + .prepareGet(tokensIndex.aliasName(), getTokenDocumentId(userToken)).request(); Consumer onFailure = ex -> listener.onFailure(traceLog("check token state", userToken.getId(), ex)); tokensIndex.checkIndexVersionThenExecute(listener::onFailure, () -> { executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN, getRequest, @@ -1600,8 +1600,8 @@ String prependVersionAndEncodeAccessToken(Version version, String accessToken) t static String prependVersionAndEncodeRefreshToken(Version version, String payload) { try (ByteArrayOutputStream os = new ByteArrayOutputStream(); - OutputStream base64 = Base64.getEncoder().wrap(os); - StreamOutput out = new OutputStreamStreamOutput(base64)) { + OutputStream base64 = Base64.getEncoder().wrap(os); + StreamOutput out = new OutputStreamStreamOutput(base64)) { out.setVersion(version); Version.writeVersion(version, out); out.writeString(payload); @@ -1612,7 +1612,6 @@ static String prependVersionAndEncodeRefreshToken(Version version, String payloa } // public for testing - /** * Unpacks a base64 encoded pair of a version tag and String payload. */ @@ -1667,14 +1666,14 @@ private void getKeyAsync(BytesKey decodedSalt, KeyAndCache keyAndCache, ActionLi * some additional latency. */ client.threadPool().executor(THREAD_POOL_NAME) - .submit(new KeyComputingRunnable(decodedSalt, keyAndCache, listener)); + .submit(new KeyComputingRunnable(decodedSalt, keyAndCache, listener)); } } private static String decryptTokenId(byte[] encryptedTokenId, Cipher cipher, Version version) throws IOException { try (ByteArrayInputStream bais = new ByteArrayInputStream(encryptedTokenId); - CipherInputStream cis = new CipherInputStream(bais, cipher); - StreamInput decryptedInput = new InputStreamStreamInput(cis)) { + CipherInputStream cis = new CipherInputStream(bais, cipher); + StreamInput decryptedInput = new InputStreamStreamInput(cis)) { decryptedInput.setVersion(version); return decryptedInput.readString(); } @@ -1736,7 +1735,7 @@ private static ElasticsearchSecurityException expiredTokenException() { */ private static ElasticsearchSecurityException malformedTokenException() { ElasticsearchSecurityException e = - new ElasticsearchSecurityException("token malformed", RestStatus.UNAUTHORIZED); + new ElasticsearchSecurityException("token malformed", RestStatus.UNAUTHORIZED); e.addHeader("WWW-Authenticate", MALFORMED_TOKEN_WWW_AUTH_VALUE); return e; } @@ -1971,14 +1970,14 @@ void rotateKeysOnMaster(ActionListener listener) { clusterService.submitStateUpdateTask("publish next key to prepare key rotation", new TokenMetadataPublishAction( tokenMetaData, ActionListener.wrap((res) -> { - if (res.isAcknowledged()) { - TokenMetaData metaData = rotateToSpareKey(); - clusterService.submitStateUpdateTask("publish next key to prepare key rotation", - new TokenMetadataPublishAction(metaData, listener)); - } else { - listener.onFailure(new IllegalStateException("not acked")); - } - }, listener::onFailure))); + if (res.isAcknowledged()) { + TokenMetaData metaData = rotateToSpareKey(); + clusterService.submitStateUpdateTask("publish next key to prepare key rotation", + new TokenMetadataPublishAction(metaData, listener)); + } else { + listener.onFailure(new IllegalStateException("not acked")); + } + }, listener::onFailure))); } private final class TokenMetadataPublishAction extends AckedClusterStateUpdateTask { @@ -2183,8 +2182,7 @@ static final class RefreshTokenStatus { private final String associatedUser; private final String associatedRealm; private final boolean refreshed; - @Nullable - private final Instant refreshInstant; + @Nullable private final Instant refreshInstant; @Nullable private final String supersedingTokens; @Nullable @@ -2222,8 +2220,7 @@ boolean isRefreshed() { return refreshed; } - @Nullable - Instant getRefreshInstant() { + @Nullable Instant getRefreshInstant() { return refreshInstant; } From d3caf0ea9c0d1ed68327f55f2c6cdee1658e30e9 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Wed, 26 Feb 2020 23:54:11 +1100 Subject: [PATCH 3/4] Fix imports --- .../xpack/security/support/FeatureNotEnabledException.java | 1 - .../elasticsearch/xpack/security/authc/ApiKeyServiceTests.java | 1 - .../elasticsearch/xpack/security/authc/TokenServiceTests.java | 1 - 3 files changed, 3 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/FeatureNotEnabledException.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/FeatureNotEnabledException.java index 752995c77ec52..d79c49657d9fd 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/FeatureNotEnabledException.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/FeatureNotEnabledException.java @@ -7,7 +7,6 @@ package org.elasticsearch.xpack.security.support; import org.elasticsearch.ElasticsearchException; -import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.rest.RestStatus; public class FeatureNotEnabledException extends ElasticsearchException { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java index ce1339658b601..880c603de9866 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java @@ -24,7 +24,6 @@ import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.test.ClusterServiceUtils; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.test.TestMatchers; import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.XPackSettings; 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 660377b2e8fb9..1a846e21fdaa0 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 @@ -44,7 +44,6 @@ import org.elasticsearch.test.ClusterServiceUtils; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.EqualsHashCodeTestUtils; -import org.elasticsearch.test.TestMatchers; import org.elasticsearch.threadpool.FixedExecutorBuilder; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.XPackSettings; From 25538dd52b98cac993814bf54e1cbdfa6584bb9b Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Thu, 5 Mar 2020 15:38:05 +1100 Subject: [PATCH 4/4] Add comments about string literals --- .../elasticsearch/xpack/security/authc/ApiKeyServiceTests.java | 2 ++ .../elasticsearch/xpack/security/authc/TokenServiceTests.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java index 880c603de9866..5a7d50c83c724 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java @@ -447,7 +447,9 @@ public void testApiKeyServiceDisabled() throws Exception { () -> service.getApiKeys(randomAlphaOfLength(6), randomAlphaOfLength(8), null, null, new PlainActionFuture<>())); assertThat(e, instanceOf(FeatureNotEnabledException.class)); + // Older Kibana version looked for this exact text: assertThat(e, throwableWithMessage("api keys are not enabled")); + // Newer Kibana versions will check the metadata for this string literal: assertThat(e.getMetadata(FeatureNotEnabledException.DISABLED_FEATURE_METADATA), contains("api_keys")); } 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 1a846e21fdaa0..af3e08a15fa7c 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 @@ -568,6 +568,7 @@ public void testTokenServiceDisabled() throws Exception { () -> tokenService.createOAuth2Tokens(null, null, null, true, null)); assertThat(e, throwableWithMessage("security tokens are not enabled")); assertThat(e, instanceOf(FeatureNotEnabledException.class)); + // Client can check the metadata for this value, and depend on an exact string match: assertThat(e.getMetadata(FeatureNotEnabledException.DISABLED_FEATURE_METADATA), contains("security_tokens")); PlainActionFuture future = new PlainActionFuture<>(); @@ -578,6 +579,7 @@ public void testTokenServiceDisabled() throws Exception { e = expectThrows(ElasticsearchException.class, () -> tokenService.invalidateAccessToken((String) null, invalidateFuture)); assertThat(e, throwableWithMessage("security tokens are not enabled")); assertThat(e, instanceOf(FeatureNotEnabledException.class)); + // Client can check the metadata for this value, and depend on an exact string match: assertThat(e.getMetadata(FeatureNotEnabledException.DISABLED_FEATURE_METADATA), contains("security_tokens")); }