From 4b8d32636a436d9af4dc0a0d572e9e812947de8c Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 27 Apr 2023 10:48:41 +1000 Subject: [PATCH 01/14] wip --- .../core/security/action/apikey/ApiKey.java | 24 +++ .../action/apikey/CreateApiKeyRequest.java | 9 ++ .../CreateCrossClusterApiKeyAction.java | 24 +++ .../security/authc/AuthenticationField.java | 1 + .../core/security/authz/RoleDescriptor.java | 29 +++- .../xpack/security/operator/Constants.java | 1 + .../xpack/security/Security.java | 5 + ...ansportCreateCrossClusterApiKeyAction.java | 54 +++++++ .../xpack/security/authc/ApiKeyService.java | 34 +++-- .../RestCreateCrossClusterApiKeyAction.java | 138 ++++++++++++++++++ .../security/authc/ApiKeyServiceTests.java | 4 + 11 files changed, 311 insertions(+), 12 deletions(-) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateCrossClusterApiKeyAction.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportCreateCrossClusterApiKeyAction.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyAction.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKey.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKey.java index 4be0988081bab..046c4baf9ade3 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKey.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKey.java @@ -24,6 +24,7 @@ import java.io.IOException; import java.time.Instant; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -36,6 +37,29 @@ */ public final class ApiKey implements ToXContentObject, Writeable { + public enum Type { + /** + * REST type API keys can authenticate on the HTTP interface + */ + REST, + /** + * Cross cluster type API keys can authenticate on the dedicated remote cluster server interface + */ + CROSS_CLUSTER; + + public static Type parse(String value) { + return switch (value.toLowerCase(Locale.ROOT)) { + case "rest" -> REST; + case "cross_cluster" -> CROSS_CLUSTER; + default -> throw new IllegalArgumentException("unknown API key type [" + value + "]"); + }; + } + + public String value() { + return name().toLowerCase(Locale.ROOT); + } + } + private final String name; private final String id; private final Instant creation; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequest.java index 69ad826c933e4..415f9fd71faf2 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequest.java @@ -38,6 +38,7 @@ public final class CreateApiKeyRequest extends ActionRequest { private final String id; private String name; + private ApiKey.Type type = ApiKey.Type.REST; private TimeValue expiration; private Map metadata; private List roleDescriptors = Collections.emptyList(); @@ -110,6 +111,14 @@ public void setName(String name) { this.name = name; } + public ApiKey.Type getType() { + return type; + } + + public void setType(ApiKey.Type type) { + this.type = type; + } + public TimeValue getExpiration() { return expiration; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateCrossClusterApiKeyAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateCrossClusterApiKeyAction.java new file mode 100644 index 0000000000000..2ae40419933b8 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateCrossClusterApiKeyAction.java @@ -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 CreateCrossClusterApiKeyAction extends ActionType { + + public static final String NAME = "cluster:admin/xpack/security/cross_cluster/api_key/create"; + public static final CreateCrossClusterApiKeyAction INSTANCE = new CreateCrossClusterApiKeyAction(); + + private CreateCrossClusterApiKeyAction() { + super(NAME, CreateApiKeyResponse::new); + } + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/AuthenticationField.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/AuthenticationField.java index 29f3d3f08e0ee..37cf09bc4607a 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/AuthenticationField.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/AuthenticationField.java @@ -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"; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java index 6962fdd9bf77e..725ccfdbcfa29 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java @@ -637,11 +637,29 @@ private static RemoteIndicesPrivileges parseRemoteIndex(String roleName, XConten private record IndicesPrivilegesWithOptionalRemoteClusters(IndicesPrivileges indicesPrivileges, String[] remoteClusters) {} + public static IndicesPrivileges parseIndexWithPrivileges(final String roleName, String[] privileges, XContentParser parser) + throws IOException { + final IndicesPrivilegesWithOptionalRemoteClusters indicesPrivilegesWithOptionalRemoteClusters = + parseIndexWithOptionalRemoteClusters(roleName, parser, false, false, privileges); + assert indicesPrivilegesWithOptionalRemoteClusters.remoteClusters == null; + return indicesPrivilegesWithOptionalRemoteClusters.indicesPrivileges; + } + private static IndicesPrivilegesWithOptionalRemoteClusters parseIndexWithOptionalRemoteClusters( final String roleName, final XContentParser parser, final boolean allow2xFormat, final boolean allowRemoteClusters + ) throws IOException { + return parseIndexWithOptionalRemoteClusters(roleName, parser, allow2xFormat, allowRemoteClusters, null); + } + + private static IndicesPrivilegesWithOptionalRemoteClusters parseIndexWithOptionalRemoteClusters( + final String roleName, + final XContentParser parser, + final boolean allow2xFormat, + final boolean allowRemoteClusters, + String[] privileges ) throws IOException { XContentParser.Token token = parser.currentToken(); if (token != XContentParser.Token.START_OBJECT) { @@ -656,7 +674,6 @@ private static IndicesPrivilegesWithOptionalRemoteClusters parseIndexWithOptiona String currentFieldName = null; String[] names = null; BytesReference query = null; - String[] privileges = null; String[] grantedFields = null; String[] deniedFields = null; boolean allowRestrictedIndices = false; @@ -780,7 +797,15 @@ private static IndicesPrivilegesWithOptionalRemoteClusters parseIndexWithOptiona ); } } else if (Fields.PRIVILEGES.match(currentFieldName, parser.getDeprecationHandler())) { - privileges = readStringArray(roleName, parser, true); + if (privileges == null) { + privileges = readStringArray(roleName, parser, true); + } else { + throw new ElasticsearchParseException( + "failed to parse indices privileges for role [{}]. [{}] field must not present", + roleName, + Fields.PRIVILEGES.getPreferredName() + ); + } } else if (Fields.FIELD_PERMISSIONS_2X.match(currentFieldName, parser.getDeprecationHandler())) { if (allow2xFormat) { grantedFields = readStringArray(roleName, parser, true); diff --git a/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java b/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java index 481ac66f4d1bf..7f4912659e0b9 100644 --- a/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java +++ b/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java @@ -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/cross_cluster/api_key/create", "cluster:admin/xpack/security/delegate_pki", "cluster:admin/xpack/security/enroll/node", "cluster:admin/xpack/security/enroll/kibana", diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 602b6c9197c0b..b2b6bfc8c2bd5 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -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.CreateCrossClusterApiKeyAction; 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; @@ -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.TransportCreateCrossClusterApiKeyAction; import org.elasticsearch.xpack.security.action.apikey.TransportGetApiKeyAction; import org.elasticsearch.xpack.security.action.apikey.TransportGrantApiKeyAction; import org.elasticsearch.xpack.security.action.apikey.TransportInvalidateApiKeyAction; @@ -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.RestCreateCrossClusterApiKeyAction; 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; @@ -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<>(CreateCrossClusterApiKeyAction.INSTANCE, TransportCreateCrossClusterApiKeyAction.class), new ActionHandler<>(GrantApiKeyAction.INSTANCE, TransportGrantApiKeyAction.class), new ActionHandler<>(InvalidateApiKeyAction.INSTANCE, TransportInvalidateApiKeyAction.class), new ActionHandler<>(GetApiKeyAction.INSTANCE, TransportGetApiKeyAction.class), @@ -1386,6 +1390,7 @@ public List getRestHandlers( new RestPutPrivilegesAction(settings, getLicenseState()), new RestDeletePrivilegesAction(settings, getLicenseState()), new RestCreateApiKeyAction(settings, getLicenseState()), + new RestCreateCrossClusterApiKeyAction(settings, getLicenseState()), new RestUpdateApiKeyAction(settings, getLicenseState()), new RestBulkUpdateApiKeyAction(settings, getLicenseState()), new RestGrantApiKeyAction(settings, getLicenseState()), diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportCreateCrossClusterApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportCreateCrossClusterApiKeyAction.java new file mode 100644 index 0000000000000..558031c1532e8 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportCreateCrossClusterApiKeyAction.java @@ -0,0 +1,54 @@ +/* + * 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.security.action.apikey; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.security.SecurityContext; +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.CreateCrossClusterApiKeyAction; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.security.authc.ApiKeyService; + +import java.util.Set; + +/** + * Implementation of the action needed to create an API key + */ +public final class TransportCreateCrossClusterApiKeyAction extends HandledTransportAction { + + private final ApiKeyService apiKeyService; + private final SecurityContext securityContext; + + @Inject + public TransportCreateCrossClusterApiKeyAction( + TransportService transportService, + ActionFilters actionFilters, + ApiKeyService apiKeyService, + SecurityContext context + ) { + super(CreateCrossClusterApiKeyAction.NAME, transportService, actionFilters, CreateApiKeyRequest::new); + this.apiKeyService = apiKeyService; + this.securityContext = context; + } + + @Override + protected void doExecute(Task task, CreateApiKeyRequest request, ActionListener listener) { + final Authentication authentication = securityContext.getAuthentication(); + if (authentication == null) { + listener.onFailure(new IllegalStateException("authentication is required")); + } else { + apiKeyService.createApiKey(authentication, request, Set.of(), listener); + } + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java index a88c4ad47fc88..87beabb8d39ca 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java @@ -300,16 +300,27 @@ public void createApiKey( listener.onFailure(new IllegalArgumentException("authentication must be provided")); } else { final Version version = getMinNodeVersion(); - if (version.before(VERSION_ADVANCED_REMOTE_CLUSTER_SECURITY) && hasRemoteIndices(request.getRoleDescriptors())) { - // Creating API keys with roles which define remote indices privileges is not allowed in a mixed cluster. - listener.onFailure( - new IllegalArgumentException( - "all nodes must have version [" - + VERSION_ADVANCED_REMOTE_CLUSTER_SECURITY - + "] or higher to support remote indices privileges for API keys" - ) - ); - return; + if (version.before(VERSION_ADVANCED_REMOTE_CLUSTER_SECURITY)) { + if (hasRemoteIndices(request.getRoleDescriptors())) { + // Creating API keys with roles which define remote indices privileges is not allowed in a mixed cluster. + listener.onFailure( + new IllegalArgumentException( + "all nodes must have version [" + + VERSION_ADVANCED_REMOTE_CLUSTER_SECURITY + + "] or higher to support remote indices privileges for API keys" + ) + ); + return; + } + if (request.getType() == ApiKey.Type.CROSS_CLUSTER) { + listener.onFailure( + new IllegalArgumentException( + "all nodes must have version [" + + VERSION_ADVANCED_REMOTE_CLUSTER_SECURITY + + "] or higher to support creating cross cluster API keys" + ) + ); + } } final Set filteredUserRoleDescriptors = maybeRemoveRemoteIndicesPrivileges( @@ -351,6 +362,7 @@ private void createApiKeyAndIndexIt( created, expiration, request.getRoleDescriptors(), + request.getType(), version, request.getMetadata() ) @@ -608,12 +620,14 @@ static XContentBuilder newDocument( Instant created, Instant expiration, List keyRoleDescriptors, + ApiKey.Type type, Version version, @Nullable Map metadata ) throws IOException { final XContentBuilder builder = XContentFactory.jsonBuilder(); builder.startObject() .field("doc_type", "api_key") + .field("type", type.value()) .field("creation_time", created.toEpochMilli()) .field("expiration_time", expiration == null ? null : expiration.toEpochMilli()) .field("api_key_invalidated", false); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyAction.java new file mode 100644 index 0000000000000..ee7903fb25557 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyAction.java @@ -0,0 +1,138 @@ +/* + * 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.security.rest.action.apikey; + +import org.elasticsearch.action.support.WriteRequest; +import org.elasticsearch.client.internal.node.NodeClient; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.CollectionUtils; +import org.elasticsearch.core.TimeValue; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.action.RestToXContentListener; +import org.elasticsearch.xcontent.ConstructingObjectParser; +import org.elasticsearch.xcontent.ParseField; +import org.elasticsearch.xpack.core.security.action.apikey.ApiKey; +import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyRequest; +import org.elasticsearch.xpack.core.security.action.apikey.CreateCrossClusterApiKeyAction; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static org.elasticsearch.rest.RestRequest.Method.POST; +import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg; + +/** + * Rest action to create an API key specific to cross cluster access via the dedicate remote cluster server port + */ +public final class RestCreateCrossClusterApiKeyAction extends ApiKeyBaseRestHandler { + + @SuppressWarnings("unchecked") + static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "cross_cluster_api_key_request_payload", + false, + (args, v) -> new Payload( + (String) args[0], + args[1] == null ? List.of() : (List) args[1], + args[2] == null ? List.of() : (List) args[2], + TimeValue.parseTimeValue((String) args[3], null, "expiration"), + (Map) args[4] + ) + ); + + static { + PARSER.declareString(constructorArg(), new ParseField("name")); + PARSER.declareObjectArray( + optionalConstructorArg(), + (p, c) -> RoleDescriptor.parseIndexWithPrivileges("cross_cluster", new String[] { "read" }, p), + new ParseField("search") + ); + PARSER.declareObjectArray( + optionalConstructorArg(), + (p, c) -> RoleDescriptor.parseIndexWithPrivileges("cross_cluster", new String[] { "read" }, p), + new ParseField("replication") + ); + PARSER.declareString(optionalConstructorArg(), new ParseField("expiration")); + PARSER.declareObject(optionalConstructorArg(), (p, c) -> p.map(), new ParseField("metadata")); + } + + /** + * @param settings the node's settings + * @param licenseState the license state that will be used to determine if + * security is licensed + */ + public RestCreateCrossClusterApiKeyAction(Settings settings, XPackLicenseState licenseState) { + super(settings, licenseState); + } + + @Override + public List routes() { + return List.of(new Route(POST, "/_security/cross_cluster/api_key")); + } + + @Override + public String getName() { + return "xpack_security_create_cross_cluster_api_key"; + } + + @Override + protected RestChannelConsumer innerPrepareRequest(final RestRequest request, final NodeClient client) throws IOException { + final Payload payload = PARSER.parse(request.contentParser(), null); + System.out.println("PAYLOAD IS " + payload); + + final CreateApiKeyRequest createApiKeyRequest = payload.toCreateApiKeyRequest(); + String refresh = request.param("refresh"); + if (refresh != null) { + createApiKeyRequest.setRefreshPolicy(WriteRequest.RefreshPolicy.parse(request.param("refresh"))); + } + return channel -> client.execute( + CreateCrossClusterApiKeyAction.INSTANCE, + createApiKeyRequest, + new RestToXContentListener<>(channel) + ); + } + + record Payload( + String name, + List search, + List replication, + TimeValue expiration, + Map metadata + ) { + public CreateApiKeyRequest toCreateApiKeyRequest() { + final CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(); + createApiKeyRequest.setName(name); + createApiKeyRequest.setType(ApiKey.Type.CROSS_CLUSTER); + createApiKeyRequest.setExpiration(expiration); + createApiKeyRequest.setMetadata(metadata); + + final String[] clusterPrivileges; + if (search.isEmpty() && replication.isEmpty()) { + throw new IllegalArgumentException("must specify non-empty indices for either [search] or [replication]"); + } else if (search.isEmpty()) { + clusterPrivileges = new String[] { "cross_cluster_access" }; + } else if (replication.isEmpty()) { + clusterPrivileges = new String[] { "cross_cluster_access" }; + } else { + clusterPrivileges = new String[] { "cross_cluster_access", "cross_cluster_access" }; + } + final RoleDescriptor roleDescriptor = new RoleDescriptor( + name, + clusterPrivileges, + CollectionUtils.concatLists(search, replication).toArray(RoleDescriptor.IndicesPrivileges[]::new), + null + ); + createApiKeyRequest.setRoleDescriptors(List.of(roleDescriptor)); + + return createApiKeyRequest; + } + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java index 5918511549a92..c8d8164308520 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java @@ -80,6 +80,7 @@ import org.elasticsearch.xpack.core.security.action.ClearSecurityCacheAction; import org.elasticsearch.xpack.core.security.action.ClearSecurityCacheRequest; import org.elasticsearch.xpack.core.security.action.ClearSecurityCacheResponse; +import org.elasticsearch.xpack.core.security.action.apikey.ApiKey; import org.elasticsearch.xpack.core.security.action.apikey.ApiKeyTests; import org.elasticsearch.xpack.core.security.action.apikey.BulkUpdateApiKeyRequest; import org.elasticsearch.xpack.core.security.action.apikey.BulkUpdateApiKeyResponse; @@ -722,6 +723,7 @@ private Map mockKeyDocument( Instant.now(), Instant.now().plus(expiry), keyRoles, + randomFrom(ApiKey.Type.values()), Version.CURRENT, metadata ); @@ -1855,6 +1857,7 @@ public void testMaybeBuildUpdatedDocument() throws IOException { Instant.now(), randomBoolean() ? null : Instant.now(), oldKeyRoles, + randomFrom(ApiKey.Type.values()), oldVersion, oldMetadata ) @@ -2141,6 +2144,7 @@ public static Authentication createApiKeyAuthentication( Instant.now(), Instant.now().plus(Duration.ofSeconds(3600)), keyRoles, + randomFrom(ApiKey.Type.values()), Version.CURRENT, randomBoolean() ? null : Map.of(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8)) ); From f425082c7884c6b31ae1cdade18cf841a63c3e25 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Mon, 1 May 2023 20:49:42 +1000 Subject: [PATCH 02/14] Working version --- .../action/apikey/CreateApiKeyRequest.java | 12 ++ .../core/security/authz/RoleDescriptor.java | 10 +- .../xpack/security/apikey/ApiKeyRestIT.java | 199 ++++++++++++++++++ .../security/authc/ApiKeyIntegTests.java | 2 + .../authc/apikey/ApiKeySingleNodeTests.java | 46 ++++ .../xpack/security/authc/ApiKeyService.java | 39 ++-- .../apikey/CrossClusterApiKeyAccess.java | 87 ++++++++ .../RestCreateCrossClusterApiKeyAction.java | 86 ++------ .../security/authc/ApiKeyServiceTests.java | 40 ++++ .../apikey/CrossClusterApiKeyAccessTests.java | 197 +++++++++++++++++ .../apikey/RestCreateApiKeyActionTests.java | 3 + ...stCreateCrossClusterApiKeyActionTests.java | 57 +++++ 12 files changed, 692 insertions(+), 86 deletions(-) create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/CrossClusterApiKeyAccess.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/CrossClusterApiKeyAccessTests.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyActionTests.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequest.java index 415f9fd71faf2..acdbea9f695da 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequest.java @@ -15,6 +15,7 @@ import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.core.Assertions; import org.elasticsearch.core.Nullable; import org.elasticsearch.core.TimeValue; import org.elasticsearch.xpack.core.security.action.role.RoleDescriptorRequestValidator; @@ -153,6 +154,17 @@ public void setMetadata(Map metadata) { @Override public ActionRequestValidationException validate() { + if (Assertions.ENABLED && type == ApiKey.Type.CROSS_CLUSTER) { + assert roleDescriptors.size() == 1; + final RoleDescriptor roleDescriptor = roleDescriptors.iterator().next(); + assert roleDescriptor.getName().equals(name); + assert false == roleDescriptor.hasApplicationPrivileges(); + assert false == roleDescriptor.hasRunAs(); + assert false == roleDescriptor.hasConfigurableClusterPrivileges(); + assert false == roleDescriptor.hasRemoteIndicesPrivileges(); + assert roleDescriptor.hasClusterPrivileges(); + assert roleDescriptor.hasIndicesPrivileges(); + } ActionRequestValidationException validationException = null; if (Strings.isNullOrEmpty(name)) { validationException = addValidationError("api key name is required", validationException); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java index 725ccfdbcfa29..1090863a41120 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java @@ -218,6 +218,10 @@ public boolean hasClusterPrivileges() { return clusterPrivileges.length != 0; } + public boolean hasIndicesPrivileges() { + return indicesPrivileges.length != 0; + } + public boolean hasApplicationPrivileges() { return applicationPrivileges.length != 0; } @@ -801,7 +805,7 @@ private static IndicesPrivilegesWithOptionalRemoteClusters parseIndexWithOptiona privileges = readStringArray(roleName, parser, true); } else { throw new ElasticsearchParseException( - "failed to parse indices privileges for role [{}]. [{}] field must not present", + "failed to parse indices privileges for role [{}]. field [{}] must not present", roleName, Fields.PRIVILEGES.getPreferredName() ); @@ -1158,6 +1162,10 @@ public boolean isUsingFieldLevelSecurity() { return hasDeniedFields() || hasGrantedFields(); } + public boolean isUsingDocumentOrFieldLevelSecurity() { + return isUsingDocumentLevelSecurity() || isUsingFieldLevelSecurity(); + } + public boolean allowRestrictedIndices() { return allowRestrictedIndices; } diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyRestIT.java index e500ed5ab3c81..2f32a2747e54d 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyRestIT.java @@ -67,6 +67,8 @@ public class ApiKeyRestIT extends SecurityOnTrialLicenseRestTestCase { private static final SecureString END_USER_PASSWORD = new SecureString("end-user-password".toCharArray()); private static final String MANAGE_OWN_API_KEY_USER = "manage_own_api_key_user"; private static final String REMOTE_INDICES_USER = "remote_indices_user"; + private static final String MANAGE_API_KEY_USER = "manage_api_key_user"; + private static final String MANAGE_SECURITY_USER = "manage_security_user"; @Before public void createUsers() throws IOException { @@ -76,6 +78,10 @@ public void createUsers() throws IOException { createRole("user_role", Set.of("monitor")); createUser(MANAGE_OWN_API_KEY_USER, END_USER_PASSWORD, List.of("manage_own_api_key_role")); createRole("manage_own_api_key_role", Set.of("manage_own_api_key")); + createUser(MANAGE_API_KEY_USER, END_USER_PASSWORD, List.of("manage_api_key_role")); + createRole("manage_api_key_role", Set.of("manage_api_key")); + createUser(MANAGE_SECURITY_USER, END_USER_PASSWORD, List.of("manage_security_role")); + createRole("manage_security_role", Set.of("manage_security")); } @After @@ -83,11 +89,17 @@ public void cleanUp() throws IOException { deleteUser(SYSTEM_USER); deleteUser(END_USER); deleteUser(MANAGE_OWN_API_KEY_USER); + deleteUser(MANAGE_API_KEY_USER); + deleteUser(MANAGE_SECURITY_USER); deleteRole("system_role"); deleteRole("user_role"); deleteRole("manage_own_api_key_role"); + deleteRole("manage_api_key_role"); + deleteRole("manage_security_role"); invalidateApiKeysForUser(END_USER); invalidateApiKeysForUser(MANAGE_OWN_API_KEY_USER); + invalidateApiKeysForUser(MANAGE_API_KEY_USER); + invalidateApiKeysForUser(MANAGE_SECURITY_USER); } @SuppressWarnings("unchecked") @@ -668,6 +680,184 @@ public void testRemoteIndicesSupportForApiKeys() throws IOException { } + public void testCreateCrossClusterApiKey() throws IOException { + assumeTrue("untrusted remote cluster feature flag must be enabled", TcpTransport.isUntrustedRemoteClusterEnabled()); + + final Request createRequest = new Request("POST", "/_security/cross_cluster/api_key"); + createRequest.setJsonEntity(""" + { + "name": "my-key", + "access": { + "search": [ + { + "names": [ "metrics" ], + "query": "{\\"term\\":{\\"score\\":42}}" + } + ], + "replication": [ + { + "names": [ "logs" ], + "allow_restricted_indices": true + } + ] + }, + "expiration": "7d", + "metadata": { "tag": "shared", "points": 0 } + }"""); + setUserForRequest(createRequest, MANAGE_SECURITY_USER, END_USER_PASSWORD); + + final ObjectPath createResponse = assertOKAndCreateObjectPath(client().performRequest(createRequest)); + final String apiKeyId = createResponse.evaluate("id"); + + final Request fetchRequest; + if (randomBoolean()) { + fetchRequest = new Request("GET", "/_security/api_key"); + fetchRequest.addParameter("id", apiKeyId); + } else { + fetchRequest = new Request("GET", "/_security/_query/api_key"); + fetchRequest.setJsonEntity(Strings.format(""" + { "query": { "ids": { "values": ["%s"] } } }""", apiKeyId)); + } + + if (randomBoolean()) { + setUserForRequest(fetchRequest, MANAGE_SECURITY_USER, END_USER_PASSWORD); + } else { + setUserForRequest(fetchRequest, MANAGE_API_KEY_USER, END_USER_PASSWORD); + } + final ObjectPath fetchResponse = assertOKAndCreateObjectPath(client().performRequest(fetchRequest)); + + assertThat(fetchResponse.evaluate("api_keys.0.id"), equalTo(apiKeyId)); + assertThat( + fetchResponse.evaluate("api_keys.0.role_descriptors"), + equalTo( + Map.of( + "my-key", + XContentTestUtils.convertToMap( + new RoleDescriptor( + "my-key", + new String[] { "cross_cluster_access", "cluster:monitor/state" }, + new RoleDescriptor.IndicesPrivileges[] { + RoleDescriptor.IndicesPrivileges.builder() + .indices("metrics") + .privileges("read", "read_cross_cluster", "view_index_metadata") + .query("{\"term\":{\"score\":42}}") + .build(), + RoleDescriptor.IndicesPrivileges.builder() + .indices("logs") + .privileges( + "manage", + "read", + "indices:internal/admin/ccr/restore/*", + "internal:transport/proxy/indices:internal/admin/ccr/restore/*" + ) + .allowRestrictedIndices(true) + .build() }, + null + ) + ) + ) + ) + ); + + final Request deleteRequest = new Request("DELETE", "/_security/api_key"); + deleteRequest.setJsonEntity(Strings.format(""" + {"ids": ["%s"]}""", apiKeyId)); + if (randomBoolean()) { + setUserForRequest(deleteRequest, MANAGE_SECURITY_USER, END_USER_PASSWORD); + } else { + setUserForRequest(deleteRequest, MANAGE_API_KEY_USER, END_USER_PASSWORD); + } + final ObjectPath deleteResponse = assertOKAndCreateObjectPath(client().performRequest(deleteRequest)); + assertThat(deleteResponse.evaluate("invalidated_api_keys"), equalTo(List.of(apiKeyId))); + + // Cannot create cross-cluster API keys with either manage_api_key or manage_own_api_key privilege + if (randomBoolean()) { + setUserForRequest(createRequest, MANAGE_API_KEY_USER, END_USER_PASSWORD); + } else { + setUserForRequest(createRequest, MANAGE_OWN_API_KEY_USER, END_USER_PASSWORD); + } + final ResponseException e = expectThrows(ResponseException.class, () -> client().performRequest(createRequest)); + assertThat(e.getResponse().getStatusLine().getStatusCode(), equalTo(403)); + assertThat(e.getMessage(), containsString("action [cluster:admin/xpack/security/cross_cluster/api_key/create] is unauthorized")); + } + + public void testCrossClusterApiKeyDoesNotAllowEmptyAccess() throws IOException { + assumeTrue("untrusted remote cluster feature flag must be enabled", TcpTransport.isUntrustedRemoteClusterEnabled()); + + assertBadCreateCrossClusterApiKeyRequest(""" + {"name": "my-key"}""", "Required [access]"); + + assertBadCreateCrossClusterApiKeyRequest(""" + {"name": "my-key", "access": null}""", "access doesn't support values of type: VALUE_NULL"); + + assertBadCreateCrossClusterApiKeyRequest(""" + {"name": "my-key", "access": {}}}""", "must specify non-empty access for either [search] or [replication]"); + + assertBadCreateCrossClusterApiKeyRequest(""" + {"name": "my-key", "access": {"search":[]}}}""", "must specify non-empty access for either [search] or [replication]"); + + assertBadCreateCrossClusterApiKeyRequest(""" + {"name": "my-key", "access": {"replication":[]}}}""", "must specify non-empty access for either [search] or [replication]"); + + assertBadCreateCrossClusterApiKeyRequest( + """ + {"name": "my-key", "access": {"search":[],"replication":[]}}}""", + "must specify non-empty access for either [search] or [replication]" + ); + } + + public void testCrossClusterApiKeyDoesNotAllowDlsFlsForReplication() throws IOException { + assumeTrue("untrusted remote cluster feature flag must be enabled", TcpTransport.isUntrustedRemoteClusterEnabled()); + + assertBadCreateCrossClusterApiKeyRequest(""" + { + "name": "key", + "access": { + "replication": [ {"names": ["logs"], "query":{"term": {"tag": 42}}} ] + } + }""", "replication does not support document or field level security"); + + assertBadCreateCrossClusterApiKeyRequest(""" + { + "name": "key", + "access": { + "replication": [ {"names": ["logs"], "field_security": {"grant": ["*"], "except": ["private"]}} ] + } + }""", "replication does not support document or field level security"); + + assertBadCreateCrossClusterApiKeyRequest(""" + { + "name": "key", + "access": { + "replication": [ { + "names": ["logs"], + "query": {"term": {"tag": 42}}, + "field_security": {"grant": ["*"], "except": ["private"]} + } ] + } + }""", "replication does not support document or field level security"); + } + + public void testCrossClusterApiKeyRequiresName() throws IOException { + assumeTrue("untrusted remote cluster feature flag must be enabled", TcpTransport.isUntrustedRemoteClusterEnabled()); + + assertBadCreateCrossClusterApiKeyRequest(""" + { + "access": { + "search": [ {"names": ["logs"]} ] + } + }""", "Required [name]"); + } + + private void assertBadCreateCrossClusterApiKeyRequest(String body, String expectedErrorMessage) throws IOException { + final Request createRequest = new Request("POST", "/_security/cross_cluster/api_key"); + createRequest.setJsonEntity(body); + setUserForRequest(createRequest, MANAGE_SECURITY_USER, END_USER_PASSWORD); + final ResponseException e = expectThrows(ResponseException.class, () -> client().performRequest(createRequest)); + assertThat(e.getResponse().getStatusLine().getStatusCode(), equalTo(400)); + assertThat(e.getMessage(), containsString(expectedErrorMessage)); + } + private Response sendRequestWithRemoteIndices(final Request request, final boolean executeAsRemoteIndicesUser) throws IOException { if (executeAsRemoteIndicesUser) { request.setOptions( @@ -788,6 +978,15 @@ private EncodedApiKey createApiKey(final String apiKeyName, final Map future = new PlainActionFuture<>(); + client().execute(CreateCrossClusterApiKeyAction.INSTANCE, createApiKeyRequest, future); + final CreateApiKeyResponse createApiKeyResponse = future.actionGet(); + + final Map document = client().execute( + GetAction.INSTANCE, + new GetRequest(SECURITY_MAIN_ALIAS, createApiKeyResponse.getId()) + ).actionGet().getSource(); + + assertThat(document.get("type"), equalTo("cross_cluster")); + + @SuppressWarnings("unchecked") + final Map roleDescriptors = (Map) document.get("role_descriptors"); + assertThat(roleDescriptors.keySet(), contains(name)); + @SuppressWarnings("unchecked") + final RoleDescriptor actualRoleDescriptor = RoleDescriptor.parse( + name, + XContentTestUtils.convertToXContent((Map) roleDescriptors.get(name), XContentType.JSON), + false, + XContentType.JSON + ); + + assertThat(actualRoleDescriptor, equalTo(roleDescriptor)); + assertThat((Map) document.get("limited_by_role_descriptors"), anEmptyMap()); + } + private GrantApiKeyRequest buildGrantApiKeyRequest(String username, SecureString password, String runAsUsername) throws IOException { final SecureString clonedPassword = password.clone(); final GrantApiKeyRequest grantApiKeyRequest = new GrantApiKeyRequest(); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java index 87beabb8d39ca..9f7bc55136d72 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java @@ -299,28 +299,25 @@ public void createApiKey( if (authentication == null) { listener.onFailure(new IllegalArgumentException("authentication must be provided")); } else { + // TODO: change to transport version final Version version = getMinNodeVersion(); - if (version.before(VERSION_ADVANCED_REMOTE_CLUSTER_SECURITY)) { - if (hasRemoteIndices(request.getRoleDescriptors())) { - // Creating API keys with roles which define remote indices privileges is not allowed in a mixed cluster. - listener.onFailure( - new IllegalArgumentException( - "all nodes must have version [" - + VERSION_ADVANCED_REMOTE_CLUSTER_SECURITY - + "] or higher to support remote indices privileges for API keys" - ) - ); - return; - } - if (request.getType() == ApiKey.Type.CROSS_CLUSTER) { - listener.onFailure( - new IllegalArgumentException( - "all nodes must have version [" - + VERSION_ADVANCED_REMOTE_CLUSTER_SECURITY - + "] or higher to support creating cross cluster API keys" - ) - ); - } + if (version.before(VERSION_ADVANCED_REMOTE_CLUSTER_SECURITY) && hasRemoteIndices(request.getRoleDescriptors())) { + // Creating API keys with roles which define remote indices privileges is not allowed in a mixed cluster. + listener.onFailure( + new IllegalArgumentException( + "all nodes must have version [" + + VERSION_ADVANCED_REMOTE_CLUSTER_SECURITY + + "] or higher to support remote indices privileges for API keys" + ) + ); + return; + } + if (version.before(Version.V_8_9_0) && request.getType() == ApiKey.Type.CROSS_CLUSTER) { + listener.onFailure( + new IllegalArgumentException( + "all nodes must have version [" + Version.V_8_9_0 + "] or higher to support creating cross cluster API keys" + ) + ); } final Set filteredUserRoleDescriptors = maybeRemoveRemoteIndicesPrivileges( diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/CrossClusterApiKeyAccess.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/CrossClusterApiKeyAccess.java new file mode 100644 index 0000000000000..759c690c28e6c --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/CrossClusterApiKeyAccess.java @@ -0,0 +1,87 @@ +/* + * 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.security.rest.action.apikey; + +import org.elasticsearch.common.util.CollectionUtils; +import org.elasticsearch.xcontent.ConstructingObjectParser; +import org.elasticsearch.xcontent.ParseField; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; + +import java.util.List; + +import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg; + +public class CrossClusterApiKeyAccess { + + private final List search; + private final List replication; + + private CrossClusterApiKeyAccess(List search, List replication) { + this.search = search == null ? List.of() : search; + this.replication = replication == null ? List.of() : replication; + } + + @SuppressWarnings("unchecked") + static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "cross_cluster_api_key_request_access", + false, + (args, v) -> new CrossClusterApiKeyAccess( + (List) args[0], + (List) args[1] + ) + ); + + static { + PARSER.declareObjectArray( + optionalConstructorArg(), + (p, c) -> RoleDescriptor.parseIndexWithPrivileges( + "cross_cluster", + new String[] { "read", "read_cross_cluster", "view_index_metadata" }, + p + ), + new ParseField("search") + ); + PARSER.declareObjectArray( + optionalConstructorArg(), + (p, c) -> RoleDescriptor.parseIndexWithPrivileges( + "cross_cluster", + new String[] { + "manage", + "read", + "indices:internal/admin/ccr/restore/*", + "internal:transport/proxy/indices:internal/admin/ccr/restore/*" }, + p + ), + new ParseField("replication") + ); + } + + RoleDescriptor toRoleDescriptor(String name) { + final String[] clusterPrivileges; + if (search.isEmpty() && replication.isEmpty()) { + throw new IllegalArgumentException("must specify non-empty access for either [search] or [replication]"); + } else if (search.isEmpty()) { + clusterPrivileges = new String[] { "cross_cluster_access", "cluster:monitor/state" }; + } else if (replication.isEmpty()) { + clusterPrivileges = new String[] { "cross_cluster_access" }; + } else { + clusterPrivileges = new String[] { "cross_cluster_access", "cluster:monitor/state" }; + } + + if (replication.stream().anyMatch(RoleDescriptor.IndicesPrivileges::isUsingDocumentOrFieldLevelSecurity)) { + throw new IllegalArgumentException("replication does not support document or field level security"); + } + + return new RoleDescriptor( + name, + clusterPrivileges, + CollectionUtils.concatLists(search, replication).toArray(RoleDescriptor.IndicesPrivileges[]::new), + null + ); + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyAction.java index ee7903fb25557..f33a46e83cff7 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyAction.java @@ -7,10 +7,8 @@ package org.elasticsearch.xpack.security.rest.action.apikey; -import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.client.internal.node.NodeClient; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.util.CollectionUtils; import org.elasticsearch.core.TimeValue; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.rest.RestRequest; @@ -20,7 +18,6 @@ import org.elasticsearch.xpack.core.security.action.apikey.ApiKey; import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyRequest; import org.elasticsearch.xpack.core.security.action.apikey.CreateCrossClusterApiKeyAction; -import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import java.io.IOException; import java.util.List; @@ -35,35 +32,6 @@ */ public final class RestCreateCrossClusterApiKeyAction extends ApiKeyBaseRestHandler { - @SuppressWarnings("unchecked") - static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( - "cross_cluster_api_key_request_payload", - false, - (args, v) -> new Payload( - (String) args[0], - args[1] == null ? List.of() : (List) args[1], - args[2] == null ? List.of() : (List) args[2], - TimeValue.parseTimeValue((String) args[3], null, "expiration"), - (Map) args[4] - ) - ); - - static { - PARSER.declareString(constructorArg(), new ParseField("name")); - PARSER.declareObjectArray( - optionalConstructorArg(), - (p, c) -> RoleDescriptor.parseIndexWithPrivileges("cross_cluster", new String[] { "read" }, p), - new ParseField("search") - ); - PARSER.declareObjectArray( - optionalConstructorArg(), - (p, c) -> RoleDescriptor.parseIndexWithPrivileges("cross_cluster", new String[] { "read" }, p), - new ParseField("replication") - ); - PARSER.declareString(optionalConstructorArg(), new ParseField("expiration")); - PARSER.declareObject(optionalConstructorArg(), (p, c) -> p.map(), new ParseField("metadata")); - } - /** * @param settings the node's settings * @param licenseState the license state that will be used to determine if @@ -86,13 +54,8 @@ public String getName() { @Override protected RestChannelConsumer innerPrepareRequest(final RestRequest request, final NodeClient client) throws IOException { final Payload payload = PARSER.parse(request.contentParser(), null); - System.out.println("PAYLOAD IS " + payload); final CreateApiKeyRequest createApiKeyRequest = payload.toCreateApiKeyRequest(); - String refresh = request.param("refresh"); - if (refresh != null) { - createApiKeyRequest.setRefreshPolicy(WriteRequest.RefreshPolicy.parse(request.param("refresh"))); - } return channel -> client.execute( CreateCrossClusterApiKeyAction.INSTANCE, createApiKeyRequest, @@ -100,39 +63,34 @@ protected RestChannelConsumer innerPrepareRequest(final RestRequest request, fin ); } - record Payload( - String name, - List search, - List replication, - TimeValue expiration, - Map metadata - ) { + record Payload(String name, CrossClusterApiKeyAccess access, TimeValue expiration, Map metadata) { public CreateApiKeyRequest toCreateApiKeyRequest() { final CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(); - createApiKeyRequest.setName(name); createApiKeyRequest.setType(ApiKey.Type.CROSS_CLUSTER); + createApiKeyRequest.setName(name); createApiKeyRequest.setExpiration(expiration); createApiKeyRequest.setMetadata(metadata); - - final String[] clusterPrivileges; - if (search.isEmpty() && replication.isEmpty()) { - throw new IllegalArgumentException("must specify non-empty indices for either [search] or [replication]"); - } else if (search.isEmpty()) { - clusterPrivileges = new String[] { "cross_cluster_access" }; - } else if (replication.isEmpty()) { - clusterPrivileges = new String[] { "cross_cluster_access" }; - } else { - clusterPrivileges = new String[] { "cross_cluster_access", "cross_cluster_access" }; - } - final RoleDescriptor roleDescriptor = new RoleDescriptor( - name, - clusterPrivileges, - CollectionUtils.concatLists(search, replication).toArray(RoleDescriptor.IndicesPrivileges[]::new), - null - ); - createApiKeyRequest.setRoleDescriptors(List.of(roleDescriptor)); - + createApiKeyRequest.setRoleDescriptors(List.of(access.toRoleDescriptor(name))); return createApiKeyRequest; } } + + @SuppressWarnings("unchecked") + static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "cross_cluster_api_key_request_payload", + false, + (args, v) -> new Payload( + (String) args[0], + (CrossClusterApiKeyAccess) args[1], + TimeValue.parseTimeValue((String) args[2], null, "expiration"), + (Map) args[3] + ) + ); + + static { + PARSER.declareString(constructorArg(), new ParseField("name")); + PARSER.declareObject(constructorArg(), CrossClusterApiKeyAccess.PARSER, new ParseField("access")); + PARSER.declareString(optionalConstructorArg(), new ParseField("expiration")); + PARSER.declareObject(optionalConstructorArg(), (p, c) -> p.map(), new ParseField("metadata")); + } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java index c8d8164308520..8292e59bec7e6 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java @@ -36,6 +36,9 @@ import org.elasticsearch.action.update.UpdateRequestBuilder; import org.elasticsearch.action.update.UpdateResponse; import org.elasticsearch.client.internal.Client; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.node.DiscoveryNodes; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; @@ -2111,6 +2114,43 @@ public void testBuildDelimitedStringWithLimit() { assertThat(e.getMessage(), equalTo("limit must be positive number")); } + public void testCreateCrossClusterApiKeyMinVersionConstraint() { + final Authentication authentication = AuthenticationTestHelper.builder().build(); + final CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(randomAlphaOfLengthBetween(3, 8), null, null); + createApiKeyRequest.setType(ApiKey.Type.CROSS_CLUSTER); + + final ClusterService clusterService = mock(ClusterService.class); + when(clusterService.getClusterSettings()).thenReturn( + new ClusterSettings(Settings.EMPTY, Set.of(ApiKeyService.DELETE_RETENTION_PERIOD)) + ); + final ClusterState clusterState = mock(ClusterState.class); + when(clusterService.state()).thenReturn(clusterState); + final DiscoveryNodes discoveryNodes = mock(DiscoveryNodes.class); + when(clusterState.nodes()).thenReturn(discoveryNodes); + final Version minNodeVersion = VersionUtils.randomPreviousCompatibleVersion(random(), Version.CURRENT); + when(discoveryNodes.getMinNodeVersion()).thenReturn(minNodeVersion); + + final ApiKeyService service = new ApiKeyService( + Settings.EMPTY, + clock, + client, + securityIndex, + clusterService, + cacheInvalidatorRegistry, + threadPool + ); + + final PlainActionFuture future = new PlainActionFuture<>(); + service.createApiKey(authentication, createApiKeyRequest, Set.of(), future); + final IllegalArgumentException e = expectThrows(IllegalArgumentException.class, future::actionGet); + + assertThat( + e.getMessage(), + containsString("all nodes must have version [8.9.0] or higher to support creating cross cluster API keys") + ); + + } + private static RoleDescriptor randomRoleDescriptorWithRemoteIndexPrivileges() { return new RoleDescriptor( randomAlphaOfLengthBetween(3, 90), diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/CrossClusterApiKeyAccessTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/CrossClusterApiKeyAccessTests.java new file mode 100644 index 0000000000000..5a54f0eca0798 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/CrossClusterApiKeyAccessTests.java @@ -0,0 +1,197 @@ +/* + * 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.security.rest.action.apikey; + +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.core.Strings; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xcontent.XContentParseException; +import org.elasticsearch.xcontent.XContentParserConfiguration; +import org.elasticsearch.xcontent.json.JsonXContent; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; + +import java.io.IOException; + +import static org.hamcrest.Matchers.arrayContainingInAnyOrder; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; + +public class CrossClusterApiKeyAccessTests extends ESTestCase { + + public void testToRoleDescriptorSearchOnly() throws IOException { + final CrossClusterApiKeyAccess access = parseForAccess(""" + { + "search": [ + { + "names": ["metrics"] + } + ] + }"""); + + final String name = randomAlphaOfLengthBetween(3, 8); + final RoleDescriptor roleDescriptor = access.toRoleDescriptor(name); + + assertRoleDescriptor( + roleDescriptor, + name, + new String[] { "cross_cluster_access" }, + new RoleDescriptor.IndicesPrivileges[] { + RoleDescriptor.IndicesPrivileges.builder() + .indices("metrics") + .privileges("read", "read_cross_cluster", "view_index_metadata") + .build() } + ); + } + + public void testToRoleDescriptorReplicationOnly() throws IOException { + final CrossClusterApiKeyAccess access = parseForAccess(""" + { + "replication": [ + { + "names": ["archive"] + } + ] + }"""); + + final String name = randomAlphaOfLengthBetween(3, 8); + final RoleDescriptor roleDescriptor = access.toRoleDescriptor(name); + + assertRoleDescriptor( + roleDescriptor, + name, + new String[] { "cross_cluster_access", "cluster:monitor/state" }, + new RoleDescriptor.IndicesPrivileges[] { + RoleDescriptor.IndicesPrivileges.builder() + .indices("archive") + .privileges( + "manage", + "read", + "indices:internal/admin/ccr/restore/*", + "internal:transport/proxy/indices:internal/admin/ccr/restore/*" + ) + .build() } + ); + } + + public void testToRolDescriptorSearchAndReplication() throws IOException { + final CrossClusterApiKeyAccess access = parseForAccess(""" + { + "search": [ + { + "names": ["metrics"], + "query": {"term":{"tag":42}} + }, + { + "names": ["logs"], + "field_security": { + "grant": ["*"], + "except": ["private"] + } + } + ], + "replication": [ + { + "names": [ "archive" ], + "allow_restricted_indices": true + } + ] + }"""); + + final String name = randomAlphaOfLengthBetween(3, 8); + final RoleDescriptor roleDescriptor = access.toRoleDescriptor(name); + + assertRoleDescriptor( + roleDescriptor, + name, + new String[] { "cross_cluster_access", "cluster:monitor/state" }, + new RoleDescriptor.IndicesPrivileges[] { + RoleDescriptor.IndicesPrivileges.builder() + .indices("metrics") + .privileges("read", "read_cross_cluster", "view_index_metadata") + .query("{\"term\":{\"tag\":42}}") + .build(), + RoleDescriptor.IndicesPrivileges.builder() + .indices("logs") + .privileges("read", "read_cross_cluster", "view_index_metadata") + .grantedFields("*") + .deniedFields("private") + .build(), + RoleDescriptor.IndicesPrivileges.builder() + .indices("archive") + .privileges( + "manage", + "read", + "indices:internal/admin/ccr/restore/*", + "internal:transport/proxy/indices:internal/admin/ccr/restore/*" + ) + .allowRestrictedIndices(true) + .build() } + ); + } + + public void testExplicitlySpecifyingPrivilegesIsNotAllowed() { + final XContentParseException e = expectThrows(XContentParseException.class, () -> parseForAccess(Strings.format(""" + { + "%s": [ + { + "names": ["metrics"], + "privileges": ["read"] + } + ] + }""", randomFrom("search", "replication")))); + + final Throwable cause = e.getCause(); + assertThat(cause, instanceOf(ElasticsearchParseException.class)); + assertThat( + cause.getMessage(), + containsString("failed to parse indices privileges for role [cross_cluster]. field [privileges] must not present") + ); + } + + public void testEmptyAccessIsNotAllowed() throws IOException { + final CrossClusterApiKeyAccess access1 = parseForAccess( + randomFrom("{}", "{\"search\":[]}", "{\"replication\":[]}", "{\"search\":[],\"replication\":[]}") + ); + final IllegalArgumentException e1 = expectThrows( + IllegalArgumentException.class, + () -> access1.toRoleDescriptor(randomAlphaOfLengthBetween(3, 8)) + ); + assertThat(e1.getMessage(), containsString("must specify non-empty access for either [search] or [replication]")); + + final XContentParseException e2 = expectThrows( + XContentParseException.class, + () -> parseForAccess(randomFrom("{\"search\":null}", "{\"replication\":null}", "{\"search\":null,\"replication\":null}")) + ); + assertThat(e2.getMessage(), containsString("doesn't support values of type: VALUE_NULL")); + } + + private static void assertRoleDescriptor( + RoleDescriptor roleDescriptor, + String name, + String[] clusterPrivileges, + RoleDescriptor.IndicesPrivileges[] indicesPrivileges + ) { + assertThat(roleDescriptor.getName().equals(name), is(true)); + assertThat(roleDescriptor.hasApplicationPrivileges(), is(false)); + assertThat(roleDescriptor.hasRunAs(), is(false)); + assertThat(roleDescriptor.hasConfigurableClusterPrivileges(), is(false)); + assertThat(roleDescriptor.hasRemoteIndicesPrivileges(), is(false)); + + assertThat(roleDescriptor.getClusterPrivileges(), arrayContainingInAnyOrder(clusterPrivileges)); + assertThat(roleDescriptor.getIndicesPrivileges(), equalTo(indicesPrivileges)); + } + + private static CrossClusterApiKeyAccess parseForAccess(String content) throws IOException { + return CrossClusterApiKeyAccess.PARSER.parse( + JsonXContent.jsonXContent.createParser(XContentParserConfiguration.EMPTY, content), + null + ); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateApiKeyActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateApiKeyActionTests.java index b56db1eae23d8..4bc4fd0ecbc85 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateApiKeyActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateApiKeyActionTests.java @@ -28,6 +28,7 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xcontent.NamedXContentRegistry; import org.elasticsearch.xcontent.XContentType; +import org.elasticsearch.xpack.core.security.action.apikey.ApiKey; import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyRequest; import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyResponse; @@ -37,6 +38,7 @@ import java.util.UUID; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; import static org.mockito.Mockito.mock; public class RestCreateApiKeyActionTests extends ESTestCase { @@ -94,6 +96,7 @@ public void doE CreateApiKeyRequest createApiKeyRequest = (CreateApiKeyRequest) request; @SuppressWarnings("unchecked") RestToXContentListener actionListener = (RestToXContentListener) listener; + assertThat(createApiKeyRequest.getType(), is(ApiKey.Type.REST)); if (createApiKeyRequest.getName().equals("my-api-key")) { actionListener.onResponse(expected); } else { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyActionTests.java new file mode 100644 index 0000000000000..6030687693822 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyActionTests.java @@ -0,0 +1,57 @@ +/* + * 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.security.rest.action.apikey; + +import org.elasticsearch.client.internal.node.NodeClient; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.rest.RestChannel; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.rest.FakeRestRequest; +import org.elasticsearch.xcontent.NamedXContentRegistry; +import org.elasticsearch.xcontent.XContentType; +import org.elasticsearch.xpack.core.security.action.apikey.ApiKey; +import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyRequest; +import org.elasticsearch.xpack.core.security.action.apikey.CreateCrossClusterApiKeyAction; +import org.mockito.ArgumentCaptor; + +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class RestCreateCrossClusterApiKeyActionTests extends ESTestCase { + + public void testCreateApiKeyRequestHasTypeOfCrossCluster() throws Exception { + final FakeRestRequest restRequest = new FakeRestRequest.Builder(NamedXContentRegistry.EMPTY).withContent(new BytesArray(""" + { + "name": "my-key", + "access": { + "search": [ + { + "names": [ + "logs" + ] + } + ] + } + }"""), XContentType.JSON).build(); + + final var action = new RestCreateCrossClusterApiKeyAction(Settings.EMPTY, mock(XPackLicenseState.class)); + final NodeClient client = mock(NodeClient.class); + action.handleRequest(restRequest, mock(RestChannel.class), client); + + final ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(CreateApiKeyRequest.class); + verify(client).execute(eq(CreateCrossClusterApiKeyAction.INSTANCE), requestCaptor.capture(), any()); + + final CreateApiKeyRequest createApiKeyRequest = requestCaptor.getValue(); + assertThat(createApiKeyRequest.getType(), is(ApiKey.Type.CROSS_CLUSTER)); + } +} From 6457d9d553d28a946dc3d3a50a9b8ab269f40944 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Tue, 2 May 2023 12:14:27 +1000 Subject: [PATCH 03/14] put API behind feature flag --- .../elasticsearch/xpack/security/Security.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index b2b6bfc8c2bd5..0f269fd63e788 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -86,6 +86,7 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.tracing.Tracer; import org.elasticsearch.transport.RemoteClusterService; +import org.elasticsearch.transport.TcpTransport; import org.elasticsearch.transport.Transport; import org.elasticsearch.transport.TransportInterceptor; import org.elasticsearch.transport.TransportRequest; @@ -369,6 +370,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.function.BiConsumer; import java.util.function.Function; @@ -1269,7 +1271,7 @@ public void onIndexModule(IndexModule module) { return Arrays.asList(usageAction, infoAction); } - return Arrays.asList( + return Stream.of( new ActionHandler<>(ClearRealmCacheAction.INSTANCE, TransportClearRealmCacheAction.class), new ActionHandler<>(ClearRolesCacheAction.INSTANCE, TransportClearRolesCacheAction.class), new ActionHandler<>(ClearPrivilegesCacheAction.INSTANCE, TransportClearPrivilegesCacheAction.class), @@ -1306,7 +1308,9 @@ 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<>(CreateCrossClusterApiKeyAction.INSTANCE, TransportCreateCrossClusterApiKeyAction.class), + TcpTransport.isUntrustedRemoteClusterEnabled() + ? new ActionHandler<>(CreateCrossClusterApiKeyAction.INSTANCE, TransportCreateCrossClusterApiKeyAction.class) + : null, new ActionHandler<>(GrantApiKeyAction.INSTANCE, TransportGrantApiKeyAction.class), new ActionHandler<>(InvalidateApiKeyAction.INSTANCE, TransportInvalidateApiKeyAction.class), new ActionHandler<>(GetApiKeyAction.INSTANCE, TransportGetApiKeyAction.class), @@ -1329,7 +1333,7 @@ public void onIndexModule(IndexModule module) { new ActionHandler<>(SetProfileEnabledAction.INSTANCE, TransportSetProfileEnabledAction.class), usageAction, infoAction - ); + ).filter(Objects::nonNull).toList(); } @Override @@ -1353,7 +1357,7 @@ public List getRestHandlers( if (enabled == false) { return emptyList(); } - return Arrays.asList( + return Stream.of( new RestAuthenticateAction(settings, securityContext.get(), getLicenseState()), new RestClearRealmCacheAction(settings, getLicenseState()), new RestClearRolesCacheAction(settings, getLicenseState()), @@ -1390,7 +1394,7 @@ public List getRestHandlers( new RestPutPrivilegesAction(settings, getLicenseState()), new RestDeletePrivilegesAction(settings, getLicenseState()), new RestCreateApiKeyAction(settings, getLicenseState()), - new RestCreateCrossClusterApiKeyAction(settings, getLicenseState()), + TcpTransport.isUntrustedRemoteClusterEnabled() ? new RestCreateCrossClusterApiKeyAction(settings, getLicenseState()) : null, new RestUpdateApiKeyAction(settings, getLicenseState()), new RestBulkUpdateApiKeyAction(settings, getLicenseState()), new RestGrantApiKeyAction(settings, getLicenseState()), @@ -1411,7 +1415,7 @@ public List getRestHandlers( new RestSuggestProfilesAction(settings, getLicenseState()), new RestEnableProfileAction(settings, getLicenseState()), new RestDisableProfileAction(settings, getLicenseState()) - ); + ).filter(Objects::nonNull).toList(); } @Override From a32b8e469e9a5c37c1611afcb33e17948551fcb2 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Tue, 2 May 2023 12:47:48 +1000 Subject: [PATCH 04/14] tweak validation --- .../action/apikey/CreateApiKeyRequest.java | 7 +- ...ossClusterApiKeyRoleDescriptorBuilder.java | 128 ++++++++++++++++++ .../core/security/authz/RoleDescriptor.java | 4 - ...sterApiKeyRoleDescriptorBuilderTests.java} | 36 +++-- .../apikey/CrossClusterApiKeyAccess.java | 87 ------------ .../RestCreateCrossClusterApiKeyAction.java | 14 +- 6 files changed, 155 insertions(+), 121 deletions(-) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilder.java rename x-pack/plugin/{security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/CrossClusterApiKeyAccessTests.java => core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilderTests.java} (82%) delete mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/CrossClusterApiKeyAccess.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequest.java index acdbea9f695da..faca820378b9f 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequest.java @@ -158,12 +158,7 @@ public ActionRequestValidationException validate() { assert roleDescriptors.size() == 1; final RoleDescriptor roleDescriptor = roleDescriptors.iterator().next(); assert roleDescriptor.getName().equals(name); - assert false == roleDescriptor.hasApplicationPrivileges(); - assert false == roleDescriptor.hasRunAs(); - assert false == roleDescriptor.hasConfigurableClusterPrivileges(); - assert false == roleDescriptor.hasRemoteIndicesPrivileges(); - assert roleDescriptor.hasClusterPrivileges(); - assert roleDescriptor.hasIndicesPrivileges(); + CrossClusterApiKeyRoleDescriptorBuilder.validate(roleDescriptor); } ActionRequestValidationException validationException = null; if (Strings.isNullOrEmpty(name)) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilder.java new file mode 100644 index 0000000000000..9d89b556da74b --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilder.java @@ -0,0 +1,128 @@ +/* + * 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.common.Strings; +import org.elasticsearch.common.util.CollectionUtils; +import org.elasticsearch.xcontent.ConstructingObjectParser; +import org.elasticsearch.xcontent.ParseField; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; + +import java.util.Arrays; +import java.util.List; + +import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg; + +public class CrossClusterApiKeyRoleDescriptorBuilder { + + private static final String[] CCS_CLUSTER_PRIVILEGE_NAMES = { "cross_cluster_access" }; + private static final String[] CCR_CLUSTER_PRIVILEGE_NAMES = { "cross_cluster_access", "cluster:monitor/state" }; + private static final String[] CCS_AND_CCR_CLUSTER_PRIVILEGE_NAMES = { "cross_cluster_access", "cluster:monitor/state" }; + private static final String[] CCS_INDICES_PRIVILEGE_NAMES = { "read", "read_cross_cluster", "view_index_metadata" }; + private static final String[] CCR_INDICES_PRIVILEGE_NAMES = { + "manage", + "read", + "indices:internal/admin/ccr/restore/*", + "internal:transport/proxy/indices:internal/admin/ccr/restore/*" }; + + private final List search; + private final List replication; + + private CrossClusterApiKeyRoleDescriptorBuilder( + List search, + List replication + ) { + this.search = search == null ? List.of() : search; + this.replication = replication == null ? List.of() : replication; + } + + @SuppressWarnings("unchecked") + public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "cross_cluster_api_key_request_access", + false, + (args, v) -> new CrossClusterApiKeyRoleDescriptorBuilder( + (List) args[0], + (List) args[1] + ) + ); + + static { + PARSER.declareObjectArray( + optionalConstructorArg(), + (p, c) -> RoleDescriptor.parseIndexWithPrivileges("cross_cluster", CCS_INDICES_PRIVILEGE_NAMES, p), + new ParseField("search") + ); + PARSER.declareObjectArray( + optionalConstructorArg(), + (p, c) -> RoleDescriptor.parseIndexWithPrivileges("cross_cluster", CCR_INDICES_PRIVILEGE_NAMES, p), + new ParseField("replication") + ); + } + + public RoleDescriptor build(String name) { + final String[] clusterPrivileges; + if (search.isEmpty() && replication.isEmpty()) { + throw new IllegalArgumentException("must specify non-empty access for either [search] or [replication]"); + } else if (search.isEmpty()) { + clusterPrivileges = CCR_CLUSTER_PRIVILEGE_NAMES; + } else if (replication.isEmpty()) { + clusterPrivileges = CCS_CLUSTER_PRIVILEGE_NAMES; + } else { + clusterPrivileges = CCS_AND_CCR_CLUSTER_PRIVILEGE_NAMES; + } + + if (replication.stream().anyMatch(RoleDescriptor.IndicesPrivileges::isUsingDocumentOrFieldLevelSecurity)) { + throw new IllegalArgumentException("replication does not support document or field level security"); + } + + return new RoleDescriptor( + name, + clusterPrivileges, + CollectionUtils.concatLists(search, replication).toArray(RoleDescriptor.IndicesPrivileges[]::new), + null + ); + } + + static void validate(RoleDescriptor roleDescriptor) { + if (roleDescriptor.hasApplicationPrivileges()) { + throw new IllegalArgumentException("application privilege must be empty"); + } + if (roleDescriptor.hasRunAs()) { + throw new IllegalArgumentException("run_as privilege must be empty"); + } + if (roleDescriptor.hasConfigurableClusterPrivileges()) { + throw new IllegalArgumentException("configurable cluster privilege must be empty"); + } + if (roleDescriptor.hasRemoteIndicesPrivileges()) { + throw new IllegalArgumentException("remote indices privileges must be empty"); + } + final String[] clusterPrivileges = roleDescriptor.getClusterPrivileges(); + if (false == Arrays.equals(clusterPrivileges, CCS_CLUSTER_PRIVILEGE_NAMES) + && false == Arrays.equals(clusterPrivileges, CCR_CLUSTER_PRIVILEGE_NAMES) + && false == Arrays.equals(clusterPrivileges, CCS_AND_CCR_CLUSTER_PRIVILEGE_NAMES)) { + throw new IllegalArgumentException( + "invalid cluster privileges: [" + Strings.arrayToCommaDelimitedString(clusterPrivileges) + "]" + ); + } + final RoleDescriptor.IndicesPrivileges[] indicesPrivileges = roleDescriptor.getIndicesPrivileges(); + if (indicesPrivileges.length == 0) { + throw new IllegalArgumentException("indices privileges must not be empty"); + } + + for (RoleDescriptor.IndicesPrivileges indexPrivilege : indicesPrivileges) { + final String[] privileges = indexPrivilege.getPrivileges(); + if (Arrays.equals(privileges, CCR_INDICES_PRIVILEGE_NAMES)) { + if (indexPrivilege.isUsingDocumentOrFieldLevelSecurity()) { + throw new IllegalArgumentException("replication does not support document or field level security"); + } + } else if (false == Arrays.equals(privileges, CCS_INDICES_PRIVILEGE_NAMES)) { + throw new IllegalArgumentException("invalid indices privileges: [" + Strings.arrayToCommaDelimitedString(privileges)); + } + } + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java index 1090863a41120..6f2e185fbb2fb 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java @@ -218,10 +218,6 @@ public boolean hasClusterPrivileges() { return clusterPrivileges.length != 0; } - public boolean hasIndicesPrivileges() { - return indicesPrivileges.length != 0; - } - public boolean hasApplicationPrivileges() { return applicationPrivileges.length != 0; } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/CrossClusterApiKeyAccessTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilderTests.java similarity index 82% rename from x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/CrossClusterApiKeyAccessTests.java rename to x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilderTests.java index 5a54f0eca0798..333190a801d08 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/CrossClusterApiKeyAccessTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilderTests.java @@ -5,7 +5,7 @@ * 2.0. */ -package org.elasticsearch.xpack.security.rest.action.apikey; +package org.elasticsearch.xpack.core.security.action.apikey; import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.core.Strings; @@ -23,10 +23,10 @@ import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; -public class CrossClusterApiKeyAccessTests extends ESTestCase { +public class CrossClusterApiKeyRoleDescriptorBuilderTests extends ESTestCase { - public void testToRoleDescriptorSearchOnly() throws IOException { - final CrossClusterApiKeyAccess access = parseForAccess(""" + public void testBuildForSearchOnly() throws IOException { + final CrossClusterApiKeyRoleDescriptorBuilder access = parseForAccess(""" { "search": [ { @@ -36,7 +36,7 @@ public void testToRoleDescriptorSearchOnly() throws IOException { }"""); final String name = randomAlphaOfLengthBetween(3, 8); - final RoleDescriptor roleDescriptor = access.toRoleDescriptor(name); + final RoleDescriptor roleDescriptor = access.build(name); assertRoleDescriptor( roleDescriptor, @@ -50,8 +50,8 @@ public void testToRoleDescriptorSearchOnly() throws IOException { ); } - public void testToRoleDescriptorReplicationOnly() throws IOException { - final CrossClusterApiKeyAccess access = parseForAccess(""" + public void testBuildForReplicationOnly() throws IOException { + final CrossClusterApiKeyRoleDescriptorBuilder access = parseForAccess(""" { "replication": [ { @@ -61,7 +61,7 @@ public void testToRoleDescriptorReplicationOnly() throws IOException { }"""); final String name = randomAlphaOfLengthBetween(3, 8); - final RoleDescriptor roleDescriptor = access.toRoleDescriptor(name); + final RoleDescriptor roleDescriptor = access.build(name); assertRoleDescriptor( roleDescriptor, @@ -80,8 +80,8 @@ public void testToRoleDescriptorReplicationOnly() throws IOException { ); } - public void testToRolDescriptorSearchAndReplication() throws IOException { - final CrossClusterApiKeyAccess access = parseForAccess(""" + public void testBuildForSearchAndReplication() throws IOException { + final CrossClusterApiKeyRoleDescriptorBuilder access = parseForAccess(""" { "search": [ { @@ -105,7 +105,7 @@ public void testToRolDescriptorSearchAndReplication() throws IOException { }"""); final String name = randomAlphaOfLengthBetween(3, 8); - final RoleDescriptor roleDescriptor = access.toRoleDescriptor(name); + final RoleDescriptor roleDescriptor = access.build(name); assertRoleDescriptor( roleDescriptor, @@ -156,12 +156,12 @@ public void testExplicitlySpecifyingPrivilegesIsNotAllowed() { } public void testEmptyAccessIsNotAllowed() throws IOException { - final CrossClusterApiKeyAccess access1 = parseForAccess( + final CrossClusterApiKeyRoleDescriptorBuilder access1 = parseForAccess( randomFrom("{}", "{\"search\":[]}", "{\"replication\":[]}", "{\"search\":[],\"replication\":[]}") ); final IllegalArgumentException e1 = expectThrows( IllegalArgumentException.class, - () -> access1.toRoleDescriptor(randomAlphaOfLengthBetween(3, 8)) + () -> access1.build(randomAlphaOfLengthBetween(3, 8)) ); assertThat(e1.getMessage(), containsString("must specify non-empty access for either [search] or [replication]")); @@ -179,17 +179,13 @@ private static void assertRoleDescriptor( RoleDescriptor.IndicesPrivileges[] indicesPrivileges ) { assertThat(roleDescriptor.getName().equals(name), is(true)); - assertThat(roleDescriptor.hasApplicationPrivileges(), is(false)); - assertThat(roleDescriptor.hasRunAs(), is(false)); - assertThat(roleDescriptor.hasConfigurableClusterPrivileges(), is(false)); - assertThat(roleDescriptor.hasRemoteIndicesPrivileges(), is(false)); - assertThat(roleDescriptor.getClusterPrivileges(), arrayContainingInAnyOrder(clusterPrivileges)); assertThat(roleDescriptor.getIndicesPrivileges(), equalTo(indicesPrivileges)); + CrossClusterApiKeyRoleDescriptorBuilder.validate(roleDescriptor); } - private static CrossClusterApiKeyAccess parseForAccess(String content) throws IOException { - return CrossClusterApiKeyAccess.PARSER.parse( + private static CrossClusterApiKeyRoleDescriptorBuilder parseForAccess(String content) throws IOException { + return CrossClusterApiKeyRoleDescriptorBuilder.PARSER.parse( JsonXContent.jsonXContent.createParser(XContentParserConfiguration.EMPTY, content), null ); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/CrossClusterApiKeyAccess.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/CrossClusterApiKeyAccess.java deleted file mode 100644 index 759c690c28e6c..0000000000000 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/CrossClusterApiKeyAccess.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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.security.rest.action.apikey; - -import org.elasticsearch.common.util.CollectionUtils; -import org.elasticsearch.xcontent.ConstructingObjectParser; -import org.elasticsearch.xcontent.ParseField; -import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; - -import java.util.List; - -import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg; - -public class CrossClusterApiKeyAccess { - - private final List search; - private final List replication; - - private CrossClusterApiKeyAccess(List search, List replication) { - this.search = search == null ? List.of() : search; - this.replication = replication == null ? List.of() : replication; - } - - @SuppressWarnings("unchecked") - static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( - "cross_cluster_api_key_request_access", - false, - (args, v) -> new CrossClusterApiKeyAccess( - (List) args[0], - (List) args[1] - ) - ); - - static { - PARSER.declareObjectArray( - optionalConstructorArg(), - (p, c) -> RoleDescriptor.parseIndexWithPrivileges( - "cross_cluster", - new String[] { "read", "read_cross_cluster", "view_index_metadata" }, - p - ), - new ParseField("search") - ); - PARSER.declareObjectArray( - optionalConstructorArg(), - (p, c) -> RoleDescriptor.parseIndexWithPrivileges( - "cross_cluster", - new String[] { - "manage", - "read", - "indices:internal/admin/ccr/restore/*", - "internal:transport/proxy/indices:internal/admin/ccr/restore/*" }, - p - ), - new ParseField("replication") - ); - } - - RoleDescriptor toRoleDescriptor(String name) { - final String[] clusterPrivileges; - if (search.isEmpty() && replication.isEmpty()) { - throw new IllegalArgumentException("must specify non-empty access for either [search] or [replication]"); - } else if (search.isEmpty()) { - clusterPrivileges = new String[] { "cross_cluster_access", "cluster:monitor/state" }; - } else if (replication.isEmpty()) { - clusterPrivileges = new String[] { "cross_cluster_access" }; - } else { - clusterPrivileges = new String[] { "cross_cluster_access", "cluster:monitor/state" }; - } - - if (replication.stream().anyMatch(RoleDescriptor.IndicesPrivileges::isUsingDocumentOrFieldLevelSecurity)) { - throw new IllegalArgumentException("replication does not support document or field level security"); - } - - return new RoleDescriptor( - name, - clusterPrivileges, - CollectionUtils.concatLists(search, replication).toArray(RoleDescriptor.IndicesPrivileges[]::new), - null - ); - } -} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyAction.java index f33a46e83cff7..2ee8d05ac3a05 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyAction.java @@ -18,6 +18,7 @@ import org.elasticsearch.xpack.core.security.action.apikey.ApiKey; import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyRequest; import org.elasticsearch.xpack.core.security.action.apikey.CreateCrossClusterApiKeyAction; +import org.elasticsearch.xpack.core.security.action.apikey.CrossClusterApiKeyRoleDescriptorBuilder; import java.io.IOException; import java.util.List; @@ -63,14 +64,19 @@ protected RestChannelConsumer innerPrepareRequest(final RestRequest request, fin ); } - record Payload(String name, CrossClusterApiKeyAccess access, TimeValue expiration, Map metadata) { + record Payload( + String name, + CrossClusterApiKeyRoleDescriptorBuilder roleDescriptorBuilder, + TimeValue expiration, + Map metadata + ) { public CreateApiKeyRequest toCreateApiKeyRequest() { final CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(); createApiKeyRequest.setType(ApiKey.Type.CROSS_CLUSTER); createApiKeyRequest.setName(name); createApiKeyRequest.setExpiration(expiration); createApiKeyRequest.setMetadata(metadata); - createApiKeyRequest.setRoleDescriptors(List.of(access.toRoleDescriptor(name))); + createApiKeyRequest.setRoleDescriptors(List.of(roleDescriptorBuilder.build(name))); return createApiKeyRequest; } } @@ -81,7 +87,7 @@ public CreateApiKeyRequest toCreateApiKeyRequest() { false, (args, v) -> new Payload( (String) args[0], - (CrossClusterApiKeyAccess) args[1], + (CrossClusterApiKeyRoleDescriptorBuilder) args[1], TimeValue.parseTimeValue((String) args[2], null, "expiration"), (Map) args[3] ) @@ -89,7 +95,7 @@ public CreateApiKeyRequest toCreateApiKeyRequest() { static { PARSER.declareString(constructorArg(), new ParseField("name")); - PARSER.declareObject(constructorArg(), CrossClusterApiKeyAccess.PARSER, new ParseField("access")); + PARSER.declareObject(constructorArg(), CrossClusterApiKeyRoleDescriptorBuilder.PARSER, new ParseField("access")); PARSER.declareString(optionalConstructorArg(), new ParseField("expiration")); PARSER.declareObject(optionalConstructorArg(), (p, c) -> p.map(), new ParseField("metadata")); } From baf40442cebb2cba0544edaf956b33adebd09765 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Wed, 3 May 2023 12:35:54 +1000 Subject: [PATCH 05/14] tweak role descriptor name --- .../action/apikey/CreateApiKeyRequest.java | 4 +-- ...ossClusterApiKeyRoleDescriptorBuilder.java | 34 +++++++++++-------- ...usterApiKeyRoleDescriptorBuilderTests.java | 20 +++-------- .../xpack/security/apikey/ApiKeyRestIT.java | 4 +-- .../authc/apikey/ApiKeySingleNodeTests.java | 11 +++--- .../RestCreateCrossClusterApiKeyAction.java | 2 +- ...stCreateCrossClusterApiKeyActionTests.java | 24 +++++++++++++ 7 files changed, 57 insertions(+), 42 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequest.java index faca820378b9f..7b820651305a7 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequest.java @@ -156,9 +156,7 @@ public void setMetadata(Map metadata) { public ActionRequestValidationException validate() { if (Assertions.ENABLED && type == ApiKey.Type.CROSS_CLUSTER) { assert roleDescriptors.size() == 1; - final RoleDescriptor roleDescriptor = roleDescriptors.iterator().next(); - assert roleDescriptor.getName().equals(name); - CrossClusterApiKeyRoleDescriptorBuilder.validate(roleDescriptor); + CrossClusterApiKeyRoleDescriptorBuilder.validate(roleDescriptors.iterator().next()); } ActionRequestValidationException validationException = null; if (Strings.isNullOrEmpty(name)) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilder.java index 9d89b556da74b..c532b3f9fdf67 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilder.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilder.java @@ -29,17 +29,7 @@ public class CrossClusterApiKeyRoleDescriptorBuilder { "read", "indices:internal/admin/ccr/restore/*", "internal:transport/proxy/indices:internal/admin/ccr/restore/*" }; - - private final List search; - private final List replication; - - private CrossClusterApiKeyRoleDescriptorBuilder( - List search, - List replication - ) { - this.search = search == null ? List.of() : search; - this.replication = replication == null ? List.of() : replication; - } + private static final String ROLE_DESCRIPTOR_NAME = "cross_cluster"; @SuppressWarnings("unchecked") public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( @@ -54,17 +44,28 @@ private CrossClusterApiKeyRoleDescriptorBuilder( static { PARSER.declareObjectArray( optionalConstructorArg(), - (p, c) -> RoleDescriptor.parseIndexWithPrivileges("cross_cluster", CCS_INDICES_PRIVILEGE_NAMES, p), + (p, c) -> RoleDescriptor.parseIndexWithPrivileges(ROLE_DESCRIPTOR_NAME, CCS_INDICES_PRIVILEGE_NAMES, p), new ParseField("search") ); PARSER.declareObjectArray( optionalConstructorArg(), - (p, c) -> RoleDescriptor.parseIndexWithPrivileges("cross_cluster", CCR_INDICES_PRIVILEGE_NAMES, p), + (p, c) -> RoleDescriptor.parseIndexWithPrivileges(ROLE_DESCRIPTOR_NAME, CCR_INDICES_PRIVILEGE_NAMES, p), new ParseField("replication") ); } - public RoleDescriptor build(String name) { + private final List search; + private final List replication; + + private CrossClusterApiKeyRoleDescriptorBuilder( + List search, + List replication + ) { + this.search = search == null ? List.of() : search; + this.replication = replication == null ? List.of() : replication; + } + + public RoleDescriptor build() { final String[] clusterPrivileges; if (search.isEmpty() && replication.isEmpty()) { throw new IllegalArgumentException("must specify non-empty access for either [search] or [replication]"); @@ -81,7 +82,7 @@ public RoleDescriptor build(String name) { } return new RoleDescriptor( - name, + ROLE_DESCRIPTOR_NAME, clusterPrivileges, CollectionUtils.concatLists(search, replication).toArray(RoleDescriptor.IndicesPrivileges[]::new), null @@ -89,6 +90,9 @@ public RoleDescriptor build(String name) { } static void validate(RoleDescriptor roleDescriptor) { + if (false == ROLE_DESCRIPTOR_NAME.equals(roleDescriptor.getName())) { + throw new IllegalArgumentException("invalid role descriptor name [" + roleDescriptor.getName() + "]"); + } if (roleDescriptor.hasApplicationPrivileges()) { throw new IllegalArgumentException("application privilege must be empty"); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilderTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilderTests.java index 333190a801d08..cf8f6470d96aa 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilderTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilderTests.java @@ -35,12 +35,10 @@ public void testBuildForSearchOnly() throws IOException { ] }"""); - final String name = randomAlphaOfLengthBetween(3, 8); - final RoleDescriptor roleDescriptor = access.build(name); + final RoleDescriptor roleDescriptor = access.build(); assertRoleDescriptor( roleDescriptor, - name, new String[] { "cross_cluster_access" }, new RoleDescriptor.IndicesPrivileges[] { RoleDescriptor.IndicesPrivileges.builder() @@ -60,12 +58,10 @@ public void testBuildForReplicationOnly() throws IOException { ] }"""); - final String name = randomAlphaOfLengthBetween(3, 8); - final RoleDescriptor roleDescriptor = access.build(name); + final RoleDescriptor roleDescriptor = access.build(); assertRoleDescriptor( roleDescriptor, - name, new String[] { "cross_cluster_access", "cluster:monitor/state" }, new RoleDescriptor.IndicesPrivileges[] { RoleDescriptor.IndicesPrivileges.builder() @@ -104,12 +100,10 @@ public void testBuildForSearchAndReplication() throws IOException { ] }"""); - final String name = randomAlphaOfLengthBetween(3, 8); - final RoleDescriptor roleDescriptor = access.build(name); + final RoleDescriptor roleDescriptor = access.build(); assertRoleDescriptor( roleDescriptor, - name, new String[] { "cross_cluster_access", "cluster:monitor/state" }, new RoleDescriptor.IndicesPrivileges[] { RoleDescriptor.IndicesPrivileges.builder() @@ -159,10 +153,7 @@ public void testEmptyAccessIsNotAllowed() throws IOException { final CrossClusterApiKeyRoleDescriptorBuilder access1 = parseForAccess( randomFrom("{}", "{\"search\":[]}", "{\"replication\":[]}", "{\"search\":[],\"replication\":[]}") ); - final IllegalArgumentException e1 = expectThrows( - IllegalArgumentException.class, - () -> access1.build(randomAlphaOfLengthBetween(3, 8)) - ); + final IllegalArgumentException e1 = expectThrows(IllegalArgumentException.class, access1::build); assertThat(e1.getMessage(), containsString("must specify non-empty access for either [search] or [replication]")); final XContentParseException e2 = expectThrows( @@ -174,11 +165,10 @@ public void testEmptyAccessIsNotAllowed() throws IOException { private static void assertRoleDescriptor( RoleDescriptor roleDescriptor, - String name, String[] clusterPrivileges, RoleDescriptor.IndicesPrivileges[] indicesPrivileges ) { - assertThat(roleDescriptor.getName().equals(name), is(true)); + assertThat(roleDescriptor.getName().equals("cross_cluster"), is(true)); assertThat(roleDescriptor.getClusterPrivileges(), arrayContainingInAnyOrder(clusterPrivileges)); assertThat(roleDescriptor.getIndicesPrivileges(), equalTo(indicesPrivileges)); CrossClusterApiKeyRoleDescriptorBuilder.validate(roleDescriptor); diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyRestIT.java index 2f32a2747e54d..33b2f821d9c33 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyRestIT.java @@ -731,10 +731,10 @@ public void testCreateCrossClusterApiKey() throws IOException { fetchResponse.evaluate("api_keys.0.role_descriptors"), equalTo( Map.of( - "my-key", + "cross_cluster", XContentTestUtils.convertToMap( new RoleDescriptor( - "my-key", + "cross_cluster", new String[] { "cross_cluster_access", "cluster:monitor/state" }, new RoleDescriptor.IndicesPrivileges[] { RoleDescriptor.IndicesPrivileges.builder() diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java index 0ad3b07dbb374..ca3ba480e4037 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java @@ -416,9 +416,8 @@ public void testInvalidateApiKeyWillRecordTimestamp() { } public void testCreateCrossClusterApiKey() throws IOException { - final String name = randomAlphaOfLengthBetween(3, 8); final RoleDescriptor roleDescriptor = new RoleDescriptor( - name, + "cross_cluster", new String[] { "cross_cluster_access" }, new RoleDescriptor.IndicesPrivileges[] { RoleDescriptor.IndicesPrivileges.builder() @@ -427,7 +426,7 @@ public void testCreateCrossClusterApiKey() throws IOException { .build() }, null ); - final var createApiKeyRequest = new CreateApiKeyRequest(name, List.of(roleDescriptor), null); + final var createApiKeyRequest = new CreateApiKeyRequest(randomAlphaOfLengthBetween(3, 8), List.of(roleDescriptor), null); createApiKeyRequest.setType(ApiKey.Type.CROSS_CLUSTER); final PlainActionFuture future = new PlainActionFuture<>(); @@ -443,11 +442,11 @@ public void testCreateCrossClusterApiKey() throws IOException { @SuppressWarnings("unchecked") final Map roleDescriptors = (Map) document.get("role_descriptors"); - assertThat(roleDescriptors.keySet(), contains(name)); + assertThat(roleDescriptors.keySet(), contains("cross_cluster")); @SuppressWarnings("unchecked") final RoleDescriptor actualRoleDescriptor = RoleDescriptor.parse( - name, - XContentTestUtils.convertToXContent((Map) roleDescriptors.get(name), XContentType.JSON), + "cross_cluster", + XContentTestUtils.convertToXContent((Map) roleDescriptors.get("cross_cluster"), XContentType.JSON), false, XContentType.JSON ); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyAction.java index 2ee8d05ac3a05..8b6799b1e90f2 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyAction.java @@ -76,7 +76,7 @@ public CreateApiKeyRequest toCreateApiKeyRequest() { createApiKeyRequest.setName(name); createApiKeyRequest.setExpiration(expiration); createApiKeyRequest.setMetadata(metadata); - createApiKeyRequest.setRoleDescriptors(List.of(roleDescriptorBuilder.build(name))); + createApiKeyRequest.setRoleDescriptors(List.of(roleDescriptorBuilder.build())); return createApiKeyRequest; } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyActionTests.java index 6030687693822..dbff0693039e4 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyActionTests.java @@ -19,9 +19,14 @@ import org.elasticsearch.xpack.core.security.action.apikey.ApiKey; import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyRequest; import org.elasticsearch.xpack.core.security.action.apikey.CreateCrossClusterApiKeyAction; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.mockito.ArgumentCaptor; +import java.util.List; + +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -53,5 +58,24 @@ public void testCreateApiKeyRequestHasTypeOfCrossCluster() throws Exception { final CreateApiKeyRequest createApiKeyRequest = requestCaptor.getValue(); assertThat(createApiKeyRequest.getType(), is(ApiKey.Type.CROSS_CLUSTER)); + assertThat(createApiKeyRequest.getName(), equalTo("my-key")); + assertThat( + createApiKeyRequest.getRoleDescriptors(), + equalTo( + List.of( + new RoleDescriptor( + "cross_cluster", + new String[] { "cross_cluster_access" }, + new RoleDescriptor.IndicesPrivileges[] { + RoleDescriptor.IndicesPrivileges.builder() + .indices("logs") + .privileges("read", "read_cross_cluster", "view_index_metadata") + .build() }, + null + ) + ) + ) + ); + assertThat(createApiKeyRequest.getMetadata(), nullValue()); } } From 58c4c46358f461e0ec0de7a5bc897658b0447371 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Wed, 3 May 2023 12:37:06 +1000 Subject: [PATCH 06/14] more tweak --- .../RestCreateCrossClusterApiKeyAction.java | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyAction.java index 8b6799b1e90f2..5d8bace2ce3f4 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyAction.java @@ -33,6 +33,25 @@ */ public final class RestCreateCrossClusterApiKeyAction extends ApiKeyBaseRestHandler { + @SuppressWarnings("unchecked") + static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "cross_cluster_api_key_request_payload", + false, + (args, v) -> new Payload( + (String) args[0], + (CrossClusterApiKeyRoleDescriptorBuilder) args[1], + TimeValue.parseTimeValue((String) args[2], null, "expiration"), + (Map) args[3] + ) + ); + + static { + PARSER.declareString(constructorArg(), new ParseField("name")); + PARSER.declareObject(constructorArg(), CrossClusterApiKeyRoleDescriptorBuilder.PARSER, new ParseField("access")); + PARSER.declareString(optionalConstructorArg(), new ParseField("expiration")); + PARSER.declareObject(optionalConstructorArg(), (p, c) -> p.map(), new ParseField("metadata")); + } + /** * @param settings the node's settings * @param licenseState the license state that will be used to determine if @@ -80,23 +99,4 @@ public CreateApiKeyRequest toCreateApiKeyRequest() { return createApiKeyRequest; } } - - @SuppressWarnings("unchecked") - static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( - "cross_cluster_api_key_request_payload", - false, - (args, v) -> new Payload( - (String) args[0], - (CrossClusterApiKeyRoleDescriptorBuilder) args[1], - TimeValue.parseTimeValue((String) args[2], null, "expiration"), - (Map) args[3] - ) - ); - - static { - PARSER.declareString(constructorArg(), new ParseField("name")); - PARSER.declareObject(constructorArg(), CrossClusterApiKeyRoleDescriptorBuilder.PARSER, new ParseField("access")); - PARSER.declareString(optionalConstructorArg(), new ParseField("expiration")); - PARSER.declareObject(optionalConstructorArg(), (p, c) -> p.map(), new ParseField("metadata")); - } } From 31485f35fb95442242968b259d33ff7e406fded2 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Wed, 3 May 2023 13:05:47 +1000 Subject: [PATCH 07/14] more test --- .../apikey/TransportCreateApiKeyAction.java | 2 + ...ansportCreateCrossClusterApiKeyAction.java | 2 + .../xpack/security/authc/ApiKeyService.java | 1 + ...rtCreateCrossClusterApiKeyActionTests.java | 70 +++++++++++++++++++ 4 files changed, 75 insertions(+) create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/apikey/TransportCreateCrossClusterApiKeyActionTests.java diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportCreateApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportCreateApiKeyAction.java index c9656d68e0dc9..a34314b1516a3 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportCreateApiKeyAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportCreateApiKeyAction.java @@ -15,6 +15,7 @@ import org.elasticsearch.transport.TransportService; import org.elasticsearch.xcontent.NamedXContentRegistry; import org.elasticsearch.xpack.core.security.SecurityContext; +import org.elasticsearch.xpack.core.security.action.apikey.ApiKey; 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; @@ -50,6 +51,7 @@ public TransportCreateApiKeyAction( @Override protected void doExecute(Task task, CreateApiKeyRequest request, ActionListener listener) { + assert request.getType() == ApiKey.Type.REST; final Authentication authentication = securityContext.getAuthentication(); if (authentication == null) { listener.onFailure(new IllegalStateException("authentication is required")); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportCreateCrossClusterApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportCreateCrossClusterApiKeyAction.java index 558031c1532e8..36c44eea9211a 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportCreateCrossClusterApiKeyAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportCreateCrossClusterApiKeyAction.java @@ -14,6 +14,7 @@ import org.elasticsearch.tasks.Task; import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.core.security.SecurityContext; +import org.elasticsearch.xpack.core.security.action.apikey.ApiKey; 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.CreateCrossClusterApiKeyAction; @@ -44,6 +45,7 @@ public TransportCreateCrossClusterApiKeyAction( @Override protected void doExecute(Task task, CreateApiKeyRequest request, ActionListener listener) { + assert request.getType() == ApiKey.Type.CROSS_CLUSTER; final Authentication authentication = securityContext.getAuthentication(); if (authentication == null) { listener.onFailure(new IllegalStateException("authentication is required")); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java index 9f7bc55136d72..61e83ee87fc6e 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java @@ -295,6 +295,7 @@ public void createApiKey( Set userRoleDescriptors, ActionListener listener ) { + assert request.getType() != ApiKey.Type.CROSS_CLUSTER || userRoleDescriptors.isEmpty(); ensureEnabled(); if (authentication == null) { listener.onFailure(new IllegalArgumentException("authentication must be provided")); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/apikey/TransportCreateCrossClusterApiKeyActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/apikey/TransportCreateCrossClusterApiKeyActionTests.java new file mode 100644 index 0000000000000..a026228d5dd20 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/apikey/TransportCreateCrossClusterApiKeyActionTests.java @@ -0,0 +1,70 @@ +/* + * 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.security.action.apikey; + +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.security.SecurityContext; +import org.elasticsearch.xpack.core.security.action.apikey.ApiKey; +import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyRequest; +import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyResponse; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authc.AuthenticationTestHelper; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.security.authc.ApiKeyService; + +import java.util.List; +import java.util.Set; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class TransportCreateCrossClusterApiKeyActionTests extends ESTestCase { + + public void testApiKeyWillBeCreatedWithUserRoleDescriptors() { + final ApiKeyService apiKeyService = mock(ApiKeyService.class); + final SecurityContext securityContext = mock(SecurityContext.class); + final Authentication authentication = AuthenticationTestHelper.builder().build(); + when(securityContext.getAuthentication()).thenReturn(authentication); + final var action = new TransportCreateCrossClusterApiKeyAction( + mock(TransportService.class), + mock(ActionFilters.class), + apiKeyService, + securityContext + ); + + final var request = new CreateApiKeyRequest( + randomAlphaOfLengthBetween(3, 8), + List.of( + new RoleDescriptor( + "cross_cluster", + new String[] { "cross_cluster_access" }, + new RoleDescriptor.IndicesPrivileges[] { + RoleDescriptor.IndicesPrivileges.builder() + .indices("idx") + .privileges("read", "read_cross_cluster", "view_index_metadata") + .build() }, + null + ) + ), + null + ); + request.setType(ApiKey.Type.CROSS_CLUSTER); + + final PlainActionFuture future = new PlainActionFuture<>(); + action.doExecute(mock(Task.class), request, future); + + verify(apiKeyService).createApiKey(same(authentication), same(request), eq(Set.of()), same(future)); + } +} From 014d9af95c7be36e98a634890d988cf54501fa22 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 4 May 2023 15:50:40 +1000 Subject: [PATCH 08/14] update for main branch changes --- ...ossClusterApiKeyRoleDescriptorBuilder.java | 12 ++++------- ...usterApiKeyRoleDescriptorBuilderTests.java | 20 +++++-------------- .../xpack/security/apikey/ApiKeyRestIT.java | 9 ++------- .../authc/apikey/ApiKeySingleNodeTests.java | 2 +- .../RestCreateCrossClusterApiKeyAction.java | 4 ++-- ...rtCreateCrossClusterApiKeyActionTests.java | 2 +- ...stCreateCrossClusterApiKeyActionTests.java | 2 +- 7 files changed, 16 insertions(+), 35 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilder.java index c532b3f9fdf67..34c525fc93536 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilder.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilder.java @@ -20,15 +20,11 @@ public class CrossClusterApiKeyRoleDescriptorBuilder { - private static final String[] CCS_CLUSTER_PRIVILEGE_NAMES = { "cross_cluster_access" }; - private static final String[] CCR_CLUSTER_PRIVILEGE_NAMES = { "cross_cluster_access", "cluster:monitor/state" }; - private static final String[] CCS_AND_CCR_CLUSTER_PRIVILEGE_NAMES = { "cross_cluster_access", "cluster:monitor/state" }; + private static final String[] CCS_CLUSTER_PRIVILEGE_NAMES = { "cross_cluster_search" }; + private static final String[] CCR_CLUSTER_PRIVILEGE_NAMES = { "cross_cluster_replication" }; + private static final String[] CCS_AND_CCR_CLUSTER_PRIVILEGE_NAMES = { "cross_cluster_search", "cross_cluster_replication" }; private static final String[] CCS_INDICES_PRIVILEGE_NAMES = { "read", "read_cross_cluster", "view_index_metadata" }; - private static final String[] CCR_INDICES_PRIVILEGE_NAMES = { - "manage", - "read", - "indices:internal/admin/ccr/restore/*", - "internal:transport/proxy/indices:internal/admin/ccr/restore/*" }; + private static final String[] CCR_INDICES_PRIVILEGE_NAMES = { "cross_cluster_replication", "cross_cluster_replication_internal" }; private static final String ROLE_DESCRIPTOR_NAME = "cross_cluster"; @SuppressWarnings("unchecked") diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilderTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilderTests.java index cf8f6470d96aa..e03ec6fa083eb 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilderTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilderTests.java @@ -39,7 +39,7 @@ public void testBuildForSearchOnly() throws IOException { assertRoleDescriptor( roleDescriptor, - new String[] { "cross_cluster_access" }, + new String[] { "cross_cluster_search" }, new RoleDescriptor.IndicesPrivileges[] { RoleDescriptor.IndicesPrivileges.builder() .indices("metrics") @@ -62,16 +62,11 @@ public void testBuildForReplicationOnly() throws IOException { assertRoleDescriptor( roleDescriptor, - new String[] { "cross_cluster_access", "cluster:monitor/state" }, + new String[] { "cross_cluster_replication" }, new RoleDescriptor.IndicesPrivileges[] { RoleDescriptor.IndicesPrivileges.builder() .indices("archive") - .privileges( - "manage", - "read", - "indices:internal/admin/ccr/restore/*", - "internal:transport/proxy/indices:internal/admin/ccr/restore/*" - ) + .privileges("cross_cluster_replication", "cross_cluster_replication_internal") .build() } ); } @@ -104,7 +99,7 @@ public void testBuildForSearchAndReplication() throws IOException { assertRoleDescriptor( roleDescriptor, - new String[] { "cross_cluster_access", "cluster:monitor/state" }, + new String[] { "cross_cluster_search", "cross_cluster_replication" }, new RoleDescriptor.IndicesPrivileges[] { RoleDescriptor.IndicesPrivileges.builder() .indices("metrics") @@ -119,12 +114,7 @@ public void testBuildForSearchAndReplication() throws IOException { .build(), RoleDescriptor.IndicesPrivileges.builder() .indices("archive") - .privileges( - "manage", - "read", - "indices:internal/admin/ccr/restore/*", - "internal:transport/proxy/indices:internal/admin/ccr/restore/*" - ) + .privileges("cross_cluster_replication", "cross_cluster_replication_internal") .allowRestrictedIndices(true) .build() } ); diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyRestIT.java index 33b2f821d9c33..5298eff047430 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyRestIT.java @@ -735,7 +735,7 @@ public void testCreateCrossClusterApiKey() throws IOException { XContentTestUtils.convertToMap( new RoleDescriptor( "cross_cluster", - new String[] { "cross_cluster_access", "cluster:monitor/state" }, + new String[] { "cross_cluster_search", "cross_cluster_replication" }, new RoleDescriptor.IndicesPrivileges[] { RoleDescriptor.IndicesPrivileges.builder() .indices("metrics") @@ -744,12 +744,7 @@ public void testCreateCrossClusterApiKey() throws IOException { .build(), RoleDescriptor.IndicesPrivileges.builder() .indices("logs") - .privileges( - "manage", - "read", - "indices:internal/admin/ccr/restore/*", - "internal:transport/proxy/indices:internal/admin/ccr/restore/*" - ) + .privileges("cross_cluster_replication", "cross_cluster_replication_internal") .allowRestrictedIndices(true) .build() }, null diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java index ca3ba480e4037..242ae348c750e 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java @@ -418,7 +418,7 @@ public void testInvalidateApiKeyWillRecordTimestamp() { public void testCreateCrossClusterApiKey() throws IOException { final RoleDescriptor roleDescriptor = new RoleDescriptor( "cross_cluster", - new String[] { "cross_cluster_access" }, + new String[] { "cross_cluster_search" }, new RoleDescriptor.IndicesPrivileges[] { RoleDescriptor.IndicesPrivileges.builder() .indices("logs") diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyAction.java index 5d8bace2ce3f4..19c29533ac8a7 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyAction.java @@ -75,7 +75,7 @@ public String getName() { protected RestChannelConsumer innerPrepareRequest(final RestRequest request, final NodeClient client) throws IOException { final Payload payload = PARSER.parse(request.contentParser(), null); - final CreateApiKeyRequest createApiKeyRequest = payload.toCreateApiKeyRequest(); + final CreateApiKeyRequest createApiKeyRequest = payload.toRequest(); return channel -> client.execute( CreateCrossClusterApiKeyAction.INSTANCE, createApiKeyRequest, @@ -89,7 +89,7 @@ record Payload( TimeValue expiration, Map metadata ) { - public CreateApiKeyRequest toCreateApiKeyRequest() { + public CreateApiKeyRequest toRequest() { final CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(); createApiKeyRequest.setType(ApiKey.Type.CROSS_CLUSTER); createApiKeyRequest.setName(name); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/apikey/TransportCreateCrossClusterApiKeyActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/apikey/TransportCreateCrossClusterApiKeyActionTests.java index a026228d5dd20..77cf0750f08cd 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/apikey/TransportCreateCrossClusterApiKeyActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/apikey/TransportCreateCrossClusterApiKeyActionTests.java @@ -49,7 +49,7 @@ public void testApiKeyWillBeCreatedWithUserRoleDescriptors() { List.of( new RoleDescriptor( "cross_cluster", - new String[] { "cross_cluster_access" }, + new String[] { "cross_cluster_search" }, new RoleDescriptor.IndicesPrivileges[] { RoleDescriptor.IndicesPrivileges.builder() .indices("idx") diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyActionTests.java index dbff0693039e4..e2f7b011be90f 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyActionTests.java @@ -65,7 +65,7 @@ public void testCreateApiKeyRequestHasTypeOfCrossCluster() throws Exception { List.of( new RoleDescriptor( "cross_cluster", - new String[] { "cross_cluster_access" }, + new String[] { "cross_cluster_search" }, new RoleDescriptor.IndicesPrivileges[] { RoleDescriptor.IndicesPrivileges.builder() .indices("logs") From 947add20acc379718d7e0b93956d1386863c5453 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 4 May 2023 16:02:08 +1000 Subject: [PATCH 09/14] tweak --- .../security/action/apikey/CreateCrossClusterApiKeyAction.java | 2 +- .../xpack/core/security/authz/RoleDescriptor.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateCrossClusterApiKeyAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateCrossClusterApiKeyAction.java index 2ae40419933b8..d5bd7f4e6c02e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateCrossClusterApiKeyAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateCrossClusterApiKeyAction.java @@ -10,7 +10,7 @@ import org.elasticsearch.action.ActionType; /** - * ActionType for the creation of an API key + * ActionType for the creation of a cross-cluster API key */ public final class CreateCrossClusterApiKeyAction extends ActionType { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java index 67723efe9b435..07f67a954da91 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java @@ -659,7 +659,7 @@ private static IndicesPrivilegesWithOptionalRemoteClusters parseIndexWithOptiona final XContentParser parser, final boolean allow2xFormat, final boolean allowRemoteClusters, - String[] privileges + final String[] predefinedPrivileges ) throws IOException { XContentParser.Token token = parser.currentToken(); if (token != XContentParser.Token.START_OBJECT) { @@ -674,6 +674,7 @@ private static IndicesPrivilegesWithOptionalRemoteClusters parseIndexWithOptiona String currentFieldName = null; String[] names = null; BytesReference query = null; + String[] privileges = predefinedPrivileges; String[] grantedFields = null; String[] deniedFields = null; boolean allowRestrictedIndices = false; From 8b5655522f4d184918212d25e884c5e69e14d765 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 4 May 2023 16:22:19 +1000 Subject: [PATCH 10/14] fix test --- .../xpack/security/authc/ApiKeyService.java | 1 + .../xpack/security/authc/ApiKeyServiceTests.java | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java index 7ca1edae48215..892a86f5e1696 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java @@ -324,6 +324,7 @@ && hasRemoteIndices(request.getRoleDescriptors())) { + "] or higher to support creating cross cluster API keys" ) ); + return; } final Set filteredUserRoleDescriptors = maybeRemoveRemoteIndicesPrivileges( diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java index d8f474eef8057..18839a15edeac 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java @@ -37,7 +37,6 @@ import org.elasticsearch.action.update.UpdateResponse; import org.elasticsearch.client.internal.Client; import org.elasticsearch.cluster.ClusterState; -import org.elasticsearch.cluster.node.DiscoveryNodes; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; @@ -2132,10 +2131,12 @@ public void testCreateCrossClusterApiKeyMinVersionConstraint() { ); final ClusterState clusterState = mock(ClusterState.class); when(clusterService.state()).thenReturn(clusterState); - final DiscoveryNodes discoveryNodes = mock(DiscoveryNodes.class); - when(clusterState.nodes()).thenReturn(discoveryNodes); - final Version minNodeVersion = VersionUtils.randomPreviousCompatibleVersion(random(), Version.CURRENT); - when(discoveryNodes.getMinNodeVersion()).thenReturn(minNodeVersion); + final TransportVersion minTransportVersion = TransportVersionUtils.randomVersionBetween( + random(), + TransportVersion.MINIMUM_COMPATIBLE, + TransportVersionUtils.getPreviousVersion(TransportVersion.V_8_9_0) + ); + when(clusterState.getMinTransportVersion()).thenReturn(minTransportVersion); final ApiKeyService service = new ApiKeyService( Settings.EMPTY, @@ -2153,9 +2154,8 @@ public void testCreateCrossClusterApiKeyMinVersionConstraint() { assertThat( e.getMessage(), - containsString("all nodes must have version [8.9.0] or higher to support creating cross cluster API keys") + containsString("all nodes must have transport version [8090099] or higher to support creating cross cluster API keys") ); - } private static RoleDescriptor randomRoleDescriptorWithRemoteIndexPrivileges() { From 4916c4b658baec485a0d6028ff51f4faf80b9d43 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Fri, 5 May 2023 14:31:31 +1000 Subject: [PATCH 11/14] subclass createCrossClusterApiKeyRequest --- .../apikey/AbstractCreateApiKeyRequest.java | 139 ++++++++++++++++++ .../action/apikey/CreateApiKeyRequest.java | 126 +--------------- .../CreateCrossClusterApiKeyRequest.java | 52 +++++++ ...va => CreateRestApiKeyRequestBuilder.java} | 17 +-- ...ossClusterApiKeyRoleDescriptorBuilder.java | 2 + .../security/authc/AuthenticationField.java | 1 - .../core/security/authz/RoleDescriptor.java | 1 + .../CreateApiKeyRequestBuilderTests.java | 4 +- .../idp/action/SamlIdentityProviderTests.java | 4 +- .../security/authc/ApiKeyIntegTests.java | 32 ++-- .../authc/apikey/ApiKeySingleNodeTests.java | 43 ++++-- .../apikey/TransportCreateApiKeyAction.java | 2 - ...ansportCreateCrossClusterApiKeyAction.java | 12 +- .../audit/logfile/LoggingAuditTrail.java | 19 +-- .../xpack/security/authc/ApiKeyService.java | 8 +- .../action/apikey/RestCreateApiKeyAction.java | 4 +- .../RestCreateCrossClusterApiKeyAction.java | 32 +--- .../action/apikey/RestGrantApiKeyAction.java | 4 +- ...rtCreateCrossClusterApiKeyActionTests.java | 35 ++--- .../security/authc/ApiKeyServiceTests.java | 7 +- ...AccessAuthenticationServiceIntegTests.java | 7 +- ...stCreateCrossClusterApiKeyActionTests.java | 16 +- 22 files changed, 316 insertions(+), 251 deletions(-) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/AbstractCreateApiKeyRequest.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateCrossClusterApiKeyRequest.java rename x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/{CreateApiKeyRequestBuilder.java => CreateRestApiKeyRequestBuilder.java} (82%) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/AbstractCreateApiKeyRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/AbstractCreateApiKeyRequest.java new file mode 100644 index 0000000000000..5309a5f6c0647 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/AbstractCreateApiKeyRequest.java @@ -0,0 +1,139 @@ +/* + * 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.TransportVersion; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.support.WriteRequest; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.UUIDs; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.core.TimeValue; +import org.elasticsearch.xpack.core.security.action.role.RoleDescriptorRequestValidator; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.support.MetadataUtils; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.elasticsearch.action.ValidateActions.addValidationError; + +public abstract class AbstractCreateApiKeyRequest extends ActionRequest { + public static final WriteRequest.RefreshPolicy DEFAULT_REFRESH_POLICY = WriteRequest.RefreshPolicy.WAIT_UNTIL; + protected final String id; + protected String name; + protected TimeValue expiration; + protected Map metadata; + protected List roleDescriptors = Collections.emptyList(); + protected WriteRequest.RefreshPolicy refreshPolicy = DEFAULT_REFRESH_POLICY; + + public AbstractCreateApiKeyRequest() { + super(); + // we generate the API key id soonest so it's part of the request body so it is audited + this.id = UUIDs.base64UUID(); // because auditing can currently only catch requests but not responses, + } + + public AbstractCreateApiKeyRequest(StreamInput in) throws IOException { + super(in); + if (in.getTransportVersion().onOrAfter(TransportVersion.V_7_10_0)) { + this.id = in.readString(); + } else { + this.id = UUIDs.base64UUID(); + } + if (in.getTransportVersion().onOrAfter(TransportVersion.V_7_5_0)) { + this.name = in.readOptionalString(); + } else { + this.name = in.readString(); + } + this.expiration = in.readOptionalTimeValue(); + this.roleDescriptors = in.readImmutableList(RoleDescriptor::new); + this.refreshPolicy = WriteRequest.RefreshPolicy.readFrom(in); + if (in.getTransportVersion().onOrAfter(TransportVersion.V_8_0_0)) { + this.metadata = in.readMap(); + } else { + this.metadata = null; + } + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public abstract ApiKey.Type getType(); + + public TimeValue getExpiration() { + return expiration; + } + + public List getRoleDescriptors() { + return roleDescriptors; + } + + public WriteRequest.RefreshPolicy getRefreshPolicy() { + return refreshPolicy; + } + + public Map getMetadata() { + return metadata; + } + + @Override + public ActionRequestValidationException validate() { + ActionRequestValidationException validationException = null; + if (Strings.isNullOrEmpty(name)) { + validationException = addValidationError("api key name is required", validationException); + } else { + if (name.length() > 256) { + validationException = addValidationError("api key name may not be more than 256 characters long", validationException); + } + if (name.equals(name.trim()) == false) { + validationException = addValidationError("api key name may not begin or end with whitespace", validationException); + } + if (name.startsWith("_")) { + validationException = addValidationError("api key name may not begin with an underscore", validationException); + } + } + if (metadata != null && MetadataUtils.containsReservedMetadata(metadata)) { + validationException = addValidationError( + "API key metadata keys may not start with [" + MetadataUtils.RESERVED_PREFIX + "]", + validationException + ); + } + for (RoleDescriptor roleDescriptor : getRoleDescriptors()) { + validationException = RoleDescriptorRequestValidator.validate(roleDescriptor, validationException); + } + return validationException; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + if (out.getTransportVersion().onOrAfter(TransportVersion.V_7_10_0)) { + out.writeString(id); + } + if (out.getTransportVersion().onOrAfter(TransportVersion.V_7_5_0)) { + out.writeOptionalString(name); + } else { + out.writeString(name); + } + out.writeOptionalTimeValue(expiration); + out.writeList(getRoleDescriptors()); + refreshPolicy.writeTo(out); + if (out.getTransportVersion().onOrAfter(TransportVersion.V_7_13_0)) { + out.writeGenericMap(metadata); + } + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequest.java index 7b820651305a7..2b0640c8f1cab 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequest.java @@ -7,48 +7,25 @@ package org.elasticsearch.xpack.core.security.action.apikey; -import org.elasticsearch.TransportVersion; -import org.elasticsearch.action.ActionRequest; -import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.support.WriteRequest; -import org.elasticsearch.common.Strings; -import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.io.stream.StreamInput; -import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.core.Assertions; import org.elasticsearch.core.Nullable; import org.elasticsearch.core.TimeValue; -import org.elasticsearch.xpack.core.security.action.role.RoleDescriptorRequestValidator; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; -import org.elasticsearch.xpack.core.security.support.MetadataUtils; import java.io.IOException; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; -import static org.elasticsearch.action.ValidateActions.addValidationError; - /** * Request class used for the creation of an API key. The request requires a name to be provided * and optionally an expiration time and permission limitation can be provided. */ -public final class CreateApiKeyRequest extends ActionRequest { - public static final WriteRequest.RefreshPolicy DEFAULT_REFRESH_POLICY = WriteRequest.RefreshPolicy.WAIT_UNTIL; - - private final String id; - private String name; - private ApiKey.Type type = ApiKey.Type.REST; - private TimeValue expiration; - private Map metadata; - private List roleDescriptors = Collections.emptyList(); - private WriteRequest.RefreshPolicy refreshPolicy = DEFAULT_REFRESH_POLICY; +public final class CreateApiKeyRequest extends AbstractCreateApiKeyRequest { public CreateApiKeyRequest() { super(); - this.id = UUIDs.base64UUID(); // because auditing can currently only catch requests but not responses, - // we generate the API key id soonest so it's part of the request body so it is audited } /** @@ -76,130 +53,35 @@ public CreateApiKeyRequest( public CreateApiKeyRequest(StreamInput in) throws IOException { super(in); - if (in.getTransportVersion().onOrAfter(TransportVersion.V_7_10_0)) { - this.id = in.readString(); - } else { - this.id = UUIDs.base64UUID(); - } - if (in.getTransportVersion().onOrAfter(TransportVersion.V_7_5_0)) { - this.name = in.readOptionalString(); - } else { - this.name = in.readString(); - } - this.expiration = in.readOptionalTimeValue(); - this.roleDescriptors = in.readImmutableList(RoleDescriptor::new); - this.refreshPolicy = WriteRequest.RefreshPolicy.readFrom(in); - if (in.getTransportVersion().onOrAfter(TransportVersion.V_8_0_0)) { - this.metadata = in.readMap(); - } else { - this.metadata = null; - } } - public String getId() { - return id; + @Override + public ApiKey.Type getType() { + return ApiKey.Type.REST; } public void setId() { throw new UnsupportedOperationException("The API Key Id cannot be set, it must be generated randomly"); } - public String getName() { - return name; - } - public void setName(String name) { this.name = name; } - public ApiKey.Type getType() { - return type; - } - - public void setType(ApiKey.Type type) { - this.type = type; - } - - public TimeValue getExpiration() { - return expiration; - } - public void setExpiration(@Nullable TimeValue expiration) { this.expiration = expiration; } - public List getRoleDescriptors() { - return roleDescriptors; - } - public void setRoleDescriptors(@Nullable List roleDescriptors) { this.roleDescriptors = (roleDescriptors == null) ? List.of() : List.copyOf(roleDescriptors); } - public WriteRequest.RefreshPolicy getRefreshPolicy() { - return refreshPolicy; - } - public void setRefreshPolicy(WriteRequest.RefreshPolicy refreshPolicy) { this.refreshPolicy = Objects.requireNonNull(refreshPolicy, "refresh policy may not be null"); } - public Map getMetadata() { - return metadata; - } - public void setMetadata(Map metadata) { this.metadata = metadata; } - @Override - public ActionRequestValidationException validate() { - if (Assertions.ENABLED && type == ApiKey.Type.CROSS_CLUSTER) { - assert roleDescriptors.size() == 1; - CrossClusterApiKeyRoleDescriptorBuilder.validate(roleDescriptors.iterator().next()); - } - ActionRequestValidationException validationException = null; - if (Strings.isNullOrEmpty(name)) { - validationException = addValidationError("api key name is required", validationException); - } else { - if (name.length() > 256) { - validationException = addValidationError("api key name may not be more than 256 characters long", validationException); - } - if (name.equals(name.trim()) == false) { - validationException = addValidationError("api key name may not begin or end with whitespace", validationException); - } - if (name.startsWith("_")) { - validationException = addValidationError("api key name may not begin with an underscore", validationException); - } - } - if (metadata != null && MetadataUtils.containsReservedMetadata(metadata)) { - validationException = addValidationError( - "API key metadata keys may not start with [" + MetadataUtils.RESERVED_PREFIX + "]", - validationException - ); - } - for (RoleDescriptor roleDescriptor : roleDescriptors) { - validationException = RoleDescriptorRequestValidator.validate(roleDescriptor, validationException); - } - return validationException; - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - super.writeTo(out); - if (out.getTransportVersion().onOrAfter(TransportVersion.V_7_10_0)) { - out.writeString(id); - } - if (out.getTransportVersion().onOrAfter(TransportVersion.V_7_5_0)) { - out.writeOptionalString(name); - } else { - out.writeString(name); - } - out.writeOptionalTimeValue(expiration); - out.writeList(roleDescriptors); - refreshPolicy.writeTo(out); - if (out.getTransportVersion().onOrAfter(TransportVersion.V_7_13_0)) { - out.writeGenericMap(metadata); - } - } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateCrossClusterApiKeyRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateCrossClusterApiKeyRequest.java new file mode 100644 index 0000000000000..a69a2b92669ad --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateCrossClusterApiKeyRequest.java @@ -0,0 +1,52 @@ +/* + * 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.ActionRequestValidationException; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.core.Assertions; +import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.TimeValue; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +public final class CreateCrossClusterApiKeyRequest extends AbstractCreateApiKeyRequest { + + public CreateCrossClusterApiKeyRequest( + String name, + CrossClusterApiKeyRoleDescriptorBuilder roleDescriptorBuilder, + @Nullable TimeValue expiration, + @Nullable Map metadata + ) { + super(); + this.name = name; + this.roleDescriptors = List.of(roleDescriptorBuilder.build()); + this.expiration = expiration; + this.metadata = metadata; + } + + public CreateCrossClusterApiKeyRequest(StreamInput in) throws IOException { + super(in); + } + + @Override + public ApiKey.Type getType() { + return ApiKey.Type.CROSS_CLUSTER; + } + + @Override + public ActionRequestValidationException validate() { + if (Assertions.ENABLED) { + assert getRoleDescriptors().size() == 1; + CrossClusterApiKeyRoleDescriptorBuilder.validate(getRoleDescriptors().iterator().next()); + } + return super.validate(); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequestBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateRestApiKeyRequestBuilder.java similarity index 82% rename from x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequestBuilder.java rename to x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateRestApiKeyRequestBuilder.java index cd4cea270de6b..999e406241b73 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequestBuilder.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateRestApiKeyRequestBuilder.java @@ -30,7 +30,7 @@ /** * Request builder for populating a {@link CreateApiKeyRequest} */ -public final class CreateApiKeyRequestBuilder extends ActionRequestBuilder { +public final class CreateRestApiKeyRequestBuilder extends ActionRequestBuilder { @SuppressWarnings("unchecked") static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( @@ -56,36 +56,36 @@ public final class CreateApiKeyRequestBuilder extends ActionRequestBuilder p.map(), new ParseField("metadata")); } - public CreateApiKeyRequestBuilder(ElasticsearchClient client) { + public CreateRestApiKeyRequestBuilder(ElasticsearchClient client) { super(client, CreateApiKeyAction.INSTANCE, new CreateApiKeyRequest()); } - public CreateApiKeyRequestBuilder setName(String name) { + public CreateRestApiKeyRequestBuilder setName(String name) { request.setName(name); return this; } - public CreateApiKeyRequestBuilder setExpiration(TimeValue expiration) { + public CreateRestApiKeyRequestBuilder setExpiration(TimeValue expiration) { request.setExpiration(expiration); return this; } - public CreateApiKeyRequestBuilder setRoleDescriptors(List roleDescriptors) { + public CreateRestApiKeyRequestBuilder setRoleDescriptors(List roleDescriptors) { request.setRoleDescriptors(roleDescriptors); return this; } - public CreateApiKeyRequestBuilder setRefreshPolicy(WriteRequest.RefreshPolicy refreshPolicy) { + public CreateRestApiKeyRequestBuilder setRefreshPolicy(WriteRequest.RefreshPolicy refreshPolicy) { request.setRefreshPolicy(refreshPolicy); return this; } - public CreateApiKeyRequestBuilder setMetadata(Map metadata) { + public CreateRestApiKeyRequestBuilder setMetadata(Map metadata) { request.setMetadata(metadata); return this; } - public CreateApiKeyRequestBuilder source(BytesReference source, XContentType xContentType) throws IOException { + public CreateRestApiKeyRequestBuilder source(BytesReference source, XContentType xContentType) throws IOException { final NamedXContentRegistry registry = NamedXContentRegistry.EMPTY; try ( InputStream stream = source.streamInput(); @@ -96,7 +96,6 @@ public CreateApiKeyRequestBuilder source(BytesReference source, XContentType xCo setRoleDescriptors(createApiKeyRequest.getRoleDescriptors()); setExpiration(createApiKeyRequest.getExpiration()); setMetadata(createApiKeyRequest.getMetadata()); - } return this; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilder.java index 34c525fc93536..da91a6b4714f7 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilder.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilder.java @@ -59,6 +59,8 @@ private CrossClusterApiKeyRoleDescriptorBuilder( ) { this.search = search == null ? List.of() : search; this.replication = replication == null ? List.of() : replication; + assert this.search.stream().allMatch(p -> Arrays.equals(p.getPrivileges(), CCS_INDICES_PRIVILEGE_NAMES)); + assert this.replication.stream().allMatch(p -> Arrays.equals(p.getPrivileges(), CCR_INDICES_PRIVILEGE_NAMES)); } public RoleDescriptor build() { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/AuthenticationField.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/AuthenticationField.java index 37cf09bc4607a..29f3d3f08e0ee 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/AuthenticationField.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/AuthenticationField.java @@ -20,7 +20,6 @@ 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"; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java index 07f67a954da91..34a8dde6945fb 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java @@ -661,6 +661,7 @@ private static IndicesPrivilegesWithOptionalRemoteClusters parseIndexWithOptiona final boolean allowRemoteClusters, final String[] predefinedPrivileges ) throws IOException { + assert predefinedPrivileges == null || predefinedPrivileges.length != 0; XContentParser.Token token = parser.currentToken(); if (token != XContentParser.Token.START_OBJECT) { throw new ElasticsearchParseException( diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequestBuilderTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequestBuilderTests.java index da0eecbccf48e..d2051a41b6d00 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequestBuilderTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequestBuilderTests.java @@ -57,7 +57,7 @@ public void testParserAndCreateApiRequestBuilder() throws IOException { }""", args); final BytesArray source = new BytesArray(json); final NodeClient mockClient = mock(NodeClient.class); - final CreateApiKeyRequest request = new CreateApiKeyRequestBuilder(mockClient).source(source, XContentType.JSON).request(); + final CreateApiKeyRequest request = new CreateRestApiKeyRequestBuilder(mockClient).source(source, XContentType.JSON).request(); final List actualRoleDescriptors = request.getRoleDescriptors(); assertThat(request.getName(), equalTo("my-api-key")); assertThat(actualRoleDescriptors.size(), is(2)); @@ -90,7 +90,7 @@ public void testParserAndCreateApiRequestBuilderWithNullOrEmptyRoleDescriptors() + "}"; final BytesArray source = new BytesArray(json); final NodeClient mockClient = mock(NodeClient.class); - final CreateApiKeyRequest request = new CreateApiKeyRequestBuilder(mockClient).source(source, XContentType.JSON).request(); + final CreateApiKeyRequest request = new CreateRestApiKeyRequestBuilder(mockClient).source(source, XContentType.JSON).request(); final List actualRoleDescriptors = request.getRoleDescriptors(); assertThat(request.getName(), equalTo("my-api-key")); assertThat(actualRoleDescriptors.size(), is(0)); diff --git a/x-pack/plugin/identity-provider/src/internalClusterTest/java/org/elasticsearch/xpack/idp/action/SamlIdentityProviderTests.java b/x-pack/plugin/identity-provider/src/internalClusterTest/java/org/elasticsearch/xpack/idp/action/SamlIdentityProviderTests.java index 551b53948c1e5..36a9ec3b70afe 100644 --- a/x-pack/plugin/identity-provider/src/internalClusterTest/java/org/elasticsearch/xpack/idp/action/SamlIdentityProviderTests.java +++ b/x-pack/plugin/identity-provider/src/internalClusterTest/java/org/elasticsearch/xpack/idp/action/SamlIdentityProviderTests.java @@ -23,8 +23,8 @@ import org.elasticsearch.test.rest.ObjectPath; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentFactory; -import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyRequestBuilder; import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyResponse; +import org.elasticsearch.xpack.core.security.action.apikey.CreateRestApiKeyRequestBuilder; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProviderDocument; import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProviderIndex; @@ -485,7 +485,7 @@ private String getApiKeyFromCredentials(String username, SecureString password) Client client = client().filterWithHeader( Collections.singletonMap("Authorization", UsernamePasswordToken.basicAuthHeaderValue(username, password)) ); - final CreateApiKeyResponse response = new CreateApiKeyRequestBuilder(client).setName("test key") + final CreateApiKeyResponse response = new CreateRestApiKeyRequestBuilder(client).setName("test key") .setExpiration(TimeValue.timeValueHours(TimeUnit.DAYS.toHours(7L))) .get(); assertNotNull(response); diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java index 44c2d9da43b92..a5d811b423e9f 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java @@ -59,8 +59,8 @@ import org.elasticsearch.xpack.core.security.action.apikey.BulkUpdateApiKeyAction; import org.elasticsearch.xpack.core.security.action.apikey.BulkUpdateApiKeyRequest; import org.elasticsearch.xpack.core.security.action.apikey.BulkUpdateApiKeyResponse; -import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyRequestBuilder; import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyResponse; +import org.elasticsearch.xpack.core.security.action.apikey.CreateRestApiKeyRequestBuilder; 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; @@ -260,7 +260,7 @@ public void testCreateApiKey() throws Exception { Client client = client().filterWithHeader( Collections.singletonMap("Authorization", basicAuthHeaderValue(ES_TEST_ROOT_USER, TEST_PASSWORD_SECURE_STRING)) ); - final CreateApiKeyResponse response = new CreateApiKeyRequestBuilder(client).setName("test key") + final CreateApiKeyResponse response = new CreateRestApiKeyRequestBuilder(client).setName("test key") .setExpiration(TimeValue.timeValueHours(TimeUnit.DAYS.toHours(7L))) .setRoleDescriptors(Collections.singletonList(descriptor)) .setMetadata(ApiKeyTests.randomMetadata()) @@ -277,7 +277,7 @@ public void testCreateApiKey() throws Exception { assertThat(getApiKeyDocument(response.getId()).get("type"), equalTo("rest")); // create simple api key - final CreateApiKeyResponse simple = new CreateApiKeyRequestBuilder(client).setName("simple").get(); + final CreateApiKeyResponse simple = new CreateRestApiKeyRequestBuilder(client).setName("simple").get(); assertEquals("simple", simple.getName()); assertNotNull(simple.getId()); assertNotNull(simple.getKey()); @@ -315,7 +315,7 @@ public void testMultipleApiKeysCanHaveSameName() { Client client = client().filterWithHeader( Collections.singletonMap("Authorization", basicAuthHeaderValue(ES_TEST_ROOT_USER, TEST_PASSWORD_SECURE_STRING)) ); - final CreateApiKeyResponse response = new CreateApiKeyRequestBuilder(client).setName(keyName) + final CreateApiKeyResponse response = new CreateRestApiKeyRequestBuilder(client).setName(keyName) .setExpiration(null) .setRoleDescriptors(Collections.singletonList(descriptor)) .setMetadata(ApiKeyTests.randomMetadata()) @@ -337,7 +337,7 @@ public void testCreateApiKeyWithoutNameWillFail() { ); final ActionRequestValidationException e = expectThrows( ActionRequestValidationException.class, - () -> new CreateApiKeyRequestBuilder(client).get() + () -> new CreateRestApiKeyRequestBuilder(client).get() ); assertThat(e.getMessage(), containsString("api key name is required")); } @@ -1593,7 +1593,7 @@ public void testDerivedKeys() throws ExecutionException, InterruptedException { Client client = client().filterWithHeader( Collections.singletonMap("Authorization", basicAuthHeaderValue(ES_TEST_ROOT_USER, TEST_PASSWORD_SECURE_STRING)) ); - final CreateApiKeyResponse response = new CreateApiKeyRequestBuilder(client).setName("key-1") + final CreateApiKeyResponse response = new CreateRestApiKeyRequestBuilder(client).setName("key-1") .setRoleDescriptors( Collections.singletonList(new RoleDescriptor("role", new String[] { "manage_api_key", "manage_token" }, null, null)) ) @@ -1623,19 +1623,19 @@ public void testDerivedKeys() throws ExecutionException, InterruptedException { final IllegalArgumentException e1 = expectThrows( IllegalArgumentException.class, - () -> new CreateApiKeyRequestBuilder(clientKey1).setName("key-2").setMetadata(ApiKeyTests.randomMetadata()).get() + () -> new CreateRestApiKeyRequestBuilder(clientKey1).setName("key-2").setMetadata(ApiKeyTests.randomMetadata()).get() ); assertThat(e1.getMessage(), containsString(expectedMessage)); final IllegalArgumentException e2 = expectThrows( IllegalArgumentException.class, - () -> new CreateApiKeyRequestBuilder(clientKey1).setName("key-3").setRoleDescriptors(Collections.emptyList()).get() + () -> new CreateRestApiKeyRequestBuilder(clientKey1).setName("key-3").setRoleDescriptors(Collections.emptyList()).get() ); assertThat(e2.getMessage(), containsString(expectedMessage)); final IllegalArgumentException e3 = expectThrows( IllegalArgumentException.class, - () -> new CreateApiKeyRequestBuilder(clientKey1).setName("key-4") + () -> new CreateRestApiKeyRequestBuilder(clientKey1).setName("key-4") .setMetadata(ApiKeyTests.randomMetadata()) .setRoleDescriptors( Collections.singletonList(new RoleDescriptor("role", new String[] { "manage_own_api_key" }, null, null)) @@ -1652,14 +1652,14 @@ public void testDerivedKeys() throws ExecutionException, InterruptedException { final IllegalArgumentException e4 = expectThrows( IllegalArgumentException.class, - () -> new CreateApiKeyRequestBuilder(clientKey1).setName("key-5") + () -> new CreateRestApiKeyRequestBuilder(clientKey1).setName("key-5") .setMetadata(ApiKeyTests.randomMetadata()) .setRoleDescriptors(roleDescriptors) .get() ); assertThat(e4.getMessage(), containsString(expectedMessage)); - final CreateApiKeyResponse key100Response = new CreateApiKeyRequestBuilder(clientKey1).setName("key-100") + final CreateApiKeyResponse key100Response = new CreateRestApiKeyRequestBuilder(clientKey1).setName("key-100") .setMetadata(ApiKeyTests.randomMetadata()) .setRoleDescriptors(Collections.singletonList(new RoleDescriptor("role", null, null, null))) .get(); @@ -1692,7 +1692,7 @@ public void testApiKeyRunAsAnotherUserCanCreateApiKey() { Client client = client().filterWithHeader( Map.of("Authorization", basicAuthHeaderValue(ES_TEST_ROOT_USER, TEST_PASSWORD_SECURE_STRING)) ); - final CreateApiKeyResponse response1 = new CreateApiKeyRequestBuilder(client).setName("run-as-key") + final CreateApiKeyResponse response1 = new CreateRestApiKeyRequestBuilder(client).setName("run-as-key") .setRoleDescriptors(List.of(descriptor)) .setMetadata(ApiKeyTests.randomMetadata()) .get(); @@ -1700,7 +1700,7 @@ public void testApiKeyRunAsAnotherUserCanCreateApiKey() { final String base64ApiKeyKeyValue = Base64.getEncoder() .encodeToString((response1.getId() + ":" + response1.getKey()).getBytes(StandardCharsets.UTF_8)); - final CreateApiKeyResponse response2 = new CreateApiKeyRequestBuilder( + final CreateApiKeyResponse response2 = new CreateRestApiKeyRequestBuilder( client().filterWithHeader( Map.of("Authorization", "ApiKey " + base64ApiKeyKeyValue, "es-security-runas-user", ES_TEST_ROOT_USER) ) @@ -1728,7 +1728,7 @@ public void testCreationAndAuthenticationReturns429WhenThreadPoolIsSaturated() t final Client client = client().filterWithHeader( Collections.singletonMap("Authorization", basicAuthHeaderValue(ES_TEST_ROOT_USER, TEST_PASSWORD_SECURE_STRING)) ); - final CreateApiKeyResponse createApiKeyResponse = new CreateApiKeyRequestBuilder(client).setName("auth only key") + final CreateApiKeyResponse createApiKeyResponse = new CreateRestApiKeyRequestBuilder(client).setName("auth only key") .setRoleDescriptors(Collections.singletonList(descriptor)) .setMetadata(ApiKeyTests.randomMetadata()) .get(); @@ -2845,7 +2845,7 @@ private Tuple createApiKeyAndAuthenticateWithIt() throws IOExcep Collections.singletonMap("Authorization", basicAuthHeaderValue(ES_TEST_ROOT_USER, TEST_PASSWORD_SECURE_STRING)) ); - final CreateApiKeyResponse createApiKeyResponse = new CreateApiKeyRequestBuilder(client).setName("test key") + final CreateApiKeyResponse createApiKeyResponse = new CreateRestApiKeyRequestBuilder(client).setName("test key") .setMetadata(ApiKeyTests.randomMetadata()) .get(); final String docId = createApiKeyResponse.getId(); @@ -3082,7 +3082,7 @@ private Tuple, List>> createApiKe Client client = client().filterWithHeader(headers); final Map metadata = ApiKeyTests.randomMetadata(); metadatas.add(metadata); - final CreateApiKeyResponse response = new CreateApiKeyRequestBuilder(client).setName( + final CreateApiKeyResponse response = new CreateRestApiKeyRequestBuilder(client).setName( namePrefix + randomAlphaOfLengthBetween(5, 9) + i ) .setExpiration(expiration) diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java index 242ae348c750e..1e030ff4a3e72 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java @@ -31,14 +31,17 @@ import org.elasticsearch.test.SecuritySingleNodeTestCase; import org.elasticsearch.test.TestSecurityClient; import org.elasticsearch.test.XContentTestUtils; +import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xcontent.XContentParserConfiguration; import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.core.security.action.Grant; -import org.elasticsearch.xpack.core.security.action.apikey.ApiKey; 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.CreateCrossClusterApiKeyAction; +import org.elasticsearch.xpack.core.security.action.apikey.CreateCrossClusterApiKeyRequest; +import org.elasticsearch.xpack.core.security.action.apikey.CrossClusterApiKeyRoleDescriptorBuilder; 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; @@ -74,6 +77,7 @@ import static org.elasticsearch.test.SecuritySettingsSource.ES_TEST_ROOT_USER; import static org.elasticsearch.test.SecuritySettingsSourceField.TEST_PASSWORD; +import static org.elasticsearch.xcontent.json.JsonXContent.jsonXContent; import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue; import static org.elasticsearch.xpack.security.support.SecuritySystemIndices.SECURITY_MAIN_ALIAS; import static org.hamcrest.Matchers.anEmptyMap; @@ -416,21 +420,16 @@ public void testInvalidateApiKeyWillRecordTimestamp() { } public void testCreateCrossClusterApiKey() throws IOException { - final RoleDescriptor roleDescriptor = new RoleDescriptor( - "cross_cluster", - new String[] { "cross_cluster_search" }, - new RoleDescriptor.IndicesPrivileges[] { - RoleDescriptor.IndicesPrivileges.builder() - .indices("logs") - .privileges("read", "read_cross_cluster", "view_index_metadata") - .build() }, - null - ); - final var createApiKeyRequest = new CreateApiKeyRequest(randomAlphaOfLengthBetween(3, 8), List.of(roleDescriptor), null); - createApiKeyRequest.setType(ApiKey.Type.CROSS_CLUSTER); + final XContentParser parser = jsonXContent.createParser(XContentParserConfiguration.EMPTY, """ + { + "search": [ {"names": ["logs"]} ] + }"""); + final var roleDescriptorBuilder = CrossClusterApiKeyRoleDescriptorBuilder.PARSER.parse(parser, null); + + final var request = new CreateCrossClusterApiKeyRequest(randomAlphaOfLengthBetween(3, 8), roleDescriptorBuilder, null, null); final PlainActionFuture future = new PlainActionFuture<>(); - client().execute(CreateCrossClusterApiKeyAction.INSTANCE, createApiKeyRequest, future); + client().execute(CreateCrossClusterApiKeyAction.INSTANCE, request, future); final CreateApiKeyResponse createApiKeyResponse = future.actionGet(); final Map document = client().execute( @@ -451,7 +450,21 @@ public void testCreateCrossClusterApiKey() throws IOException { XContentType.JSON ); - assertThat(actualRoleDescriptor, equalTo(roleDescriptor)); + assertThat( + actualRoleDescriptor, + equalTo( + new RoleDescriptor( + "cross_cluster", + new String[] { "cross_cluster_search" }, + new RoleDescriptor.IndicesPrivileges[] { + RoleDescriptor.IndicesPrivileges.builder() + .indices("logs") + .privileges("read", "read_cross_cluster", "view_index_metadata") + .build() }, + null + ) + ) + ); assertThat((Map) document.get("limited_by_role_descriptors"), anEmptyMap()); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportCreateApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportCreateApiKeyAction.java index a34314b1516a3..c9656d68e0dc9 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportCreateApiKeyAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportCreateApiKeyAction.java @@ -15,7 +15,6 @@ import org.elasticsearch.transport.TransportService; import org.elasticsearch.xcontent.NamedXContentRegistry; import org.elasticsearch.xpack.core.security.SecurityContext; -import org.elasticsearch.xpack.core.security.action.apikey.ApiKey; 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; @@ -51,7 +50,6 @@ public TransportCreateApiKeyAction( @Override protected void doExecute(Task task, CreateApiKeyRequest request, ActionListener listener) { - assert request.getType() == ApiKey.Type.REST; final Authentication authentication = securityContext.getAuthentication(); if (authentication == null) { listener.onFailure(new IllegalStateException("authentication is required")); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportCreateCrossClusterApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportCreateCrossClusterApiKeyAction.java index 36c44eea9211a..2c0df3cd59dfc 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportCreateCrossClusterApiKeyAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportCreateCrossClusterApiKeyAction.java @@ -14,10 +14,9 @@ import org.elasticsearch.tasks.Task; import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.core.security.SecurityContext; -import org.elasticsearch.xpack.core.security.action.apikey.ApiKey; -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.CreateCrossClusterApiKeyAction; +import org.elasticsearch.xpack.core.security.action.apikey.CreateCrossClusterApiKeyRequest; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.security.authc.ApiKeyService; @@ -26,7 +25,9 @@ /** * Implementation of the action needed to create an API key */ -public final class TransportCreateCrossClusterApiKeyAction extends HandledTransportAction { +public final class TransportCreateCrossClusterApiKeyAction extends HandledTransportAction< + CreateCrossClusterApiKeyRequest, + CreateApiKeyResponse> { private final ApiKeyService apiKeyService; private final SecurityContext securityContext; @@ -38,14 +39,13 @@ public TransportCreateCrossClusterApiKeyAction( ApiKeyService apiKeyService, SecurityContext context ) { - super(CreateCrossClusterApiKeyAction.NAME, transportService, actionFilters, CreateApiKeyRequest::new); + super(CreateCrossClusterApiKeyAction.NAME, transportService, actionFilters, CreateCrossClusterApiKeyRequest::new); this.apiKeyService = apiKeyService; this.securityContext = context; } @Override - protected void doExecute(Task task, CreateApiKeyRequest request, ActionListener listener) { - assert request.getType() == ApiKey.Type.CROSS_CLUSTER; + protected void doExecute(Task task, CreateCrossClusterApiKeyRequest request, ActionListener listener) { final Authentication authentication = securityContext.getAuthentication(); if (authentication == null) { listener.onFailure(new IllegalStateException("authentication is required")); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java index 6091773d8ced9..bc76c8e2c8261 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java @@ -44,6 +44,7 @@ import org.elasticsearch.xcontent.json.JsonXContent; import org.elasticsearch.xpack.core.security.SecurityContext; import org.elasticsearch.xpack.core.security.action.Grant; +import org.elasticsearch.xpack.core.security.action.apikey.AbstractCreateApiKeyRequest; import org.elasticsearch.xpack.core.security.action.apikey.BaseUpdateApiKeyRequest; import org.elasticsearch.xpack.core.security.action.apikey.BulkUpdateApiKeyAction; import org.elasticsearch.xpack.core.security.action.apikey.BulkUpdateApiKeyRequest; @@ -1219,11 +1220,11 @@ LogEntryBuilder withRequestBody(PutPrivilegesRequest putPrivilegesRequest) throw return this; } - LogEntryBuilder withRequestBody(CreateApiKeyRequest createApiKeyRequest) throws IOException { + LogEntryBuilder withRequestBody(AbstractCreateApiKeyRequest abstractCreateApiKeyRequest) throws IOException { logEntry.with(EVENT_ACTION_FIELD_NAME, "create_apikey"); XContentBuilder builder = JsonXContent.contentBuilder().humanReadable(true); builder.startObject(); - withRequestBody(builder, createApiKeyRequest); + withRequestBody(builder, abstractCreateApiKeyRequest); builder.endObject(); logEntry.with(CREATE_CONFIG_FIELD_NAME, Strings.toString(builder)); return this; @@ -1261,19 +1262,19 @@ LogEntryBuilder withRequestBody(final BulkUpdateApiKeyRequest bulkUpdateApiKeyRe return this; } - private void withRequestBody(XContentBuilder builder, CreateApiKeyRequest createApiKeyRequest) throws IOException { - TimeValue expiration = createApiKeyRequest.getExpiration(); + private void withRequestBody(XContentBuilder builder, AbstractCreateApiKeyRequest abstractCreateApiKeyRequest) throws IOException { + TimeValue expiration = abstractCreateApiKeyRequest.getExpiration(); builder.startObject("apikey") - .field("id", createApiKeyRequest.getId()) - .field("name", createApiKeyRequest.getName()) + .field("id", abstractCreateApiKeyRequest.getId()) + .field("name", abstractCreateApiKeyRequest.getName()) .field("expiration", expiration != null ? expiration.toString() : null) .startArray("role_descriptors"); - for (RoleDescriptor roleDescriptor : createApiKeyRequest.getRoleDescriptors()) { + for (RoleDescriptor roleDescriptor : abstractCreateApiKeyRequest.getRoleDescriptors()) { withRoleDescriptor(builder, roleDescriptor); } builder.endArray(); // role_descriptors - if (createApiKeyRequest.getMetadata() != null && createApiKeyRequest.getMetadata().isEmpty() == false) { - builder.field("metadata", createApiKeyRequest.getMetadata()); + if (abstractCreateApiKeyRequest.getMetadata() != null && abstractCreateApiKeyRequest.getMetadata().isEmpty() == false) { + builder.field("metadata", abstractCreateApiKeyRequest.getMetadata()); } builder.endObject(); // apikey } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java index 892a86f5e1696..45e038a7e40a9 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java @@ -82,11 +82,11 @@ import org.elasticsearch.xpack.core.security.action.ClearSecurityCacheAction; import org.elasticsearch.xpack.core.security.action.ClearSecurityCacheRequest; import org.elasticsearch.xpack.core.security.action.ClearSecurityCacheResponse; +import org.elasticsearch.xpack.core.security.action.apikey.AbstractCreateApiKeyRequest; import org.elasticsearch.xpack.core.security.action.apikey.ApiKey; import org.elasticsearch.xpack.core.security.action.apikey.BaseUpdateApiKeyRequest; import org.elasticsearch.xpack.core.security.action.apikey.BulkUpdateApiKeyRequest; import org.elasticsearch.xpack.core.security.action.apikey.BulkUpdateApiKeyResponse; -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.GetApiKeyResponse; import org.elasticsearch.xpack.core.security.action.apikey.InvalidateApiKeyResponse; @@ -293,7 +293,7 @@ public void invalidateAll() { */ public void createApiKey( Authentication authentication, - CreateApiKeyRequest request, + AbstractCreateApiKeyRequest request, Set userRoleDescriptors, ActionListener listener ) { @@ -347,7 +347,7 @@ private static boolean hasRemoteIndices(Collection roleDescripto private void createApiKeyAndIndexIt( Authentication authentication, - CreateApiKeyRequest request, + AbstractCreateApiKeyRequest request, Set userRoleDescriptors, ActionListener listener ) { @@ -1183,7 +1183,7 @@ protected void verifyKeyAgainstHash(String apiKeyHash, ApiKeyCredentials credent })); } - private static Instant getApiKeyExpiration(Instant now, CreateApiKeyRequest request) { + private static Instant getApiKeyExpiration(Instant now, AbstractCreateApiKeyRequest request) { if (request.getExpiration() != null) { return now.plusSeconds(request.getExpiration().getSeconds()); } else { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateApiKeyAction.java index 7d4ad8f2ebdeb..9bc1c4f994017 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateApiKeyAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateApiKeyAction.java @@ -16,7 +16,7 @@ import org.elasticsearch.rest.ServerlessScope; import org.elasticsearch.rest.action.RestToXContentListener; import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyRequest; -import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyRequestBuilder; +import org.elasticsearch.xpack.core.security.action.apikey.CreateRestApiKeyRequestBuilder; import java.io.IOException; import java.util.List; @@ -52,7 +52,7 @@ public String getName() { @Override protected RestChannelConsumer innerPrepareRequest(final RestRequest request, final NodeClient client) throws IOException { String refresh = request.param("refresh"); - CreateApiKeyRequestBuilder builder = new CreateApiKeyRequestBuilder(client).source( + CreateRestApiKeyRequestBuilder builder = new CreateRestApiKeyRequestBuilder(client).source( request.requiredContent(), request.getXContentType() ) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyAction.java index 19c29533ac8a7..9f003314c7898 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyAction.java @@ -15,9 +15,8 @@ import org.elasticsearch.rest.action.RestToXContentListener; import org.elasticsearch.xcontent.ConstructingObjectParser; import org.elasticsearch.xcontent.ParseField; -import org.elasticsearch.xpack.core.security.action.apikey.ApiKey; -import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyRequest; import org.elasticsearch.xpack.core.security.action.apikey.CreateCrossClusterApiKeyAction; +import org.elasticsearch.xpack.core.security.action.apikey.CreateCrossClusterApiKeyRequest; import org.elasticsearch.xpack.core.security.action.apikey.CrossClusterApiKeyRoleDescriptorBuilder; import java.io.IOException; @@ -34,10 +33,10 @@ public final class RestCreateCrossClusterApiKeyAction extends ApiKeyBaseRestHandler { @SuppressWarnings("unchecked") - static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( - "cross_cluster_api_key_request_payload", + static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "cross_cluster_api_key_request", false, - (args, v) -> new Payload( + (args, v) -> new CreateCrossClusterApiKeyRequest( (String) args[0], (CrossClusterApiKeyRoleDescriptorBuilder) args[1], TimeValue.parseTimeValue((String) args[2], null, "expiration"), @@ -73,30 +72,11 @@ public String getName() { @Override protected RestChannelConsumer innerPrepareRequest(final RestRequest request, final NodeClient client) throws IOException { - final Payload payload = PARSER.parse(request.contentParser(), null); - - final CreateApiKeyRequest createApiKeyRequest = payload.toRequest(); + final CreateCrossClusterApiKeyRequest createCrossClusterApiKeyRequest = PARSER.parse(request.contentParser(), null); return channel -> client.execute( CreateCrossClusterApiKeyAction.INSTANCE, - createApiKeyRequest, + createCrossClusterApiKeyRequest, new RestToXContentListener<>(channel) ); } - - record Payload( - String name, - CrossClusterApiKeyRoleDescriptorBuilder roleDescriptorBuilder, - TimeValue expiration, - Map metadata - ) { - public CreateApiKeyRequest toRequest() { - final CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(); - createApiKeyRequest.setType(ApiKey.Type.CROSS_CLUSTER); - createApiKeyRequest.setName(name); - createApiKeyRequest.setExpiration(expiration); - createApiKeyRequest.setMetadata(metadata); - createApiKeyRequest.setRoleDescriptors(List.of(roleDescriptorBuilder.build())); - return createApiKeyRequest; - } - } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGrantApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGrantApiKeyAction.java index 1758b9db47201..fb75a95980be9 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGrantApiKeyAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGrantApiKeyAction.java @@ -23,8 +23,8 @@ import org.elasticsearch.xcontent.ObjectParser; import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xcontent.XContentParser; -import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyRequestBuilder; import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyResponse; +import org.elasticsearch.xpack.core.security.action.apikey.CreateRestApiKeyRequestBuilder; import org.elasticsearch.xpack.core.security.action.apikey.GrantApiKeyAction; import org.elasticsearch.xpack.core.security.action.apikey.GrantApiKeyRequest; @@ -62,7 +62,7 @@ public final class RestGrantApiKeyAction extends ApiKeyBaseRestHandler implement PARSER.declareString((req, str) -> req.getGrant().setRunAsUsername(str), new ParseField("run_as")); PARSER.declareObject( (req, api) -> req.setApiKeyRequest(api), - (parser, ignore) -> CreateApiKeyRequestBuilder.parse(parser), + (parser, ignore) -> CreateRestApiKeyRequestBuilder.parse(parser), new ParseField("api_key") ); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/apikey/TransportCreateCrossClusterApiKeyActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/apikey/TransportCreateCrossClusterApiKeyActionTests.java index 77cf0750f08cd..26b594bca9a01 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/apikey/TransportCreateCrossClusterApiKeyActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/apikey/TransportCreateCrossClusterApiKeyActionTests.java @@ -12,18 +12,20 @@ import org.elasticsearch.tasks.Task; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xcontent.XContentParserConfiguration; import org.elasticsearch.xpack.core.security.SecurityContext; -import org.elasticsearch.xpack.core.security.action.apikey.ApiKey; -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.CreateCrossClusterApiKeyRequest; +import org.elasticsearch.xpack.core.security.action.apikey.CrossClusterApiKeyRoleDescriptorBuilder; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.AuthenticationTestHelper; -import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.security.authc.ApiKeyService; -import java.util.List; +import java.io.IOException; import java.util.Set; +import static org.elasticsearch.xcontent.json.JsonXContent.jsonXContent; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.mock; @@ -32,7 +34,7 @@ public class TransportCreateCrossClusterApiKeyActionTests extends ESTestCase { - public void testApiKeyWillBeCreatedWithUserRoleDescriptors() { + public void testApiKeyWillBeCreatedWithEmptyUserRoleDescriptors() throws IOException { final ApiKeyService apiKeyService = mock(ApiKeyService.class); final SecurityContext securityContext = mock(SecurityContext.class); final Authentication authentication = AuthenticationTestHelper.builder().build(); @@ -44,27 +46,20 @@ public void testApiKeyWillBeCreatedWithUserRoleDescriptors() { securityContext ); - final var request = new CreateApiKeyRequest( + final XContentParser parser = jsonXContent.createParser(XContentParserConfiguration.EMPTY, """ + { + "search": [ {"names": ["idx"]} ] + }"""); + + final CreateCrossClusterApiKeyRequest request = new CreateCrossClusterApiKeyRequest( randomAlphaOfLengthBetween(3, 8), - List.of( - new RoleDescriptor( - "cross_cluster", - new String[] { "cross_cluster_search" }, - new RoleDescriptor.IndicesPrivileges[] { - RoleDescriptor.IndicesPrivileges.builder() - .indices("idx") - .privileges("read", "read_cross_cluster", "view_index_metadata") - .build() }, - null - ) - ), + CrossClusterApiKeyRoleDescriptorBuilder.PARSER.parse(parser, null), + null, null ); - request.setType(ApiKey.Type.CROSS_CLUSTER); final PlainActionFuture future = new PlainActionFuture<>(); action.doExecute(mock(Task.class), request, future); - verify(apiKeyService).createApiKey(same(authentication), same(request), eq(Set.of()), same(future)); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java index 18839a15edeac..908bdea189f46 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java @@ -82,6 +82,7 @@ import org.elasticsearch.xpack.core.security.action.ClearSecurityCacheAction; import org.elasticsearch.xpack.core.security.action.ClearSecurityCacheRequest; import org.elasticsearch.xpack.core.security.action.ClearSecurityCacheResponse; +import org.elasticsearch.xpack.core.security.action.apikey.AbstractCreateApiKeyRequest; import org.elasticsearch.xpack.core.security.action.apikey.ApiKey; import org.elasticsearch.xpack.core.security.action.apikey.ApiKeyTests; import org.elasticsearch.xpack.core.security.action.apikey.BulkUpdateApiKeyRequest; @@ -2122,8 +2123,8 @@ public void testBuildDelimitedStringWithLimit() { public void testCreateCrossClusterApiKeyMinVersionConstraint() { final Authentication authentication = AuthenticationTestHelper.builder().build(); - final CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(randomAlphaOfLengthBetween(3, 8), null, null); - createApiKeyRequest.setType(ApiKey.Type.CROSS_CLUSTER); + final AbstractCreateApiKeyRequest request = mock(AbstractCreateApiKeyRequest.class); + when(request.getType()).thenReturn(ApiKey.Type.CROSS_CLUSTER); final ClusterService clusterService = mock(ClusterService.class); when(clusterService.getClusterSettings()).thenReturn( @@ -2149,7 +2150,7 @@ public void testCreateCrossClusterApiKeyMinVersionConstraint() { ); final PlainActionFuture future = new PlainActionFuture<>(); - service.createApiKey(authentication, createApiKeyRequest, Set.of(), future); + service.createApiKey(authentication, request, Set.of(), future); final IllegalArgumentException e = expectThrows(IllegalArgumentException.class, future::actionGet); assertThat( diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/crossclusteraccess/CrossClusterAccessAuthenticationServiceIntegTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/crossclusteraccess/CrossClusterAccessAuthenticationServiceIntegTests.java index c00e1a5d28cd5..b1bcd55302874 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/crossclusteraccess/CrossClusterAccessAuthenticationServiceIntegTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/crossclusteraccess/CrossClusterAccessAuthenticationServiceIntegTests.java @@ -16,8 +16,8 @@ import org.elasticsearch.test.SecurityIntegTestCase; import org.elasticsearch.transport.TcpTransport; import org.elasticsearch.xpack.core.security.SecurityContext; -import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyRequestBuilder; import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyResponse; +import org.elasticsearch.xpack.core.security.action.apikey.CreateRestApiKeyRequestBuilder; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.AuthenticationTestHelper; import org.elasticsearch.xpack.core.security.authc.CrossClusterAccessSubjectInfo; @@ -139,8 +139,9 @@ public void testInvalidHeaders() throws IOException { } private String getEncodedCrossClusterAccessApiKey() { - final CreateApiKeyResponse response = new CreateApiKeyRequestBuilder(client().admin().cluster()).setName("cross_cluster_access_key") - .get(); + final CreateApiKeyResponse response = new CreateRestApiKeyRequestBuilder(client().admin().cluster()).setName( + "cross_cluster_access_key" + ).get(); return ApiKeyService.withApiKeyPrefix( Base64.getEncoder().encodeToString((response.getId() + ":" + response.getKey()).getBytes(StandardCharsets.UTF_8)) ); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyActionTests.java index e2f7b011be90f..d971e06f09481 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateCrossClusterApiKeyActionTests.java @@ -17,8 +17,8 @@ import org.elasticsearch.xcontent.NamedXContentRegistry; import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xpack.core.security.action.apikey.ApiKey; -import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyRequest; import org.elasticsearch.xpack.core.security.action.apikey.CreateCrossClusterApiKeyAction; +import org.elasticsearch.xpack.core.security.action.apikey.CreateCrossClusterApiKeyRequest; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.mockito.ArgumentCaptor; @@ -53,14 +53,16 @@ public void testCreateApiKeyRequestHasTypeOfCrossCluster() throws Exception { final NodeClient client = mock(NodeClient.class); action.handleRequest(restRequest, mock(RestChannel.class), client); - final ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(CreateApiKeyRequest.class); + final ArgumentCaptor requestCaptor = ArgumentCaptor.forClass( + CreateCrossClusterApiKeyRequest.class + ); verify(client).execute(eq(CreateCrossClusterApiKeyAction.INSTANCE), requestCaptor.capture(), any()); - final CreateApiKeyRequest createApiKeyRequest = requestCaptor.getValue(); - assertThat(createApiKeyRequest.getType(), is(ApiKey.Type.CROSS_CLUSTER)); - assertThat(createApiKeyRequest.getName(), equalTo("my-key")); + final CreateCrossClusterApiKeyRequest request = requestCaptor.getValue(); + assertThat(request.getType(), is(ApiKey.Type.CROSS_CLUSTER)); + assertThat(request.getName(), equalTo("my-key")); assertThat( - createApiKeyRequest.getRoleDescriptors(), + request.getRoleDescriptors(), equalTo( List.of( new RoleDescriptor( @@ -76,6 +78,6 @@ public void testCreateApiKeyRequestHasTypeOfCrossCluster() throws Exception { ) ) ); - assertThat(createApiKeyRequest.getMetadata(), nullValue()); + assertThat(request.getMetadata(), nullValue()); } } From 3d6223ea80b11d0914c831ed05c697a1e88a85fa Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Fri, 5 May 2023 15:27:51 +1000 Subject: [PATCH 12/14] tweak --- ...r.java => CreateApiKeyRequestBuilder.java} | 16 +++++----- .../CreateApiKeyRequestBuilderTests.java | 4 +-- .../idp/action/SamlIdentityProviderTests.java | 4 +-- .../security/authc/ApiKeyIntegTests.java | 32 +++++++++---------- .../action/apikey/RestCreateApiKeyAction.java | 4 +-- .../action/apikey/RestGrantApiKeyAction.java | 4 +-- ...AccessAuthenticationServiceIntegTests.java | 7 ++-- 7 files changed, 35 insertions(+), 36 deletions(-) rename x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/{CreateRestApiKeyRequestBuilder.java => CreateApiKeyRequestBuilder.java} (82%) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateRestApiKeyRequestBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequestBuilder.java similarity index 82% rename from x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateRestApiKeyRequestBuilder.java rename to x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequestBuilder.java index 999e406241b73..cf8845ef721f2 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateRestApiKeyRequestBuilder.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequestBuilder.java @@ -30,7 +30,7 @@ /** * Request builder for populating a {@link CreateApiKeyRequest} */ -public final class CreateRestApiKeyRequestBuilder extends ActionRequestBuilder { +public final class CreateApiKeyRequestBuilder extends ActionRequestBuilder { @SuppressWarnings("unchecked") static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( @@ -56,36 +56,36 @@ public final class CreateRestApiKeyRequestBuilder extends ActionRequestBuilder p.map(), new ParseField("metadata")); } - public CreateRestApiKeyRequestBuilder(ElasticsearchClient client) { + public CreateApiKeyRequestBuilder(ElasticsearchClient client) { super(client, CreateApiKeyAction.INSTANCE, new CreateApiKeyRequest()); } - public CreateRestApiKeyRequestBuilder setName(String name) { + public CreateApiKeyRequestBuilder setName(String name) { request.setName(name); return this; } - public CreateRestApiKeyRequestBuilder setExpiration(TimeValue expiration) { + public CreateApiKeyRequestBuilder setExpiration(TimeValue expiration) { request.setExpiration(expiration); return this; } - public CreateRestApiKeyRequestBuilder setRoleDescriptors(List roleDescriptors) { + public CreateApiKeyRequestBuilder setRoleDescriptors(List roleDescriptors) { request.setRoleDescriptors(roleDescriptors); return this; } - public CreateRestApiKeyRequestBuilder setRefreshPolicy(WriteRequest.RefreshPolicy refreshPolicy) { + public CreateApiKeyRequestBuilder setRefreshPolicy(WriteRequest.RefreshPolicy refreshPolicy) { request.setRefreshPolicy(refreshPolicy); return this; } - public CreateRestApiKeyRequestBuilder setMetadata(Map metadata) { + public CreateApiKeyRequestBuilder setMetadata(Map metadata) { request.setMetadata(metadata); return this; } - public CreateRestApiKeyRequestBuilder source(BytesReference source, XContentType xContentType) throws IOException { + public CreateApiKeyRequestBuilder source(BytesReference source, XContentType xContentType) throws IOException { final NamedXContentRegistry registry = NamedXContentRegistry.EMPTY; try ( InputStream stream = source.streamInput(); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequestBuilderTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequestBuilderTests.java index d2051a41b6d00..da0eecbccf48e 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequestBuilderTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequestBuilderTests.java @@ -57,7 +57,7 @@ public void testParserAndCreateApiRequestBuilder() throws IOException { }""", args); final BytesArray source = new BytesArray(json); final NodeClient mockClient = mock(NodeClient.class); - final CreateApiKeyRequest request = new CreateRestApiKeyRequestBuilder(mockClient).source(source, XContentType.JSON).request(); + final CreateApiKeyRequest request = new CreateApiKeyRequestBuilder(mockClient).source(source, XContentType.JSON).request(); final List actualRoleDescriptors = request.getRoleDescriptors(); assertThat(request.getName(), equalTo("my-api-key")); assertThat(actualRoleDescriptors.size(), is(2)); @@ -90,7 +90,7 @@ public void testParserAndCreateApiRequestBuilderWithNullOrEmptyRoleDescriptors() + "}"; final BytesArray source = new BytesArray(json); final NodeClient mockClient = mock(NodeClient.class); - final CreateApiKeyRequest request = new CreateRestApiKeyRequestBuilder(mockClient).source(source, XContentType.JSON).request(); + final CreateApiKeyRequest request = new CreateApiKeyRequestBuilder(mockClient).source(source, XContentType.JSON).request(); final List actualRoleDescriptors = request.getRoleDescriptors(); assertThat(request.getName(), equalTo("my-api-key")); assertThat(actualRoleDescriptors.size(), is(0)); diff --git a/x-pack/plugin/identity-provider/src/internalClusterTest/java/org/elasticsearch/xpack/idp/action/SamlIdentityProviderTests.java b/x-pack/plugin/identity-provider/src/internalClusterTest/java/org/elasticsearch/xpack/idp/action/SamlIdentityProviderTests.java index 36a9ec3b70afe..551b53948c1e5 100644 --- a/x-pack/plugin/identity-provider/src/internalClusterTest/java/org/elasticsearch/xpack/idp/action/SamlIdentityProviderTests.java +++ b/x-pack/plugin/identity-provider/src/internalClusterTest/java/org/elasticsearch/xpack/idp/action/SamlIdentityProviderTests.java @@ -23,8 +23,8 @@ import org.elasticsearch.test.rest.ObjectPath; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentFactory; +import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyRequestBuilder; import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyResponse; -import org.elasticsearch.xpack.core.security.action.apikey.CreateRestApiKeyRequestBuilder; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProviderDocument; import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProviderIndex; @@ -485,7 +485,7 @@ private String getApiKeyFromCredentials(String username, SecureString password) Client client = client().filterWithHeader( Collections.singletonMap("Authorization", UsernamePasswordToken.basicAuthHeaderValue(username, password)) ); - final CreateApiKeyResponse response = new CreateRestApiKeyRequestBuilder(client).setName("test key") + final CreateApiKeyResponse response = new CreateApiKeyRequestBuilder(client).setName("test key") .setExpiration(TimeValue.timeValueHours(TimeUnit.DAYS.toHours(7L))) .get(); assertNotNull(response); diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java index a5d811b423e9f..44c2d9da43b92 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java @@ -59,8 +59,8 @@ import org.elasticsearch.xpack.core.security.action.apikey.BulkUpdateApiKeyAction; import org.elasticsearch.xpack.core.security.action.apikey.BulkUpdateApiKeyRequest; import org.elasticsearch.xpack.core.security.action.apikey.BulkUpdateApiKeyResponse; +import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyRequestBuilder; import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyResponse; -import org.elasticsearch.xpack.core.security.action.apikey.CreateRestApiKeyRequestBuilder; 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; @@ -260,7 +260,7 @@ public void testCreateApiKey() throws Exception { Client client = client().filterWithHeader( Collections.singletonMap("Authorization", basicAuthHeaderValue(ES_TEST_ROOT_USER, TEST_PASSWORD_SECURE_STRING)) ); - final CreateApiKeyResponse response = new CreateRestApiKeyRequestBuilder(client).setName("test key") + final CreateApiKeyResponse response = new CreateApiKeyRequestBuilder(client).setName("test key") .setExpiration(TimeValue.timeValueHours(TimeUnit.DAYS.toHours(7L))) .setRoleDescriptors(Collections.singletonList(descriptor)) .setMetadata(ApiKeyTests.randomMetadata()) @@ -277,7 +277,7 @@ public void testCreateApiKey() throws Exception { assertThat(getApiKeyDocument(response.getId()).get("type"), equalTo("rest")); // create simple api key - final CreateApiKeyResponse simple = new CreateRestApiKeyRequestBuilder(client).setName("simple").get(); + final CreateApiKeyResponse simple = new CreateApiKeyRequestBuilder(client).setName("simple").get(); assertEquals("simple", simple.getName()); assertNotNull(simple.getId()); assertNotNull(simple.getKey()); @@ -315,7 +315,7 @@ public void testMultipleApiKeysCanHaveSameName() { Client client = client().filterWithHeader( Collections.singletonMap("Authorization", basicAuthHeaderValue(ES_TEST_ROOT_USER, TEST_PASSWORD_SECURE_STRING)) ); - final CreateApiKeyResponse response = new CreateRestApiKeyRequestBuilder(client).setName(keyName) + final CreateApiKeyResponse response = new CreateApiKeyRequestBuilder(client).setName(keyName) .setExpiration(null) .setRoleDescriptors(Collections.singletonList(descriptor)) .setMetadata(ApiKeyTests.randomMetadata()) @@ -337,7 +337,7 @@ public void testCreateApiKeyWithoutNameWillFail() { ); final ActionRequestValidationException e = expectThrows( ActionRequestValidationException.class, - () -> new CreateRestApiKeyRequestBuilder(client).get() + () -> new CreateApiKeyRequestBuilder(client).get() ); assertThat(e.getMessage(), containsString("api key name is required")); } @@ -1593,7 +1593,7 @@ public void testDerivedKeys() throws ExecutionException, InterruptedException { Client client = client().filterWithHeader( Collections.singletonMap("Authorization", basicAuthHeaderValue(ES_TEST_ROOT_USER, TEST_PASSWORD_SECURE_STRING)) ); - final CreateApiKeyResponse response = new CreateRestApiKeyRequestBuilder(client).setName("key-1") + final CreateApiKeyResponse response = new CreateApiKeyRequestBuilder(client).setName("key-1") .setRoleDescriptors( Collections.singletonList(new RoleDescriptor("role", new String[] { "manage_api_key", "manage_token" }, null, null)) ) @@ -1623,19 +1623,19 @@ public void testDerivedKeys() throws ExecutionException, InterruptedException { final IllegalArgumentException e1 = expectThrows( IllegalArgumentException.class, - () -> new CreateRestApiKeyRequestBuilder(clientKey1).setName("key-2").setMetadata(ApiKeyTests.randomMetadata()).get() + () -> new CreateApiKeyRequestBuilder(clientKey1).setName("key-2").setMetadata(ApiKeyTests.randomMetadata()).get() ); assertThat(e1.getMessage(), containsString(expectedMessage)); final IllegalArgumentException e2 = expectThrows( IllegalArgumentException.class, - () -> new CreateRestApiKeyRequestBuilder(clientKey1).setName("key-3").setRoleDescriptors(Collections.emptyList()).get() + () -> new CreateApiKeyRequestBuilder(clientKey1).setName("key-3").setRoleDescriptors(Collections.emptyList()).get() ); assertThat(e2.getMessage(), containsString(expectedMessage)); final IllegalArgumentException e3 = expectThrows( IllegalArgumentException.class, - () -> new CreateRestApiKeyRequestBuilder(clientKey1).setName("key-4") + () -> new CreateApiKeyRequestBuilder(clientKey1).setName("key-4") .setMetadata(ApiKeyTests.randomMetadata()) .setRoleDescriptors( Collections.singletonList(new RoleDescriptor("role", new String[] { "manage_own_api_key" }, null, null)) @@ -1652,14 +1652,14 @@ public void testDerivedKeys() throws ExecutionException, InterruptedException { final IllegalArgumentException e4 = expectThrows( IllegalArgumentException.class, - () -> new CreateRestApiKeyRequestBuilder(clientKey1).setName("key-5") + () -> new CreateApiKeyRequestBuilder(clientKey1).setName("key-5") .setMetadata(ApiKeyTests.randomMetadata()) .setRoleDescriptors(roleDescriptors) .get() ); assertThat(e4.getMessage(), containsString(expectedMessage)); - final CreateApiKeyResponse key100Response = new CreateRestApiKeyRequestBuilder(clientKey1).setName("key-100") + final CreateApiKeyResponse key100Response = new CreateApiKeyRequestBuilder(clientKey1).setName("key-100") .setMetadata(ApiKeyTests.randomMetadata()) .setRoleDescriptors(Collections.singletonList(new RoleDescriptor("role", null, null, null))) .get(); @@ -1692,7 +1692,7 @@ public void testApiKeyRunAsAnotherUserCanCreateApiKey() { Client client = client().filterWithHeader( Map.of("Authorization", basicAuthHeaderValue(ES_TEST_ROOT_USER, TEST_PASSWORD_SECURE_STRING)) ); - final CreateApiKeyResponse response1 = new CreateRestApiKeyRequestBuilder(client).setName("run-as-key") + final CreateApiKeyResponse response1 = new CreateApiKeyRequestBuilder(client).setName("run-as-key") .setRoleDescriptors(List.of(descriptor)) .setMetadata(ApiKeyTests.randomMetadata()) .get(); @@ -1700,7 +1700,7 @@ public void testApiKeyRunAsAnotherUserCanCreateApiKey() { final String base64ApiKeyKeyValue = Base64.getEncoder() .encodeToString((response1.getId() + ":" + response1.getKey()).getBytes(StandardCharsets.UTF_8)); - final CreateApiKeyResponse response2 = new CreateRestApiKeyRequestBuilder( + final CreateApiKeyResponse response2 = new CreateApiKeyRequestBuilder( client().filterWithHeader( Map.of("Authorization", "ApiKey " + base64ApiKeyKeyValue, "es-security-runas-user", ES_TEST_ROOT_USER) ) @@ -1728,7 +1728,7 @@ public void testCreationAndAuthenticationReturns429WhenThreadPoolIsSaturated() t final Client client = client().filterWithHeader( Collections.singletonMap("Authorization", basicAuthHeaderValue(ES_TEST_ROOT_USER, TEST_PASSWORD_SECURE_STRING)) ); - final CreateApiKeyResponse createApiKeyResponse = new CreateRestApiKeyRequestBuilder(client).setName("auth only key") + final CreateApiKeyResponse createApiKeyResponse = new CreateApiKeyRequestBuilder(client).setName("auth only key") .setRoleDescriptors(Collections.singletonList(descriptor)) .setMetadata(ApiKeyTests.randomMetadata()) .get(); @@ -2845,7 +2845,7 @@ private Tuple createApiKeyAndAuthenticateWithIt() throws IOExcep Collections.singletonMap("Authorization", basicAuthHeaderValue(ES_TEST_ROOT_USER, TEST_PASSWORD_SECURE_STRING)) ); - final CreateApiKeyResponse createApiKeyResponse = new CreateRestApiKeyRequestBuilder(client).setName("test key") + final CreateApiKeyResponse createApiKeyResponse = new CreateApiKeyRequestBuilder(client).setName("test key") .setMetadata(ApiKeyTests.randomMetadata()) .get(); final String docId = createApiKeyResponse.getId(); @@ -3082,7 +3082,7 @@ private Tuple, List>> createApiKe Client client = client().filterWithHeader(headers); final Map metadata = ApiKeyTests.randomMetadata(); metadatas.add(metadata); - final CreateApiKeyResponse response = new CreateRestApiKeyRequestBuilder(client).setName( + final CreateApiKeyResponse response = new CreateApiKeyRequestBuilder(client).setName( namePrefix + randomAlphaOfLengthBetween(5, 9) + i ) .setExpiration(expiration) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateApiKeyAction.java index 9bc1c4f994017..7d4ad8f2ebdeb 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateApiKeyAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateApiKeyAction.java @@ -16,7 +16,7 @@ import org.elasticsearch.rest.ServerlessScope; import org.elasticsearch.rest.action.RestToXContentListener; import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyRequest; -import org.elasticsearch.xpack.core.security.action.apikey.CreateRestApiKeyRequestBuilder; +import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyRequestBuilder; import java.io.IOException; import java.util.List; @@ -52,7 +52,7 @@ public String getName() { @Override protected RestChannelConsumer innerPrepareRequest(final RestRequest request, final NodeClient client) throws IOException { String refresh = request.param("refresh"); - CreateRestApiKeyRequestBuilder builder = new CreateRestApiKeyRequestBuilder(client).source( + CreateApiKeyRequestBuilder builder = new CreateApiKeyRequestBuilder(client).source( request.requiredContent(), request.getXContentType() ) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGrantApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGrantApiKeyAction.java index fb75a95980be9..1758b9db47201 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGrantApiKeyAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGrantApiKeyAction.java @@ -23,8 +23,8 @@ import org.elasticsearch.xcontent.ObjectParser; import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyRequestBuilder; import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyResponse; -import org.elasticsearch.xpack.core.security.action.apikey.CreateRestApiKeyRequestBuilder; import org.elasticsearch.xpack.core.security.action.apikey.GrantApiKeyAction; import org.elasticsearch.xpack.core.security.action.apikey.GrantApiKeyRequest; @@ -62,7 +62,7 @@ public final class RestGrantApiKeyAction extends ApiKeyBaseRestHandler implement PARSER.declareString((req, str) -> req.getGrant().setRunAsUsername(str), new ParseField("run_as")); PARSER.declareObject( (req, api) -> req.setApiKeyRequest(api), - (parser, ignore) -> CreateRestApiKeyRequestBuilder.parse(parser), + (parser, ignore) -> CreateApiKeyRequestBuilder.parse(parser), new ParseField("api_key") ); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/crossclusteraccess/CrossClusterAccessAuthenticationServiceIntegTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/crossclusteraccess/CrossClusterAccessAuthenticationServiceIntegTests.java index b1bcd55302874..c00e1a5d28cd5 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/crossclusteraccess/CrossClusterAccessAuthenticationServiceIntegTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/crossclusteraccess/CrossClusterAccessAuthenticationServiceIntegTests.java @@ -16,8 +16,8 @@ import org.elasticsearch.test.SecurityIntegTestCase; import org.elasticsearch.transport.TcpTransport; import org.elasticsearch.xpack.core.security.SecurityContext; +import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyRequestBuilder; import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyResponse; -import org.elasticsearch.xpack.core.security.action.apikey.CreateRestApiKeyRequestBuilder; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.AuthenticationTestHelper; import org.elasticsearch.xpack.core.security.authc.CrossClusterAccessSubjectInfo; @@ -139,9 +139,8 @@ public void testInvalidHeaders() throws IOException { } private String getEncodedCrossClusterAccessApiKey() { - final CreateApiKeyResponse response = new CreateRestApiKeyRequestBuilder(client().admin().cluster()).setName( - "cross_cluster_access_key" - ).get(); + final CreateApiKeyResponse response = new CreateApiKeyRequestBuilder(client().admin().cluster()).setName("cross_cluster_access_key") + .get(); return ApiKeyService.withApiKeyPrefix( Base64.getEncoder().encodeToString((response.getId() + ":" + response.getKey()).getBytes(StandardCharsets.UTF_8)) ); From 2dee69d0dd68899e0a90cc589ddce64933cc4e9b Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Fri, 5 May 2023 15:34:04 +1000 Subject: [PATCH 13/14] more tweak --- .../core/security/action/apikey/CreateApiKeyRequestBuilder.java | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequestBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequestBuilder.java index cf8845ef721f2..cd4cea270de6b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequestBuilder.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequestBuilder.java @@ -96,6 +96,7 @@ public CreateApiKeyRequestBuilder source(BytesReference source, XContentType xCo setRoleDescriptors(createApiKeyRequest.getRoleDescriptors()); setExpiration(createApiKeyRequest.getExpiration()); setMetadata(createApiKeyRequest.getMetadata()); + } return this; } From ef4565431638fce947fa43bb41c50136c0f90d2e Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Mon, 8 May 2023 14:11:48 +1000 Subject: [PATCH 14/14] address feedback --- .../apikey/AbstractCreateApiKeyRequest.java | 46 +----- .../action/apikey/CreateApiKeyRequest.java | 54 +++++++ .../CreateCrossClusterApiKeyRequest.java | 52 ++++++- ...ossClusterApiKeyRoleDescriptorBuilder.java | 4 +- .../core/security/authz/RoleDescriptor.java | 2 +- .../CreateCrossClusterApiKeyRequestTests.java | 139 ++++++++++++++++++ .../authc/apikey/ApiKeySingleNodeTests.java | 3 + 7 files changed, 251 insertions(+), 49 deletions(-) create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/CreateCrossClusterApiKeyRequestTests.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/AbstractCreateApiKeyRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/AbstractCreateApiKeyRequest.java index 5309a5f6c0647..38587d67ae708 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/AbstractCreateApiKeyRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/AbstractCreateApiKeyRequest.java @@ -7,16 +7,13 @@ package org.elasticsearch.xpack.core.security.action.apikey; -import org.elasticsearch.TransportVersion; import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.common.Strings; import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.io.stream.StreamInput; -import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.core.TimeValue; -import org.elasticsearch.xpack.core.security.action.role.RoleDescriptorRequestValidator; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.support.MetadataUtils; @@ -44,26 +41,11 @@ public AbstractCreateApiKeyRequest() { public AbstractCreateApiKeyRequest(StreamInput in) throws IOException { super(in); - if (in.getTransportVersion().onOrAfter(TransportVersion.V_7_10_0)) { - this.id = in.readString(); - } else { - this.id = UUIDs.base64UUID(); - } - if (in.getTransportVersion().onOrAfter(TransportVersion.V_7_5_0)) { - this.name = in.readOptionalString(); - } else { - this.name = in.readString(); - } - this.expiration = in.readOptionalTimeValue(); - this.roleDescriptors = in.readImmutableList(RoleDescriptor::new); - this.refreshPolicy = WriteRequest.RefreshPolicy.readFrom(in); - if (in.getTransportVersion().onOrAfter(TransportVersion.V_8_0_0)) { - this.metadata = in.readMap(); - } else { - this.metadata = null; - } + this.id = doReadId(in); } + protected abstract String doReadId(StreamInput in) throws IOException; + public String getId() { return id; } @@ -112,28 +94,6 @@ public ActionRequestValidationException validate() { validationException ); } - for (RoleDescriptor roleDescriptor : getRoleDescriptors()) { - validationException = RoleDescriptorRequestValidator.validate(roleDescriptor, validationException); - } return validationException; } - - @Override - public void writeTo(StreamOutput out) throws IOException { - super.writeTo(out); - if (out.getTransportVersion().onOrAfter(TransportVersion.V_7_10_0)) { - out.writeString(id); - } - if (out.getTransportVersion().onOrAfter(TransportVersion.V_7_5_0)) { - out.writeOptionalString(name); - } else { - out.writeString(name); - } - out.writeOptionalTimeValue(expiration); - out.writeList(getRoleDescriptors()); - refreshPolicy.writeTo(out); - if (out.getTransportVersion().onOrAfter(TransportVersion.V_7_13_0)) { - out.writeGenericMap(metadata); - } - } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequest.java index 2b0640c8f1cab..889257b1b1c0a 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateApiKeyRequest.java @@ -7,10 +7,15 @@ package org.elasticsearch.xpack.core.security.action.apikey; +import org.elasticsearch.TransportVersion; +import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.support.WriteRequest; +import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.core.Nullable; import org.elasticsearch.core.TimeValue; +import org.elasticsearch.xpack.core.security.action.role.RoleDescriptorRequestValidator; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import java.io.IOException; @@ -53,6 +58,28 @@ public CreateApiKeyRequest( public CreateApiKeyRequest(StreamInput in) throws IOException { super(in); + if (in.getTransportVersion().onOrAfter(TransportVersion.V_7_5_0)) { + this.name = in.readOptionalString(); + } else { + this.name = in.readString(); + } + this.expiration = in.readOptionalTimeValue(); + this.roleDescriptors = in.readImmutableList(RoleDescriptor::new); + this.refreshPolicy = WriteRequest.RefreshPolicy.readFrom(in); + if (in.getTransportVersion().onOrAfter(TransportVersion.V_8_0_0)) { + this.metadata = in.readMap(); + } else { + this.metadata = null; + } + } + + @Override + protected String doReadId(StreamInput in) throws IOException { + if (in.getTransportVersion().onOrAfter(TransportVersion.V_7_10_0)) { + return in.readString(); + } else { + return UUIDs.base64UUID(); + } } @Override @@ -84,4 +111,31 @@ public void setMetadata(Map metadata) { this.metadata = metadata; } + @Override + public ActionRequestValidationException validate() { + ActionRequestValidationException validationException = super.validate(); + for (RoleDescriptor roleDescriptor : getRoleDescriptors()) { + validationException = RoleDescriptorRequestValidator.validate(roleDescriptor, validationException); + } + return validationException; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + if (out.getTransportVersion().onOrAfter(TransportVersion.V_7_10_0)) { + out.writeString(id); + } + if (out.getTransportVersion().onOrAfter(TransportVersion.V_7_5_0)) { + out.writeOptionalString(name); + } else { + out.writeString(name); + } + out.writeOptionalTimeValue(expiration); + out.writeList(getRoleDescriptors()); + refreshPolicy.writeTo(out); + if (out.getTransportVersion().onOrAfter(TransportVersion.V_7_13_0)) { + out.writeGenericMap(metadata); + } + } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateCrossClusterApiKeyRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateCrossClusterApiKeyRequest.java index a69a2b92669ad..12be4c833efb0 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateCrossClusterApiKeyRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateCrossClusterApiKeyRequest.java @@ -8,14 +8,19 @@ package org.elasticsearch.xpack.core.security.action.apikey; import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.core.Assertions; import org.elasticsearch.core.Nullable; import org.elasticsearch.core.TimeValue; +import org.elasticsearch.xpack.core.security.action.role.RoleDescriptorRequestValidator; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import java.io.IOException; import java.util.List; import java.util.Map; +import java.util.Objects; public final class CreateCrossClusterApiKeyRequest extends AbstractCreateApiKeyRequest { @@ -26,7 +31,7 @@ public CreateCrossClusterApiKeyRequest( @Nullable Map metadata ) { super(); - this.name = name; + this.name = Objects.requireNonNull(name); this.roleDescriptors = List.of(roleDescriptorBuilder.build()); this.expiration = expiration; this.metadata = metadata; @@ -34,6 +39,16 @@ public CreateCrossClusterApiKeyRequest( public CreateCrossClusterApiKeyRequest(StreamInput in) throws IOException { super(in); + this.name = in.readString(); + this.expiration = in.readOptionalTimeValue(); + this.roleDescriptors = in.readImmutableList(RoleDescriptor::new); + this.refreshPolicy = WriteRequest.RefreshPolicy.readFrom(in); + this.metadata = in.readMap(); + } + + @Override + protected String doReadId(StreamInput in) throws IOException { + return in.readString(); } @Override @@ -44,9 +59,40 @@ public ApiKey.Type getType() { @Override public ActionRequestValidationException validate() { if (Assertions.ENABLED) { - assert getRoleDescriptors().size() == 1; - CrossClusterApiKeyRoleDescriptorBuilder.validate(getRoleDescriptors().iterator().next()); + assert roleDescriptors.size() == 1; + final RoleDescriptor roleDescriptor = roleDescriptors.iterator().next(); + CrossClusterApiKeyRoleDescriptorBuilder.validate(roleDescriptor); + assert RoleDescriptorRequestValidator.validate(roleDescriptor) == null; } return super.validate(); } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(id); + out.writeString(name); + out.writeOptionalTimeValue(expiration); + out.writeList(roleDescriptors); + refreshPolicy.writeTo(out); + out.writeGenericMap(metadata); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CreateCrossClusterApiKeyRequest that = (CreateCrossClusterApiKeyRequest) o; + return Objects.equals(id, that.id) + && Objects.equals(name, that.name) + && Objects.equals(expiration, that.expiration) + && Objects.equals(metadata, that.metadata) + && Objects.equals(roleDescriptors, that.roleDescriptors) + && refreshPolicy == that.refreshPolicy; + } + + @Override + public int hashCode() { + return Objects.hash(id, name, expiration, metadata, roleDescriptors, refreshPolicy); + } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilder.java index da91a6b4714f7..13a5aa141579c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilder.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilder.java @@ -40,12 +40,12 @@ public class CrossClusterApiKeyRoleDescriptorBuilder { static { PARSER.declareObjectArray( optionalConstructorArg(), - (p, c) -> RoleDescriptor.parseIndexWithPrivileges(ROLE_DESCRIPTOR_NAME, CCS_INDICES_PRIVILEGE_NAMES, p), + (p, c) -> RoleDescriptor.parseIndexWithPredefinedPrivileges(ROLE_DESCRIPTOR_NAME, CCS_INDICES_PRIVILEGE_NAMES, p), new ParseField("search") ); PARSER.declareObjectArray( optionalConstructorArg(), - (p, c) -> RoleDescriptor.parseIndexWithPrivileges(ROLE_DESCRIPTOR_NAME, CCR_INDICES_PRIVILEGE_NAMES, p), + (p, c) -> RoleDescriptor.parseIndexWithPredefinedPrivileges(ROLE_DESCRIPTOR_NAME, CCR_INDICES_PRIVILEGE_NAMES, p), new ParseField("replication") ); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java index 34a8dde6945fb..e61f2594ffe00 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java @@ -637,7 +637,7 @@ private static RemoteIndicesPrivileges parseRemoteIndex(String roleName, XConten private record IndicesPrivilegesWithOptionalRemoteClusters(IndicesPrivileges indicesPrivileges, String[] remoteClusters) {} - public static IndicesPrivileges parseIndexWithPrivileges(final String roleName, String[] privileges, XContentParser parser) + public static IndicesPrivileges parseIndexWithPredefinedPrivileges(final String roleName, String[] privileges, XContentParser parser) throws IOException { final IndicesPrivilegesWithOptionalRemoteClusters indicesPrivilegesWithOptionalRemoteClusters = parseIndexWithOptionalRemoteClusters(roleName, parser, false, false, privileges); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/CreateCrossClusterApiKeyRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/CreateCrossClusterApiKeyRequestTests.java new file mode 100644 index 0000000000000..eb2e4b4a8a300 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/CreateCrossClusterApiKeyRequestTests.java @@ -0,0 +1,139 @@ +/* + * 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.common.io.stream.Writeable; +import org.elasticsearch.core.TimeValue; +import org.elasticsearch.core.Tuple; +import org.elasticsearch.test.AbstractWireSerializingTestCase; +import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xcontent.XContentParserConfiguration; +import org.junit.Before; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.List; +import java.util.Map; + +import static org.elasticsearch.xcontent.json.JsonXContent.jsonXContent; + +public class CreateCrossClusterApiKeyRequestTests extends AbstractWireSerializingTestCase { + + private static final List ACCESS_CANDIDATES = List.of(""" + { + "search": [ {"names": ["logs"]} ] + }""", """ + { + "search": [ {"names": ["logs"], "query": "abc" } ] + }""", """ + { + "search": [ {"names": ["logs"], "field_security": {"grant": ["*"], "except": ["private"]} } ] + }""", """ + { + "search": [ {"names": ["logs"], "query": "abc", "field_security": {"grant": ["*"], "except": ["private"]} } ] + }""", """ + { + "replication": [ {"names": ["archive"], "allow_restricted_indices": true } ] + }""", """ + { + "replication": [ {"names": ["archive"]} ] + }""", """ + { + "search": [ {"names": ["logs"]} ], + "replication": [ {"names": ["archive"]} ] + }"""); + + private String access; + private CrossClusterApiKeyRoleDescriptorBuilder roleDescriptorBuilder; + + @Before + public void init() { + access = randomFrom(ACCESS_CANDIDATES); + roleDescriptorBuilder = parseForCrossClusterApiKeyRoleDescriptorBuilder(access); + } + + @Override + protected Writeable.Reader instanceReader() { + return CreateCrossClusterApiKeyRequest::new; + } + + @Override + protected CreateCrossClusterApiKeyRequest createTestInstance() { + return new CreateCrossClusterApiKeyRequest( + randomAlphaOfLengthBetween(3, 8), + roleDescriptorBuilder, + randomExpiration(), + randomMetadata() + ); + } + + @Override + protected CreateCrossClusterApiKeyRequest mutateInstance(CreateCrossClusterApiKeyRequest instance) throws IOException { + switch (randomIntBetween(1, 4)) { + case 1 -> { + return new CreateCrossClusterApiKeyRequest( + randomValueOtherThan(instance.getName(), () -> randomAlphaOfLengthBetween(3, 8)), + roleDescriptorBuilder, + instance.getExpiration(), + instance.getMetadata() + ); + } + case 2 -> { + return new CreateCrossClusterApiKeyRequest( + instance.getName(), + parseForCrossClusterApiKeyRoleDescriptorBuilder(randomValueOtherThan(access, () -> randomFrom(ACCESS_CANDIDATES))), + instance.getExpiration(), + instance.getMetadata() + ); + } + case 3 -> { + return new CreateCrossClusterApiKeyRequest( + instance.getName(), + roleDescriptorBuilder, + randomValueOtherThan(instance.getExpiration(), CreateCrossClusterApiKeyRequestTests::randomExpiration), + instance.getMetadata() + ); + } + default -> { + return new CreateCrossClusterApiKeyRequest( + instance.getName(), + roleDescriptorBuilder, + instance.getExpiration(), + randomValueOtherThan(instance.getMetadata(), CreateCrossClusterApiKeyRequestTests::randomMetadata) + ); + } + } + } + + private CrossClusterApiKeyRoleDescriptorBuilder parseForCrossClusterApiKeyRoleDescriptorBuilder(String access) { + try { + final XContentParser parser = jsonXContent.createParser(XContentParserConfiguration.EMPTY, access); + return CrossClusterApiKeyRoleDescriptorBuilder.PARSER.parse(parser, null); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private static TimeValue randomExpiration() { + return randomFrom(TimeValue.timeValueHours(randomIntBetween(1, 999)), null); + } + + private static Map randomMetadata() { + return randomFrom( + randomMap( + 0, + 3, + () -> new Tuple<>( + randomAlphaOfLengthBetween(3, 8), + randomFrom(randomAlphaOfLengthBetween(3, 8), randomInt(), randomBoolean()) + ) + ), + null + ); + } +} diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java index 1e030ff4a3e72..f8a7225dfb4a4 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java @@ -31,6 +31,7 @@ import org.elasticsearch.test.SecuritySingleNodeTestCase; import org.elasticsearch.test.TestSecurityClient; import org.elasticsearch.test.XContentTestUtils; +import org.elasticsearch.transport.TcpTransport; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xcontent.XContentParserConfiguration; import org.elasticsearch.xcontent.XContentType; @@ -420,6 +421,8 @@ public void testInvalidateApiKeyWillRecordTimestamp() { } public void testCreateCrossClusterApiKey() throws IOException { + assumeTrue("untrusted remote cluster feature flag must be enabled", TcpTransport.isUntrustedRemoteClusterEnabled()); + final XContentParser parser = jsonXContent.createParser(XContentParserConfiguration.EMPTY, """ { "search": [ {"names": ["logs"]} ]