Skip to content

Commit ff5d47d

Browse files
committed
Security: add create api key transport action
In order to support api keys for access to elasticsearch, we need the ability to generate these api keys. A transport action has been added along with the request and response objects that allow for the generation of api keys. The api keys require a name and optionally allow a role to be specified which defines the amount of access the key has. Additionally an expiration may also be provided. This change does not include the restriction that the role needs to be a subset of the user's permissions, which will be added seperately. As it exists in this change, the api key is currently not usable which is another aspect that will come later. Relates elastic#34383
1 parent b0e98cb commit ff5d47d

File tree

20 files changed

+728
-46
lines changed

20 files changed

+728
-46
lines changed

server/src/main/java/org/elasticsearch/common/io/stream/StreamInput.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,23 @@ public Object readGenericValue() throws IOException {
578578
}
579579
}
580580

581+
/**
582+
* Read an {@link Instant} from the stream with nanosecond resolution
583+
*/
584+
public final Instant readInstant() throws IOException {
585+
return Instant.ofEpochSecond(readLong(), readInt());
586+
}
587+
588+
/**
589+
* Read an optional {@link Instant} from the stream. Returns <code>null</code> when
590+
* no instant is present.
591+
*/
592+
@Nullable
593+
public final Instant readOptionalInstant() throws IOException {
594+
final boolean present = readBoolean();
595+
return present ? readInstant() : null;
596+
}
597+
581598
@SuppressWarnings("unchecked")
582599
private List readArrayList() throws IOException {
583600
int size = readArraySize();

server/src/main/java/org/elasticsearch/common/io/stream/StreamOutput.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import java.nio.file.FileSystemLoopException;
5656
import java.nio.file.NoSuchFileException;
5757
import java.nio.file.NotDirectoryException;
58+
import java.time.Instant;
5859
import java.time.ZonedDateTime;
5960
import java.util.Collection;
6061
import java.util.Collections;
@@ -560,6 +561,26 @@ public final <K, V> void writeMap(final Map<K, V> map, final Writer<K> keyWriter
560561
}
561562
}
562563

564+
/**
565+
* Writes an {@link Instant} to the stream with nanosecond resolution
566+
*/
567+
public final void writeInstant(Instant instant) throws IOException {
568+
writeLong(instant.getEpochSecond());
569+
writeInt(instant.getNano());
570+
}
571+
572+
/**
573+
* Writes an {@link Instant} to the stream, which could possibly be null
574+
*/
575+
public final void writeOptionalInstant(@Nullable Instant instant) throws IOException {
576+
if (instant == null) {
577+
writeBoolean(false);
578+
} else {
579+
writeBoolean(true);
580+
writeInstant(instant);
581+
}
582+
}
583+
563584
private static final Map<Class<?>, Writer> WRITERS;
564585

