Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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 @@ -187,7 +187,7 @@ public void executeAfterRewritingAuthentication(Consumer<StoredContext> consumer
private Map<String, Object> rewriteMetadataForApiKeyRoleDescriptors(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.isAuthenticatedAsApiKey()) {
if (authentication.getVersion().onOrAfter(VERSION_API_KEY_ROLES_AS_BYTES)
&& streamVersion.before(VERSION_API_KEY_ROLES_AS_BYTES)) {
metadata = new HashMap<>(metadata);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,22 +121,34 @@ 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;
}

/**
* 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;
}

/**
* 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 @@ -292,7 +304,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
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 @@ -6,6 +6,7 @@
*/
package org.elasticsearch.xpack.idp.action;

import org.elasticsearch.Version;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.ActionFilters;
import org.elasticsearch.action.support.PlainActionFuture;
Expand All @@ -19,6 +20,7 @@
import org.elasticsearch.transport.TransportService;
import org.elasticsearch.xpack.core.security.SecurityContext;
import org.elasticsearch.xpack.core.security.authc.Authentication;
import org.elasticsearch.xpack.core.security.authc.AuthenticationField;
import org.elasticsearch.xpack.core.security.authc.support.SecondaryAuthentication;
import org.elasticsearch.xpack.core.security.user.User;
import org.elasticsearch.xpack.idp.privileges.ServiceProviderPrivileges;
Expand All @@ -40,6 +42,7 @@
import java.time.Duration;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import static org.hamcrest.CoreMatchers.containsString;
Expand Down Expand Up @@ -162,7 +165,10 @@ private TransportSamlInitiateSingleSignOnAction setupTransportAction(boolean wit
true
),
new Authentication.RealmRef("_es_api_key", "_es_api_key", "node_name"),
new Authentication.RealmRef("_es_api_key", "_es_api_key", "node_name")
new Authentication.RealmRef("_es_api_key", "_es_api_key", "node_name"),
Version.CURRENT,
Authentication.AuthenticationType.API_KEY,
Map.of(AuthenticationField.API_KEY_ID_KEY, randomAlphaOfLength(20))
)
).writeToContext(threadContext);
}
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 @@ -109,6 +112,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 @@ -1111,7 +1115,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 @@ -1122,7 +1128,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 @@ -1371,10 +1371,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 ["
+ authentication.getAuthenticatedBy().getType()
+ "]"
);
}
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