Skip to content

Commit ce18c5b

Browse files
gmjehovichelasticsearchmachine
andauthored
Adds certificate identity field to cross-cluster API keys (#134604)
* Add integration testing for CrossCluster API Key certificate_identity field * [CI] Auto commit changes from spotless * Update docs/changelog/134604.yaml * Add 'certificate_identity' field to API Key, modify create/update CrossClusterAPIKey methods to account for new field * fix merge conflicts * [CI] Auto commit changes from spotless * Fix constructors, spotlessApply * Fix createCrossClusterApiKey unit test * [CI] Auto commit changes from spotless * Fix updateCrossClusterApiKeyRequestTest * [CI] Auto commit changes from spotless * Fix CC API Key Update Message and Test Assertion # Conflicts: # x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/UpdateCrossClusterApiKeyRequestTests.java * spotlessApply * [CI] Auto commit changes from spotless * Fix ApiKeyBackwardsCompatibilityIT, fix update error message check * Fix ApiKeyBackwardsCompatibilityIT.testCertificateIdentityBackwardsCompatibility() * [CI] Update transport version definitions * Remove authenticateWithApiKey from ApiKeyBackwardsCompatibilityIT.testCertificateIdentityBackwardsCompatibility UPGRADED Case * Add minimum version check to ApiKeyBackwardsCompatibilityIT.testCertificateIdentityBackwardsCompatibility() * [CI] Update transport version definitions * Add validation for certificate_identity to update path * Fix validation logic, add capability to delete certificate_identity from API key * Fix NPE * [CI] Update transport version definitions * Fix testing bugs that resulted from new CertificateIdentity record * Fix ApiKeyIntegTest assertion bug * Remove certificate_identity field from UpdateApiKeyRequestTranslator, UpdateApiKeyRequest class. Add testing * Clean up certificate_identity testing in ApiKeyServiceTests * Delete redundant integ tests * Consolidate redundant code in ApiKeyBackwardsCompatibilityIT * Change cert_identity validation failure message * Update cert_identity version to 9.3.0 in APIKey BWC Test * [CI] Auto commit changes from spotless * CertID parser no longer differentiates between explicit vs implicit null * [CI] Auto commit changes from spotless * Move shared bwc test logic to AbstractUpgradeTestCase * [CI] Auto commit changes from spotless * Rename cleanUp method in TokenBackwwardsCompatbilityIT --------- Co-authored-by: elasticsearchmachine <[email protected]>
1 parent 0c2c9ce commit ce18c5b

File tree

44 files changed

+1349
-195
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1349
-195
lines changed

docs/changelog/134604.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 134604
2+
summary: Adds certificate identity field to cross-cluster API keys
3+
area: Security
4+
type: enhancement
5+
issues: []

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

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ public VersionId<?> versionNumber() {
121121
private final List<RoleDescriptor> roleDescriptors;
122122
@Nullable
123123
private final RoleDescriptorsIntersection limitedBy;
124+
@Nullable
125+
private final String certificateIdentity;
124126

125127
public ApiKey(
126128
String name,
@@ -135,7 +137,8 @@ public ApiKey(
135137
@Nullable String realmType,
136138
@Nullable Map<String, Object> metadata,
137139
@Nullable List<RoleDescriptor> roleDescriptors,
138-
@Nullable List<RoleDescriptor> limitedByRoleDescriptors
140+
@Nullable List<RoleDescriptor> limitedByRoleDescriptors,
141+
@Nullable String certificateIdentity
139142
) {
140143
this(
141144
name,
@@ -150,7 +153,8 @@ public ApiKey(
150153
realmType,
151154
metadata,
152155
roleDescriptors,
153-
limitedByRoleDescriptors == null ? null : new RoleDescriptorsIntersection(List.of(Set.copyOf(limitedByRoleDescriptors)))
156+
limitedByRoleDescriptors == null ? null : new RoleDescriptorsIntersection(List.of(Set.copyOf(limitedByRoleDescriptors))),
157+
certificateIdentity
154158
);
155159
}
156160

@@ -167,7 +171,8 @@ private ApiKey(
167171
@Nullable String realmType,
168172
@Nullable Map<String, Object> metadata,
169173
@Nullable List<RoleDescriptor> roleDescriptors,
170-
@Nullable RoleDescriptorsIntersection limitedBy
174+
@Nullable RoleDescriptorsIntersection limitedBy,
175+
@Nullable String certificateIdentity
171176
) {
172177
this.name = name;
173178
this.id = id;
@@ -187,6 +192,7 @@ private ApiKey(
187192
// This assertion will need to be changed (or removed) when derived keys are properly supported
188193
assert limitedBy == null || limitedBy.roleDescriptorsList().size() == 1 : "can only have one set of limited-by role descriptors";
189194
this.limitedBy = limitedBy;
195+
this.certificateIdentity = certificateIdentity;
190196
}
191197

192198
// Should only be used by XContent parsers
@@ -205,7 +211,8 @@ private ApiKey(
205211
(String) parsed[9],
206212
(parsed[10] == null) ? null : (Map<String, Object>) parsed[10],
207213
(List<RoleDescriptor>) parsed[11],
208-
(RoleDescriptorsIntersection) parsed[12]
214+
(RoleDescriptorsIntersection) parsed[12],
215+
(String) parsed[13]
209216
);
210217
}
211218

@@ -268,6 +275,10 @@ public RoleDescriptorsIntersection getLimitedBy() {
268275
return limitedBy;
269276
}
270277

278+
public @Nullable String getCertificateIdentity() {
279+
return certificateIdentity;
280+
}
281+
271282
@Override
272283
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
273284
builder.startObject();
@@ -306,6 +317,11 @@ public XContentBuilder innerToXContent(XContentBuilder builder, Params params) t
306317
assert type != Type.CROSS_CLUSTER;
307318
builder.field("limited_by", limitedBy);
308319
}
320+
321+
if (certificateIdentity != null) {
322+
builder.field("certificate_identity", certificateIdentity);
323+
}
324+
309325
return builder;
310326
}
311327

@@ -357,7 +373,8 @@ public int hashCode() {
357373
realmType,
358374
metadata,
359375
roleDescriptors,
360-
limitedBy
376+
limitedBy,
377+
certificateIdentity
361378
);
362379
}
363380

@@ -385,7 +402,9 @@ public boolean equals(Object obj) {
385402
&& Objects.equals(realmType, other.realmType)
386403
&& Objects.equals(metadata, other.metadata)
387404
&& Objects.equals(roleDescriptors, other.roleDescriptors)
388-
&& Objects.equals(limitedBy, other.limitedBy);
405+
&& Objects.equals(limitedBy, other.limitedBy)
406+
&& Objects.equals(certificateIdentity, other.certificateIdentity);
407+
389408
}
390409

391410
@Override
@@ -416,6 +435,8 @@ public String toString() {
416435
+ roleDescriptors
417436
+ ", limited_by="
418437
+ limitedBy
438+
+ ", certificate_identity="
439+
+ certificateIdentity
419440
+ "]";
420441
}
421442

@@ -452,6 +473,8 @@ static int initializeParser(AbstractObjectParser<?, Void> parser) {
452473
new ParseField("limited_by"),
453474
ObjectParser.ValueType.OBJECT_ARRAY
454475
);
455-
return 13; // the number of fields to parse
476+
parser.declareStringOrNull(optionalConstructorArg(), new ParseField("certificate_identity"));
477+
478+
return 14; // the number of fields to parse
456479
}
457480
}

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

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ public BaseBulkUpdateApiKeyRequest(
2626
final List<String> ids,
2727
@Nullable final List<RoleDescriptor> roleDescriptors,
2828
@Nullable final Map<String, Object> metadata,
29-
@Nullable final TimeValue expiration
29+
@Nullable final TimeValue expiration,
30+
@Nullable final CertificateIdentity certificateIdentity
3031
) {
31-
super(roleDescriptors, metadata, expiration);
32+
super(roleDescriptors, metadata, expiration, certificateIdentity);
3233
this.ids = Objects.requireNonNull(ids, "API key IDs must not be null");
3334
}
3435

