Skip to content

Commit

Permalink
HLRC: Add ability to put user with a password hash
Browse files Browse the repository at this point in the history
Update PutUserRequest to support password_hash (see: elastic#35242)

This also updates the documentation to bring it in line with our more
recent approach to HLRC docs.
  • Loading branch information
tvernum committed Nov 15, 2018
1 parent 95a09ab commit 17d8d61
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 39 deletions.
Expand Up @@ -39,9 +39,46 @@ public final class PutUserRequest implements Validatable, ToXContentObject {

private final User user;
private final @Nullable char[] password;
private final @Nullable char[] passwordHash;
private final boolean enabled;
private final RefreshPolicy refreshPolicy;

/**
* Create or update a user in the native realm, with the user's new or updated password specified in plaintext.
* @param user the user to be created or updated
* @param password the password of the user. The password array is not modified by this class.
* It is the responsibility of the caller to clear the password after receiving
* a response.
* @param enabled true if the user is enabled and allowed to access elasticsearch
* @param refreshPolicy the refresh policy for the request.
*/
public static PutUserRequest withPassword(User user, char[] password, boolean enabled, RefreshPolicy refreshPolicy) {
return new PutUserRequest(user, password, null, enabled, refreshPolicy);
}

/**
* Create or update a user in the native realm, with the user's new or updated password specified as a cryptographic hash.
* @param user the user to be created or updated
* @param passwordHash the hash of the password of the user. It must be in the correct format for the password hashing algorithm in
* use on this elasticsearch cluster. The array is not modified by this class.
* It is the responsibility of the caller to clear the hash after receiving a response.
* @param enabled true if the user is enabled and allowed to access elasticsearch
* @param refreshPolicy the refresh policy for the request.
*/
public static PutUserRequest withPasswordHash(User user, char[] passwordHash, boolean enabled, RefreshPolicy refreshPolicy) {
return new PutUserRequest(user, null, passwordHash, enabled, refreshPolicy);
}

/**
* Update an existing user in the native realm without modifying their password.
* @param user the user to be created or updated
* @param enabled true if the user is enabled and allowed to access elasticsearch
* @param refreshPolicy the refresh policy for the request.
*/
public static PutUserRequest updateUser(User user, boolean enabled, RefreshPolicy refreshPolicy) {
return new PutUserRequest(user, null, null, enabled, refreshPolicy);
}

/**
* Creates a new request that is used to create or update a user in the native realm.
*
Expand All @@ -51,10 +88,33 @@ public final class PutUserRequest implements Validatable, ToXContentObject {
* a response.
* @param enabled true if the user is enabled and allowed to access elasticsearch
* @param refreshPolicy the refresh policy for the request.
* @deprecated Use {@link #withPassword(User, char[], boolean, RefreshPolicy)} or
* {@link #updateUser(User, boolean, RefreshPolicy)} instead.
*/
@Deprecated
public PutUserRequest(User user, @Nullable char[] password, boolean enabled, @Nullable RefreshPolicy refreshPolicy) {
this(user, password, null, enabled, refreshPolicy);
}

/**
* Creates a new request that is used to create or update a user in the native realm.
* @param user the user to be created or updated
* @param password the password of the user. The password array is not modified by this class.
* It is the responsibility of the caller to clear the password after receiving
* a response.
* @param passwordHash the hash of the password. Only one of "password" or "passwordHash" may be populated.
* The other parameter must be {@code null}.
* @param enabled true if the user is enabled and allowed to access elasticsearch
* @param refreshPolicy the refresh policy for the request.
*/
private PutUserRequest(User user, @Nullable char[] password, @Nullable char[] passwordHash, boolean enabled,
RefreshPolicy refreshPolicy) {
this.user = Objects.requireNonNull(user, "user is required, cannot be null");
if (password != null && passwordHash != null) {
throw new IllegalArgumentException("cannot specify both password and passwordHash");
}
this.password = password;
this.passwordHash = passwordHash;
this.enabled = enabled;
this.refreshPolicy = refreshPolicy == null ? RefreshPolicy.getDefault() : refreshPolicy;
}
Expand Down Expand Up @@ -82,6 +142,7 @@ public boolean equals(Object o) {
final PutUserRequest that = (PutUserRequest) o;
return Objects.equals(user, that.user)
&& Arrays.equals(password, that.password)
&& Arrays.equals(passwordHash, that.passwordHash)
&& enabled == that.enabled
&& refreshPolicy == that.refreshPolicy;
}
Expand All @@ -90,6 +151,7 @@ public boolean equals(Object o) {
public int hashCode() {
int result = Objects.hash(user, enabled, refreshPolicy);
result = 31 * result + Arrays.hashCode(password);
result = 31 * result + Arrays.hashCode(passwordHash);
return result;
}

Expand All @@ -108,12 +170,10 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
builder.startObject();
builder.field("username", user.getUsername());
if (password != null) {
byte[] charBytes = CharArrays.toUtf8Bytes(password);
try {
builder.field("password").utf8Value(charBytes, 0, charBytes.length);
} finally {
Arrays.fill(charBytes, (byte) 0);
}
charField(builder, "password", password);
}
if (passwordHash != null) {
charField(builder, "password_hash", passwordHash);
}
builder.field("roles", user.getRoles());
if (user.getFullName() != null) {
Expand All @@ -126,4 +186,13 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
builder.field("enabled", enabled);
return builder.endObject();
}

private void charField(XContentBuilder builder, String fieldName, char[] chars) throws IOException {
byte[] charBytes = CharArrays.toUtf8Bytes(chars);
try {
builder.field(fieldName).utf8Value(charBytes, 0, charBytes.length);
} finally {
Arrays.fill(charBytes, (byte) 0);
}
}
}
Expand Up @@ -22,6 +22,7 @@
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.nio.entity.NStringEntity;
import org.elasticsearch.ElasticsearchStatusException;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.LatchedActionListener;
import org.elasticsearch.action.support.PlainActionFuture;
Expand Down Expand Up @@ -68,7 +69,11 @@
import org.elasticsearch.rest.RestStatus;
import org.hamcrest.Matchers;

import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
Expand All @@ -79,6 +84,7 @@

import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
Expand All @@ -92,10 +98,13 @@ public void testPutUser() throws Exception {
RestHighLevelClient client = highLevelClient();

{
//tag::put-user-execute
//tag::put-user-password-request
char[] password = new char[]{'p', 'a', 's', 's', 'w', 'o', 'r', 'd'};
User user = new User("example", Collections.singletonList("superuser"));
PutUserRequest request = new PutUserRequest(user, password, true, RefreshPolicy.NONE);
PutUserRequest request = PutUserRequest.withPassword(user, password, true, RefreshPolicy.NONE);
//end::put-user-password-request

//tag::put-user-execute
PutUserResponse response = client.security().putUser(request, RequestOptions.DEFAULT);
//end::put-user-execute

Expand All @@ -105,11 +114,37 @@ public void testPutUser() throws Exception {

assertTrue(isCreated);
}

{
byte[] salt = new byte[32];
SecureRandom.getInstanceStrong().nextBytes(salt);
char[] password = new char[]{'p', 'a', 's', 's', 'w', 'o', 'r', 'd'};
User user2 = new User("example2", Collections.singletonList("superuser"));
PutUserRequest request = new PutUserRequest(user2, password, true, RefreshPolicy.NONE);
User user = new User("example2", Collections.singletonList("superuser"));

//tag::put-user-hash-request
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2withHMACSHA512");
PBEKeySpec keySpec = new PBEKeySpec(password, salt, 10000, 256);
final byte[] pbkdfEncoded = secretKeyFactory.generateSecret(keySpec).getEncoded();
char[] passwordHash = ("{PBKDF2}10000$" + Base64.getEncoder().encodeToString(salt)
+ "$" + Base64.getEncoder().encodeToString(pbkdfEncoded)).toCharArray();

PutUserRequest request = PutUserRequest.withPasswordHash(user, passwordHash, true, RefreshPolicy.NONE);
//end::put-user-hash-request

try {
client.security().putUser(request, RequestOptions.DEFAULT);
} catch (ElasticsearchStatusException e) {
// This is expected to fail as the server will not be using PBKDF2, but that's easiest hasher to support
// in a standard JVM without introducing additional libraries.
assertThat(e.getDetailedMessage(), containsString("PBKDF2"));
}
}

{
User user = new User("example", Collections.singletonList("superuser"));
//tag::put-user-update-request
PutUserRequest request = PutUserRequest.updateUser(user, true, RefreshPolicy.NONE);
//end::put-user-update-request

// tag::put-user-execute-listener
ActionListener<PutUserResponse> listener = new ActionListener<PutUserResponse>() {
@Override
Expand Down
62 changes: 34 additions & 28 deletions docs/java-rest/high-level/security/put-user.asciidoc
@@ -1,52 +1,58 @@
[[java-rest-high-security-put-user]]
--
:api: put-user
:request: PutUserRequest
:response: PutUserResponse
--

[id="{upid}-{api}"]
=== Put User API

[[java-rest-high-security-put-user-execution]]
==== Execution
[id="{upid}-{api}-request"]
==== Put User Request Request

The +{request}+ class is used to create or update a user in the Native Realm.
There are 3 different factory methods for creating a request.

Creating and updating a user can be performed using the `security().putUser()`
method:
===== Create or Update User with a Password

If you wish to create a new user (or update an existing user) and directly specifying the user's new password, use the
`withPassword` method as shown below:

["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests}/SecurityDocumentationIT.java[put-user-execute]
include-tagged::{doc-tests-file}[{api}-password-request]
--------------------------------------------------

[[java-rest-high-security-put-user-response]]
==== Response
===== Create or Update User with a Hashed Password

The returned `PutUserResponse` contains a single field, `created`. This field
serves as an indication if a user was created or if an existing entry was updated.
If you wish to create a new user (or update an existing user) and perform password hashing on the client,
then use the `withPasswordHash` method:

["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests}/SecurityDocumentationIT.java[put-user-response]
--------------------------------------------------
<1> `created` is a boolean indicating whether the user was created or updated
include-tagged::{doc-tests-file}[{api}-hash-request]
[[java-rest-high-security-put-user-async]]
==== Asynchronous Execution
--------------------------------------------------
===== Update a User without changing their password

This request can be executed asynchronously:
If you wish to update an existing user, and do not wish to change the user's password,
then use the `updateUserProperties` method:

["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests}/SecurityDocumentationIT.java[put-user-execute-async]
include-tagged::{doc-tests-file}[{api}-update-request]
--------------------------------------------------
<1> The `PutUserRequest` to execute and the `ActionListener` to use when
the execution completes.

The asynchronous method does not block and returns immediately. Once the request
has completed the `ActionListener` is called back using the `onResponse` method
if the execution successfully completed or using the `onFailure` method if
it failed.
include::../execution.asciidoc[]

A typical listener for a `PutUserResponse` looks like:
[id="{upid}-{api}-response"]
==== Put User Response

The returned `PutUserResponse` contains a single field, `created`. This field
serves as an indication if a user was created or if an existing entry was updated.

["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests}/SecurityDocumentationIT.java[put-user-execute-listener]
include-tagged::{doc-tests}/SecurityDocumentationIT.java[put-user-response]
--------------------------------------------------
<1> Called when the execution is successfully completed. The response is
provided as an argument.
<2> Called in case of failure. The raised exception is provided as an argument.
<1> `created` is a boolean indicating whether the user was created or updated

0 comments on commit 17d8d61

Please sign in to comment.