Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix isApiKey test and apply it consistently #84396

Merged
merged 3 commits into from
Feb 28, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,14 @@ public boolean isAuthenticatedWithServiceAccount() {
return ServiceAccountSettings.REALM_TYPE.equals(getAuthenticatedBy().getType());
}

public boolean isAuthenticatedWithApiKey() {
return AuthenticationType.API_KEY.equals(getAuthenticationType());
/**
* Whether the authenticating user is an API key, including a simple API key or a token created by an API key.
* @return
*/
public boolean isAuthenticatedAsApiKey() {
final boolean result = AuthenticationField.API_KEY_REALM_TYPE.equals(getAuthenticatedBy().getType());
assert false == result || AuthenticationField.API_KEY_REALM_NAME.equals(getAuthenticatedBy().getName());
return result;
}

public boolean isAuthenticatedAnonymously() {
Expand All @@ -245,14 +251,20 @@ public boolean isAuthenticatedInternally() {
* Authenticate with a service account and no run-as
*/
public boolean isServiceAccount() {
return isAuthenticatedWithServiceAccount() && false == getUser().isRunAs();
final boolean result = ServiceAccountSettings.REALM_TYPE.equals(getSourceRealm().getType());
assert false == result || ServiceAccountSettings.REALM_NAME.equals(getSourceRealm().getName())
: "service account realm name mismatch";
return result;
}
Comment on lines 253 to 258
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not strictly needed since Service Account cannot create tokens. But updated for consistency.


/**
* Authenticated with an API key and no run-as
* Whether the effective user is an API key, this including a simple API key authentication
* or a token created by the API key.
*/
public boolean isApiKey() {
return isAuthenticatedWithApiKey() && false == getUser().isRunAs();
final boolean result = AuthenticationField.API_KEY_REALM_TYPE.equals(getSourceRealm().getType());
assert false == result || AuthenticationField.API_KEY_REALM_NAME.equals(getSourceRealm().getName()) : "api key realm name mismatch";
return result;
}

/**
Expand Down Expand Up @@ -419,7 +431,7 @@ public void toXContentFragment(XContentBuilder builder) throws IOException {
}

private void assertApiKeyMetadata() {
assert (false == isAuthenticatedWithApiKey()) || (this.metadata.get(AuthenticationField.API_KEY_ID_KEY) != null)
assert (false == isAuthenticatedAsApiKey()) || (this.metadata.get(AuthenticationField.API_KEY_ID_KEY) != null)
: "API KEY authentication requires metadata to contain API KEY id, and the value must be non-null.";
}

Expand Down Expand Up @@ -695,8 +707,9 @@ private static RealmRef maybeRewriteRealmRef(Version streamVersion, RealmRef rea
@SuppressWarnings("unchecked")
private static Map<String, Object> maybeRewriteMetadataForApiKeyRoleDescriptors(Version streamVersion, Authentication authentication) {
Map<String, Object> metadata = authentication.getMetadata();
// If authentication type is API key, regardless whether it has run-as, the metadata must contain API key role descriptors
if (authentication.isAuthenticatedWithApiKey()) {
// If authentication user is an API key or a token created by an API key,
// regardless whether it has run-as, the metadata must contain API key role descriptors
if (authentication.isAuthenticatedAsApiKey()) {
assert metadata.containsKey(AuthenticationField.API_KEY_ROLE_DESCRIPTORS_KEY)
: "metadata must contain role descriptor for API key authentication";
assert metadata.containsKey(AuthenticationField.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,9 @@ private Authentication createMockAuthentication(
when(authentication.getSourceRealm()).thenReturn(authenticatedBy);
when(authentication.getAuthenticationType()).thenReturn(authenticationType);
when(authenticatedBy.getName()).thenReturn(realmName);
when(authenticatedBy.getType()).thenReturn(realmName);
when(authentication.getMetadata()).thenReturn(metadata);
when(authentication.isAuthenticatedWithApiKey()).thenCallRealMethod();
when(authentication.isAuthenticatedAsApiKey()).thenCallRealMethod();
when(authentication.isApiKey()).thenCallRealMethod();
return authentication;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@
import org.elasticsearch.xpack.core.security.action.apikey.InvalidateApiKeyAction;
import org.elasticsearch.xpack.core.security.action.apikey.InvalidateApiKeyRequest;
import org.elasticsearch.xpack.core.security.action.apikey.InvalidateApiKeyResponse;
import org.elasticsearch.xpack.core.security.action.token.CreateTokenAction;
import org.elasticsearch.xpack.core.security.action.token.CreateTokenRequestBuilder;
import org.elasticsearch.xpack.core.security.action.token.CreateTokenResponse;
import org.elasticsearch.xpack.core.security.action.user.PutUserAction;
import org.elasticsearch.xpack.core.security.action.user.PutUserRequest;
import org.elasticsearch.xpack.core.security.action.user.PutUserResponse;
Expand Down Expand Up @@ -112,6 +115,7 @@ public Settings nodeSettings(int nodeOrdinal, Settings otherSettings) {
return Settings.builder()
.put(super.nodeSettings(nodeOrdinal, otherSettings))
.put(XPackSettings.API_KEY_SERVICE_ENABLED_SETTING.getKey(), true)
.put(XPackSettings.TOKEN_SERVICE_ENABLED_SETTING.getKey(), true)
.put(ApiKeyService.DELETE_INTERVAL.getKey(), TimeValue.timeValueMillis(DELETE_INTERVAL_MILLIS))
.put(ApiKeyService.DELETE_TIMEOUT.getKey(), TimeValue.timeValueSeconds(5L))
.put("xpack.security.crypto.thread_pool.queue_size", CRYPTO_THREAD_POOL_QUEUE_SIZE)
Expand Down Expand Up @@ -1109,7 +1113,9 @@ public void testDerivedKeys() throws ExecutionException, InterruptedException {
Collections.singletonMap("Authorization", basicAuthHeaderValue(ES_TEST_ROOT_USER, TEST_PASSWORD_SECURE_STRING))
);
final CreateApiKeyResponse response = new CreateApiKeyRequestBuilder(client).setName("key-1")
.setRoleDescriptors(Collections.singletonList(new RoleDescriptor("role", new String[] { "manage_api_key" }, null, null)))
.setRoleDescriptors(
Collections.singletonList(new RoleDescriptor("role", new String[] { "manage_api_key", "manage_token" }, null, null))
)
.setMetadata(ApiKeyTests.randomMetadata())
.get();

Expand All @@ -1120,7 +1126,17 @@ public void testDerivedKeys() throws ExecutionException, InterruptedException {
// use the first ApiKey for authorized action
final String base64ApiKeyKeyValue = Base64.getEncoder()
.encodeToString((response.getId() + ":" + response.getKey().toString()).getBytes(StandardCharsets.UTF_8));
final Client clientKey1 = client().filterWithHeader(Collections.singletonMap("Authorization", "ApiKey " + base64ApiKeyKeyValue));

final Client clientKey1;
if (randomBoolean()) {
clientKey1 = client().filterWithHeader(Collections.singletonMap("Authorization", "ApiKey " + base64ApiKeyKeyValue));
} else {
final CreateTokenResponse createTokenResponse = new CreateTokenRequestBuilder(
client().filterWithHeader(Collections.singletonMap("Authorization", "ApiKey " + base64ApiKeyKeyValue)),
CreateTokenAction.INSTANCE
).setGrantType("client_credentials").get();
clientKey1 = client().filterWithHeader(Map.of("Authorization", "Bearer " + createTokenResponse.getTokenString()));
}

final String expectedMessage = "creating derived api keys requires an explicit role descriptor that is empty";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ public void testRunAsUsingApiKey() throws IOException {
createApiKeyResponse.getEntity().getContent()
);

final boolean runAsTestUser = false;
final boolean runAsTestUser = randomBoolean();

final Request authenticateRequest = new Request("GET", "/_security/_authenticate");
authenticateRequest.setOptions(
Expand Down Expand Up @@ -194,6 +194,32 @@ public void testRunAsUsingApiKey() throws IOException {
final ResponseException e = expectThrows(ResponseException.class, () -> getRestClient().performRequest(getUserRequest));
assertThat(e.getResponse().getStatusLine().getStatusCode(), equalTo(403));
}

// Run-as ignored if using a token created by the API key
final Request createTokenRequest = new Request("POST", "/_security/oauth2/token");
createTokenRequest.setOptions(
createTokenRequest.getOptions().toBuilder().addHeader("Authorization", "ApiKey " + apiKeyMapView.get("encoded"))
);
createTokenRequest.setJsonEntity("{\"grant_type\":\"client_credentials\"}");
final Response createTokenResponse = getRestClient().performRequest(createTokenRequest);
final XContentTestUtils.JsonMapView createTokenJsonView = XContentTestUtils.createJsonMapView(
createTokenResponse.getEntity().getContent()
);

authenticateRequest.setOptions(
RequestOptions.DEFAULT.toBuilder()
.addHeader("Authorization", "Bearer " + createTokenJsonView.get("access_token"))
.addHeader(
AuthenticationServiceField.RUN_AS_USER_HEADER,
runAsTestUser ? SecuritySettingsSource.TEST_USER_NAME : NO_ROLE_USER
)
);
final Response authenticateResponse2 = getRestClient().performRequest(authenticateRequest);
final XContentTestUtils.JsonMapView authenticateJsonView2 = XContentTestUtils.createJsonMapView(
authenticateResponse2.getEntity().getContent()
);
// run-as header is ignored, the user is still the run_as_user
assertThat(authenticateJsonView2.get("username"), equalTo(RUN_AS_USER));
}

public void testRunAsIgnoredForOAuthToken() throws IOException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.main.MainAction;
import org.elasticsearch.action.main.MainRequest;
import org.elasticsearch.client.internal.Client;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.settings.Settings;
Expand All @@ -27,6 +28,9 @@
import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyAction;
import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyRequest;
import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyResponse;
import org.elasticsearch.xpack.core.security.action.apikey.GetApiKeyAction;
import org.elasticsearch.xpack.core.security.action.apikey.GetApiKeyRequest;
import org.elasticsearch.xpack.core.security.action.apikey.GetApiKeyResponse;
import org.elasticsearch.xpack.core.security.action.apikey.GrantApiKeyAction;
import org.elasticsearch.xpack.core.security.action.apikey.GrantApiKeyRequest;
import org.elasticsearch.xpack.core.security.action.apikey.QueryApiKeyAction;
Expand All @@ -35,6 +39,9 @@
import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenAction;
import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenRequest;
import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenResponse;
import org.elasticsearch.xpack.core.security.action.token.CreateTokenAction;
import org.elasticsearch.xpack.core.security.action.token.CreateTokenRequestBuilder;
import org.elasticsearch.xpack.core.security.action.token.CreateTokenResponse;
import org.elasticsearch.xpack.core.security.action.user.PutUserAction;
import org.elasticsearch.xpack.core.security.action.user.PutUserRequest;
import org.elasticsearch.xpack.core.security.authc.support.Hasher;
Expand All @@ -45,6 +52,7 @@
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Map;

Expand All @@ -60,6 +68,7 @@ public class ApiKeySingleNodeTests extends SecuritySingleNodeTestCase {
protected Settings nodeSettings() {
Settings.Builder builder = Settings.builder().put(super.nodeSettings());
builder.put(XPackSettings.API_KEY_SERVICE_ENABLED_SETTING.getKey(), true);
builder.put(XPackSettings.TOKEN_SERVICE_ENABLED_SETTING.getKey(), true);
return builder.build();
}

Expand Down Expand Up @@ -185,6 +194,50 @@ public void testServiceAccountApiKey() throws IOException {
assertThat(roleDescriptor, equalTo(ServiceAccountService.getServiceAccounts().get("elastic/fleet-server").roleDescriptor()));
}

public void testGetApiKeyWorksForTheApiKeyItself() {
final String apiKeyName = randomAlphaOfLength(10);
final CreateApiKeyResponse createApiKeyResponse = client().execute(
CreateApiKeyAction.INSTANCE,
new CreateApiKeyRequest(
apiKeyName,
List.of(new RoleDescriptor("x", new String[] { "manage_own_api_key", "manage_token" }, null, null, null, null, null, null)),
null,
null
)
).actionGet();

final String apiKeyId = createApiKeyResponse.getId();
final String base64ApiKeyKeyValue = Base64.getEncoder()
.encodeToString((apiKeyId + ":" + createApiKeyResponse.getKey().toString()).getBytes(StandardCharsets.UTF_8));

// Works for both the API key itself or the token created by it
final Client clientKey1;
if (randomBoolean()) {
clientKey1 = client().filterWithHeader(Collections.singletonMap("Authorization", "ApiKey " + base64ApiKeyKeyValue));
} else {
final CreateTokenResponse createTokenResponse = new CreateTokenRequestBuilder(
client().filterWithHeader(Collections.singletonMap("Authorization", "ApiKey " + base64ApiKeyKeyValue)),
CreateTokenAction.INSTANCE
).setGrantType("client_credentials").get();
clientKey1 = client().filterWithHeader(Map.of("Authorization", "Bearer " + createTokenResponse.getTokenString()));
}

// Can get its own info
final GetApiKeyResponse getApiKeyResponse = clientKey1.execute(
GetApiKeyAction.INSTANCE,
GetApiKeyRequest.usingApiKeyId(apiKeyId, randomBoolean())
).actionGet();
assertThat(getApiKeyResponse.getApiKeyInfos().length, equalTo(1));
assertThat(getApiKeyResponse.getApiKeyInfos()[0].getId(), equalTo(apiKeyId));

// Cannot get any other keys
final ElasticsearchSecurityException e = expectThrows(
ElasticsearchSecurityException.class,
() -> clientKey1.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.forAllApiKeys()).actionGet()
);
assertThat(e.getMessage(), containsString("unauthorized for API key id [" + apiKeyId + "]"));
}

private Map<String, Object> getApiKeyDocument(String apiKeyId) {
final GetResponse getResponse = client().execute(GetAction.INSTANCE, new GetRequest(".security-7", apiKeyId)).actionGet();
return getResponse.getSource();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1350,10 +1350,12 @@ public static String getCreatorRealmType(final Authentication authentication) {
* @return A map for the metadata or an empty map if no metadata is found.
*/
public static Map<String, Object> getApiKeyMetadata(Authentication authentication) {
if (false == authentication.isAuthenticatedWithApiKey()) {
if (false == authentication.isAuthenticatedAsApiKey()) {
throw new IllegalArgumentException(
"authentication type must be [api_key], got ["
+ authentication.getAuthenticationType().name().toLowerCase(Locale.ROOT)
"authentication realm must be ["
+ AuthenticationField.API_KEY_REALM_TYPE
+ "], got ["
+ AuthenticationField.API_KEY_REALM_TYPE
+ "]"
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -879,7 +879,7 @@ private ElasticsearchSecurityException denialException(
userText = userText + " run as [" + authentication.getUser().principal() + "]";
}
// check for authentication by API key
if (authentication.isAuthenticatedWithApiKey()) {
if (authentication.isAuthenticatedAsApiKey()) {
final String apiKeyId = (String) authentication.getMetadata().get(AuthenticationField.API_KEY_ID_KEY);
assert apiKeyId != null : "api key id must be present in the metadata";
userText = "API key id [" + apiKeyId + "] of " + userText;
Expand Down
Loading