Skip to content
Closed
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 @@ -119,7 +119,7 @@ public class RcsCcsCommonYamlTestSuiteIT extends ESClientYamlSuiteTestCase {

private static Map<String, Object> createCrossClusterAccessApiKey() throws IOException {
assert fulfillingCluster != null;
final var createApiKeyRequest = new Request("POST", "/_security/api_key");
final var createApiKeyRequest = new Request("POST", "/_security/_ccs/api_key");
createApiKeyRequest.setJsonEntity("""
{
"name": "cross_cluster_access_key",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

package org.elasticsearch.xpack.core.security.action.apikey;

import org.elasticsearch.core.Nullable;

public enum ApiKeyType {

DEFAULT() {
@Override
public String getDocType() {
return "api_key";
}

@Override
public String getCredentialsPrefix() {
return "";
}
},
CCS() {
@Override
public String getDocType() {
return "api_key_ccs";
}

@Override
public String getCredentialsPrefix() {
return "ccs_";
}
};

public abstract String getDocType();

public abstract String getCredentialsPrefix();

public static ApiKeyType fromDocType(@Nullable String docType) {
if (docType == null) {
return DEFAULT;
}
return switch (docType) {
case "api_key" -> DEFAULT;
case "api_key_ccs" -> CCS;
default -> throw new IllegalArgumentException("unknown doc type [" + docType + "]");
};
}

public static ApiKeyType fromCredentialsPrefix(String prefix) {
return switch (prefix) {
case "ccs_" -> CCS;
default -> throw new IllegalArgumentException("unknown credentials prefix [" + prefix + "]");
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public final class CreateApiKeyRequest extends ActionRequest {
private Map<String, Object> metadata;
private List<RoleDescriptor> roleDescriptors = Collections.emptyList();
private WriteRequest.RefreshPolicy refreshPolicy = DEFAULT_REFRESH_POLICY;
private ApiKeyType type = ApiKeyType.DEFAULT;

public CreateApiKeyRequest() {
super();
Expand Down Expand Up @@ -142,6 +143,14 @@ public void setMetadata(Map<String, Object> metadata) {
this.metadata = metadata;
}

public ApiKeyType getType() {
return type;
}

public void setType(ApiKeyType type) {
this.type = type;
}

@Override
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ public CreateApiKeyRequestBuilder setMetadata(Map<String, Object> metadata) {
return this;
}

public CreateApiKeyRequestBuilder setType(ApiKeyType apiKeyType) {
request.setType(apiKeyType);
return this;
}

public CreateApiKeyRequestBuilder source(BytesReference source, XContentType xContentType) throws IOException {
final NamedXContentRegistry registry = NamedXContentRegistry.EMPTY;
try (
Expand All @@ -96,7 +101,6 @@ public CreateApiKeyRequestBuilder source(BytesReference source, XContentType xCo
setRoleDescriptors(createApiKeyRequest.getRoleDescriptors());
setExpiration(createApiKeyRequest.getExpiration());
setMetadata(createApiKeyRequest.getMetadata());

}
return this;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

package org.elasticsearch.xpack.core.security.action.apikey;

import org.elasticsearch.TransportVersion;
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
Expand Down Expand Up @@ -55,15 +56,21 @@ public final class CreateApiKeyResponse extends ActionResponse implements ToXCon
private final String id;
private final SecureString key;
private final Instant expiration;
private final ApiKeyType type;

public CreateApiKeyResponse(String name, String id, SecureString key, Instant expiration) {
this(name, id, key, expiration, ApiKeyType.DEFAULT);
}

public CreateApiKeyResponse(String name, String id, SecureString key, Instant expiration, ApiKeyType type) {
this.name = name;
this.id = id;
this.key = key;
// As we do not yet support the nanosecond precision when we serialize to JSON,
// here creating the 'Instant' of milliseconds precision.
// This Instant can then be used for date comparison.
this.expiration = (expiration != null) ? Instant.ofEpochMilli(expiration.toEpochMilli()) : null;
this.type = type;
}

public CreateApiKeyResponse(StreamInput in) throws IOException {
Expand All @@ -80,6 +87,11 @@ public CreateApiKeyResponse(StreamInput in) throws IOException {
}
}
this.expiration = in.readOptionalInstant();
if (in.getTransportVersion().onOrAfter(TransportVersion.V_8_8_0)) {
this.type = in.readEnum(ApiKeyType.class);
} else {
this.type = ApiKeyType.DEFAULT;
}
}

public String getName() {
Expand All @@ -99,12 +111,21 @@ public Instant getExpiration() {
return expiration;
}

public String getEncoded() {
final String encoded = Base64.getEncoder().encodeToString((id + ":" + key).getBytes(StandardCharsets.UTF_8));
if (type == ApiKeyType.DEFAULT) {
return encoded;
} else {
return type.getCredentialsPrefix() + encoded;
}
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((expiration == null) ? 0 : expiration.hashCode());
result = prime * result + Objects.hash(id, name, key);
result = prime * result + Objects.hash(id, name, key, type);
return result;
}

Expand All @@ -123,7 +144,8 @@ public boolean equals(Object obj) {
return Objects.equals(expiration, other.expiration)
&& Objects.equals(id, other.id)
&& Objects.equals(key, other.key)
&& Objects.equals(name, other.name);
&& Objects.equals(name, other.name)
&& Objects.equals(type, other.type);
}

@Override
Expand All @@ -140,6 +162,9 @@ public void writeTo(StreamOutput out) throws IOException {
}
}
out.writeOptionalInstant(expiration);
if (out.getTransportVersion().onOrAfter(TransportVersion.V_8_8_0)) {
out.writeEnum(type);
}
}

public static CreateApiKeyResponse fromXContent(XContentParser parser) throws IOException {
Expand All @@ -152,13 +177,15 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
if (expiration != null) {
builder.field("expiration", expiration.toEpochMilli());
}
byte[] charBytes = CharArrays.toUtf8Bytes(key.getChars());
try {
builder.field("api_key").utf8Value(charBytes, 0, charBytes.length);
} finally {
Arrays.fill(charBytes, (byte) 0);
if (type == ApiKeyType.DEFAULT) {
byte[] charBytes = CharArrays.toUtf8Bytes(key.getChars());
try {
builder.field("api_key").utf8Value(charBytes, 0, charBytes.length);
} finally {
Arrays.fill(charBytes, (byte) 0);
}
}
builder.field("encoded", Base64.getEncoder().encodeToString((id + ":" + key).getBytes(StandardCharsets.UTF_8)));
builder.field("encoded", getEncoded());
return builder.endObject();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

package org.elasticsearch.xpack.core.security.action.apikey;

import org.elasticsearch.action.ActionType;

/**
* ActionType for the creation of an API key
*/
public final class CreateCcsApiKeyAction extends ActionType<CreateApiKeyResponse> {

public static final String NAME = "cluster:admin/xpack/security/ccs/api_key/create";
public static final CreateCcsApiKeyAction INSTANCE = new CreateCcsApiKeyAction();

private CreateCcsApiKeyAction() {
super(NAME, CreateApiKeyResponse::new);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public final class AuthenticationField {
public static final String API_KEY_CREATOR_REALM_TYPE = "_security_api_key_creator_realm_type";
public static final String API_KEY_ID_KEY = "_security_api_key_id";
public static final String API_KEY_NAME_KEY = "_security_api_key_name";
public static final String API_KEY_TYPE_KEY = "_security_api_key_type";
public static final String API_KEY_METADATA_KEY = "_security_api_key_metadata";
public static final String API_KEY_ROLE_DESCRIPTORS_KEY = "_security_api_key_role_descriptors";
public static final String API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY = "_security_api_key_limited_by_role_descriptors";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.util.ArrayUtils;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.xpack.core.security.action.apikey.ApiKeyType;
import org.elasticsearch.xpack.core.security.authc.CrossClusterAccessSubjectInfo.RoleDescriptorsBytes;
import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings;
import org.elasticsearch.xpack.core.security.authz.store.RoleReference;
Expand Down Expand Up @@ -251,18 +252,31 @@ private RoleReferenceIntersection buildRoleReferencesForApiKey() {
if (roleDescriptorsBytes == null && limitedByRoleDescriptorsBytes == null) {
throw new ElasticsearchSecurityException("no role descriptors found for API key");
}
final RoleReference.ApiKeyRoleReference limitedByRoleReference = new RoleReference.ApiKeyRoleReference(
apiKeyId,
limitedByRoleDescriptorsBytes,
RoleReference.ApiKeyRoleType.LIMITED_BY
);
if (isEmptyRoleDescriptorsBytes(roleDescriptorsBytes)) {
return new RoleReferenceIntersection(limitedByRoleReference);
final ApiKeyType apiKeyType = ApiKeyType.fromDocType((String) metadata.get(AuthenticationField.API_KEY_TYPE_KEY));
switch (apiKeyType) {
case DEFAULT -> {
final RoleReference.ApiKeyRoleReference limitedByRoleReference = new RoleReference.ApiKeyRoleReference(
apiKeyId,
limitedByRoleDescriptorsBytes,
RoleReference.ApiKeyRoleType.LIMITED_BY
);
if (isEmptyRoleDescriptorsBytes(roleDescriptorsBytes)) {
return new RoleReferenceIntersection(limitedByRoleReference);
}
return new RoleReferenceIntersection(
new RoleReference.ApiKeyRoleReference(apiKeyId, roleDescriptorsBytes, RoleReference.ApiKeyRoleType.ASSIGNED),
limitedByRoleReference
);
}
case CCS -> {
// TODO: assert assigned and limited-by role descriptors are identical
return new RoleReferenceIntersection(
new RoleReference.ApiKeyRoleReference(apiKeyId, roleDescriptorsBytes, RoleReference.ApiKeyRoleType.ASSIGNED)
);
}
default -> throw new IllegalArgumentException("unknown API key type [" + apiKeyType + "]");
}
return new RoleReferenceIntersection(
new RoleReference.ApiKeyRoleReference(apiKeyId, roleDescriptorsBytes, RoleReference.ApiKeyRoleType.ASSIGNED),
limitedByRoleReference
);

}

private RoleReferenceIntersection buildRoleReferencesForCrossClusterAccess() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ protected static Map<String, Object> createCrossClusterAccessApiKey(String roleD

static Map<String, Object> createCrossClusterAccessApiKey(RestClient targetClusterClient, String roleDescriptorsJson) {
// Create API key on FC
final var createApiKeyRequest = new Request("POST", "/_security/api_key");
final var createApiKeyRequest = new Request("POST", "/_security/_ccs/api_key");
createApiKeyRequest.setJsonEntity(Strings.format("""
{
"name": "cross_cluster_access_key",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ public class Constants {
"cluster:admin/xpack/security/api_key/update",
"cluster:admin/xpack/security/api_key/bulk_update",
"cluster:admin/xpack/security/cache/clear",
"cluster:admin/xpack/security/ccs/api_key/create",
"cluster:admin/xpack/security/delegate_pki",
"cluster:admin/xpack/security/enroll/node",
"cluster:admin/xpack/security/enroll/kibana",
Expand Down
8 changes: 4 additions & 4 deletions x-pack/plugin/security/qa/security-trial/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ testClusters.matching { it.name == 'javaRestTest' }.configureEach {
setting 'xpack.security.authc.api_key.enabled', 'true'
setting 'xpack.security.remote_cluster_client.ssl.enabled', 'false'

keystore 'cluster.remote.my_remote_cluster_a.credentials', 'cluster_a_credentials'
keystore 'cluster.remote.my_remote_cluster_b.credentials', 'cluster_b_credentials'
keystore 'cluster.remote.my_remote_cluster_a_1.credentials', 'cluster_a_credentials'
keystore 'cluster.remote.my_remote_cluster_a_2.credentials', 'cluster_a_credentials'
keystore 'cluster.remote.my_remote_cluster_a.credentials', 'ccs_cluster_a_credentials'
keystore 'cluster.remote.my_remote_cluster_b.credentials', 'ccs_cluster_b_credentials'
keystore 'cluster.remote.my_remote_cluster_a_1.credentials', 'ccs_cluster_a_credentials'
keystore 'cluster.remote.my_remote_cluster_a_2.credentials', 'ccs_cluster_a_credentials'

rolesFile file('src/javaRestTest/resources/roles.yml')
user username: "admin_user", password: "admin-password"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ public static void checkFeatureFlag() {

private static final String CLUSTER_A = "my_remote_cluster_a";
private static final String CLUSTER_B = "my_remote_cluster_b";
private static final String CLUSTER_A_CREDENTIALS = "cluster_a_credentials";
private static final String CLUSTER_B_CREDENTIALS = "cluster_b_credentials";
private static final String CLUSTER_A_CREDENTIALS = "ccs_cluster_a_credentials";
private static final String CLUSTER_B_CREDENTIALS = "ccs_cluster_b_credentials";
private static final String REMOTE_SEARCH_USER = "remote_search_user";
private static final SecureString PASSWORD = new SecureString("super-secret-password".toCharArray());
private static final String REMOTE_SEARCH_ROLE = "remote_search";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
import org.elasticsearch.xpack.core.security.action.DelegatePkiAuthenticationAction;
import org.elasticsearch.xpack.core.security.action.apikey.BulkUpdateApiKeyAction;
import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyAction;
import org.elasticsearch.xpack.core.security.action.apikey.CreateCcsApiKeyAction;
import org.elasticsearch.xpack.core.security.action.apikey.GetApiKeyAction;
import org.elasticsearch.xpack.core.security.action.apikey.GrantApiKeyAction;
import org.elasticsearch.xpack.core.security.action.apikey.InvalidateApiKeyAction;
Expand Down Expand Up @@ -191,6 +192,7 @@
import org.elasticsearch.xpack.security.action.TransportDelegatePkiAuthenticationAction;
import org.elasticsearch.xpack.security.action.apikey.TransportBulkUpdateApiKeyAction;
import org.elasticsearch.xpack.security.action.apikey.TransportCreateApiKeyAction;
import org.elasticsearch.xpack.security.action.apikey.TransportCreateCcsApiKeyAction;
import org.elasticsearch.xpack.security.action.apikey.TransportGetApiKeyAction;
import org.elasticsearch.xpack.security.action.apikey.TransportGrantApiKeyAction;
import org.elasticsearch.xpack.security.action.apikey.TransportInvalidateApiKeyAction;
Expand Down Expand Up @@ -294,6 +296,7 @@
import org.elasticsearch.xpack.security.rest.action.apikey.RestBulkUpdateApiKeyAction;
import org.elasticsearch.xpack.security.rest.action.apikey.RestClearApiKeyCacheAction;
import org.elasticsearch.xpack.security.rest.action.apikey.RestCreateApiKeyAction;
import org.elasticsearch.xpack.security.rest.action.apikey.RestCreateCcsApiKeyAction;
import org.elasticsearch.xpack.security.rest.action.apikey.RestGetApiKeyAction;
import org.elasticsearch.xpack.security.rest.action.apikey.RestGrantApiKeyAction;
import org.elasticsearch.xpack.security.rest.action.apikey.RestInvalidateApiKeyAction;
Expand Down Expand Up @@ -1303,6 +1306,7 @@ public void onIndexModule(IndexModule module) {
new ActionHandler<>(PutPrivilegesAction.INSTANCE, TransportPutPrivilegesAction.class),
new ActionHandler<>(DeletePrivilegesAction.INSTANCE, TransportDeletePrivilegesAction.class),
new ActionHandler<>(CreateApiKeyAction.INSTANCE, TransportCreateApiKeyAction.class),
new ActionHandler<>(CreateCcsApiKeyAction.INSTANCE, TransportCreateCcsApiKeyAction.class),
new ActionHandler<>(GrantApiKeyAction.INSTANCE, TransportGrantApiKeyAction.class),
new ActionHandler<>(InvalidateApiKeyAction.INSTANCE, TransportInvalidateApiKeyAction.class),
new ActionHandler<>(GetApiKeyAction.INSTANCE, TransportGetApiKeyAction.class),
Expand Down Expand Up @@ -1386,6 +1390,7 @@ public List<RestHandler> getRestHandlers(
new RestPutPrivilegesAction(settings, getLicenseState()),
new RestDeletePrivilegesAction(settings, getLicenseState()),
new RestCreateApiKeyAction(settings, getLicenseState()),
new RestCreateCcsApiKeyAction(settings, getLicenseState()),
new RestUpdateApiKeyAction(settings, getLicenseState()),
new RestBulkUpdateApiKeyAction(settings, getLicenseState()),
new RestGrantApiKeyAction(settings, getLicenseState()),
Expand Down
Loading