Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
c919d6c
Add integration testing for CrossCluster API Key certificate_identity…
gmjehovich Sep 18, 2025
7458dd3
[CI] Auto commit changes from spotless
Sep 18, 2025
f181837
Update docs/changelog/134604.yaml
gmjehovich Sep 18, 2025
0a639c6
Add 'certificate_identity' field to API Key, modify create/update Cro…
gmjehovich Sep 12, 2025
3a25dfa
fix merge conflicts
gmjehovich Sep 18, 2025
2d32743
[CI] Auto commit changes from spotless
Sep 18, 2025
cf45055
Merge branch 'main' into rcs_improvements
gmjehovich Sep 18, 2025
0f04676
Fix constructors, spotlessApply
gmjehovich Sep 18, 2025
ef5a9e5
Fix createCrossClusterApiKey unit test
gmjehovich Sep 19, 2025
8883e07
[CI] Auto commit changes from spotless
Sep 19, 2025
539302b
Fix updateCrossClusterApiKeyRequestTest
gmjehovich Sep 19, 2025
13a9a69
[CI] Auto commit changes from spotless
Sep 19, 2025
fa1dff4
Merge branch 'main' into rcs_improvements
gmjehovich Sep 19, 2025
2018d3a
Fix CC API Key Update Message and Test Assertion
gmjehovich Sep 19, 2025
092a670
spotlessApply
gmjehovich Sep 19, 2025
4e5b362
[CI] Auto commit changes from spotless
Sep 19, 2025
d8b64d2
Fix ApiKeyBackwardsCompatibilityIT, fix update error message check
gmjehovich Sep 19, 2025
43fbfb9
Fix ApiKeyBackwardsCompatibilityIT.testCertificateIdentityBackwardsCo…
gmjehovich Sep 20, 2025
baeb233
[CI] Update transport version definitions
Sep 20, 2025
755647c
Remove authenticateWithApiKey from ApiKeyBackwardsCompatibilityIT.tes…
gmjehovich Sep 22, 2025
540a69c
Add minimum version check to ApiKeyBackwardsCompatibilityIT.testCerti…
gmjehovich Sep 22, 2025
101aca6
[CI] Update transport version definitions
Sep 22, 2025
a427d52
Merge branch 'main' into rcs_improvements
gmjehovich Sep 22, 2025
0d38963
Add validation for certificate_identity to update path
gmjehovich Sep 23, 2025
bb773b5
Merge branch 'main' into rcs_improvements
gmjehovich Sep 23, 2025
414f279
Fix validation logic, add capability to delete certificate_identity f…
gmjehovich Sep 24, 2025
18f37d5
Fix NPE
gmjehovich Sep 24, 2025
c413dc3
[CI] Update transport version definitions
Sep 24, 2025
b156e8f
Fix testing bugs that resulted from new CertificateIdentity record
gmjehovich Sep 24, 2025
e3bc11b
Merge branch 'main' into rcs_improvements
gmjehovich Sep 24, 2025
d9286fe
Fix ApiKeyIntegTest assertion bug
gmjehovich Sep 24, 2025
da9d23d
Merge branch 'main' into rcs_improvements
gmjehovich Sep 24, 2025
7395b7f
Remove certificate_identity field from UpdateApiKeyRequestTranslator,…
gmjehovich Sep 26, 2025
8dd9e75
Merge branch 'main' into rcs_improvements
gmjehovich Sep 26, 2025
2808fe4
Clean up certificate_identity testing in ApiKeyServiceTests
gmjehovich Sep 26, 2025
4ef30ed
Delete redundant integ tests
gmjehovich Sep 26, 2025
6de1798
Consolidate redundant code in ApiKeyBackwardsCompatibilityIT
gmjehovich Sep 26, 2025
05ffd83
Merge branch 'main' into rcs_improvements
gmjehovich Oct 2, 2025
95233d1
Merge branch 'main' into rcs_improvements
gmjehovich Oct 2, 2025
01f4c89
Merge branch 'main' into rcs_improvements
gmjehovich Oct 2, 2025
af01316
Change cert_identity validation failure message
gmjehovich Oct 3, 2025
9706f85
Update cert_identity version to 9.3.0 in APIKey BWC Test
gmjehovich Oct 3, 2025
7049572
[CI] Auto commit changes from spotless
Oct 3, 2025
e0562b9
CertID parser no longer differentiates between explicit vs implicit null
gmjehovich Oct 3, 2025
2e440b5
[CI] Auto commit changes from spotless
Oct 3, 2025
98c4896
Move shared bwc test logic to AbstractUpgradeTestCase
gmjehovich Oct 3, 2025
e8af6c3
[CI] Auto commit changes from spotless
Oct 3, 2025
13e010a
Rename cleanUp method in TokenBackwwardsCompatbilityIT
gmjehovich Oct 3, 2025
9c008eb
Merge branch 'main' into rcs_improvements
gmjehovich Oct 3, 2025
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/changelog/134604.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 134604
summary: Adds certificate identity field to cross-cluster API keys
area: Security
type: enhancement
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ public VersionId<?> versionNumber() {
private final List<RoleDescriptor> roleDescriptors;
@Nullable
private final RoleDescriptorsIntersection limitedBy;
@Nullable
private final String certificateIdentity;

public ApiKey(
String name,
Expand All @@ -135,7 +137,8 @@ public ApiKey(
@Nullable String realmType,
@Nullable Map<String, Object> metadata,
@Nullable List<RoleDescriptor> roleDescriptors,
@Nullable List<RoleDescriptor> limitedByRoleDescriptors
@Nullable List<RoleDescriptor> limitedByRoleDescriptors,
@Nullable String certificateIdentity
) {
this(
name,
Expand All @@ -150,7 +153,8 @@ public ApiKey(
realmType,
metadata,
roleDescriptors,
limitedByRoleDescriptors == null ? null : new RoleDescriptorsIntersection(List.of(Set.copyOf(limitedByRoleDescriptors)))
limitedByRoleDescriptors == null ? null : new RoleDescriptorsIntersection(List.of(Set.copyOf(limitedByRoleDescriptors))),
certificateIdentity
);
}

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

// Should only be used by XContent parsers
Expand All @@ -205,7 +211,8 @@ private ApiKey(
(String) parsed[9],
(parsed[10] == null) ? null : (Map<String, Object>) parsed[10],
(List<RoleDescriptor>) parsed[11],
(RoleDescriptorsIntersection) parsed[12]
(RoleDescriptorsIntersection) parsed[12],
(String) parsed[13]
);
}

Expand Down Expand Up @@ -268,6 +275,10 @@ public RoleDescriptorsIntersection getLimitedBy() {
return limitedBy;
}

public @Nullable String getCertificateIdentity() {
return certificateIdentity;
}

@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
Expand Down Expand Up @@ -306,6 +317,11 @@ public XContentBuilder innerToXContent(XContentBuilder builder, Params params) t
assert type != Type.CROSS_CLUSTER;
builder.field("limited_by", limitedBy);
}

if (certificateIdentity != null) {
builder.field("certificate_identity", certificateIdentity);
}

return builder;
}

