Skip to content

Commit

Permalink
Fix API key role descriptors rewrite bug for upgraded clusters (#62917)
Browse files Browse the repository at this point in the history
This PR ensures that API key role descriptors are always rewritten to a target node 
compatible format before a request is sent.
  • Loading branch information
ywangd committed Sep 30, 2020
1 parent be33573 commit feab123
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 15 deletions.
Expand Up @@ -14,6 +14,7 @@
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.common.util.concurrent.ThreadContext.StoredContext;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentHelper;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.node.Node;
Expand Down Expand Up @@ -167,19 +168,36 @@ public void executeAfterRewritingAuthentication(Consumer<StoredContext> consumer

private Map<String, Object> rewriteMetadataForApiKeyRoleDescriptors(Version streamVersion, Authentication authentication) {
Map<String, Object> metadata = authentication.getMetadata();
if (authentication.getAuthenticationType() == AuthenticationType.API_KEY
&& authentication.getVersion().onOrAfter(VERSION_API_KEY_ROLES_AS_BYTES)
&& streamVersion.before(VERSION_API_KEY_ROLES_AS_BYTES)) {
metadata = new HashMap<>(metadata);
metadata.put(
API_KEY_ROLE_DESCRIPTORS_KEY,
XContentHelper.convertToMap(
(BytesReference) metadata.get(API_KEY_ROLE_DESCRIPTORS_KEY), false, XContentType.JSON).v2());
metadata.put(
API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY,
XContentHelper.convertToMap(
(BytesReference) metadata.get(API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY), false, XContentType.JSON).v2());
if (authentication.getAuthenticationType() == AuthenticationType.API_KEY) {
if (authentication.getVersion().onOrAfter(VERSION_API_KEY_ROLES_AS_BYTES)
&& streamVersion.before(VERSION_API_KEY_ROLES_AS_BYTES)) {
metadata = new HashMap<>(metadata);
metadata.put(API_KEY_ROLE_DESCRIPTORS_KEY,
convertRoleDescriptorsBytesToMap((BytesReference) metadata.get(API_KEY_ROLE_DESCRIPTORS_KEY)));
metadata.put(API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY,
convertRoleDescriptorsBytesToMap((BytesReference) metadata.get(API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY)));
} else if (authentication.getVersion().before(VERSION_API_KEY_ROLES_AS_BYTES)
&& streamVersion.onOrAfter(VERSION_API_KEY_ROLES_AS_BYTES)) {
metadata = new HashMap<>(metadata);
metadata.put(API_KEY_ROLE_DESCRIPTORS_KEY,
convertRoleDescriptorsMapToBytes((Map<String, Object>)metadata.get(API_KEY_ROLE_DESCRIPTORS_KEY)));
metadata.put(API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY,
convertRoleDescriptorsMapToBytes((Map<String, Object>) metadata.get(API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY)));
}
}
return metadata;
}

private Map<String, Object> convertRoleDescriptorsBytesToMap(BytesReference roleDescriptorsBytes) {
return XContentHelper.convertToMap(roleDescriptorsBytes, false, XContentType.JSON).v2();
}

private BytesReference convertRoleDescriptorsMapToBytes(Map<String, Object> roleDescriptorsMap) {
try(XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) {
builder.map(roleDescriptorsMap);
return BytesReference.bytes(builder);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
Expand Up @@ -7,6 +7,7 @@

import org.elasticsearch.Version;
import org.elasticsearch.common.bytes.BytesArray;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.common.util.concurrent.ThreadContext.StoredContext;
Expand All @@ -28,6 +29,7 @@
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import static org.elasticsearch.xpack.core.security.authc.Authentication.VERSION_API_KEY_ROLES_AS_BYTES;
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY;
import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY;
import static org.hamcrest.Matchers.instanceOf;
Expand Down Expand Up @@ -136,7 +138,7 @@ public void testExecuteAfterRewritingAuthentication() throws IOException {
assertEquals(original, securityContext.getAuthentication());
}

public void testExecuteAfterRewritingAuthenticationShouldRewriteApiKeyMetadataForBwc() throws IOException {
public void testExecuteAfterRewritingAuthenticationWillConditionallyRewriteNewApiKeyMetadata() throws IOException {
User user = new User("test", null, new User("authUser"));
RealmRef authBy = new RealmRef("_es_api_key", "_es_api_key", "node1");
final Map<String, Object> metadata = Map.of(
Expand All @@ -147,16 +149,23 @@ API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY, new BytesArray("{\"limitedBy role\": {\"cl
AuthenticationType.API_KEY, metadata);
original.writeToContext(threadContext);

// If target is old node, rewrite new style API key metadata to old format
securityContext.executeAfterRewritingAuthentication(originalCtx -> {
Authentication authentication = securityContext.getAuthentication();
assertEquals(Map.of("a role", Map.of("cluster", List.of("all"))),
authentication.getMetadata().get(API_KEY_ROLE_DESCRIPTORS_KEY));
assertEquals(Map.of("limitedBy role", Map.of("cluster", List.of("all"))),
authentication.getMetadata().get(API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY));
}, Version.V_7_8_0);

// If target is new node, no need to rewrite the new style API key metadata
securityContext.executeAfterRewritingAuthentication(originalCtx -> {
Authentication authentication = securityContext.getAuthentication();
assertSame(metadata, authentication.getMetadata());
}, VersionUtils.randomVersionBetween(random(), VERSION_API_KEY_ROLES_AS_BYTES, Version.CURRENT));
}

public void testExecuteAfterRewritingAuthenticationShouldNotRewriteApiKeyMetadataForOldAuthenticationObject() throws IOException {
public void testExecuteAfterRewritingAuthenticationWillConditionallyRewriteOldApiKeyMetadata() throws IOException {
User user = new User("test", null, new User("authUser"));
RealmRef authBy = new RealmRef("_es_api_key", "_es_api_key", "node1");
final Map<String, Object> metadata = Map.of(
Expand All @@ -166,9 +175,19 @@ public void testExecuteAfterRewritingAuthenticationShouldNotRewriteApiKeyMetadat
final Authentication original = new Authentication(user, authBy, authBy, Version.V_7_8_0, AuthenticationType.API_KEY, metadata);
original.writeToContext(threadContext);

// If target is old node, no need to rewrite old style API key metadata
securityContext.executeAfterRewritingAuthentication(originalCtx -> {
Authentication authentication = securityContext.getAuthentication();
assertSame(metadata, authentication.getMetadata());
}, randomFrom(Version.V_8_0_0, Version.V_7_8_0));
}, Version.V_7_8_0);

// If target is new old, ensure old map style API key metadata is rewritten to bytesreference
securityContext.executeAfterRewritingAuthentication(originalCtx -> {
Authentication authentication = securityContext.getAuthentication();
assertEquals("{\"a role\":{\"cluster\":[\"all\"]}}",
((BytesReference)authentication.getMetadata().get(API_KEY_ROLE_DESCRIPTORS_KEY)).utf8ToString());
assertEquals("{\"limitedBy role\":{\"cluster\":[\"all\"]}}",
((BytesReference)authentication.getMetadata().get(API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY)).utf8ToString());
}, VersionUtils.randomVersionBetween(random(), VERSION_API_KEY_ROLES_AS_BYTES, Version.CURRENT));
}
}
2 changes: 2 additions & 0 deletions x-pack/qa/full-cluster-restart/build.gradle
Expand Up @@ -63,6 +63,8 @@ for (Version bwcVersion : BuildParams.bwcVersions.indexCompatible) {
setting 'xpack.security.transport.ssl.certificate', 'testnode.crt'
keystore 'xpack.security.transport.ssl.secure_key_passphrase', 'testnode'
setting 'logger.org.elasticsearch.xpack.watcher', 'DEBUG'

setting 'xpack.security.authc.api_key.enabled', 'true'
}
}

Expand Down
Expand Up @@ -10,6 +10,7 @@
import org.apache.http.util.EntityUtils;
import org.elasticsearch.Version;
import org.elasticsearch.client.Request;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.Response;
import org.elasticsearch.client.ResponseException;
import org.elasticsearch.client.RestClient;
Expand Down Expand Up @@ -52,6 +53,7 @@
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.everyItem;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.hasEntry;
import static org.hamcrest.Matchers.hasItems;
Expand Down Expand Up @@ -222,6 +224,45 @@ public void testWatcher() throws Exception {
}
}

@SuppressWarnings("unchecked")
public void testWatcherWithApiKey() throws Exception {
final Request getWatchStatusRequest = new Request("GET", "/_watcher/watch/watch_with_api_key");
getWatchStatusRequest.addParameter("filter_path", "status");

if (isRunningAgainstOldCluster()) {
final Request createApiKeyRequest = new Request("PUT", "/_security/api_key");
createApiKeyRequest.setJsonEntity("{\"name\":\"key-1\"}");
final Response response = client().performRequest(createApiKeyRequest);
final Map<String, Object> createApiKeyResponse = entityAsMap(response);

Request createWatchWithApiKeyRequest = new Request("PUT", "/_watcher/watch/watch_with_api_key");
createWatchWithApiKeyRequest.setJsonEntity(loadWatch("simple-watch.json"));
final byte[] keyBytes =
(createApiKeyResponse.get("id") + ":" + createApiKeyResponse.get("api_key")).getBytes(StandardCharsets.UTF_8);
final String authHeader = "ApiKey " + Base64.getEncoder().encodeToString(keyBytes);
createWatchWithApiKeyRequest.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", authHeader));
client().performRequest(createWatchWithApiKeyRequest);

assertBusy(() -> {
final Map<String, Object> getWatchStatusResponse = entityAsMap(client().performRequest(getWatchStatusRequest));
final Map<String, Object> status = (Map<String, Object>) getWatchStatusResponse.get("status");
assertEquals("executed", status.get("execution_state"));
});

} else {
final Map<String, Object> getWatchStatusResponse = entityAsMap(client().performRequest(getWatchStatusRequest));
final Map<String, Object> status = (Map<String, Object>) getWatchStatusResponse.get("status");
final int version = (int) status.get("version");

assertBusy(() -> {
final Map<String, Object> newGetWatchStatusResponse = entityAsMap(client().performRequest(getWatchStatusRequest));
final Map<String, Object> newStatus = (Map<String, Object>) newGetWatchStatusResponse.get("status");
assertThat((int) newStatus.get("version"), greaterThan(version + 2));
assertEquals("executed", newStatus.get("execution_state"));
});
}
}

/**
* Tests that a RollUp job created on a old cluster is correctly restarted after the upgrade.
*/
Expand Down

0 comments on commit feab123

Please sign in to comment.