@@ -38,6 +39,16 @@ public ActionRequestValidationException validate() {
3839
if (ids.isEmpty()) {
3940
validationException = addValidationError("Field [ids] cannot be empty", validationException);
4041
}
42+
43+
if (getCertificateIdentity() != null && ids.size() > 1) {
44+
validationException = addValidationError(
45+
"Certificate identity can only be updated for a single API key at a time. Found ["
46+
+ ids.size()
47+
+ "] API key IDs in the request.",
48+
validationException
49+
);
50+
}
51+
4152
return validationException;
4253
}
4354

@@ -54,11 +65,12 @@ public boolean equals(Object o) {
5465
return Objects.equals(getIds(), that.getIds())
5566
&& Objects.equals(metadata, that.metadata)
5667
&& Objects.equals(expiration, that.expiration)
57-
&& Objects.equals(roleDescriptors, that.roleDescriptors);
68+
&& Objects.equals(roleDescriptors, that.roleDescriptors)
69+
&& Objects.equals(certificateIdentity, that.certificateIdentity);
5870
}
5971

6072
@Override
6173
public int hashCode() {
62-
return Objects.hash(getIds(), expiration, metadata, roleDescriptors);
74+
return Objects.hash(getIds(), expiration, metadata, roleDescriptors, certificateIdentity);
6375
}
6476
}

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ public BaseSingleUpdateApiKeyRequest(
2323
@Nullable final List<RoleDescriptor> roleDescriptors,
2424
@Nullable final Map<String, Object> metadata,
2525
@Nullable final TimeValue expiration,
26-
String id
26+
String id,
27+
@Nullable CertificateIdentity certificateIdentity
2728
) {
28-
super(roleDescriptors, metadata, expiration);
29+
super(roleDescriptors, metadata, expiration, certificateIdentity);
2930
this.id = Objects.requireNonNull(id, "API key ID must not be null");
3031
}
3132

