Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ff5d47d
Security: add create api key transport action
jaymode Oct 15, 2018
7446f3d
Merge branch 'master' into create_api_key
jaymode Oct 18, 2018
aac38fe
Merge branch 'master' into create_api_key
jaymode Oct 19, 2018
4dfb7ed
add streaminput super constructors
jaymode Oct 19, 2018
710ab24
check name when setting
jaymode Oct 19, 2018
eacd0da
fix class name
jaymode Oct 19, 2018
28d2e72
fail if role retrieval was not a success
jaymode Oct 19, 2018
758abac
add api key service
jaymode Oct 19, 2018
a4c2db8
fix test
jaymode Oct 19, 2018
a8bccfa
add setting
jaymode Oct 19, 2018
02bd86d
add client action
jaymode Oct 19, 2018
9a0260e
Merge branch 'master' into create_api_key
jaymode Oct 23, 2018
6e74f5a
add super calls for reading
jaymode Oct 23, 2018
60a06fd
delete unimplemented rest action
jaymode Oct 23, 2018
68cce7c
use unmodifiable list
jaymode Oct 23, 2018
fddba0c
use securestring in response
jaymode Oct 23, 2018
447fb30
fix securestring to xcontent
jaymode Oct 24, 2018
6a44c9b
Merge branch 'security_api_keys' into create_api_key
jaymode Oct 24, 2018
e6e82c7
provide id with key, only store hash of key
jaymode Oct 25, 2018
54d753f
Merge branch 'security_api_keys' into create_api_key
jaymode Oct 29, 2018
03a45ea
Merge branch 'security_api_keys' into create_api_key
jaymode Nov 1, 2018
a9e9c61
use writeable.reader for response
jaymode Nov 1, 2018
a9b7e3f
s/LOGGER/logger
jaymode Nov 1, 2018
a4d163d
no key in id
jaymode Nov 1, 2018
2a06c0f
assert no key in id
jaymode Nov 1, 2018
a818b45
allow configurable hash
jaymode Nov 1, 2018
40aea03
rename setting
jaymode Nov 1, 2018
75ae8d4
more name restrictions
jaymode Nov 1, 2018
68104e2
Merge branch 'security_api_keys' into create_api_key
jaymode Nov 1, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we have any other restrictions on names?
I feel like starting or ending in whitespace is likely to cause confusion, and perhaps we should reserve names start with _ just in case we need them in the future?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added those restrictions

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