Formal support for "password_hash" in Put User (#35242)
For some time, the PutUser REST API has supported storing a pre-hashed password for a user. The change adds validation and tests around that feature so that it can be documented & officially supported. It also prevents the request from containing both a "password" and a "password_hash".
This commit is contained in:
parent
d8b1c23e1d
commit
231f6c1595
|
@ -54,9 +54,10 @@ public class PutUserRequestBuilder extends ActionRequestBuilder<PutUserRequest,
|
||||||
if (password != null) {
|
if (password != null) {
|
||||||
Validation.Error error = Validation.Users.validatePassword(password);
|
Validation.Error error = Validation.Users.validatePassword(password);
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
ValidationException validationException = new ValidationException();
|
throw validationException(error.toString());
|
||||||
validationException.addValidationError(error.toString());
|
}
|
||||||
throw validationException;
|
if (request.passwordHash() != null) {
|
||||||
|
throw validationException("password_hash has already been set");
|
||||||
}
|
}
|
||||||
request.passwordHash(hasher.hash(new SecureString(password)));
|
request.passwordHash(hasher.hash(new SecureString(password)));
|
||||||
} else {
|
} else {
|
||||||
|
@ -80,7 +81,15 @@ public class PutUserRequestBuilder extends ActionRequestBuilder<PutUserRequest,
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PutUserRequestBuilder passwordHash(char[] passwordHash) {
|
public PutUserRequestBuilder passwordHash(char[] passwordHash, Hasher configuredHasher) {
|
||||||
|
final Hasher resolvedHasher = Hasher.resolveFromHash(passwordHash);
|
||||||
|
if (resolvedHasher.equals(configuredHasher) == false) {
|
||||||
|
throw new IllegalArgumentException("Provided password hash uses [" + resolvedHasher
|
||||||
|
+ "] but the configured hashing algorithm is [" + configuredHasher + "]");
|
||||||
|
}
|
||||||
|
if (request.passwordHash() != null) {
|
||||||
|
throw validationException("password_hash has already been set");
|
||||||
|
}
|
||||||
request.passwordHash(passwordHash);
|
request.passwordHash(passwordHash);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
@ -120,7 +129,7 @@ public class PutUserRequestBuilder extends ActionRequestBuilder<PutUserRequest,
|
||||||
} else if (User.Fields.PASSWORD_HASH.match(currentFieldName, parser.getDeprecationHandler())) {
|
} else if (User.Fields.PASSWORD_HASH.match(currentFieldName, parser.getDeprecationHandler())) {
|
||||||
if (token == XContentParser.Token.VALUE_STRING) {
|
if (token == XContentParser.Token.VALUE_STRING) {
|
||||||
char[] passwordChars = parser.text().toCharArray();
|
char[] passwordChars = parser.text().toCharArray();
|
||||||
passwordHash(passwordChars);
|
passwordHash(passwordChars, hasher);
|
||||||
} else {
|
} else {
|
||||||
throw new ElasticsearchParseException(
|
throw new ElasticsearchParseException(
|
||||||
"expected field [{}] to be of type string, but found [{}] instead", currentFieldName, token);
|
"expected field [{}] to be of type string, but found [{}] instead", currentFieldName, token);
|
||||||
|
@ -177,4 +186,9 @@ public class PutUserRequestBuilder extends ActionRequestBuilder<PutUserRequest,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ValidationException validationException(String abc) {
|
||||||
|
ValidationException validationException = new ValidationException();
|
||||||
|
validationException.addValidationError(abc);
|
||||||
|
return validationException;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,11 @@ package org.elasticsearch.xpack.security.action.user;
|
||||||
|
|
||||||
import org.elasticsearch.ElasticsearchParseException;
|
import org.elasticsearch.ElasticsearchParseException;
|
||||||
import org.elasticsearch.client.Client;
|
import org.elasticsearch.client.Client;
|
||||||
|
import org.elasticsearch.common.ValidationException;
|
||||||
import org.elasticsearch.common.bytes.BytesArray;
|
import org.elasticsearch.common.bytes.BytesArray;
|
||||||
|
import org.elasticsearch.common.bytes.BytesReference;
|
||||||
|
import org.elasticsearch.common.settings.SecureString;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||||
import org.elasticsearch.common.xcontent.XContentType;
|
import org.elasticsearch.common.xcontent.XContentType;
|
||||||
import org.elasticsearch.test.ESTestCase;
|
import org.elasticsearch.test.ESTestCase;
|
||||||
import org.elasticsearch.xpack.core.security.action.user.PutUserRequest;
|
import org.elasticsearch.xpack.core.security.action.user.PutUserRequest;
|
||||||
|
@ -16,9 +20,12 @@ import org.elasticsearch.xpack.core.security.authc.support.Hasher;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
|
||||||
import static org.hamcrest.Matchers.arrayContaining;
|
import static org.hamcrest.Matchers.arrayContaining;
|
||||||
import static org.hamcrest.Matchers.containsString;
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
import static org.hamcrest.Matchers.is;
|
import static org.hamcrest.Matchers.is;
|
||||||
import static org.hamcrest.Matchers.nullValue;
|
import static org.hamcrest.Matchers.nullValue;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
@ -135,4 +142,69 @@ public class PutUserRequestBuilderTests extends ESTestCase {
|
||||||
builder.source("kibana4", new BytesArray(json.getBytes(StandardCharsets.UTF_8)), XContentType.JSON, Hasher.BCRYPT).request();
|
builder.source("kibana4", new BytesArray(json.getBytes(StandardCharsets.UTF_8)), XContentType.JSON, Hasher.BCRYPT).request();
|
||||||
assertFalse(request.enabled());
|
assertFalse(request.enabled());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void testWithValidPasswordHash() throws IOException {
|
||||||
|
final Hasher hasher = Hasher.BCRYPT4; // this is the fastest hasher we officially support
|
||||||
|
final char[] hash = hasher.hash(new SecureString("secret".toCharArray()));
|
||||||
|
final String json = "{\n" +
|
||||||
|
" \"password_hash\": \"" + new String(hash) + "\"," +
|
||||||
|
" \"roles\": []\n" +
|
||||||
|
"}";
|
||||||
|
|
||||||
|
PutUserRequestBuilder requestBuilder = new PutUserRequestBuilder(mock(Client.class));
|
||||||
|
PutUserRequest request = requestBuilder.source("hash_user",
|
||||||
|
new BytesArray(json.getBytes(StandardCharsets.UTF_8)), XContentType.JSON, hasher).request();
|
||||||
|
assertThat(request.passwordHash(), equalTo(hash));
|
||||||
|
assertThat(request.username(), equalTo("hash_user"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testWithMismatchedPasswordHash() throws IOException {
|
||||||
|
final Hasher systemHasher = Hasher.BCRYPT8;
|
||||||
|
final Hasher userHasher = Hasher.BCRYPT4; // this is the fastest hasher we officially support
|
||||||
|
final char[] hash = userHasher.hash(new SecureString("secret".toCharArray()));
|
||||||
|
final String json = "{\n" +
|
||||||
|
" \"password_hash\": \"" + new String(hash) + "\"," +
|
||||||
|
" \"roles\": []\n" +
|
||||||
|
"}";
|
||||||
|
|
||||||
|
PutUserRequestBuilder builder = new PutUserRequestBuilder(mock(Client.class));
|
||||||
|
final IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, () -> {
|
||||||
|
builder.source("hash_user", new BytesArray(json.getBytes(StandardCharsets.UTF_8)), XContentType.JSON, systemHasher).request();
|
||||||
|
});
|
||||||
|
assertThat(ex.getMessage(), containsString(userHasher.name()));
|
||||||
|
assertThat(ex.getMessage(), containsString(systemHasher.name()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testWithPasswordHashThatsNotReallyAHash() throws IOException {
|
||||||
|
final Hasher systemHasher = Hasher.PBKDF2;
|
||||||
|
final String json = "{\n" +
|
||||||
|
" \"password_hash\": \"not-a-hash\"," +
|
||||||
|
" \"roles\": []\n" +
|
||||||
|
"}";
|
||||||
|
|
||||||
|
PutUserRequestBuilder builder = new PutUserRequestBuilder(mock(Client.class));
|
||||||
|
final IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, () -> {
|
||||||
|
builder.source("hash_user", new BytesArray(json.getBytes(StandardCharsets.UTF_8)), XContentType.JSON, systemHasher).request();
|
||||||
|
});
|
||||||
|
assertThat(ex.getMessage(), containsString(Hasher.NOOP.name()));
|
||||||
|
assertThat(ex.getMessage(), containsString(systemHasher.name()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testWithBothPasswordAndHash() throws IOException {
|
||||||
|
final Hasher hasher = randomFrom(Hasher.BCRYPT4, Hasher.PBKDF2_1000);
|
||||||
|
final String password = randomAlphaOfLength(12);
|
||||||
|
final char[] hash = hasher.hash(new SecureString(password.toCharArray()));
|
||||||
|
final LinkedHashMap<String, Object> fields = new LinkedHashMap<>();
|
||||||
|
fields.put("password", password);
|
||||||
|
fields.put("password_hash", new String(hash));
|
||||||
|
fields.put("roles", Collections.emptyList());
|
||||||
|
BytesReference json = BytesReference.bytes(XContentBuilder.builder(XContentType.JSON.xContent())
|
||||||
|
.map(shuffleMap(fields, Collections.emptySet())));
|
||||||
|
|
||||||
|
PutUserRequestBuilder builder = new PutUserRequestBuilder(mock(Client.class));
|
||||||
|
final IllegalArgumentException ex = expectThrows(ValidationException.class, () -> {
|
||||||
|
builder.source("hash_user", json, XContentType.JSON, hasher).request();
|
||||||
|
});
|
||||||
|
assertThat(ex.getMessage(), containsString("password_hash has already been set"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,10 @@ teardown:
|
||||||
xpack.security.delete_user:
|
xpack.security.delete_user:
|
||||||
username: "joe"
|
username: "joe"
|
||||||
ignore: 404
|
ignore: 404
|
||||||
|
- do:
|
||||||
|
xpack.security.delete_user:
|
||||||
|
username: "bob"
|
||||||
|
ignore: 404
|
||||||
|
|
||||||
---
|
---
|
||||||
"Test put user api":
|
"Test put user api":
|
||||||
|
@ -101,3 +105,47 @@ teardown:
|
||||||
"key2" : "val2"
|
"key2" : "val2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
---
|
||||||
|
"Test put user with password hash":
|
||||||
|
|
||||||
|
# Mostly this chain of put_user , search index, set value is to work around the fact that the
|
||||||
|
# rest tests treat anything with a leading "$" as a stashed value, and bcrypt passwords start with "$"
|
||||||
|
# But it has the nice side effect of automatically adjusting to any changes in the default hasher for
|
||||||
|
# the ES cluster
|
||||||
|
- do:
|
||||||
|
xpack.security.put_user:
|
||||||
|
username: "bob"
|
||||||
|
body: >
|
||||||
|
{
|
||||||
|
"password" : "correct horse battery staple",
|
||||||
|
"roles" : [ ]
|
||||||
|
}
|
||||||
|
|
||||||
|
- do:
|
||||||
|
get:
|
||||||
|
index: .security
|
||||||
|
type: doc
|
||||||
|
id: user-bob
|
||||||
|
- set: { _source.password: "hash" }
|
||||||
|
|
||||||
|
- do:
|
||||||
|
xpack.security.put_user:
|
||||||
|
username: "joe"
|
||||||
|
body: >
|
||||||
|
{
|
||||||
|
"password_hash" : "$hash",
|
||||||
|
"roles" : [ "superuser" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# base64("joe:correct horse battery staple") = "am9lOmNvcnJlY3QgaG9yc2UgYmF0dGVyeSBzdGFwbGU="
|
||||||
|
- do:
|
||||||
|
headers:
|
||||||
|
Authorization: "Basic am9lOmNvcnJlY3QgaG9yc2UgYmF0dGVyeSBzdGFwbGU="
|
||||||
|
xpack.security.authenticate: {}
|
||||||
|
- match: { username: "joe" }
|
||||||
|
|
||||||
|
- do:
|
||||||
|
catch: unauthorized
|
||||||
|
headers:
|
||||||
|
Authorization: "Basic am9lOnMza3JpdA=="
|
||||||
|
xpack.security.authenticate: {}
|
||||||
|
|
Loading…
Reference in New Issue