@@ -42,11 +43,12 @@ public boolean equals(Object o) {
4243
return Objects.equals(getId(), that.getId())
4344
&& Objects.equals(metadata, that.metadata)
4445
&& Objects.equals(expiration, that.expiration)
45-
&& Objects.equals(roleDescriptors, that.roleDescriptors);
46+
&& Objects.equals(roleDescriptors, that.roleDescriptors)
47+
&& Objects.equals(certificateIdentity, that.certificateIdentity);
4648
}
4749

4850
@Override
4951
public int hashCode() {
50-
return Objects.hash(getId(), expiration, metadata, roleDescriptors);
52+
return Objects.hash(getId(), expiration, metadata, roleDescriptors, certificateIdentity);
5153
}
5254
}

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,19 @@ public abstract class BaseUpdateApiKeyRequest extends LegacyActionRequest {
3131
protected final Map<String, Object> metadata;
3232
@Nullable
3333
protected final TimeValue expiration;
34+
@Nullable
35+
protected final CertificateIdentity certificateIdentity;
3436

3537
public BaseUpdateApiKeyRequest(
3638
@Nullable final List<RoleDescriptor> roleDescriptors,
3739
@Nullable final Map<String, Object> metadata,
38-
@Nullable final TimeValue expiration
40+
@Nullable final TimeValue expiration,
41+
@Nullable final CertificateIdentity certificateIdentity
3942
) {
4043
this.roleDescriptors = roleDescriptors;
4144
this.metadata = metadata;
4245
this.expiration = expiration;
46+
this.certificateIdentity = certificateIdentity;
4347
}
4448

4549
public Map<String, Object> getMetadata() {
@@ -54,6 +58,10 @@ public TimeValue getExpiration() {
5458
return expiration;
5559
}
5660

61+
public CertificateIdentity getCertificateIdentity() {
62+
return certificateIdentity;
63+
}
64+
5765
public abstract ApiKey.Type getType();
5866

5967
@Override

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import java.util.List;
1616
import java.util.Map;
1717

18-
public final class BulkUpdateApiKeyRequest extends BaseBulkUpdateApiKeyRequest {
18+
public class BulkUpdateApiKeyRequest extends BaseBulkUpdateApiKeyRequest {
1919

2020
public static BulkUpdateApiKeyRequest usingApiKeyIds(String... ids) {
2121
return new BulkUpdateApiKeyRequest(Arrays.stream(ids).toList(), null, null, null);
@@ -36,7 +36,7 @@ public BulkUpdateApiKeyRequest(
3636
@Nullable final Map<String, Object> metadata,
3737
@Nullable final TimeValue expiration
3838
) {
39-
super(ids, roleDescriptors, metadata, expiration);
39+
super(ids, roleDescriptors, metadata, expiration, null);
4040
}
4141

4242
@Override

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import org.elasticsearch.core.TimeValue;
1212
import org.elasticsearch.rest.RestRequest;
1313
import org.elasticsearch.xcontent.ConstructingObjectParser;
14+
import org.elasticsearch.xcontent.ObjectParser;
1415
import org.elasticsearch.xcontent.ParseField;
1516
import org.elasticsearch.xcontent.XContentParser;
1617
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
@@ -51,6 +52,14 @@ protected static ConstructingObjectParser<BulkUpdateApiKeyRequest, Void> createP
5152
}, new ParseField("role_descriptors"));
5253
parser.declareObject(optionalConstructorArg(), (p, c) -> p.map(), new ParseField("metadata"));
5354
parser.declareString(optionalConstructorArg(), new ParseField("expiration"));
55+
parser.declareField(
56+
optionalConstructorArg(),
57+
(p) -> p.currentToken() == XContentParser.Token.VALUE_NULL
58+
? new CertificateIdentity(null)
59+
: new CertificateIdentity(p.text()),
60+
new ParseField("certificate_identity"),
61+
ObjectParser.ValueType.STRING_OR_NULL
62+
);
5463
return parser;
5564
}
5665

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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.core.Nullable;
11+
12+
import java.util.regex.Pattern;
13+
import java.util.regex.PatternSyntaxException;
14+
15+
public record CertificateIdentity(@Nullable String value) {
16+
17+
public CertificateIdentity {
18+
if (value != null) {
19+
try {
20+
Pattern.compile(value);
21+
} catch (PatternSyntaxException e) {
22+
throw new IllegalArgumentException("Invalid certificate_identity format: [" + value + "]. Must be a valid regex.", e);
23+
}
24+
}
25+
}
26+
}

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,21 @@
2121

2222
public final class CreateCrossClusterApiKeyRequest extends AbstractCreateApiKeyRequest {
2323

24+
private final CertificateIdentity certificateIdentity;
25+
2426
public CreateCrossClusterApiKeyRequest(
2527
String name,
2628
CrossClusterApiKeyRoleDescriptorBuilder roleDescriptorBuilder,
2729
@Nullable TimeValue expiration,
28-
@Nullable Map<String, Object> metadata
30+
@Nullable Map<String, Object> metadata,
31+
@Nullable CertificateIdentity certificateIdentity
2932
) {
3033
super();
3134
this.name = Objects.requireNonNull(name);
3235
this.roleDescriptors = List.of(roleDescriptorBuilder.build());
3336
this.expiration = expiration;
3437
this.metadata = metadata;
38+
this.certificateIdentity = certificateIdentity;
3539
}
3640

3741
@Override
@@ -60,15 +64,21 @@ public boolean equals(Object o) {
6064
&& Objects.equals(expiration, that.expiration)
6165
&& Objects.equals(metadata, that.metadata)
6266
&& Objects.equals(roleDescriptors, that.roleDescriptors)
63-
&& refreshPolicy == that.refreshPolicy;
67+
&& refreshPolicy == that.refreshPolicy
68+
&& Objects.equals(certificateIdentity, that.certificateIdentity);
6469
}
6570

6671
@Override
6772
public int hashCode() {
68-
return Objects.hash(id, name, expiration, metadata, roleDescriptors, refreshPolicy);
73+
return Objects.hash(id, name, expiration, metadata, roleDescriptors, refreshPolicy, certificateIdentity);
6974
}
7075

7176
public static CreateCrossClusterApiKeyRequest withNameAndAccess(String name, String access) throws IOException {
72-
return new CreateCrossClusterApiKeyRequest(name, CrossClusterApiKeyRoleDescriptorBuilder.parse(access), null, null);
77+
return new CreateCrossClusterApiKeyRequest(name, CrossClusterApiKeyRoleDescriptorBuilder.parse(access), null, null, null);
78+
}
79+
80+
public CertificateIdentity getCertificateIdentity() {
81+
return certificateIdentity;
7382
}
83+
7484
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ public String toString() {
126126

127127
static final ConstructingObjectParser<GetApiKeyResponse, Void> RESPONSE_PARSER;
128128
static {
129-
int nFieldsForParsingApiKeyInfo = 13; // this must be changed whenever ApiKey#initializeParser is changed for the number of parsers
129+
int nFieldsForParsingApiKeyInfo = 14; // this must be changed whenever ApiKey#initializeParser is changed for the number of parsers
130130
ConstructingObjectParser<Item, Void> keyInfoParser = new ConstructingObjectParser<>(
131131
"api_key_with_profile_uid",
132132
true,

0 commit comments

Comments
 (0)