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 8b9fda5b9c3..36144899d28 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 @@ -656,9 +656,9 @@ public final class TokenService { ensureEnabled(); findTokenFromRefreshToken(refreshToken, ActionListener.wrap(tuple -> { - final Authentication userAuth = Authentication.readFromContext(client.threadPool().getThreadContext()); + final Authentication clientAuth = Authentication.readFromContext(client.threadPool().getThreadContext()); final String tokenDocId = tuple.v1().getHits().getHits()[0].getId(); - innerRefresh(tokenDocId, userAuth, listener, tuple.v2()); + innerRefresh(tokenDocId, clientAuth, listener, tuple.v2()); }, listener::onFailure), new AtomicInteger(0)); } @@ -719,7 +719,7 @@ public final class TokenService { * may be recoverable. The refresh involves retrieval of the token document and then * updating the token document to indicate that the document has been refreshed. */ - private void innerRefresh(String tokenDocId, Authentication userAuth, ActionListener> listener, + private void innerRefresh(String tokenDocId, Authentication clientAuth, ActionListener> listener, AtomicInteger attemptCount) { if (attemptCount.getAndIncrement() > MAX_RETRY_ATTEMPTS) { logger.warn("Failed to refresh token for doc [{}] after [{}] attempts", tokenDocId, attemptCount.get()); @@ -731,7 +731,7 @@ public final class TokenService { ActionListener.wrap(response -> { if (response.isExists()) { final Map source = response.getSource(); - final Optional invalidSource = checkTokenDocForRefresh(source, userAuth); + final Optional invalidSource = checkTokenDocForRefresh(source, clientAuth); if (invalidSource.isPresent()) { onFailure.accept(invalidSource.get()); @@ -754,12 +754,12 @@ public final class TokenService { updateRequest.setIfPrimaryTerm(response.getPrimaryTerm()); executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN, updateRequest.request(), ActionListener.wrap( - updateResponse -> createUserToken(authentication, userAuth, listener, metadata, true), + updateResponse -> createUserToken(authentication, clientAuth, listener, metadata, true), e -> { Throwable cause = ExceptionsHelper.unwrapCause(e); if (cause instanceof VersionConflictEngineException || isShardNotAvailableException(e)) { - innerRefresh(tokenDocId, userAuth, + innerRefresh(tokenDocId, clientAuth, listener, attemptCount); } else { onFailure.accept(e); @@ -774,7 +774,7 @@ public final class TokenService { } }, e -> { if (isShardNotAvailableException(e)) { - innerRefresh(tokenDocId, userAuth, listener, attemptCount); + innerRefresh(tokenDocId, clientAuth, listener, attemptCount); } else { listener.onFailure(e); } @@ -786,7 +786,7 @@ public final class TokenService { * Performs checks on the retrieved source and returns an {@link Optional} with the exception * if there is an issue */ - private Optional checkTokenDocForRefresh(Map source, Authentication userAuth) { + private Optional checkTokenDocForRefresh(Map source, Authentication clientAuth) { final Map refreshTokenSrc = (Map) source.get("refresh_token"); final Map accessTokenSrc = (Map) source.get("access_token"); if (refreshTokenSrc == null || refreshTokenSrc.isEmpty()) { @@ -820,18 +820,18 @@ public final class TokenService { } else if (userTokenSrc.get("metadata") == null) { return Optional.of(invalidGrantException("token is missing metadata")); } else { - return checkClient(refreshTokenSrc, userAuth); + return checkClient(refreshTokenSrc, clientAuth); } } } - private Optional checkClient(Map refreshTokenSource, Authentication userAuth) { + private Optional checkClient(Map refreshTokenSource, Authentication clientAuth) { Map clientInfo = (Map) refreshTokenSource.get("client"); if (clientInfo == null) { return Optional.of(invalidGrantException("token is missing client information")); - } else if (userAuth.getUser().principal().equals(clientInfo.get("user")) == false) { + } else if (clientAuth.getUser().principal().equals(clientInfo.get("user")) == false) { return Optional.of(invalidGrantException("tokens must be refreshed by the creating client")); - } else if (userAuth.getAuthenticatedBy().getName().equals(clientInfo.get("realm")) == false) { + } else if (clientAuth.getAuthenticatedBy().getName().equals(clientInfo.get("realm")) == false) { return Optional.of(invalidGrantException("tokens must be refreshed by the creating client")); } else { return Optional.empty(); diff --git a/x-pack/qa/rolling-upgrade/build.gradle b/x-pack/qa/rolling-upgrade/build.gradle index 6aba0792bbb..afef2515c1f 100644 --- a/x-pack/qa/rolling-upgrade/build.gradle +++ b/x-pack/qa/rolling-upgrade/build.gradle @@ -238,6 +238,8 @@ subprojects { setting 'node.name', "upgraded-node-${stopNode}" dependsOn copyTestNodeKeystore extraConfigFile 'testnode.jks', new File(outputDir + '/testnode.jks') + setting 'xpack.security.authc.realms.file.file1.order', '0' + setting 'xpack.security.authc.realms.native.native1.order', '1' if (withSystemKey) { setting 'xpack.watcher.encrypt_sensitive_data', 'true' keystoreFile 'xpack.watcher.encryption_key', "${mainProject.projectDir}/src/test/resources/system_key" diff --git a/x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/TokenBackwardsCompatibilityIT.java b/x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/TokenBackwardsCompatibilityIT.java index b9da82ce2ab..0a8e9354d98 100644 --- a/x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/TokenBackwardsCompatibilityIT.java +++ b/x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/TokenBackwardsCompatibilityIT.java @@ -15,15 +15,12 @@ import org.elasticsearch.client.ResponseException; import org.elasticsearch.client.RestClient; import org.elasticsearch.common.Strings; import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.rest.action.document.RestGetAction; -import org.elasticsearch.rest.action.document.RestIndexAction; import org.elasticsearch.test.rest.yaml.ObjectPath; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.function.Predicate; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.hamcrest.Matchers.equalTo; @@ -52,6 +49,8 @@ public class TokenBackwardsCompatibilityIT extends AbstractUpgradeTestCase { } } + // Create a couple of tokens and store them in the token_backwards_compatibility_it index to be used for tests in the mixed/upgraded + // clusters Request createTokenRequest = new Request("POST", "/_security/oauth2/token"); createTokenRequest.setJsonEntity( "{\n" + @@ -67,17 +66,13 @@ public class TokenBackwardsCompatibilityIT extends AbstractUpgradeTestCase { assertTokenWorks(token); // In this test either all or none tests or on a specific version: - boolean postSevenDotZeroNodes = getNodeId(v -> v.onOrAfter(Version.V_7_0_0)) != null; - Request indexRequest1 = new Request("PUT", "token_backwards_compatibility_it/doc/old_cluster_token1"); - if (postSevenDotZeroNodes) { - indexRequest1.setOptions(expectWarnings(RestIndexAction.TYPES_DEPRECATION_MESSAGE)); - } + Request indexRequest1 = new Request("PUT", "token_backwards_compatibility_it/_doc/old_cluster_token1"); indexRequest1.setJsonEntity( "{\n" + " \"token\": \"" + token + "\"\n" + "}"); - client().performRequest(indexRequest1); - + Response indexResponse1 = client().performRequest(indexRequest1); + assertOK(indexResponse1); Request createSecondTokenRequest = new Request("POST", "/_security/oauth2/token"); createSecondTokenRequest.setEntity(createTokenRequest.getEntity()); response = client().performRequest(createSecondTokenRequest); @@ -85,57 +80,45 @@ public class TokenBackwardsCompatibilityIT extends AbstractUpgradeTestCase { token = (String) responseMap.get("access_token"); assertNotNull(token); assertTokenWorks(token); - Request indexRequest2 = new Request("PUT", "token_backwards_compatibility_it/doc/old_cluster_token2"); - if (postSevenDotZeroNodes) { - indexRequest2.setOptions(expectWarnings(RestIndexAction.TYPES_DEPRECATION_MESSAGE)); - } + Request indexRequest2 = new Request("PUT", "token_backwards_compatibility_it/_doc/old_cluster_token2"); indexRequest2.setJsonEntity( "{\n" + " \"token\": \"" + token + "\"\n" + "}"); - client().performRequest(indexRequest2); + Response indexResponse2 = client().performRequest(indexRequest2); + assertOK(indexResponse2); } - private String getNodeId(Predicate versionPredicate) throws IOException { - Response response = client().performRequest(new Request("GET", "_nodes")); - ObjectPath objectPath = ObjectPath.createFromResponse(response); - Map nodesAsMap = objectPath.evaluate("nodes"); - for (String id : nodesAsMap.keySet()) { - Version version = Version.fromString(objectPath.evaluate("nodes." + id + ".version")); - if (versionPredicate.test(version)) { - return id; - } - } - return null; - } - - public void testTokenWorksInMixedOrUpgradedCluster() throws Exception { - assumeTrue("this test should only run against the mixed or upgraded cluster", - CLUSTER_TYPE == ClusterType.MIXED || CLUSTER_TYPE == ClusterType.UPGRADED); - Request getRequest = new Request("GET", "token_backwards_compatibility_it/doc/old_cluster_token1"); - getRequest.setOptions(expectWarnings(RestGetAction.TYPES_DEPRECATION_MESSAGE)); + public void testTokenWorksInMixedCluster() throws Exception { + // Verify that an old token continues to work during all stages of the rolling upgrade + assumeTrue("this test should only run against the mixed cluster", CLUSTER_TYPE == ClusterType.MIXED); + Request getRequest = new Request("GET", "token_backwards_compatibility_it/_doc/old_cluster_token1"); Response getResponse = client().performRequest(getRequest); assertOK(getResponse); Map source = (Map) entityAsMap(getResponse).get("_source"); assertTokenWorks((String) source.get("token")); } - public void testMixedCluster() throws Exception { + public void testInvalidatingTokenInMixedCluster() throws Exception { + // Verify that we can invalidate a token in a mixed cluster assumeTrue("this test should only run against the mixed cluster", CLUSTER_TYPE == ClusterType.MIXED); - assumeTrue("the master must be on the latest version before we can write", isMasterOnLatestVersion()); - Request getRequest = new Request("GET", "token_backwards_compatibility_it/doc/old_cluster_token2"); - getRequest.setOptions(expectWarnings(RestGetAction.TYPES_DEPRECATION_MESSAGE)); - + Request getRequest = new Request("GET", "token_backwards_compatibility_it/_doc/old_cluster_token2"); Response getResponse = client().performRequest(getRequest); + assertOK(getResponse); Map source = (Map) entityAsMap(getResponse).get("_source"); - final String token = (String) source.get("token"); - assertTokenWorks(token); - + String token = (String) source.get("token"); + // The token might be already invalidated by running testInvalidatingTokenInMixedCluster in a previous stage + // we don't try to assert it works before invalidating. This case is handled by testTokenWorksInMixedCluster Request invalidateRequest = new Request("DELETE", "/_security/oauth2/token"); invalidateRequest.setJsonEntity("{\"token\": \"" + token + "\"}"); invalidateRequest.addParameter("error_trace", "true"); client().performRequest(invalidateRequest); assertTokenDoesNotWork(token); + } + + public void testMixedClusterWithUpgradedMaster() throws Exception { + assumeTrue("this test should only run against the mixed cluster", CLUSTER_TYPE == ClusterType.MIXED); + assumeTrue("the master must be on the latest version before we can write", isMasterOnLatestVersion()); // create token and refresh on version that supports it Request createTokenRequest = new Request("POST", "/_security/oauth2/token"); @@ -170,66 +153,33 @@ public class TokenBackwardsCompatibilityIT extends AbstractUpgradeTestCase { assertTokenWorks(accessToken); assertNotEquals(accessToken, updatedAccessToken); assertNotEquals(refreshToken, updatedRefreshToken); + // Invalidate the new access token and ensure that it no longer works + Request invalidateTokenRequest = new Request("DELETE", "/_security/oauth2/token"); + invalidateTokenRequest.setJsonEntity( + "{\n" + + " \"token\": \"" + updatedAccessToken + "\"\n" + + "}"); + Response invalidateTokenResponse = client.performRequest(invalidateTokenRequest); + assertOK(invalidateTokenResponse); + assertTokenDoesNotWork(updatedAccessToken); } } public void testUpgradedCluster() throws Exception { - assumeTrue("this test should only run against the mixed cluster", CLUSTER_TYPE == ClusterType.UPGRADED); - Request getRequest = new Request("GET", "token_backwards_compatibility_it/doc/old_cluster_token2"); - getRequest.setOptions(expectWarnings(RestGetAction.TYPES_DEPRECATION_MESSAGE)); - + assumeTrue("this test should only run against the upgraded cluster", CLUSTER_TYPE == ClusterType.UPGRADED); + // Use an old token to authenticate, then invalidate it and verify that it can no longer be used + Request getRequest = new Request("GET", "token_backwards_compatibility_it/_doc/old_cluster_token1"); Response getResponse = client().performRequest(getRequest); assertOK(getResponse); Map source = (Map) entityAsMap(getResponse).get("_source"); final String token = (String) source.get("token"); - // invalidate again since this may not have been invalidated in the mixed cluster Request invalidateRequest = new Request("DELETE", "/_security/oauth2/token"); invalidateRequest.setJsonEntity("{\"token\": \"" + token + "\"}"); invalidateRequest.addParameter("error_trace", "true"); Response invalidationResponse = client().performRequest(invalidateRequest); assertOK(invalidationResponse); assertTokenDoesNotWork(token); - - getRequest = new Request("GET", "token_backwards_compatibility_it/doc/old_cluster_token1"); - getRequest.setOptions(expectWarnings(RestGetAction.TYPES_DEPRECATION_MESSAGE)); - - getResponse = client().performRequest(getRequest); - source = (Map) entityAsMap(getResponse).get("_source"); - final String workingToken = (String) source.get("token"); - assertTokenWorks(workingToken); - - Request getTokenRequest = new Request("POST", "/_security/oauth2/token"); - getTokenRequest.setJsonEntity( - "{\n" + - " \"username\": \"test_user\",\n" + - " \"password\": \"x-pack-test-password\",\n" + - " \"grant_type\": \"password\"\n" + - "}"); - Response response = client().performRequest(getTokenRequest); - Map responseMap = entityAsMap(response); - String accessToken = (String) responseMap.get("access_token"); - String refreshToken = (String) responseMap.get("refresh_token"); - assertNotNull(accessToken); - assertNotNull(refreshToken); - assertTokenWorks(accessToken); - - Request refreshTokenRequest = new Request("POST", "/_security/oauth2/token"); - refreshTokenRequest.setJsonEntity( - "{\n" + - " \"refresh_token\": \"" + refreshToken + "\",\n" + - " \"grant_type\": \"refresh_token\"\n" + - "}"); - response = client().performRequest(refreshTokenRequest); - responseMap = entityAsMap(response); - String updatedAccessToken = (String) responseMap.get("access_token"); - String updatedRefreshToken = (String) responseMap.get("refresh_token"); - assertNotNull(updatedAccessToken); - assertNotNull(updatedRefreshToken); - assertTokenWorks(updatedAccessToken); - assertTokenWorks(accessToken); - assertNotEquals(accessToken, updatedAccessToken); - assertNotEquals(refreshToken, updatedRefreshToken); } private void assertTokenWorks(String token) throws IOException { @@ -261,6 +211,7 @@ public class TokenBackwardsCompatibilityIT extends AbstractUpgradeTestCase { response = client().performRequest(new Request("GET", "_nodes")); assertOK(response); ObjectPath objectPath = ObjectPath.createFromResponse(response); + logger.info("Master node is on version: " + objectPath.evaluate("nodes." + masterNodeId + ".version")); return Version.CURRENT.equals(Version.fromString(objectPath.evaluate("nodes." + masterNodeId + ".version"))); }