Skip to content

Commit f087b54

Browse files
authored
Security: add create api key transport action (#34572)
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 #34383
1 parent 781130c commit f087b54

File tree

24 files changed

+951
-50
lines changed

24 files changed

+951
-50
lines changed

server/src/main/java/org/elasticsearch/common/RandomBasedUUIDGenerator.java

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
package org.elasticsearch.common;
2121

2222

23+
import org.elasticsearch.common.settings.SecureString;
24+
25+
import java.util.Arrays;
2326
import java.util.Base64;
2427
import java.util.Random;
2528

@@ -34,12 +37,37 @@ public String getBase64UUID() {
3437
return getBase64UUID(SecureRandomHolder.INSTANCE);
3538
}
3639

40+
/**
41+
* Returns a Base64 encoded {@link SecureString} of a Version 4.0 compatible UUID
42+
* as defined here: http://www.ietf.org/rfc/rfc4122.txt
43+
*/
44+
public SecureString getBase64UUIDSecureString() {
45+
byte[] uuidBytes = null;
46+
byte[] encodedBytes = null;
47+
try {
48+
uuidBytes = getUUIDBytes(SecureRandomHolder.INSTANCE);
49+
encodedBytes = Base64.getUrlEncoder().withoutPadding().encode(uuidBytes);
50+
return new SecureString(CharArrays.utf8BytesToChars(encodedBytes));
51+
} finally {
52+
if (uuidBytes != null) {
53+
Arrays.fill(uuidBytes, (byte) 0);
54+
}
55+
if (encodedBytes != null) {
56+
Arrays.fill(encodedBytes, (byte) 0);
57+
}
58+
}
59+
}
60+
3761
/**
3862
* Returns a Base64 encoded version of a Version 4.0 compatible UUID
3963
* randomly initialized by the given {@link java.util.Random} instance
4064
* as defined here: http://www.ietf.org/rfc/rfc4122.txt
4165
*/
4266
public String getBase64UUID(Random random) {
67+
return Base64.getUrlEncoder().withoutPadding().encodeToString(getUUIDBytes(random));
68+
}
69+
70+
private byte[] getUUIDBytes(Random random) {
4371
final byte[] randomBytes = new byte[16];
4472
random.nextBytes(randomBytes);
4573
/* Set the version to version 4 (see http://www.ietf.org/rfc/rfc4122.txt)
@@ -48,12 +76,12 @@ public String getBase64UUID(Random random) {
4876
* stamp (bits 4 through 7 of the time_hi_and_version field).*/
4977
randomBytes[6] &= 0x0f; /* clear the 4 most significant bits for the version */
5078
randomBytes[6] |= 0x40; /* set the version to 0100 / 0x40 */
51-
52-
/* Set the variant:
79+
80+
/* Set the variant:
5381
* The high field of th clock sequence multiplexed with the variant.
5482
* We set only the MSB of the variant*/
5583
randomBytes[8] &= 0x3f; /* clear the 2 most significant bits */
5684
randomBytes[8] |= 0x80; /* set the variant (MSB is set)*/
57-
return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes);
85+
return randomBytes;
5886
}
5987
}

server/src/main/java/org/elasticsearch/common/UUIDs.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
package org.elasticsearch.common;
2121

22+
import org.elasticsearch.common.settings.SecureString;
23+
2224
import java.util.Random;
2325

