Remove uniqueness constraint for API key name and make it optional (#47549)

Since we cannot guarantee the uniqueness of the API key `name` this commit removes the constraint and makes this field optional.

Closes #46646
This commit is contained in:
Yogesh Gaikwad 2019-10-12 16:58:39 +11:00 committed by GitHub
parent 1fc7a69038
commit 4ac25f3016
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 60 additions and 113 deletions

View file

@ -22,7 +22,6 @@ package org.elasticsearch.client.security;
import org.elasticsearch.client.Validatable; import org.elasticsearch.client.Validatable;
import org.elasticsearch.client.security.user.privileges.Role; import org.elasticsearch.client.security.user.privileges.Role;
import org.elasticsearch.common.Nullable; import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentBuilder;
@ -47,12 +46,9 @@ public final class CreateApiKeyRequest implements Validatable, ToXContentObject
* @param roles list of {@link Role}s * @param roles list of {@link Role}s
* @param expiration to specify expiration for the API key * @param expiration to specify expiration for the API key
*/ */
public CreateApiKeyRequest(String name, List<Role> roles, @Nullable TimeValue expiration, @Nullable final RefreshPolicy refreshPolicy) { public CreateApiKeyRequest(@Nullable String name, List<Role> roles, @Nullable TimeValue expiration,
if (Strings.hasText(name)) { @Nullable final RefreshPolicy refreshPolicy) {
this.name = name; this.name = name;
} else {
throw new IllegalArgumentException("name must not be null or empty");
}
this.roles = Objects.requireNonNull(roles, "roles may not be null"); this.roles = Objects.requireNonNull(roles, "roles may not be null");
this.expiration = expiration; this.expiration = expiration;
this.refreshPolicy = (refreshPolicy == null) ? RefreshPolicy.getDefault() : refreshPolicy; this.refreshPolicy = (refreshPolicy == null) ? RefreshPolicy.getDefault() : refreshPolicy;

View file

@ -21,6 +21,7 @@ package org.elasticsearch.client.security.support;
import org.elasticsearch.common.ParseField; import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.ConstructingObjectParser;
import org.elasticsearch.common.xcontent.ObjectParser;
import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentParser;
import java.io.IOException; import java.io.IOException;
@ -131,7 +132,8 @@ public final class ApiKey {
(args[3] == null) ? null : Instant.ofEpochMilli((Long) args[3]), (Boolean) args[4], (String) args[5], (String) args[6]); (args[3] == null) ? null : Instant.ofEpochMilli((Long) args[3]), (Boolean) args[4], (String) args[5], (String) args[6]);
}); });
static { static {
PARSER.declareString(constructorArg(), new ParseField("name")); PARSER.declareField(optionalConstructorArg(), (p, c) -> p.textOrNull(), new ParseField("name"),
ObjectParser.ValueType.STRING_OR_NULL);
PARSER.declareString(constructorArg(), new ParseField("id")); PARSER.declareString(constructorArg(), new ParseField("id"));
PARSER.declareLong(constructorArg(), new ParseField("creation")); PARSER.declareLong(constructorArg(), new ParseField("creation"));
PARSER.declareLong(optionalConstructorArg(), new ParseField("expiration")); PARSER.declareLong(optionalConstructorArg(), new ParseField("expiration"));

View file

@ -40,7 +40,9 @@ public class GetApiKeyResponseTests extends ESTestCase {
"user-a", "realm-x"); "user-a", "realm-x");
ApiKey apiKeyInfo2 = createApiKeyInfo("name2", "id-2", Instant.ofEpochMilli(100000L), Instant.ofEpochMilli(10000000L), true, ApiKey apiKeyInfo2 = createApiKeyInfo("name2", "id-2", Instant.ofEpochMilli(100000L), Instant.ofEpochMilli(10000000L), true,
"user-b", "realm-y"); "user-b", "realm-y");
GetApiKeyResponse response = new GetApiKeyResponse(Arrays.asList(apiKeyInfo1, apiKeyInfo2)); ApiKey apiKeyInfo3 = createApiKeyInfo(null, "id-3", Instant.ofEpochMilli(100000L), null, true,
"user-c", "realm-z");
GetApiKeyResponse response = new GetApiKeyResponse(Arrays.asList(apiKeyInfo1, apiKeyInfo2, apiKeyInfo3));
final XContentType xContentType = randomFrom(XContentType.values()); final XContentType xContentType = randomFrom(XContentType.values());
final XContentBuilder builder = XContentFactory.contentBuilder(xContentType); final XContentBuilder builder = XContentFactory.contentBuilder(xContentType);
toXContent(response, builder); toXContent(response, builder);

View file

@ -12,8 +12,8 @@ API Key can be created using this API.
[id="{upid}-{api}-request"] [id="{upid}-{api}-request"]
==== Create API Key Request ==== Create API Key Request
A +{request}+ contains name for the API key, A +{request}+ contains an optional name for the API key,
list of role descriptors to define permissions and an optional list of role descriptors to define permissions and
optional expiration for the generated API key. optional expiration for the generated API key.
If expiration is not provided then by default the API If expiration is not provided then by default the API
keys do not expire. keys do not expire.

View file

@ -44,7 +44,7 @@ service.
The following parameters can be specified in the body of a POST or PUT request: The following parameters can be specified in the body of a POST or PUT request:
`name`:: `name`::
(string) Specifies the name for this API key. (Optional, string) Specifies the name for this API key.
`role_descriptors`:: `role_descriptors`::
(Optional, array-of-role-descriptor) An array of role descriptors for this API (Optional, array-of-role-descriptor) An array of role descriptors for this API

View file

@ -6,6 +6,7 @@
package org.elasticsearch.xpack.core.security.action; package org.elasticsearch.xpack.core.security.action;
import org.elasticsearch.Version;
import org.elasticsearch.common.ParseField; import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.StreamOutput;
@ -49,7 +50,11 @@ public final class ApiKey implements ToXContentObject, Writeable {
} }
public ApiKey(StreamInput in) throws IOException { public ApiKey(StreamInput in) throws IOException {
if (in.getVersion().onOrAfter(Version.V_7_5_0)) {
this.name = in.readOptionalString();
} else {
this.name = in.readString(); this.name = in.readString();
}
this.id = in.readString(); this.id = in.readString();
this.creation = in.readInstant(); this.creation = in.readInstant();
this.expiration = in.readOptionalInstant(); this.expiration = in.readOptionalInstant();
@ -103,7 +108,11 @@ public final class ApiKey implements ToXContentObject, Writeable {
@Override @Override
public void writeTo(StreamOutput out) throws IOException { public void writeTo(StreamOutput out) throws IOException {
if (out.getVersion().onOrAfter(Version.V_7_5_0)) {
out.writeOptionalString(name);
} else {
out.writeString(name); out.writeString(name);
}
out.writeString(id); out.writeString(id);
out.writeInstant(creation); out.writeInstant(creation);
out.writeOptionalInstant(expiration); out.writeOptionalInstant(expiration);

View file

@ -6,11 +6,11 @@
package org.elasticsearch.xpack.core.security.action; package org.elasticsearch.xpack.core.security.action;
import org.elasticsearch.Version;
import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.action.support.WriteRequest;
import org.elasticsearch.common.Nullable; import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.unit.TimeValue;
@ -43,19 +43,19 @@ public final class CreateApiKeyRequest extends ActionRequest {
* @param roleDescriptors list of {@link RoleDescriptor}s * @param roleDescriptors list of {@link RoleDescriptor}s
* @param expiration to specify expiration for the API key * @param expiration to specify expiration for the API key
*/ */
public CreateApiKeyRequest(String name, @Nullable List<RoleDescriptor> roleDescriptors, @Nullable TimeValue expiration) { public CreateApiKeyRequest(@Nullable String name, @Nullable List<RoleDescriptor> roleDescriptors, @Nullable TimeValue expiration) {
if (Strings.hasText(name)) {
this.name = name; this.name = name;
} else {
throw new IllegalArgumentException("name must not be null or empty");
}
this.roleDescriptors = (roleDescriptors == null) ? List.of() : List.copyOf(roleDescriptors); this.roleDescriptors = (roleDescriptors == null) ? List.of() : List.copyOf(roleDescriptors);
this.expiration = expiration; this.expiration = expiration;
} }
public CreateApiKeyRequest(StreamInput in) throws IOException { public CreateApiKeyRequest(StreamInput in) throws IOException {
super(in); super(in);
if (in.getVersion().onOrAfter(Version.V_7_5_0)) {
this.name = in.readOptionalString();
} else {
this.name = in.readString(); this.name = in.readString();
}
this.expiration = in.readOptionalTimeValue(); this.expiration = in.readOptionalTimeValue();
this.roleDescriptors = List.copyOf(in.readList(RoleDescriptor::new)); this.roleDescriptors = List.copyOf(in.readList(RoleDescriptor::new));
this.refreshPolicy = WriteRequest.RefreshPolicy.readFrom(in); this.refreshPolicy = WriteRequest.RefreshPolicy.readFrom(in);
@ -65,12 +65,8 @@ public final class CreateApiKeyRequest extends ActionRequest {
return name; return name;
} }
public void setName(String name) { public void setName(@Nullable String name) {
if (Strings.hasText(name)) {
this.name = name; this.name = name;
} else {
throw new IllegalArgumentException("name must not be null or empty");
}
} }
public TimeValue getExpiration() { public TimeValue getExpiration() {
@ -100,9 +96,7 @@ public final class CreateApiKeyRequest extends ActionRequest {
@Override @Override
public ActionRequestValidationException validate() { public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = null; ActionRequestValidationException validationException = null;
if (Strings.isNullOrEmpty(name)) { if (name != null) {
validationException = addValidationError("name is required", validationException);
} else {
if (name.length() > 256) { if (name.length() > 256) {
validationException = addValidationError("name may not be more than 256 characters long", validationException); validationException = addValidationError("name may not be more than 256 characters long", validationException);
} }
@ -119,7 +113,11 @@ public final class CreateApiKeyRequest extends ActionRequest {
@Override @Override
public void writeTo(StreamOutput out) throws IOException { public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out); super.writeTo(out);
if (out.getVersion().onOrAfter(Version.V_7_5_0)) {
out.writeOptionalString(name);
} else {
out.writeString(name); out.writeString(name);
}
out.writeOptionalTimeValue(expiration); out.writeOptionalTimeValue(expiration);
out.writeList(roleDescriptors); out.writeList(roleDescriptors);
refreshPolicy.writeTo(out); refreshPolicy.writeTo(out);

View file

@ -28,20 +28,12 @@ public class CreateApiKeyRequestTests extends ESTestCase {
CreateApiKeyRequest request = new CreateApiKeyRequest(); CreateApiKeyRequest request = new CreateApiKeyRequest();
ActionRequestValidationException ve = request.validate(); ActionRequestValidationException ve = request.validate();
assertNotNull(ve); assertNull(ve);
assertThat(ve.validationErrors().size(), is(1));
assertThat(ve.validationErrors().get(0), containsString("name is required"));
request.setName(name); request.setName(name);
ve = request.validate(); ve = request.validate();
assertNull(ve); assertNull(ve);
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> request.setName(""));
assertThat(e.getMessage(), containsString("name must not be null or empty"));
e = expectThrows(IllegalArgumentException.class, () -> request.setName(null));
assertThat(e.getMessage(), containsString("name must not be null or empty"));
request.setName(randomAlphaOfLength(257)); request.setName(randomAlphaOfLength(257));
ve = request.validate(); ve = request.validate();
assertNotNull(ve); assertNotNull(ve);

View file

@ -24,8 +24,9 @@ import static org.hamcrest.Matchers.equalTo;
public class GetApiKeyResponseTests extends ESTestCase { public class GetApiKeyResponseTests extends ESTestCase {
public void testSerialization() throws IOException { public void testSerialization() throws IOException {
boolean withApiKeyName = randomBoolean();
boolean withExpiration = randomBoolean(); boolean withExpiration = randomBoolean();
ApiKey apiKeyInfo = createApiKeyInfo(randomAlphaOfLength(4), randomAlphaOfLength(5), Instant.now(), ApiKey apiKeyInfo = createApiKeyInfo((withApiKeyName) ? randomAlphaOfLength(4) : null, randomAlphaOfLength(5), Instant.now(),
(withExpiration) ? Instant.now() : null, false, randomAlphaOfLength(4), randomAlphaOfLength(5)); (withExpiration) ? Instant.now() : null, false, randomAlphaOfLength(4), randomAlphaOfLength(5));
GetApiKeyResponse response = new GetApiKeyResponse(Collections.singletonList(apiKeyInfo)); GetApiKeyResponse response = new GetApiKeyResponse(Collections.singletonList(apiKeyInfo));
try (BytesStreamOutput output = new BytesStreamOutput()) { try (BytesStreamOutput output = new BytesStreamOutput()) {
@ -42,7 +43,9 @@ public class GetApiKeyResponseTests extends ESTestCase {
"user-a", "realm-x"); "user-a", "realm-x");
ApiKey apiKeyInfo2 = createApiKeyInfo("name2", "id-2", Instant.ofEpochMilli(100000L), Instant.ofEpochMilli(10000000L), true, ApiKey apiKeyInfo2 = createApiKeyInfo("name2", "id-2", Instant.ofEpochMilli(100000L), Instant.ofEpochMilli(10000000L), true,
"user-b", "realm-y"); "user-b", "realm-y");
GetApiKeyResponse response = new GetApiKeyResponse(Arrays.asList(apiKeyInfo1, apiKeyInfo2)); ApiKey apiKeyInfo3 = createApiKeyInfo(null, "id-3", Instant.ofEpochMilli(100000L), null, true,
"user-c", "realm-z");
GetApiKeyResponse response = new GetApiKeyResponse(Arrays.asList(apiKeyInfo1, apiKeyInfo2, apiKeyInfo3));
XContentBuilder builder = XContentFactory.jsonBuilder(); XContentBuilder builder = XContentFactory.jsonBuilder();
response.toXContent(builder, ToXContent.EMPTY_PARAMS); response.toXContent(builder, ToXContent.EMPTY_PARAMS);
assertThat(Strings.toString(builder), equalTo( assertThat(Strings.toString(builder), equalTo(
@ -51,7 +54,9 @@ public class GetApiKeyResponseTests extends ESTestCase {
+ "{\"id\":\"id-1\",\"name\":\"name1\",\"creation\":100000,\"expiration\":10000000,\"invalidated\":false," + "{\"id\":\"id-1\",\"name\":\"name1\",\"creation\":100000,\"expiration\":10000000,\"invalidated\":false,"
+ "\"username\":\"user-a\",\"realm\":\"realm-x\"}," + "\"username\":\"user-a\",\"realm\":\"realm-x\"},"
+ "{\"id\":\"id-2\",\"name\":\"name2\",\"creation\":100000,\"expiration\":10000000,\"invalidated\":true," + "{\"id\":\"id-2\",\"name\":\"name2\",\"creation\":100000,\"expiration\":10000000,\"invalidated\":true,"
+ "\"username\":\"user-b\",\"realm\":\"realm-y\"}" + "\"username\":\"user-b\",\"realm\":\"realm-y\"},"
+ "{\"id\":\"id-3\",\"name\":null,\"creation\":100000,\"invalidated\":true,"
+ "\"username\":\"user-c\",\"realm\":\"realm-z\"}"
+ "]" + "]"
+ "}")); + "}"));
} }

View file

@ -22,7 +22,6 @@ import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse; import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexAction; import org.elasticsearch.action.index.IndexAction;
import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchAction;
import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.support.WriteRequest.RefreshPolicy; import org.elasticsearch.action.support.WriteRequest.RefreshPolicy;
import org.elasticsearch.action.update.UpdateRequest; import org.elasticsearch.action.update.UpdateRequest;
@ -192,45 +191,9 @@ public class ApiKeyService {
ensureEnabled(); ensureEnabled();
if (authentication == null) { if (authentication == null) {
listener.onFailure(new IllegalArgumentException("authentication must be provided")); listener.onFailure(new IllegalArgumentException("authentication must be provided"));
} else {
/*
* Check if requested API key name already exists to avoid duplicate key names,
* this check is best effort as there could be two nodes executing search and
* then index concurrently allowing a duplicate name.
*/
checkDuplicateApiKeyNameAndCreateApiKey(authentication, request, userRoles, listener);
}
}
private void checkDuplicateApiKeyNameAndCreateApiKey(Authentication authentication, CreateApiKeyRequest request,
Set<RoleDescriptor> userRoles,
ActionListener<CreateApiKeyResponse> listener) {
final BoolQueryBuilder boolQuery = QueryBuilders.boolQuery()
.filter(QueryBuilders.termQuery("doc_type", "api_key"))
.filter(QueryBuilders.termQuery("name", request.getName()))
.filter(QueryBuilders.termQuery("api_key_invalidated", false));
final BoolQueryBuilder expiredQuery = QueryBuilders.boolQuery()
.should(QueryBuilders.rangeQuery("expiration_time").lte(Instant.now().toEpochMilli()))
.should(QueryBuilders.boolQuery().mustNot(QueryBuilders.existsQuery("expiration_time")));
boolQuery.filter(expiredQuery);
final SearchRequest searchRequest = client.prepareSearch(SECURITY_MAIN_ALIAS)
.setQuery(boolQuery)
.setVersion(false)
.setSize(1)
.request();
securityIndex.prepareIndexIfNeededThenExecute(listener::onFailure, () ->
executeAsyncWithOrigin(client, SECURITY_ORIGIN, SearchAction.INSTANCE, searchRequest,
ActionListener.wrap(
indexResponse -> {
if (indexResponse.getHits().getTotalHits().value > 0) {
listener.onFailure(traceLog("create api key", new ElasticsearchSecurityException(
"Error creating api key as api key with name [{}] already exists", request.getName())));
} else { } else {
createApiKeyAndIndexIt(authentication, request, userRoles, listener); createApiKeyAndIndexIt(authentication, request, userRoles, listener);
} }
},
listener::onFailure)));
} }
private void createApiKeyAndIndexIt(Authentication authentication, CreateApiKeyRequest request, Set<RoleDescriptor> roleDescriptorSet, private void createApiKeyAndIndexIt(Authentication authentication, CreateApiKeyRequest request, Set<RoleDescriptor> roleDescriptorSet,

View file

@ -7,7 +7,6 @@
package org.elasticsearch.xpack.security.authc; package org.elasticsearch.xpack.security.authc;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.ElasticsearchSecurityException;
import org.elasticsearch.action.DocWriteResponse; import org.elasticsearch.action.DocWriteResponse;
import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse; import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse;
@ -175,11 +174,12 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
assertThat(e.status(), is(RestStatus.FORBIDDEN)); assertThat(e.status(), is(RestStatus.FORBIDDEN));
} }
public void testCreateApiKeyFailsWhenApiKeyWithSameNameAlreadyExists() throws InterruptedException, ExecutionException { public void testMultipleApiKeysCanHaveSameName() {
String keyName = randomAlphaOfLength(5); String keyName = randomAlphaOfLength(5);
int noOfApiKeys = randomIntBetween(2, 5);
List<CreateApiKeyResponse> responses = new ArrayList<>(); List<CreateApiKeyResponse> responses = new ArrayList<>();
{ for (int i = 0; i < noOfApiKeys; i++) {
final RoleDescriptor descriptor = new RoleDescriptor("role", new String[] { "monitor" }, null, null); final RoleDescriptor descriptor = new RoleDescriptor("role", new String[]{"monitor"}, null, null);
Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken
.basicAuthHeaderValue(SecuritySettingsSource.TEST_SUPERUSER, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); .basicAuthHeaderValue(SecuritySettingsSource.TEST_SUPERUSER, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING)));
final CreateApiKeyResponse response = new CreateApiKeyRequestBuilder(client).setName(keyName).setExpiration(null) final CreateApiKeyResponse response = new CreateApiKeyRequestBuilder(client).setName(keyName).setExpiration(null)
@ -188,30 +188,10 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
assertNotNull(response.getKey()); assertNotNull(response.getKey());
responses.add(response); responses.add(response);
} }
assertThat(responses.size(), is(noOfApiKeys));
final RoleDescriptor descriptor = new RoleDescriptor("role", new String[] { "monitor" }, null, null); for (int i = 0; i < noOfApiKeys; i++) {
Client client = client().filterWithHeader(Collections.singletonMap("Authorization", assertThat(responses.get(i).getName(), is(keyName));
UsernamePasswordToken.basicAuthHeaderValue(SecuritySettingsSource.TEST_SUPERUSER, }
SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING)));
ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, () -> new CreateApiKeyRequestBuilder(client)
.setName(keyName)
.setExpiration(TimeValue.timeValueHours(TimeUnit.DAYS.toHours(7L)))
.setRoleDescriptors(Collections.singletonList(descriptor))
.get());
assertThat(e.getMessage(), equalTo("Error creating api key as api key with name ["+keyName+"] already exists"));
// Now invalidate the API key
PlainActionFuture<InvalidateApiKeyResponse> listener = new PlainActionFuture<>();
client.execute(InvalidateApiKeyAction.INSTANCE, InvalidateApiKeyRequest.usingApiKeyName(keyName, false), listener);
InvalidateApiKeyResponse invalidateResponse = listener.get();
verifyInvalidateResponse(1, responses, invalidateResponse);
// try to create API key with same name, should succeed now
CreateApiKeyResponse createResponse = new CreateApiKeyRequestBuilder(client).setName(keyName)
.setExpiration(TimeValue.timeValueHours(TimeUnit.DAYS.toHours(7L)))
.setRoleDescriptors(Collections.singletonList(descriptor)).get();
assertNotNull(createResponse.getId());
assertNotNull(createResponse.getKey());
} }
public void testInvalidateApiKeysForRealm() throws InterruptedException, ExecutionException { public void testInvalidateApiKeysForRealm() throws InterruptedException, ExecutionException {