Skip to content

Commit

Permalink
Security: add create api key transport action (#34572)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
jaymode authored Nov 2, 2018
1 parent 781130c commit f087b54
Show file tree
Hide file tree
Showing 24 changed files with 951 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
package org.elasticsearch.common;


import org.elasticsearch.common.settings.SecureString;

import java.util.Arrays;
import java.util.Base64;
import java.util.Random;

Expand All @@ -34,12 +37,37 @@ public String getBase64UUID() {
return getBase64UUID(SecureRandomHolder.INSTANCE);
}

/**
* Returns a Base64 encoded {@link SecureString} of a Version 4.0 compatible UUID
* as defined here: http://www.ietf.org/rfc/rfc4122.txt
*/
public SecureString getBase64UUIDSecureString() {
byte[] uuidBytes = null;
byte[] encodedBytes = null;
try {
uuidBytes = getUUIDBytes(SecureRandomHolder.INSTANCE);
encodedBytes = Base64.getUrlEncoder().withoutPadding().encode(uuidBytes);
return new SecureString(CharArrays.utf8BytesToChars(encodedBytes));
} finally {
if (uuidBytes != null) {
Arrays.fill(uuidBytes, (byte) 0);
}
if (encodedBytes != null) {
Arrays.fill(encodedBytes, (byte) 0);
}
}
}

/**
* Returns a Base64 encoded version of a Version 4.0 compatible UUID
* randomly initialized by the given {@link java.util.Random} instance
* as defined here: http://www.ietf.org/rfc/rfc4122.txt
*/
public String getBase64UUID(Random random) {
return Base64.getUrlEncoder().withoutPadding().encodeToString(getUUIDBytes(random));
}

private byte[] getUUIDBytes(Random random) {
final byte[] randomBytes = new byte[16];
random.nextBytes(randomBytes);
/* Set the version to version 4 (see http://www.ietf.org/rfc/rfc4122.txt)
Expand All @@ -48,12 +76,12 @@ public String getBase64UUID(Random random) {
* stamp (bits 4 through 7 of the time_hi_and_version field).*/
randomBytes[6] &= 0x0f; /* clear the 4 most significant bits for the version */
randomBytes[6] |= 0x40; /* set the version to 0100 / 0x40 */
/* Set the variant:

/* Set the variant:
* The high field of th clock sequence multiplexed with the variant.
* We set only the MSB of the variant*/
randomBytes[8] &= 0x3f; /* clear the 2 most significant bits */
randomBytes[8] |= 0x80; /* set the variant (MSB is set)*/
return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes);
return randomBytes;
}
}
7 changes: 7 additions & 0 deletions server/src/main/java/org/elasticsearch/common/UUIDs.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

package org.elasticsearch.common;

import org.elasticsearch.common.settings.SecureString;

import java.util.Random;

