diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserRequestBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserRequestBuilder.java index eea804d81fe..f667bff513b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserRequestBuilder.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserRequestBuilder.java @@ -54,9 +54,10 @@ public class PutUserRequestBuilder extends ActionRequestBuilder { + 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 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")); + } } diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/users/10_basic.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/users/10_basic.yml index ea152bd677c..fd41df11ac4 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/users/10_basic.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/users/10_basic.yml @@ -13,6 +13,10 @@ teardown: xpack.security.delete_user: username: "joe" ignore: 404 + - do: + xpack.security.delete_user: + username: "bob" + ignore: 404 --- "Test put user api": @@ -101,3 +105,47 @@ teardown: "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: {}