2426
public class UUIDs {
@@ -50,4 +52,9 @@ public static String randomBase64UUID() {
5052
return RANDOM_UUID_GENERATOR.getBase64UUID();
5153
}
5254

55+
/** Returns a Base64 encoded {@link SecureString} of a Version 4.0 compatible UUID as defined here: http://www.ietf.org/rfc/rfc4122.txt,
56+
* using a private {@code SecureRandom} instance */
57+
public static SecureString randomBase64UUIDSecureString() {
58+
return RANDOM_UUID_GENERATOR.getBase64UUIDSecureString();
59+
}
5360
}

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

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
import org.elasticsearch.xpack.core.security.SecurityFeatureSetUsage;
113113
import org.elasticsearch.xpack.core.security.SecurityField;
114114
import org.elasticsearch.xpack.core.security.SecuritySettings;
115+
import org.elasticsearch.xpack.core.security.action.CreateApiKeyAction;
115116
import org.elasticsearch.xpack.core.security.action.realm.ClearRealmCacheAction;
116117
import org.elasticsearch.xpack.core.security.action.role.ClearRolesCacheAction;
117118
import org.elasticsearch.xpack.core.security.action.role.DeleteRoleAction;
@@ -287,6 +288,7 @@ public List<Action<? extends ActionResponse>> getClientActions() {
287288
InvalidateTokenAction.INSTANCE,
288289
GetCertificateInfoAction.INSTANCE,
289290
RefreshTokenAction.INSTANCE,
291+
CreateApiKeyAction.INSTANCE,
290292
// upgrade
291293
IndexUpgradeInfoAction.INSTANCE,
292294
IndexUpgradeAction.INSTANCE,

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackSettings.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,14 @@ private XPackSettings() {
9090
public static final Setting<Boolean> RESERVED_REALM_ENABLED_SETTING = Setting.boolSetting("xpack.security.authc.reserved_realm.enabled",
9191
true, Setting.Property.NodeScope);
9292

93-
/** Setting for enabling or disabling the token service. Defaults to true */
93+
/** Setting for enabling or disabling the token service. Defaults to the value of https being enabled */
9494
public static final Setting<Boolean> TOKEN_SERVICE_ENABLED_SETTING = Setting.boolSetting("xpack.security.authc.token.enabled",
9595
XPackSettings.HTTP_SSL_ENABLED::getRaw, Setting.Property.NodeScope);
9696

97+
/** Setting for enabling or disabling the api key service. Defaults to the value of https being enabled */
98+
public static final Setting<Boolean> API_KEY_SERVICE_ENABLED_SETTING = Setting.boolSetting("xpack.security.authc.api_key.enabled",
99+
XPackSettings.HTTP_SSL_ENABLED::getRaw, Setting.Property.NodeScope);
100+
97101
/** Setting for enabling or disabling FIPS mode. Defaults to false */
98102
public static final Setting<Boolean> FIPS_MODE_ENABLED =
99103
Setting.boolSetting("xpack.security.fips_mode.enabled", false, Property.NodeScope);
@@ -182,6 +186,7 @@ public static List<Setting<?>> getAllSettings() {
182186
settings.add(HTTP_SSL_ENABLED);
183187
settings.add(RESERVED_REALM_ENABLED_SETTING);
184188
settings.add(TOKEN_SERVICE_ENABLED_SETTING);
189+
settings.add(API_KEY_SERVICE_ENABLED_SETTING);
185190
settings.add(SQL_ENABLED);
186191
settings.add(USER_SETTING);
187192
settings.add(ROLLUP_ENABLED);
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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+
import org.elasticsearch.common.io.stream.Writeable;
11+
12+
/**
13+
* Action for the creation of an API key
14+
*/
15+
public final class CreateApiKeyAction extends Action<CreateApiKeyResponse> {
16+
17+
public static final String NAME = "cluster:admin/xpack/security/api_key/create";
18+
public static final CreateApiKeyAction INSTANCE = new CreateApiKeyAction();
19+
20+
private CreateApiKeyAction() {
21+
super(NAME);
22+
}
23+
24+
@Override
25+
public CreateApiKeyResponse newResponse() {
26+
throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable");
27+
}
28+
29+
@Override
30+
public Writeable.Reader<CreateApiKeyResponse> getResponseReader() {
31+
return CreateApiKeyResponse::new;
32+
}
33+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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 = Collections.unmodifiableList(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+
if (Strings.hasText(name)) {
52+
this.name = name;
53+
} else {
54+
throw new IllegalArgumentException("name must not be null or empty");
55+
}
56+
}
57+
58+
public TimeValue getExpiration() {
59+
return expiration;
60+
}
61+
62+
public void setExpiration(TimeValue expiration) {
63+
this.expiration = expiration;
64+
}
65+
66+
public List<RoleDescriptor> getRoleDescriptors() {
67+
return roleDescriptors;
68+
}
69+
70+
public void setRoleDescriptors(List<RoleDescriptor> roleDescriptors) {
71+
this.roleDescriptors = Collections.unmodifiableList(Objects.requireNonNull(roleDescriptors, "role descriptors may not be null"));
72+
}
73+
74+
public WriteRequest.RefreshPolicy getRefreshPolicy() {
75+
return refreshPolicy;
76+
}
77+
78+
public void setRefreshPolicy(WriteRequest.RefreshPolicy refreshPolicy) {
79+
this.refreshPolicy = Objects.requireNonNull(refreshPolicy, "refresh policy may not be null");
80+
}
81+
82+
@Override
83+
public ActionRequestValidationException validate() {
84+
ActionRequestValidationException validationException = null;
85+
if (Strings.isNullOrEmpty(name)) {
86+
validationException = addValidationError("name is required", validationException);
87+
} else {
88+
if (name.length() > 256) {
89+
validationException = addValidationError("name may not be more than 256 characters long", validationException);
90+
}
91+
if (name.equals(name.trim()) == false) {
92+
validationException = addValidationError("name may not begin or end with whitespace", validationException);
93+
}
94+
if (name.startsWith("_")) {
95+
validationException = addValidationError("name may not begin with an underscore", validationException);
96+
}
97+
}
98+
return validationException;
99+
}
100+
101+
@Override
102+
public void writeTo(StreamOutput out) throws IOException {
103+
super.writeTo(out);
104+
out.writeString(name);
105+
out.writeOptionalTimeValue(expiration);
106+
out.writeList(roleDescriptors);
107+
refreshPolicy.writeTo(out);
108+
}
109+
110+
@Override
111+
public void readFrom(StreamInput in) {
112+
throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable");
113+
}
114+
}

0 commit comments

Comments
 (0)