public class UUIDs {
Expand Down Expand Up @@ -50,4 +52,9 @@ public static String randomBase64UUID() {
return RANDOM_UUID_GENERATOR.getBase64UUID();
}

/** Returns a Base64 encoded {@link SecureString} of a Version 4.0 compatible UUID as defined here: http://www.ietf.org/rfc/rfc4122.txt,
* using a private {@code SecureRandom} instance */
public static SecureString randomBase64UUIDSecureString() {
return RANDOM_UUID_GENERATOR.getBase64UUIDSecureString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,23 @@ public Object readGenericValue() throws IOException {
}
}

/**
* Read an {@link Instant} from the stream with nanosecond resolution
*/
public final Instant readInstant() throws IOException {
return Instant.ofEpochSecond(readLong(), readInt());
}

/**
* Read an optional {@link Instant} from the stream. Returns <code>null</code> when
* no instant is present.
*/
@Nullable
public final Instant readOptionalInstant() throws IOException {
final boolean present = readBoolean();
return present ? readInstant() : null;
}

@SuppressWarnings("unchecked")
private List readArrayList() throws IOException {
int size = readArraySize();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
import java.nio.file.FileSystemLoopException;
import java.nio.file.NoSuchFileException;
import java.nio.file.NotDirectoryException;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.Collection;
import java.util.Collections;
Expand Down Expand Up @@ -560,6 +561,26 @@ public final <K, V> void writeMap(final Map<K, V> map, final Writer<K> keyWriter
}
}

/**
* Writes an {@link Instant} to the stream with nanosecond resolution
*/
public final void writeInstant(Instant instant) throws IOException {
writeLong(instant.getEpochSecond());
writeInt(instant.getNano());
}

/**
* Writes an {@link Instant} to the stream, which could possibly be null
*/
public final void writeOptionalInstant(@Nullable Instant instant) throws IOException {
if (instant == null) {
writeBoolean(false);
} else {
writeBoolean(true);
writeInstant(instant);
}
}

private static final Map<Class<?>, Writer> WRITERS;

static {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
Expand Down Expand Up @@ -248,6 +249,37 @@ public void testSetOfLongs() throws IOException {
assertThat(targetSet, equalTo(sourceSet));
}

public void testInstantSerialization() throws IOException {
final Instant instant = Instant.now();
try (BytesStreamOutput out = new BytesStreamOutput()) {
out.writeInstant(instant);
try (StreamInput in = out.bytes().streamInput()) {
final Instant serialized = in.readInstant();
assertEquals(instant, serialized);
}
}
}

public void testOptionalInstantSerialization() throws IOException {
final Instant instant = Instant.now();
try (BytesStreamOutput out = new BytesStreamOutput()) {
out.writeOptionalInstant(instant);
try (StreamInput in = out.bytes().streamInput()) {
final Instant serialized = in.readOptionalInstant();
assertEquals(instant, serialized);
}
}

final Instant missing = null;
try (BytesStreamOutput out = new BytesStreamOutput()) {
out.writeOptionalInstant(missing);
try (StreamInput in = out.bytes().streamInput()) {
final Instant serialized = in.readOptionalInstant();
assertEquals(missing, serialized);
}
}
}

static final class WriteableString implements Writeable {
final String string;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
import org.elasticsearch.xpack.core.security.SecurityFeatureSetUsage;
import org.elasticsearch.xpack.core.security.SecurityField;
import org.elasticsearch.xpack.core.security.SecuritySettings;
import org.elasticsearch.xpack.core.security.action.CreateApiKeyAction;
import org.elasticsearch.xpack.core.security.action.realm.ClearRealmCacheAction;
import org.elasticsearch.xpack.core.security.action.role.ClearRolesCacheAction;
import org.elasticsearch.xpack.core.security.action.role.DeleteRoleAction;
Expand Down Expand Up @@ -287,6 +288,7 @@ public List<Action<? extends ActionResponse>> getClientActions() {
InvalidateTokenAction.INSTANCE,
GetCertificateInfoAction.INSTANCE,
RefreshTokenAction.INSTANCE,
CreateApiKeyAction.INSTANCE,
// upgrade
IndexUpgradeInfoAction.INSTANCE,
IndexUpgradeAction.INSTANCE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,14 @@ private XPackSettings() {
public static final Setting<Boolean> RESERVED_REALM_ENABLED_SETTING = Setting.boolSetting("xpack.security.authc.reserved_realm.enabled",
true, Setting.Property.NodeScope);

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

/** Setting for enabling or disabling the api key service. Defaults to the value of https being enabled */
public static final Setting<Boolean> API_KEY_SERVICE_ENABLED_SETTING = Setting.boolSetting("xpack.security.authc.api_key.enabled",
XPackSettings.HTTP_SSL_ENABLED::getRaw, Setting.Property.NodeScope);

/** Setting for enabling or disabling FIPS mode. Defaults to false */
public static final Setting<Boolean> FIPS_MODE_ENABLED =
Setting.boolSetting("xpack.security.fips_mode.enabled", false, Property.NodeScope);
Expand Down Expand Up @@ -182,6 +186,7 @@ public static List<Setting<?>> getAllSettings() {
settings.add(HTTP_SSL_ENABLED);
settings.add(RESERVED_REALM_ENABLED_SETTING);
settings.add(TOKEN_SERVICE_ENABLED_SETTING);
settings.add(API_KEY_SERVICE_ENABLED_SETTING);
settings.add(SQL_ENABLED);
settings.add(USER_SETTING);
settings.add(ROLLUP_ENABLED);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

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

import org.elasticsearch.action.Action;
import org.elasticsearch.common.io.stream.Writeable;

/**
* Action for the creation of an API key
*/
public final class CreateApiKeyAction extends Action<CreateApiKeyResponse> {

public static final String NAME = "cluster:admin/xpack/security/api_key/create";
public static final CreateApiKeyAction INSTANCE = new CreateApiKeyAction();

private CreateApiKeyAction() {
super(NAME);
}

@Override
public CreateApiKeyResponse newResponse() {
throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable");
}

@Override
public Writeable.Reader<CreateApiKeyResponse> getResponseReader() {
return CreateApiKeyResponse::new;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

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

import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.support.WriteRequest;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

import static org.elasticsearch.action.ValidateActions.addValidationError;

/**
* Request class used for the creation of an API key. The request requires a name to be provided
* and optionally an expiration time and permission limitation can be provided.
*/
public final class CreateApiKeyRequest extends ActionRequest {

private String name;
private TimeValue expiration;
private List<RoleDescriptor> roleDescriptors = Collections.emptyList();
private WriteRequest.RefreshPolicy refreshPolicy = WriteRequest.RefreshPolicy.WAIT_UNTIL;

public CreateApiKeyRequest() {}

public CreateApiKeyRequest(StreamInput in) throws IOException {
super(in);
this.name = in.readString();
this.expiration = in.readOptionalTimeValue();
this.roleDescriptors = Collections.unmodifiableList(in.readList(RoleDescriptor::new));
this.refreshPolicy = WriteRequest.RefreshPolicy.readFrom(in);
}

public String getName() {
return name;
}

public void setName(String name) {
if (Strings.hasText(name)) {
this.name = name;
} else {
throw new IllegalArgumentException("name must not be null or empty");
}
}

public TimeValue getExpiration() {
return expiration;
}

public void setExpiration(TimeValue expiration) {
this.expiration = expiration;
}

public List<RoleDescriptor> getRoleDescriptors() {
return roleDescriptors;
}

public void setRoleDescriptors(List<RoleDescriptor> roleDescriptors) {
this.roleDescriptors = Collections.unmodifiableList(Objects.requireNonNull(roleDescriptors, "role descriptors may not be null"));
}

public WriteRequest.RefreshPolicy getRefreshPolicy() {
return refreshPolicy;
}

public void setRefreshPolicy(WriteRequest.RefreshPolicy refreshPolicy) {
this.refreshPolicy = Objects.requireNonNull(refreshPolicy, "refresh policy may not be null");
}

@Override
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = null;
if (Strings.isNullOrEmpty(name)) {
validationException = addValidationError("name is required", validationException);
} else {
if (name.length() > 256) {
validationException = addValidationError("name may not be more than 256 characters long", validationException);
}
if (name.equals(name.trim()) == false) {
validationException = addValidationError("name may not begin or end with whitespace", validationException);
}
if (name.startsWith("_")) {
validationException = addValidationError("name may not begin with an underscore", validationException);
}
}
return validationException;
}

@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeString(name);
out.writeOptionalTimeValue(expiration);
out.writeList(roleDescriptors);
refreshPolicy.writeTo(out);
}

@Override
public void readFrom(StreamInput in) {
throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable");
}
}
Loading

0 comments on commit f087b54

Please sign in to comment.