Expand Down Expand Up @@ -357,7 +373,8 @@ public int hashCode() {
realmType,
metadata,
roleDescriptors,
limitedBy
limitedBy,
certificateIdentity
);
}

Expand Down Expand Up @@ -385,7 +402,9 @@ public boolean equals(Object obj) {
&& Objects.equals(realmType, other.realmType)
&& Objects.equals(metadata, other.metadata)
&& Objects.equals(roleDescriptors, other.roleDescriptors)
&& Objects.equals(limitedBy, other.limitedBy);
&& Objects.equals(limitedBy, other.limitedBy)
&& Objects.equals(certificateIdentity, other.certificateIdentity);

}

@Override
Expand Down Expand Up @@ -416,6 +435,8 @@ public String toString() {
+ roleDescriptors
+ ", limited_by="
+ limitedBy
+ ", certificate_identity="
+ certificateIdentity
+ "]";
}

Expand Down Expand Up @@ -452,6 +473,8 @@ static int initializeParser(AbstractObjectParser<?, Void> parser) {
new ParseField("limited_by"),
ObjectParser.ValueType.OBJECT_ARRAY
);
return 13; // the number of fields to parse
parser.declareStringOrNull(optionalConstructorArg(), new ParseField("certificate_identity"));

return 14; // the number of fields to parse
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ public BaseBulkUpdateApiKeyRequest(
final List<String> ids,
@Nullable final List<RoleDescriptor> roleDescriptors,
@Nullable final Map<String, Object> metadata,
@Nullable final TimeValue expiration
@Nullable final TimeValue expiration,
@Nullable final CertificateIdentity certificateIdentity
) {
super(roleDescriptors, metadata, expiration);
super(roleDescriptors, metadata, expiration, certificateIdentity);
this.ids = Objects.requireNonNull(ids, "API key IDs must not be null");
}

