Skip to content

Commit 471b3f5

Browse files
authored
Add a new API to update RCS specific API keys (#96085)
This PR adds a new endpoint to update RCS specific API keys. It works similarly to the existing UpdateApiKey API except: * It requires manage_security permission * It does not permit empty request body because it does not use limited-by model * It takes the simplified "access" payload as the CreateCrossClusterApiKey API Relates: #95714
1 parent 301ebc5 commit 471b3f5

File tree

28 files changed

+1475
-219
lines changed

28 files changed

+1475
-219
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.core.security.action.apikey;
9+
10+
import org.elasticsearch.action.ActionRequestValidationException;
11+
import org.elasticsearch.common.io.stream.StreamInput;
12+
import org.elasticsearch.common.io.stream.StreamOutput;
13+
import org.elasticsearch.core.Nullable;
14+
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
15+
16+
import java.io.IOException;
17+
import java.util.List;
18+
import java.util.Map;
19+
import java.util.Objects;
20+
21+
import static org.elasticsearch.action.ValidateActions.addValidationError;
22+
23+
public abstract class BaseBulkUpdateApiKeyRequest extends BaseUpdateApiKeyRequest {
24+
25+
private final List<String> ids;
26+
27+
public BaseBulkUpdateApiKeyRequest(
28+
final List<String> ids,
29+
@Nullable final List<RoleDescriptor> roleDescriptors,
30+
@Nullable final Map<String, Object> metadata
31+
) {
32+
super(roleDescriptors, metadata);
33+
this.ids = Objects.requireNonNull(ids, "API key IDs must not be null");
34+
}
35+
36+
public BaseBulkUpdateApiKeyRequest(StreamInput in) throws IOException {
37+
super(in);
38+
this.ids = in.readStringList();
39+
}
40+
41+
@Override
42+
public ActionRequestValidationException validate() {
43+
ActionRequestValidationException validationException = super.validate();
44+
if (ids.isEmpty()) {
45+
validationException = addValidationError("Field [ids] cannot be empty", validationException);
46+
}
47+
return validationException;
48+
}
49+
50+
@Override
51+
public void writeTo(StreamOutput out) throws IOException {
52+
super.writeTo(out);
53+
out.writeStringCollection(ids);
54+
}
55+
56+
public List<String> getIds() {
57+
return ids;
58+
}
59+
}

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/BaseUpdateApiKeyRequest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ public List<RoleDescriptor> getRoleDescriptors() {
4848
return roleDescriptors;
4949
}
5050

51+
public abstract ApiKey.Type getType();
52+
5153
@Override
5254
public ActionRequestValidationException validate() {
5355
ActionRequestValidationException validationException = null;

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/BulkUpdateApiKeyRequest.java

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,16 @@
77

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

10-
import org.elasticsearch.action.ActionRequestValidationException;
1110
import org.elasticsearch.common.io.stream.StreamInput;
12-
import org.elasticsearch.common.io.stream.StreamOutput;
1311
import org.elasticsearch.core.Nullable;
1412
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
1513

1614
import java.io.IOException;
1715
import java.util.Arrays;
1816
import java.util.List;
1917
import java.util.Map;
20-
import java.util.Objects;
2118

22-
import static org.elasticsearch.action.ValidateActions.addValidationError;
23-
24-
public final class BulkUpdateApiKeyRequest extends BaseUpdateApiKeyRequest {
19+
public final class BulkUpdateApiKeyRequest extends BaseBulkUpdateApiKeyRequest {
2520

2621
public static BulkUpdateApiKeyRequest usingApiKeyIds(String... ids) {
2722
return new BulkUpdateApiKeyRequest(Arrays.stream(ids).toList(), null, null);
@@ -31,38 +26,20 @@ public static BulkUpdateApiKeyRequest wrap(final UpdateApiKeyRequest request) {
3126
return new BulkUpdateApiKeyRequest(List.of(request.getId()), request.getRoleDescriptors(), request.getMetadata());
3227
}
3328

34-
private final List<String> ids;
35-
3629
public BulkUpdateApiKeyRequest(
3730
final List<String> ids,
3831
@Nullable final List<RoleDescriptor> roleDescriptors,
3932
@Nullable final Map<String, Object> metadata
4033
) {
41-
super(roleDescriptors, metadata);
42-
this.ids = Objects.requireNonNull(ids, "API key IDs must not be null");
34+
super(ids, roleDescriptors, metadata);
4335
}
4436

4537
public BulkUpdateApiKeyRequest(StreamInput in) throws IOException {
4638
super(in);
47-
this.ids = in.readStringList();
4839
}
4940

5041
@Override
51-
public ActionRequestValidationException validate() {
52-
ActionRequestValidationException validationException = super.validate();
53-
if (ids.isEmpty()) {
54-
validationException = addValidationError("Field [ids] cannot be empty", validationException);
55-
}
56-
return validationException;
57-
}
58-
59-
@Override
60-
public void writeTo(StreamOutput out) throws IOException {
61-
super.writeTo(out);
62-
out.writeStringCollection(ids);
63-
}
64-
65-
public List<String> getIds() {
66-
return ids;
42+
public ApiKey.Type getType() {
43+
return ApiKey.Type.REST;
6744
}
6845
}

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CreateCrossClusterApiKeyRequest.java

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414
import org.elasticsearch.core.Assertions;
1515
import org.elasticsearch.core.Nullable;
1616
import org.elasticsearch.core.TimeValue;
17-
import org.elasticsearch.xcontent.XContentParserConfiguration;
18-
import org.elasticsearch.xcontent.json.JsonXContent;
1917
import org.elasticsearch.xpack.core.security.action.role.RoleDescriptorRequestValidator;
2018
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
2119

@@ -99,14 +97,6 @@ public int hashCode() {
9997
}
10098

10199
public static CreateCrossClusterApiKeyRequest withNameAndAccess(String name, String access) throws IOException {
102-
return new CreateCrossClusterApiKeyRequest(
103-
name,
104-
CrossClusterApiKeyRoleDescriptorBuilder.PARSER.parse(
105-
JsonXContent.jsonXContent.createParser(XContentParserConfiguration.EMPTY, access),
106-
null
107-
),
108-
null,
109-
null
110-
);
100+
return new CreateCrossClusterApiKeyRequest(name, CrossClusterApiKeyRoleDescriptorBuilder.parse(access), null, null);
111101
}
112102
}

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/CrossClusterApiKeyRoleDescriptorBuilder.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@
1111
import org.elasticsearch.common.util.CollectionUtils;
1212
import org.elasticsearch.xcontent.ConstructingObjectParser;
1313
import org.elasticsearch.xcontent.ParseField;
14+
import org.elasticsearch.xcontent.XContentParserConfiguration;
15+
import org.elasticsearch.xcontent.json.JsonXContent;
1416
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
1517

18+
import java.io.IOException;
1619
import java.util.Arrays;
1720
import java.util.List;
1821

@@ -87,6 +90,13 @@ public RoleDescriptor build() {
8790
);
8891
}
8992

93+
public static CrossClusterApiKeyRoleDescriptorBuilder parse(String access) throws IOException {
94+
return CrossClusterApiKeyRoleDescriptorBuilder.PARSER.parse(
95+
JsonXContent.jsonXContent.createParser(XContentParserConfiguration.EMPTY, access),
96+
null
97+
);
98+
}
99+
90100
static void validate(RoleDescriptor roleDescriptor) {
91101
if (false == ROLE_DESCRIPTOR_NAME.equals(roleDescriptor.getName())) {
92102
throw new IllegalArgumentException("invalid role descriptor name [" + roleDescriptor.getName() + "]");

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/UpdateApiKeyRequest.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,9 @@ public void writeTo(StreamOutput out) throws IOException {
4747
public String getId() {
4848
return id;
4949
}
50+
51+
@Override
52+
public ApiKey.Type getType() {
53+
return ApiKey.Type.REST;
54+
}
5055
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.core.security.action.apikey;
9+
10+
import org.elasticsearch.action.ActionType;
11+
12+
public final class UpdateCrossClusterApiKeyAction extends ActionType<UpdateApiKeyResponse> {
13+
14+
public static final String NAME = "cluster:admin/xpack/security/cross_cluster/api_key/update";
15+
public static final UpdateCrossClusterApiKeyAction INSTANCE = new UpdateCrossClusterApiKeyAction();
16+
17+
private UpdateCrossClusterApiKeyAction() {
18+
super(NAME, UpdateApiKeyResponse::new);
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.core.security.action.apikey;
9+
10+
import org.elasticsearch.action.ActionRequestValidationException;
11+
import org.elasticsearch.common.io.stream.StreamInput;
12+
import org.elasticsearch.common.io.stream.StreamOutput;
13+
import org.elasticsearch.core.Nullable;
14+
15+
import java.io.IOException;
16+
import java.util.List;
17+
import java.util.Map;
18+
import java.util.Objects;
19+
20+
import static org.elasticsearch.action.ValidateActions.addValidationError;
21+
22+
public final class UpdateCrossClusterApiKeyRequest extends BaseUpdateApiKeyRequest {
23+
private final String id;
24+
25+
public UpdateCrossClusterApiKeyRequest(
26+
final String id,
27+
@Nullable CrossClusterApiKeyRoleDescriptorBuilder roleDescriptorBuilder,
28+
@Nullable final Map<String, Object> metadata
29+
) {
30+
super(roleDescriptorBuilder == null ? null : List.of(roleDescriptorBuilder.build()), metadata);
31+
this.id = Objects.requireNonNull(id, "API key ID must not be null");
32+
}
33+
34+
public UpdateCrossClusterApiKeyRequest(StreamInput in) throws IOException {
35+
super(in);
36+
this.id = in.readString();
37+
}
38+
39+
@Override
40+
public void writeTo(StreamOutput out) throws IOException {
41+
super.writeTo(out);
42+
out.writeString(id);
43+
}
44+
45+
public String getId() {
46+
return id;
47+
}
48+
49+
@Override
50+
public ApiKey.Type getType() {
51+
return ApiKey.Type.CROSS_CLUSTER;
52+
}
53+
54+
@Override
55+
public ActionRequestValidationException validate() {
56+
ActionRequestValidationException validationException = super.validate();
57+
if (roleDescriptors == null && metadata == null) {
58+
validationException = addValidationError(
59+
"must update either [access] or [metadata] for cross-cluster API keys",
60+
validationException
61+
);
62+
}
63+
return validationException;
64+
}
65+
}

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/CreateCrossClusterApiKeyRequestTests.java

Lines changed: 34 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -11,50 +11,21 @@
1111
import org.elasticsearch.core.TimeValue;
1212
import org.elasticsearch.core.Tuple;
1313
import org.elasticsearch.test.AbstractWireSerializingTestCase;
14-
import org.elasticsearch.xcontent.XContentParser;
15-
import org.elasticsearch.xcontent.XContentParserConfiguration;
1614
import org.junit.Before;
1715

1816
import java.io.IOException;
19-
import java.io.UncheckedIOException;
2017
import java.util.List;
2118
import java.util.Map;
2219

23-
import static org.elasticsearch.xcontent.json.JsonXContent.jsonXContent;
24-
2520
public class CreateCrossClusterApiKeyRequestTests extends AbstractWireSerializingTestCase<CreateCrossClusterApiKeyRequest> {
2621

27-
private static final List<String> ACCESS_CANDIDATES = List.of("""
28-
{
29-
"search": [ {"names": ["logs"]} ]
30-
}""", """
31-
{
32-
"search": [ {"names": ["logs"], "query": "abc" } ]
33-
}""", """
34-
{
35-
"search": [ {"names": ["logs"], "field_security": {"grant": ["*"], "except": ["private"]} } ]
36-
}""", """
37-
{
38-
"search": [ {"names": ["logs"], "query": "abc", "field_security": {"grant": ["*"], "except": ["private"]} } ]
39-
}""", """
40-
{
41-
"replication": [ {"names": ["archive"], "allow_restricted_indices": true } ]
42-
}""", """
43-
{
44-
"replication": [ {"names": ["archive"]} ]
45-
}""", """
46-
{
47-
"search": [ {"names": ["logs"]} ],
48-
"replication": [ {"names": ["archive"]} ]
49-
}""");
50-
5122
private String access;
5223
private CrossClusterApiKeyRoleDescriptorBuilder roleDescriptorBuilder;
5324

5425
@Before
55-
public void init() {
56-
access = randomFrom(ACCESS_CANDIDATES);
57-
roleDescriptorBuilder = parseForCrossClusterApiKeyRoleDescriptorBuilder(access);
26+
public void init() throws IOException {
27+
access = randomCrossClusterApiKeyAccessField();
28+
roleDescriptorBuilder = CrossClusterApiKeyRoleDescriptorBuilder.parse(access);
5829
}
5930

6031
@Override
@@ -86,7 +57,9 @@ protected CreateCrossClusterApiKeyRequest mutateInstance(CreateCrossClusterApiKe
8657
case 2 -> {
8758
return new CreateCrossClusterApiKeyRequest(
8859
instance.getName(),
89-
parseForCrossClusterApiKeyRoleDescriptorBuilder(randomValueOtherThan(access, () -> randomFrom(ACCESS_CANDIDATES))),
60+
CrossClusterApiKeyRoleDescriptorBuilder.parse(
61+
randomValueOtherThan(access, CreateCrossClusterApiKeyRequestTests::randomCrossClusterApiKeyAccessField)
62+
),
9063
instance.getExpiration(),
9164
instance.getMetadata()
9265
);
@@ -110,15 +83,6 @@ protected CreateCrossClusterApiKeyRequest mutateInstance(CreateCrossClusterApiKe
11083
}
11184
}
11285

113-
private CrossClusterApiKeyRoleDescriptorBuilder parseForCrossClusterApiKeyRoleDescriptorBuilder(String access) {
114-
try {
115-
final XContentParser parser = jsonXContent.createParser(XContentParserConfiguration.EMPTY, access);
116-
return CrossClusterApiKeyRoleDescriptorBuilder.PARSER.parse(parser, null);
117-
} catch (IOException e) {
118-
throw new UncheckedIOException(e);
119-
}
120-
}
121-
12286
private static TimeValue randomExpiration() {
12387
return randomFrom(TimeValue.timeValueHours(randomIntBetween(1, 999)), null);
12488
}
@@ -136,4 +100,32 @@ private static Map<String, Object> randomMetadata() {
136100
null
137101
);
138102
}
103+
104+
private static final List<String> ACCESS_CANDIDATES = List.of("""
105+
{
106+
"search": [ {"names": ["logs"]} ]
107+
}""", """
108+
{
109+
"search": [ {"names": ["logs"], "query": "abc" } ]
110+
}""", """
111+
{
112+
"search": [ {"names": ["logs"], "field_security": {"grant": ["*"], "except": ["private"]} } ]
113+
}""", """
114+
{
115+
"search": [ {"names": ["logs"], "query": "abc", "field_security": {"grant": ["*"], "except": ["private"]} } ]
116+
}""", """
117+
{
118+
"replication": [ {"names": ["archive"], "allow_restricted_indices": true } ]
119+
}""", """
120+
{
121+
"replication": [ {"names": ["archive"]} ]
122+
}""", """
123+
{
124+
"search": [ {"names": ["logs"]} ],
125+
"replication": [ {"names": ["archive"]} ]
126+
}""");
127+
128+
public static String randomCrossClusterApiKeyAccessField() {
129+
return randomFrom(ACCESS_CANDIDATES);
130+
}
139131
}

0 commit comments

Comments
 (0)