Skip to content

Commit 4ac25f3

Browse files
authored
Remove uniqueness constraint for API key name and make it optional (elastic#47549)
Since we cannot guarantee the uniqueness of the API key `name` this commit removes the constraint and makes this field optional. Closes elastic#46646
1 parent 1fc7a69 commit 4ac25f3

File tree

11 files changed

+60
-113
lines changed

11 files changed

+60
-113
lines changed

client/rest-high-level/src/main/java/org/elasticsearch/client/security/CreateApiKeyRequest.java

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import org.elasticsearch.client.Validatable;
2323
import org.elasticsearch.client.security.user.privileges.Role;
2424
import org.elasticsearch.common.Nullable;
25-
import org.elasticsearch.common.Strings;
2625
import org.elasticsearch.common.unit.TimeValue;
2726
import org.elasticsearch.common.xcontent.ToXContentObject;
2827
import org.elasticsearch.common.xcontent.XContentBuilder;
@@ -47,12 +46,9 @@ public final class CreateApiKeyRequest implements Validatable, ToXContentObject
4746
* @param roles list of {@link Role}s
4847
* @param expiration to specify expiration for the API key
4948
*/
50-
public CreateApiKeyRequest(String name, List<Role> roles, @Nullable TimeValue expiration, @Nullable final RefreshPolicy refreshPolicy) {
51-
if (Strings.hasText(name)) {
52-
this.name = name;
53-
} else {
54-
throw new IllegalArgumentException("name must not be null or empty");
55-
}
49+
public CreateApiKeyRequest(@Nullable String name, List<Role> roles, @Nullable TimeValue expiration,
50+
@Nullable final RefreshPolicy refreshPolicy) {
51+
this.name = name;
5652
this.roles = Objects.requireNonNull(roles, "roles may not be null");
5753
this.expiration = expiration;
5854
this.refreshPolicy = (refreshPolicy == null) ? RefreshPolicy.getDefault() : refreshPolicy;

client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/ApiKey.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import org.elasticsearch.common.ParseField;
2323
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
24+
import org.elasticsearch.common.xcontent.ObjectParser;
2425
import org.elasticsearch.common.xcontent.XContentParser;
2526

2627
import java.io.IOException;
@@ -131,7 +132,8 @@ public boolean equals(Object obj) {
131132
(args[3] == null) ? null : Instant.ofEpochMilli((Long) args[3]), (Boolean) args[4], (String) args[5], (String) args[6]);
132133
});
133134
static {
134-
PARSER.declareString(constructorArg(), new ParseField("name"));
135+
PARSER.declareField(optionalConstructorArg(), (p, c) -> p.textOrNull(), new ParseField("name"),
136+
ObjectParser.ValueType.STRING_OR_NULL);
135137
PARSER.declareString(constructorArg(), new ParseField("id"));
136138
PARSER.declareLong(constructorArg(), new ParseField("creation"));
137139
PARSER.declareLong(optionalConstructorArg(), new ParseField("expiration"));

client/rest-high-level/src/test/java/org/elasticsearch/client/security/GetApiKeyResponseTests.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ public void testFromXContent() throws IOException {
4040
"user-a", "realm-x");
4141
ApiKey apiKeyInfo2 = createApiKeyInfo("name2", "id-2", Instant.ofEpochMilli(100000L), Instant.ofEpochMilli(10000000L), true,
4242
"user-b", "realm-y");
43-
GetApiKeyResponse response = new GetApiKeyResponse(Arrays.asList(apiKeyInfo1, apiKeyInfo2));
43+
ApiKey apiKeyInfo3 = createApiKeyInfo(null, "id-3", Instant.ofEpochMilli(100000L), null, true,
44+
"user-c", "realm-z");
45+
GetApiKeyResponse response = new GetApiKeyResponse(Arrays.asList(apiKeyInfo1, apiKeyInfo2, apiKeyInfo3));
4446
final XContentType xContentType = randomFrom(XContentType.values());
4547
final XContentBuilder builder = XContentFactory.contentBuilder(xContentType);
4648
toXContent(response, builder);

docs/java-rest/high-level/security/create-api-key.asciidoc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ API Key can be created using this API.
1212
[id="{upid}-{api}-request"]
1313
==== Create API Key Request
1414

15-
A +{request}+ contains name for the API key,
16-
list of role descriptors to define permissions and
15+
A +{request}+ contains an optional name for the API key,
16+
an optional list of role descriptors to define permissions and
1717
optional expiration for the generated API key.
1818
If expiration is not provided then by default the API
1919
keys do not expire.
@@ -37,4 +37,4 @@ expiration.
3737
include-tagged::{doc-tests-file}[{api}-response]
3838
--------------------------------------------------
3939
<1> the API key that can be used to authenticate to Elasticsearch.
40-
<2> expiration if the API keys expire
40+
<2> expiration if the API keys expire

x-pack/docs/en/rest-api/security/create-api-keys.asciidoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ service.
4444
The following parameters can be specified in the body of a POST or PUT request:
4545

4646
`name`::
47-
(string) Specifies the name for this API key.
47+
(Optional, string) Specifies the name for this API key.
4848

4949
`role_descriptors`::
5050
(Optional, array-of-role-descriptor) An array of role descriptors for this API

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
package org.elasticsearch.xpack.core.security.action;
88

9+
import org.elasticsearch.Version;
910
import org.elasticsearch.common.ParseField;
1011
import org.elasticsearch.common.io.stream.StreamInput;
1112
import org.elasticsearch.common.io.stream.StreamOutput;
@@ -49,7 +50,11 @@ public ApiKey(String name, String id, Instant creation, Instant expiration, bool
4950
}
5051

5152
public ApiKey(StreamInput in) throws IOException {
52-
this.name = in.readString();
53+
if (in.getVersion().onOrAfter(Version.V_7_5_0)) {
54+
this.name = in.readOptionalString();
55+
} else {
56+
this.name = in.readString();
57+
}
5358
this.id = in.readString();
5459
this.creation = in.readInstant();
5560
this.expiration = in.readOptionalInstant();
@@ -103,7 +108,11 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
103108

104109
@Override
105110
public void writeTo(StreamOutput out) throws IOException {
106-
out.writeString(name);
111+
if (out.getVersion().onOrAfter(Version.V_7_5_0)) {
112+
out.writeOptionalString(name);
113+
} else {
114+
out.writeString(name);
115+
}
107116
out.writeString(id);
108117
out.writeInstant(creation);
109118
out.writeOptionalInstant(expiration);

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

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66

77
package org.elasticsearch.xpack.core.security.action;
88

9+
import org.elasticsearch.Version;
910
import org.elasticsearch.action.ActionRequest;
1011
import org.elasticsearch.action.ActionRequestValidationException;
1112
import org.elasticsearch.action.support.WriteRequest;
1213
import org.elasticsearch.common.Nullable;
13-
import org.elasticsearch.common.Strings;
1414
import org.elasticsearch.common.io.stream.StreamInput;
1515
import org.elasticsearch.common.io.stream.StreamOutput;
1616
import org.elasticsearch.common.unit.TimeValue;
@@ -43,19 +43,19 @@ public CreateApiKeyRequest() {}
4343
* @param roleDescriptors list of {@link RoleDescriptor}s
4444
* @param expiration to specify expiration for the API key
4545
*/
46-
public CreateApiKeyRequest(String name, @Nullable List<RoleDescriptor> roleDescriptors, @Nullable TimeValue expiration) {
47-
if (Strings.hasText(name)) {
48-
this.name = name;
49-
} else {
50-
throw new IllegalArgumentException("name must not be null or empty");
51-
}
46+
public CreateApiKeyRequest(@Nullable String name, @Nullable List<RoleDescriptor> roleDescriptors, @Nullable TimeValue expiration) {
47+
this.name = name;
5248
this.roleDescriptors = (roleDescriptors == null) ? List.of() : List.copyOf(roleDescriptors);
5349
this.expiration = expiration;
5450
}
5551

5652
public CreateApiKeyRequest(StreamInput in) throws IOException {
5753
super(in);
58-
this.name = in.readString();
54+
if (in.getVersion().onOrAfter(Version.V_7_5_0)) {
55+
this.name = in.readOptionalString();
56+
} else {
57+
this.name = in.readString();
58+
}
5959
this.expiration = in.readOptionalTimeValue();
6060
this.roleDescriptors = List.copyOf(in.readList(RoleDescriptor::new));
6161
this.refreshPolicy = WriteRequest.RefreshPolicy.readFrom(in);
@@ -65,12 +65,8 @@ public String getName() {
6565
return name;
6666
}
6767

68-
public void setName(String name) {
69-
if (Strings.hasText(name)) {
70-
this.name = name;
71-
} else {
72-
throw new IllegalArgumentException("name must not be null or empty");
73-
}
68+
public void setName(@Nullable String name) {
69+
this.name = name;
7470
}
7571

7672
public TimeValue getExpiration() {
@@ -100,9 +96,7 @@ public void setRefreshPolicy(WriteRequest.RefreshPolicy refreshPolicy) {
10096
@Override
10197
public ActionRequestValidationException validate() {
10298
ActionRequestValidationException validationException = null;
103-
if (Strings.isNullOrEmpty(name)) {
104-
validationException = addValidationError("name is required", validationException);
105-
} else {
99+
if (name != null) {
106100
if (name.length() > 256) {
107101
validationException = addValidationError("name may not be more than 256 characters long", validationException);
108102
}
@@ -119,7 +113,11 @@ public ActionRequestValidationException validate() {
119113
@Override
120114
public void writeTo(StreamOutput out) throws IOException {
121115
super.writeTo(out);
122-
out.writeString(name);
116+
if (out.getVersion().onOrAfter(Version.V_7_5_0)) {
117+
out.writeOptionalString(name);
118+
} else {
119+
out.writeString(name);
120+
}
123121
out.writeOptionalTimeValue(expiration);
124122
out.writeList(roleDescriptors);
125123
refreshPolicy.writeTo(out);

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

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,12 @@ public void testNameValidation() {
2828
CreateApiKeyRequest request = new CreateApiKeyRequest();
2929

3030
ActionRequestValidationException ve = request.validate();
31-
assertNotNull(ve);
32-
assertThat(ve.validationErrors().size(), is(1));
33-
assertThat(ve.validationErrors().get(0), containsString("name is required"));
31+
assertNull(ve);
3432

3533
request.setName(name);
3634
ve = request.validate();
3735
assertNull(ve);
3836

39-
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> request.setName(""));
40-
assertThat(e.getMessage(), containsString("name must not be null or empty"));
41-
42-
e = expectThrows(IllegalArgumentException.class, () -> request.setName(null));
43-
assertThat(e.getMessage(), containsString("name must not be null or empty"));
44-
4537
request.setName(randomAlphaOfLength(257));
4638
ve = request.validate();
4739
assertNotNull(ve);

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@
2424
public class GetApiKeyResponseTests extends ESTestCase {
2525

2626
public void testSerialization() throws IOException {
27+
boolean withApiKeyName = randomBoolean();
2728
boolean withExpiration = randomBoolean();
28-
ApiKey apiKeyInfo = createApiKeyInfo(randomAlphaOfLength(4), randomAlphaOfLength(5), Instant.now(),
29+
ApiKey apiKeyInfo = createApiKeyInfo((withApiKeyName) ? randomAlphaOfLength(4) : null, randomAlphaOfLength(5), Instant.now(),
2930
(withExpiration) ? Instant.now() : null, false, randomAlphaOfLength(4), randomAlphaOfLength(5));
3031
GetApiKeyResponse response = new GetApiKeyResponse(Collections.singletonList(apiKeyInfo));
3132
try (BytesStreamOutput output = new BytesStreamOutput()) {
@@ -42,7 +43,9 @@ public void testToXContent() throws IOException {
4243
"user-a", "realm-x");
4344
ApiKey apiKeyInfo2 = createApiKeyInfo("name2", "id-2", Instant.ofEpochMilli(100000L), Instant.ofEpochMilli(10000000L), true,
4445
"user-b", "realm-y");
45-
GetApiKeyResponse response = new GetApiKeyResponse(Arrays.asList(apiKeyInfo1, apiKeyInfo2));
46+
ApiKey apiKeyInfo3 = createApiKeyInfo(null, "id-3", Instant.ofEpochMilli(100000L), null, true,
47+
"user-c", "realm-z");
48+
GetApiKeyResponse response = new GetApiKeyResponse(Arrays.asList(apiKeyInfo1, apiKeyInfo2, apiKeyInfo3));
4649
XContentBuilder builder = XContentFactory.jsonBuilder();
4750
response.toXContent(builder, ToXContent.EMPTY_PARAMS);
4851
assertThat(Strings.toString(builder), equalTo(
@@ -51,7 +54,9 @@ public void testToXContent() throws IOException {
5154
+ "{\"id\":\"id-1\",\"name\":\"name1\",\"creation\":100000,\"expiration\":10000000,\"invalidated\":false,"
5255
+ "\"username\":\"user-a\",\"realm\":\"realm-x\"},"
5356
+ "{\"id\":\"id-2\",\"name\":\"name2\",\"creation\":100000,\"expiration\":10000000,\"invalidated\":true,"
54-
+ "\"username\":\"user-b\",\"realm\":\"realm-y\"}"
57+
+ "\"username\":\"user-b\",\"realm\":\"realm-y\"},"
58+
+ "{\"id\":\"id-3\",\"name\":null,\"creation\":100000,\"invalidated\":true,"
59+
+ "\"username\":\"user-c\",\"realm\":\"realm-z\"}"
5560
+ "]"
5661
+ "}"));
5762
}

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java

Lines changed: 1 addition & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import org.elasticsearch.action.get.GetResponse;
2323
import org.elasticsearch.action.index.IndexAction;
2424
import org.elasticsearch.action.index.IndexRequest;
25-
import org.elasticsearch.action.search.SearchAction;
2625
import org.elasticsearch.action.search.SearchRequest;
2726
import org.elasticsearch.action.support.WriteRequest.RefreshPolicy;
2827
import org.elasticsearch.action.update.UpdateRequest;
@@ -193,46 +192,10 @@ public void createApiKey(Authentication authentication, CreateApiKeyRequest requ
193192
if (authentication == null) {
194193
listener.onFailure(new IllegalArgumentException("authentication must be provided"));
195194
} else {
196-
/*
197-
* Check if requested API key name already exists to avoid duplicate key names,
198-
* this check is best effort as there could be two nodes executing search and
199-
* then index concurrently allowing a duplicate name.
200-
*/
201-
checkDuplicateApiKeyNameAndCreateApiKey(authentication, request, userRoles, listener);
195+
createApiKeyAndIndexIt(authentication, request, userRoles, listener);
202196
}
203197
}
204198

205-
private void checkDuplicateApiKeyNameAndCreateApiKey(Authentication authentication, CreateApiKeyRequest request,
206-
Set<RoleDescriptor> userRoles,
207-
ActionListener<CreateApiKeyResponse> listener) {
208-
final BoolQueryBuilder boolQuery = QueryBuilders.boolQuery()
209-
.filter(QueryBuilders.termQuery("doc_type", "api_key"))
210-
.filter(QueryBuilders.termQuery("name", request.getName()))
211-
.filter(QueryBuilders.termQuery("api_key_invalidated", false));
212-
final BoolQueryBuilder expiredQuery = QueryBuilders.boolQuery()
213-
.should(QueryBuilders.rangeQuery("expiration_time").lte(Instant.now().toEpochMilli()))
214-
.should(QueryBuilders.boolQuery().mustNot(QueryBuilders.existsQuery("expiration_time")));
215-
boolQuery.filter(expiredQuery);
216-
217-
final SearchRequest searchRequest = client.prepareSearch(SECURITY_MAIN_ALIAS)
218-
.setQuery(boolQuery)
219-
.setVersion(false)
220-
.setSize(1)
221-
.request();
222-
securityIndex.prepareIndexIfNeededThenExecute(listener::onFailure, () ->
223-
executeAsyncWithOrigin(client, SECURITY_ORIGIN, SearchAction.INSTANCE, searchRequest,
224-
ActionListener.wrap(
225-
indexResponse -> {
226-
if (indexResponse.getHits().getTotalHits().value > 0) {
227-
listener.onFailure(traceLog("create api key", new ElasticsearchSecurityException(
228-
"Error creating api key as api key with name [{}] already exists", request.getName())));
229-
} else {
230-
createApiKeyAndIndexIt(authentication, request, userRoles, listener);
231-
}
232-
},
233-
listener::onFailure)));
234-
}
235-
236199
private void createApiKeyAndIndexIt(Authentication authentication, CreateApiKeyRequest request, Set<RoleDescriptor> roleDescriptorSet,
237200
ActionListener<CreateApiKeyResponse> listener) {
238201
final Instant created = clock.instant();

0 commit comments

Comments
 (0)