Expand All @@ -38,6 +39,16 @@ public ActionRequestValidationException validate() {
if (ids.isEmpty()) {
validationException = addValidationError("Field [ids] cannot be empty", validationException);
}

if (getCertificateIdentity() != null && ids.size() > 1) {
validationException = addValidationError(
"Certificate identity can only be updated for a single API key at a time. Found ["
+ ids.size()
+ "] API key IDs in the request.",
validationException
);
}

return validationException;
}

Expand All @@ -54,11 +65,12 @@ public boolean equals(Object o) {
return Objects.equals(getIds(), that.getIds())
&& Objects.equals(metadata, that.metadata)
&& Objects.equals(expiration, that.expiration)
&& Objects.equals(roleDescriptors, that.roleDescriptors);
&& Objects.equals(roleDescriptors, that.roleDescriptors)
&& Objects.equals(certificateIdentity, that.certificateIdentity);
}

@Override
public int hashCode() {
return Objects.hash(getIds(), expiration, metadata, roleDescriptors);
return Objects.hash(getIds(), expiration, metadata, roleDescriptors, certificateIdentity);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ public BaseSingleUpdateApiKeyRequest(
@Nullable final List<RoleDescriptor> roleDescriptors,
@Nullable final Map<String, Object> metadata,
@Nullable final TimeValue expiration,
String id
String id,
@Nullable CertificateIdentity certificateIdentity
) {
super(roleDescriptors, metadata, expiration);
super(roleDescriptors, metadata, expiration, certificateIdentity);
this.id = Objects.requireNonNull(id, "API key ID must not be null");
}

Expand All @@ -42,11 +43,12 @@ public boolean equals(Object o) {
return Objects.equals(getId(), that.getId())
&& Objects.equals(metadata, that.metadata)
&& Objects.equals(expiration, that.expiration)
&& Objects.equals(roleDescriptors, that.roleDescriptors);
&& Objects.equals(roleDescriptors, that.roleDescriptors)
&& Objects.equals(certificateIdentity, that.certificateIdentity);
}

@Override
public int hashCode() {
return Objects.hash(getId(), expiration, metadata, roleDescriptors);
return Objects.hash(getId(), expiration, metadata, roleDescriptors, certificateIdentity);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,19 @@ public abstract class BaseUpdateApiKeyRequest extends LegacyActionRequest {
protected final Map<String, Object> metadata;
@Nullable
protected final TimeValue expiration;
@Nullable
protected final CertificateIdentity certificateIdentity;

public BaseUpdateApiKeyRequest(
@Nullable final List<RoleDescriptor> roleDescriptors,
@Nullable final Map<String, Object> metadata,
@Nullable final TimeValue expiration
@Nullable final TimeValue expiration,
@Nullable final CertificateIdentity certificateIdentity
) {
this.roleDescriptors = roleDescriptors;
this.metadata = metadata;
this.expiration = expiration;
this.certificateIdentity = certificateIdentity;
}

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

public CertificateIdentity getCertificateIdentity() {
return certificateIdentity;
}

public abstract ApiKey.Type getType();

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import java.util.List;
import java.util.Map;

public final class BulkUpdateApiKeyRequest extends BaseBulkUpdateApiKeyRequest {
public class BulkUpdateApiKeyRequest extends BaseBulkUpdateApiKeyRequest {

public static BulkUpdateApiKeyRequest usingApiKeyIds(String... ids) {
return new BulkUpdateApiKeyRequest(Arrays.stream(ids).toList(), null, null, null);
Expand All @@ -36,7 +36,7 @@ public BulkUpdateApiKeyRequest(
@Nullable final Map<String, Object> metadata,
@Nullable final TimeValue expiration
) {
super(ids, roleDescriptors, metadata, expiration);
super(ids, roleDescriptors, metadata, expiration, null);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.xcontent.ConstructingObjectParser;
import org.elasticsearch.xcontent.ObjectParser;
import org.elasticsearch.xcontent.ParseField;
import org.elasticsearch.xcontent.XContentParser;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
Expand Down Expand Up @@ -51,6 +52,14 @@ protected static ConstructingObjectParser<BulkUpdateApiKeyRequest, Void> createP
}, new ParseField("role_descriptors"));
parser.declareObject(optionalConstructorArg(), (p, c) -> p.map(), new ParseField("metadata"));
parser.declareString(optionalConstructorArg(), new ParseField("expiration"));
parser.declareField(
optionalConstructorArg(),
(p) -> p.currentToken() == XContentParser.Token.VALUE_NULL
? new CertificateIdentity(null)
: new CertificateIdentity(p.text()),
new ParseField("certificate_identity"),
ObjectParser.ValueType.STRING_OR_NULL
);
return parser;
}

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

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

import org.elasticsearch.core.Nullable;

import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

public record CertificateIdentity(@Nullable String value) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This turned out nice! 👍


public CertificateIdentity {
if (value != null) {
try {
Pattern.compile(value);
} catch (PatternSyntaxException e) {
throw new IllegalArgumentException("Invalid certificate_identity format: [" + value + "]. Must be a valid regex.", e);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,21 @@

public final class CreateCrossClusterApiKeyRequest extends AbstractCreateApiKeyRequest {

private final CertificateIdentity certificateIdentity;

public CreateCrossClusterApiKeyRequest(
String name,
CrossClusterApiKeyRoleDescriptorBuilder roleDescriptorBuilder,
@Nullable TimeValue expiration,
@Nullable Map<String, Object> metadata
@Nullable Map<String, Object> metadata,
@Nullable CertificateIdentity certificateIdentity
) {
super();
this.name = Objects.requireNonNull(name);
this.roleDescriptors = List.of(roleDescriptorBuilder.build());
this.expiration = expiration;
this.metadata = metadata;
this.certificateIdentity = certificateIdentity;
}

@Override
Expand Down Expand Up @@ -60,15 +64,21 @@ public boolean equals(Object o) {
&& Objects.equals(expiration, that.expiration)
&& Objects.equals(metadata, that.metadata)
&& Objects.equals(roleDescriptors, that.roleDescriptors)
&& refreshPolicy == that.refreshPolicy;
&& refreshPolicy == that.refreshPolicy
&& Objects.equals(certificateIdentity, that.certificateIdentity);
}

@Override
public int hashCode() {
return Objects.hash(id, name, expiration, metadata, roleDescriptors, refreshPolicy);
return Objects.hash(id, name, expiration, metadata, roleDescriptors, refreshPolicy, certificateIdentity);
}

public static CreateCrossClusterApiKeyRequest withNameAndAccess(String name, String access) throws IOException {
return new CreateCrossClusterApiKeyRequest(name, CrossClusterApiKeyRoleDescriptorBuilder.parse(access), null, null);
return new CreateCrossClusterApiKeyRequest(name, CrossClusterApiKeyRoleDescriptorBuilder.parse(access), null, null, null);
}

public CertificateIdentity getCertificateIdentity() {
return certificateIdentity;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ public String toString() {

static final ConstructingObjectParser<GetApiKeyResponse, Void> RESPONSE_PARSER;
static {
int nFieldsForParsingApiKeyInfo = 13; // this must be changed whenever ApiKey#initializeParser is changed for the number of parsers
int nFieldsForParsingApiKeyInfo = 14; // this must be changed whenever ApiKey#initializeParser is changed for the number of parsers
ConstructingObjectParser<Item, Void> keyInfoParser = new ConstructingObjectParser<>(
"api_key_with_profile_uid",
true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public UpdateApiKeyRequest(
@Nullable final Map<String, Object> metadata,
@Nullable final TimeValue expiration
) {
super(roleDescriptors, metadata, expiration, id);
super(roleDescriptors, metadata, expiration, id, null);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ public UpdateCrossClusterApiKeyRequest(
final String id,
@Nullable CrossClusterApiKeyRoleDescriptorBuilder roleDescriptorBuilder,
@Nullable final Map<String, Object> metadata,
@Nullable TimeValue expiration
@Nullable TimeValue expiration,
@Nullable CertificateIdentity certificateIdentity
) {
super(roleDescriptorBuilder == null ? null : List.of(roleDescriptorBuilder.build()), metadata, expiration, id);
super(roleDescriptorBuilder == null ? null : List.of(roleDescriptorBuilder.build()), metadata, expiration, id, certificateIdentity);
}

@Override
Expand All @@ -35,9 +36,9 @@ public ApiKey.Type getType() {
@Override
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = super.validate();
if (roleDescriptors == null && metadata == null) {
if (roleDescriptors == null && metadata == null && certificateIdentity == null) {
validationException = addValidationError(
"must update either [access] or [metadata] for cross-cluster API keys",
"must update [access], [metadata], or [certificate_identity] for cross-cluster API keys",
validationException
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,8 @@ public static ApiKey randomApiKeyInstance() {
realmType,
metadata,
roleDescriptors,
limitedByRoleDescriptors
limitedByRoleDescriptors,
null
);
}
}
Loading