Skip to content

Commit

Permalink
Add exception metadata for disabled features (#52811)
Browse files Browse the repository at this point in the history
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, #47759
  • Loading branch information
tvernum committed Mar 5, 2020
1 parent cc0fdaf commit f5ba0f6
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* 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.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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -38,6 +39,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;
Expand All @@ -59,8 +61,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;
Expand Down Expand Up @@ -435,6 +439,20 @@ 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));
// 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"));
}

public void testApiKeyCache() {
final String apiKey = randomAlphaOfLength(16);
Hasher hasher = randomFrom(Hasher.PBKDF2, Hasher.BCRYPT4, Hasher.BCRYPT);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -54,6 +55,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;
Expand All @@ -63,7 +65,6 @@
import org.junit.BeforeClass;

import javax.crypto.SecretKey;

import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
Expand All @@ -79,8 +80,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;
Expand Down Expand Up @@ -560,20 +564,23 @@ 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));
// 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<UserToken> future = new PlainActionFuture<>();
tokenService.getAndValidateToken(null, future);
assertNull(future.get());

e = expectThrows(IllegalStateException.class, () -> {
PlainActionFuture<TokensInvalidationResult> invalidateFuture = new PlainActionFuture<>();
tokenService.invalidateAccessToken((String) null, invalidateFuture);
invalidateFuture.actionGet();
});
assertEquals("security tokens are not enabled", e.getMessage());
PlainActionFuture<TokensInvalidationResult> 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));
// 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"));
}

public void testBytesKeyEqualsHashCode() {
Expand Down

0 comments on commit f5ba0f6

Please sign in to comment.