565586
static {

server/src/test/java/org/elasticsearch/common/io/stream/StreamTests.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.io.ByteArrayInputStream;
2929
import java.io.EOFException;
3030
import java.io.IOException;
31+
import java.time.Instant;
3132
import java.util.ArrayList;
3233
import java.util.Arrays;
3334
import java.util.Collections;
@@ -248,6 +249,37 @@ public void testSetOfLongs() throws IOException {
248249
assertThat(targetSet, equalTo(sourceSet));
249250
}
250251

252+
public void testInstantSerialization() throws IOException {
253+
final Instant instant = Instant.now();
254+
try (BytesStreamOutput out = new BytesStreamOutput()) {
255+
out.writeInstant(instant);
256+
try (StreamInput in = out.bytes().streamInput()) {
257+
final Instant serialized = in.readInstant();
258+
assertEquals(instant, serialized);
259+
}
260+
}
261+
}
262+
263+
public void testOptionalInstantSerialization() throws IOException {
264+
final Instant instant = Instant.now();
265+
try (BytesStreamOutput out = new BytesStreamOutput()) {
266+
out.writeOptionalInstant(instant);
267+
try (StreamInput in = out.bytes().streamInput()) {
268+
final Instant serialized = in.readOptionalInstant();
269+
assertEquals(instant, serialized);
270+
}
271+
}
272+
273+
final Instant missing = null;
274+
try (BytesStreamOutput out = new BytesStreamOutput()) {
275+
out.writeOptionalInstant(missing);
276+
try (StreamInput in = out.bytes().streamInput()) {
277+
final Instant serialized = in.readOptionalInstant();
278+
assertEquals(missing, serialized);
279+
}
280+
}
281+
}
282+
251283
static final class WriteableString implements Writeable {
252284
final String string;
253285

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.xpack.core.security.action;
8+
9+
import org.elasticsearch.action.Action;
10+
11+
/**
12+
* Action for the creation of an API key
13+
*/
14+
public final class CreateApiKeyAction extends Action<CreateApiKeyResponse> {
15+
16+
public static final String NAME = "cluster:admin/xpack/security/api_key/create";
17+
public static final CreateApiKeyAction INSTANCE = new CreateApiKeyAction();
18+
19+
private CreateApiKeyAction() {
20+
super(NAME);
21+
}
22+
23+
@Override
24+
public CreateApiKeyResponse newResponse() {
25+
return new CreateApiKeyResponse();
26+
}
27+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.xpack.core.security.action;
8+
9+
import org.elasticsearch.action.ActionRequest;
10+
import org.elasticsearch.action.ActionRequestValidationException;
11+
import org.elasticsearch.action.support.WriteRequest;
12+
import org.elasticsearch.common.Strings;
13+
import org.elasticsearch.common.io.stream.StreamInput;
14+
import org.elasticsearch.common.io.stream.StreamOutput;
15+
import org.elasticsearch.common.unit.TimeValue;
16+
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
17+
18+
import java.io.IOException;
19+
import java.util.Collections;
20+
import java.util.List;
21+
import java.util.Objects;
22+
23+
import static org.elasticsearch.action.ValidateActions.addValidationError;
24+
25+
/**
26+
* Request class used for the creation of an API key. The request requires a name to be provided
27+
* and optionally an expiration time and permission limitation can be provided.
28+
*/
29+
public final class CreateApiKeyRequest extends ActionRequest {
30+
31+
private String name;
32+
private TimeValue expiration;
33+
private List<RoleDescriptor> roleDescriptors = Collections.emptyList();
34+
private WriteRequest.RefreshPolicy refreshPolicy = WriteRequest.RefreshPolicy.WAIT_UNTIL;
35+
36+
public CreateApiKeyRequest() {}
37+
38+
public CreateApiKeyRequest(StreamInput in) throws IOException {
39+
super(in);
40+
this.name = in.readString();
41+
this.expiration = in.readOptionalTimeValue();
42+
this.roleDescriptors = in.readList(RoleDescriptor::new);
43+
this.refreshPolicy = WriteRequest.RefreshPolicy.readFrom(in);
44+
}
45+
46+
public String getName() {
47+
return name;
48+
}
49+
50+
public void setName(String name) {
51+
this.name = name;
52+
}
53+
54+
public TimeValue getExpiration() {
55+
return expiration;
56+
}
57+
58+
public void setExpiration(TimeValue expiration) {
59+
this.expiration = expiration;
60+
}
61+
62+
public List<RoleDescriptor> getRoleDescriptors() {
63+
return roleDescriptors;
64+
}
65+
66+
public void setRoleDescriptors(List<RoleDescriptor> roleDescriptors) {
67+
this.roleDescriptors = Objects.requireNonNull(roleDescriptors, "role descriptors may not be null");
68+
}
69+
70+
public WriteRequest.RefreshPolicy getRefreshPolicy() {
71+
return refreshPolicy;
72+
}
73+
74+
public void setRefreshPolicy(WriteRequest.RefreshPolicy refreshPolicy) {
75+
this.refreshPolicy = Objects.requireNonNull(refreshPolicy, "refresh policy may not be null");
76+
}
77+
78+
@Override
79+
public ActionRequestValidationException validate() {
80+
ActionRequestValidationException validationException = null;
81+
if (Strings.isNullOrEmpty(name)) {
82+
validationException = addValidationError("name is required", validationException);
83+
} else if (name.length() > 256) {
84+
validationException = addValidationError("name may not be more than 256 characters long", validationException);
85+
}
86+
return validationException;
87+
}
88+
89+
@Override
90+
public void writeTo(StreamOutput out) throws IOException {
91+
super.writeTo(out);
92+
out.writeString(name);
93+
out.writeOptionalTimeValue(expiration);
94+
out.writeList(roleDescriptors);
95+
refreshPolicy.writeTo(out);
96+
}
97+
98+
@Override
99+
public void readFrom(StreamInput in) {
100+
throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable");
101+
}
102+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
package org.elasticsearch.xpack.core.security.action;
7+
8+
import org.elasticsearch.action.ActionRequestBuilder;
9+
import org.elasticsearch.action.support.WriteRequest;
10+
import org.elasticsearch.client.ElasticsearchClient;
11+
import org.elasticsearch.common.unit.TimeValue;
12+
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
13+
14+
import java.util.List;
15+
16+
/**
17+
* Request builder for populating a {@link CreateApiKeyRequest}
18+
*/
19+
public final class CreateApiKeyRequestBuilder extends ActionRequestBuilder<CreateApiKeyRequest, CreateApiKeyResponse> {
20+
21+
public CreateApiKeyRequestBuilder(ElasticsearchClient client) {
22+
super(client, CreateApiKeyAction.INSTANCE, new CreateApiKeyRequest());
23+
}
24+
25+
public CreateApiKeyRequestBuilder setName(String name) {
26+
request.setName(name);
27+
return this;
28+
}
29+
30+
public CreateApiKeyRequestBuilder setExpiration(TimeValue expiration) {
31+
request.setExpiration(expiration);
32+
return this;
33+
}
34+
35+
public CreateApiKeyRequestBuilder setRoleDescriptors(List<RoleDescriptor> roleDescriptors) {
36+
request.setRoleDescriptors(roleDescriptors);
37+
return this;
38+
}
39+
40+
public CreateApiKeyRequestBuilder setRefreshPolicy(WriteRequest.RefreshPolicy refreshPolicy) {
41+
request.setRefreshPolicy(refreshPolicy);
42+
return this;
43+
}
44+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.xpack.core.security.action;
8+
9+
import org.elasticsearch.action.ActionResponse;
10+
import org.elasticsearch.common.Nullable;
11+
import org.elasticsearch.common.io.stream.StreamInput;
12+
import org.elasticsearch.common.io.stream.StreamOutput;
13+
14+
import java.io.IOException;
15+
import java.time.Instant;
16+
17+
/**
18+
* Response for the successful creation of an api key
19+
*/
20+
public final class CreateApiKeyResponse extends ActionResponse {
21+
22+
private String name;
23+
private String key;
24+
private Instant expiration;
25+
26+
CreateApiKeyResponse() {}
27+
28+
public CreateApiKeyResponse(String name, String key, Instant expiration) {
29+
this.name = name;
30+
this.key = key;
31+
this.expiration = expiration;
32+
}
33+
34+
public CreateApiKeyResponse(StreamInput in) throws IOException {
35+
this.name = in.readString();
36+
this.key = in.readString();
37+
this.expiration = in.readOptionalInstant();
38+
}
39+
40+
public String getName() {
41+
return name;
42+
}
43+
44+
public String getKey() {
45+
return key;
46+
}
47+
48+
@Nullable
49+
public Instant getExpiration() {
50+
return expiration;
51+
}
52+
53+
@Override
54+
public void writeTo(StreamOutput out) throws IOException {
55+
super.writeTo(out);
56+
out.writeString(name);
57+
out.writeString(key);
58+
out.writeOptionalInstant(expiration);
59+
}
60+
61+
@Override
62+
public void readFrom(StreamInput in) throws IOException {
63+
// TODO(jaymode) remove this once transport supports reading writeables
64+
//throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable");
65+
this.name = in.readString();
66+
this.key = in.readString();
67+
this.expiration = in.readOptionalInstant();
68+
}
69+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public void readFrom(StreamInput in) throws IOException {
3737
int size = in.readVInt();
3838
roles = new RoleDescriptor[size];
3939
for (int i = 0; i < size; i++) {
40-
roles[i] = RoleDescriptor.readFrom(in);
40+
roles[i] = new RoleDescriptor(in);
4141
}
4242
}
4343

@@ -46,7 +46,7 @@ public void writeTo(StreamOutput out) throws IOException {
4646
super.writeTo(out);
4747
out.writeVInt(roles.length);
4848
for (RoleDescriptor role : roles) {
49-
RoleDescriptor.writeTo(role, out);
49+
role.writeTo(out);
5050
}
5151
}
5252
}

0 commit comments

Comments
 (0)