security: allow enabled and username fields in put user request body
The enabled and username fields are both now allowed in the request body for the put user request. This makes it easier to perform a get and update a user without needing to edit more of the request body than necessary. Closes elastic/elasticsearch#3391 Original commit: elastic/x-pack-elasticsearch@ab763e843b
This commit is contained in:
parent
5f4e6164e5
commit
2358309f72
|
@ -8,7 +8,6 @@ package org.elasticsearch.xpack.security.action.user;
|
||||||
import org.elasticsearch.action.ActionRequest;
|
import org.elasticsearch.action.ActionRequest;
|
||||||
import org.elasticsearch.action.ActionRequestValidationException;
|
import org.elasticsearch.action.ActionRequestValidationException;
|
||||||
import org.elasticsearch.action.support.WriteRequest;
|
import org.elasticsearch.action.support.WriteRequest;
|
||||||
import org.elasticsearch.action.support.WriteRequest.RefreshPolicy;
|
|
||||||
import org.elasticsearch.common.Nullable;
|
import org.elasticsearch.common.Nullable;
|
||||||
import org.elasticsearch.common.bytes.BytesArray;
|
import org.elasticsearch.common.bytes.BytesArray;
|
||||||
import org.elasticsearch.common.bytes.BytesReference;
|
import org.elasticsearch.common.bytes.BytesReference;
|
||||||
|
@ -33,6 +32,7 @@ public class PutUserRequest extends ActionRequest<PutUserRequest> implements Use
|
||||||
private String email;
|
private String email;
|
||||||
private Map<String, Object> metadata;
|
private Map<String, Object> metadata;
|
||||||
private char[] passwordHash;
|
private char[] passwordHash;
|
||||||
|
private boolean enabled = true;
|
||||||
private RefreshPolicy refreshPolicy = RefreshPolicy.IMMEDIATE;
|
private RefreshPolicy refreshPolicy = RefreshPolicy.IMMEDIATE;
|
||||||
|
|
||||||
public PutUserRequest() {
|
public PutUserRequest() {
|
||||||
|
@ -79,6 +79,10 @@ public class PutUserRequest extends ActionRequest<PutUserRequest> implements Use
|
||||||
this.passwordHash = passwordHash;
|
this.passwordHash = passwordHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean enabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Should this request trigger a refresh ({@linkplain RefreshPolicy#IMMEDIATE}, the default), wait for a refresh (
|
* Should this request trigger a refresh ({@linkplain RefreshPolicy#IMMEDIATE}, the default), wait for a refresh (
|
||||||
* {@linkplain RefreshPolicy#WAIT_UNTIL}), or proceed ignore refreshes entirely ({@linkplain RefreshPolicy#NONE}).
|
* {@linkplain RefreshPolicy#WAIT_UNTIL}), or proceed ignore refreshes entirely ({@linkplain RefreshPolicy#NONE}).
|
||||||
|
@ -119,6 +123,10 @@ public class PutUserRequest extends ActionRequest<PutUserRequest> implements Use
|
||||||
return passwordHash;
|
return passwordHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void enabled(boolean enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String[] usernames() {
|
public String[] usernames() {
|
||||||
return new String[] { username };
|
return new String[] { username };
|
||||||
|
@ -139,6 +147,7 @@ public class PutUserRequest extends ActionRequest<PutUserRequest> implements Use
|
||||||
email = in.readOptionalString();
|
email = in.readOptionalString();
|
||||||
metadata = in.readBoolean() ? in.readMap() : null;
|
metadata = in.readBoolean() ? in.readMap() : null;
|
||||||
refreshPolicy = RefreshPolicy.readFrom(in);
|
refreshPolicy = RefreshPolicy.readFrom(in);
|
||||||
|
enabled = in.readBoolean();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -162,5 +171,6 @@ public class PutUserRequest extends ActionRequest<PutUserRequest> implements Use
|
||||||
out.writeMap(metadata);
|
out.writeMap(metadata);
|
||||||
}
|
}
|
||||||
refreshPolicy.writeTo(out);
|
refreshPolicy.writeTo(out);
|
||||||
|
out.writeBoolean(enabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import org.elasticsearch.common.ValidationException;
|
||||||
import org.elasticsearch.common.bytes.BytesReference;
|
import org.elasticsearch.common.bytes.BytesReference;
|
||||||
import org.elasticsearch.common.xcontent.XContentHelper;
|
import org.elasticsearch.common.xcontent.XContentHelper;
|
||||||
import org.elasticsearch.common.xcontent.XContentParser;
|
import org.elasticsearch.common.xcontent.XContentParser;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentParser.Token;
|
||||||
import org.elasticsearch.xpack.security.authc.support.Hasher;
|
import org.elasticsearch.xpack.security.authc.support.Hasher;
|
||||||
import org.elasticsearch.xpack.security.authc.support.SecuredString;
|
import org.elasticsearch.xpack.security.authc.support.SecuredString;
|
||||||
import org.elasticsearch.xpack.security.support.Validation;
|
import org.elasticsearch.xpack.security.support.Validation;
|
||||||
|
@ -84,6 +85,11 @@ public class PutUserRequestBuilder extends ActionRequestBuilder<PutUserRequest,
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PutUserRequestBuilder enabled(boolean enabled) {
|
||||||
|
request.enabled(enabled);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public PutUserRequestBuilder source(String username, BytesReference source) throws IOException {
|
public PutUserRequestBuilder source(String username, BytesReference source) throws IOException {
|
||||||
username(username);
|
username(username);
|
||||||
try (XContentParser parser = XContentHelper.createParser(source)) {
|
try (XContentParser parser = XContentHelper.createParser(source)) {
|
||||||
|
@ -98,7 +104,6 @@ public class PutUserRequestBuilder extends ActionRequestBuilder<PutUserRequest,
|
||||||
String password = parser.text();
|
String password = parser.text();
|
||||||
char[] passwordChars = password.toCharArray();
|
char[] passwordChars = password.toCharArray();
|
||||||
password(passwordChars);
|
password(passwordChars);
|
||||||
password = null;
|
|
||||||
Arrays.fill(passwordChars, (char) 0);
|
Arrays.fill(passwordChars, (char) 0);
|
||||||
} else {
|
} else {
|
||||||
throw new ElasticsearchParseException(
|
throw new ElasticsearchParseException(
|
||||||
|
@ -139,6 +144,23 @@ public class PutUserRequestBuilder extends ActionRequestBuilder<PutUserRequest,
|
||||||
throw new ElasticsearchParseException(
|
throw new ElasticsearchParseException(
|
||||||
"expected field [{}] to be of type object, but found [{}] instead", currentFieldName, token);
|
"expected field [{}] to be of type object, but found [{}] instead", currentFieldName, token);
|
||||||
}
|
}
|
||||||
|
} else if (ParseFieldMatcher.STRICT.match(currentFieldName, User.Fields.ENABLED)) {
|
||||||
|
if (token == XContentParser.Token.VALUE_BOOLEAN) {
|
||||||
|
enabled(parser.booleanValue());
|
||||||
|
} else {
|
||||||
|
throw new ElasticsearchParseException(
|
||||||
|
"expected field [{}] to be of type boolean, but found [{}] instead", currentFieldName, token);
|
||||||
|
}
|
||||||
|
} else if (ParseFieldMatcher.STRICT.match(currentFieldName, User.Fields.USERNAME)) {
|
||||||
|
if (token == Token.VALUE_STRING) {
|
||||||
|
if (username.equals(parser.text()) == false) {
|
||||||
|
throw new IllegalArgumentException("[username] in source does not match the username provided [" +
|
||||||
|
username + "]");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new ElasticsearchParseException(
|
||||||
|
"expected field [{}] to be of type string, but found [{}] instead", currentFieldName, token);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new ElasticsearchParseException("failed to parse add user request. unexpected field [{}]", currentFieldName);
|
throw new ElasticsearchParseException("failed to parse add user request. unexpected field [{}]", currentFieldName);
|
||||||
}
|
}
|
||||||
|
|
|
@ -379,7 +379,7 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL
|
||||||
if (request.passwordHash() == null) {
|
if (request.passwordHash() == null) {
|
||||||
updateUserWithoutPassword(request, listener);
|
updateUserWithoutPassword(request, listener);
|
||||||
} else {
|
} else {
|
||||||
upsertUser(request, listener);
|
indexUser(request, listener);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error((Supplier<?>) () -> new ParameterizedMessage("unable to put user [{}]", request.username()), e);
|
logger.error((Supplier<?>) () -> new ParameterizedMessage("unable to put user [{}]", request.username()), e);
|
||||||
|
@ -398,7 +398,8 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL
|
||||||
User.Fields.ROLES.getPreferredName(), putUserRequest.roles(),
|
User.Fields.ROLES.getPreferredName(), putUserRequest.roles(),
|
||||||
User.Fields.FULL_NAME.getPreferredName(), putUserRequest.fullName(),
|
User.Fields.FULL_NAME.getPreferredName(), putUserRequest.fullName(),
|
||||||
User.Fields.EMAIL.getPreferredName(), putUserRequest.email(),
|
User.Fields.EMAIL.getPreferredName(), putUserRequest.email(),
|
||||||
User.Fields.METADATA.getPreferredName(), putUserRequest.metadata())
|
User.Fields.METADATA.getPreferredName(), putUserRequest.metadata(),
|
||||||
|
User.Fields.ENABLED.getPreferredName(), putUserRequest.enabled())
|
||||||
.setRefreshPolicy(putUserRequest.getRefreshPolicy())
|
.setRefreshPolicy(putUserRequest.getRefreshPolicy())
|
||||||
.execute(new ActionListener<UpdateResponse>() {
|
.execute(new ActionListener<UpdateResponse>() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -424,27 +425,21 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void upsertUser(final PutUserRequest putUserRequest, final ActionListener<Boolean> listener) {
|
private void indexUser(final PutUserRequest putUserRequest, final ActionListener<Boolean> listener) {
|
||||||
assert putUserRequest.passwordHash() != null;
|
assert putUserRequest.passwordHash() != null;
|
||||||
client.prepareUpdate(SecurityTemplateService.SECURITY_INDEX_NAME,
|
client.prepareIndex(SecurityTemplateService.SECURITY_INDEX_NAME,
|
||||||
USER_DOC_TYPE, putUserRequest.username())
|
USER_DOC_TYPE, putUserRequest.username())
|
||||||
.setDoc(User.Fields.USERNAME.getPreferredName(), putUserRequest.username(),
|
.setSource(User.Fields.USERNAME.getPreferredName(), putUserRequest.username(),
|
||||||
User.Fields.PASSWORD.getPreferredName(), String.valueOf(putUserRequest.passwordHash()),
|
|
||||||
User.Fields.ROLES.getPreferredName(), putUserRequest.roles(),
|
|
||||||
User.Fields.FULL_NAME.getPreferredName(), putUserRequest.fullName(),
|
|
||||||
User.Fields.EMAIL.getPreferredName(), putUserRequest.email(),
|
|
||||||
User.Fields.METADATA.getPreferredName(), putUserRequest.metadata())
|
|
||||||
.setUpsert(User.Fields.USERNAME.getPreferredName(), putUserRequest.username(),
|
|
||||||
User.Fields.PASSWORD.getPreferredName(), String.valueOf(putUserRequest.passwordHash()),
|
User.Fields.PASSWORD.getPreferredName(), String.valueOf(putUserRequest.passwordHash()),
|
||||||
User.Fields.ROLES.getPreferredName(), putUserRequest.roles(),
|
User.Fields.ROLES.getPreferredName(), putUserRequest.roles(),
|
||||||
User.Fields.FULL_NAME.getPreferredName(), putUserRequest.fullName(),
|
User.Fields.FULL_NAME.getPreferredName(), putUserRequest.fullName(),
|
||||||
User.Fields.EMAIL.getPreferredName(), putUserRequest.email(),
|
User.Fields.EMAIL.getPreferredName(), putUserRequest.email(),
|
||||||
User.Fields.METADATA.getPreferredName(), putUserRequest.metadata(),
|
User.Fields.METADATA.getPreferredName(), putUserRequest.metadata(),
|
||||||
User.Fields.ENABLED.getPreferredName(), true)
|
User.Fields.ENABLED.getPreferredName(), putUserRequest.enabled())
|
||||||
.setRefreshPolicy(putUserRequest.getRefreshPolicy())
|
.setRefreshPolicy(putUserRequest.getRefreshPolicy())
|
||||||
.execute(new ActionListener<UpdateResponse>() {
|
.execute(new ActionListener<IndexResponse>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(UpdateResponse updateResponse) {
|
public void onResponse(IndexResponse updateResponse) {
|
||||||
clearRealmCache(putUserRequest.username(), listener, updateResponse.getResult() == DocWriteResponse.Result.CREATED);
|
clearRealmCache(putUserRequest.username(), listener, updateResponse.getResult() == DocWriteResponse.Result.CREATED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,7 @@ public class PutUserRequestBuilderTests extends ESTestCase {
|
||||||
assertThat(request.fullName(), nullValue());
|
assertThat(request.fullName(), nullValue());
|
||||||
assertThat(request.email(), nullValue());
|
assertThat(request.email(), nullValue());
|
||||||
assertThat(request.metadata().isEmpty(), is(true));
|
assertThat(request.metadata().isEmpty(), is(true));
|
||||||
|
assertTrue(request.enabled());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testMissingEmailFullName() throws Exception {
|
public void testMissingEmailFullName() throws Exception {
|
||||||
|
@ -113,4 +114,20 @@ public class PutUserRequestBuilderTests extends ESTestCase {
|
||||||
() -> builder.source("kibana4", new BytesArray(json.getBytes(StandardCharsets.UTF_8))));
|
() -> builder.source("kibana4", new BytesArray(json.getBytes(StandardCharsets.UTF_8))));
|
||||||
assertThat(e.getMessage(), containsString("expected field [email] to be of type string"));
|
assertThat(e.getMessage(), containsString("expected field [email] to be of type string"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void testWithEnabled() throws IOException {
|
||||||
|
final String json = "{\n" +
|
||||||
|
" \"roles\": [\n" +
|
||||||
|
" \"kibana4\"\n" +
|
||||||
|
" ],\n" +
|
||||||
|
" \"full_name\": \"Kibana User\",\n" +
|
||||||
|
" \"email\": \"kibana@elastic.co\",\n" +
|
||||||
|
" \"metadata\": {}\n," +
|
||||||
|
" \"enabled\": false\n" +
|
||||||
|
"}";
|
||||||
|
|
||||||
|
PutUserRequestBuilder builder = new PutUserRequestBuilder(mock(Client.class));
|
||||||
|
PutUserRequest request = builder.source("kibana4", new BytesArray(json.getBytes(StandardCharsets.UTF_8))).request();
|
||||||
|
assertFalse(request.enabled());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,3 +47,57 @@ teardown:
|
||||||
- match: { joe.email: "joe@bazooka.gum" }
|
- match: { joe.email: "joe@bazooka.gum" }
|
||||||
- match: { joe.metadata.key1: "val1" }
|
- match: { joe.metadata.key1: "val1" }
|
||||||
- match: { joe.metadata.key2: "val2" }
|
- match: { joe.metadata.key2: "val2" }
|
||||||
|
|
||||||
|
---
|
||||||
|
"Test put user with username in body":
|
||||||
|
- do:
|
||||||
|
xpack.security.put_user:
|
||||||
|
username: "joe"
|
||||||
|
body: >
|
||||||
|
{
|
||||||
|
"username": "joe",
|
||||||
|
"password" : "s3krit",
|
||||||
|
"roles" : [ "superuser" ],
|
||||||
|
"full_name" : "Bazooka Joe",
|
||||||
|
"email" : "joe@bazooka.gum",
|
||||||
|
"metadata" : {
|
||||||
|
"key1" : "val1",
|
||||||
|
"key2" : "val2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
- match: { user: { created: true } }
|
||||||
|
|
||||||
|
- do:
|
||||||
|
headers:
|
||||||
|
Authorization: "Basic am9lOnMza3JpdA=="
|
||||||
|
cluster.health: {}
|
||||||
|
- match: { timed_out: false }
|
||||||
|
|
||||||
|
- do:
|
||||||
|
xpack.security.get_user:
|
||||||
|
username: "joe"
|
||||||
|
- match: { joe.username: "joe" }
|
||||||
|
- match: { joe.roles.0: "superuser" }
|
||||||
|
- match: { joe.full_name: "Bazooka Joe" }
|
||||||
|
- match: { joe.email: "joe@bazooka.gum" }
|
||||||
|
- match: { joe.metadata.key1: "val1" }
|
||||||
|
- match: { joe.metadata.key2: "val2" }
|
||||||
|
|
||||||
|
---
|
||||||
|
"Test put user with different username in body":
|
||||||
|
- do:
|
||||||
|
catch: request
|
||||||
|
xpack.security.put_user:
|
||||||
|
username: "joe"
|
||||||
|
body: >
|
||||||
|
{
|
||||||
|
"username": "joey",
|
||||||
|
"password" : "s3krit",
|
||||||
|
"roles" : [ "superuser" ],
|
||||||
|
"full_name" : "Bazooka Joe",
|
||||||
|
"email" : "joe@bazooka.gum",
|
||||||
|
"metadata" : {
|
||||||
|
"key1" : "val1",
|
||||||
|
"key2" : "val2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
---
|
||||||
|
setup:
|
||||||
|
- skip:
|
||||||
|
features: [headers, catch_unauthorized]
|
||||||
|
- do:
|
||||||
|
cluster.health:
|
||||||
|
wait_for_status: yellow
|
||||||
|
|
||||||
|
- do:
|
||||||
|
xpack.security.put_user:
|
||||||
|
username: "joe"
|
||||||
|
body: >
|
||||||
|
{
|
||||||
|
"password": "s3krit",
|
||||||
|
"roles" : [ "superuser" ],
|
||||||
|
"enabled": false
|
||||||
|
}
|
||||||
|
---
|
||||||
|
teardown:
|
||||||
|
- do:
|
||||||
|
xpack.security.delete_user:
|
||||||
|
username: "joe"
|
||||||
|
ignore: 404
|
||||||
|
|
||||||
|
---
|
||||||
|
"Test disable then enable user":
|
||||||
|
# validate user cannot login
|
||||||
|
- do:
|
||||||
|
catch: unauthorized
|
||||||
|
headers:
|
||||||
|
Authorization: "Basic am9lOnMza3JpdA=="
|
||||||
|
cluster.health: {}
|
||||||
|
|
||||||
|
# enable
|
||||||
|
- do:
|
||||||
|
xpack.security.enable_user:
|
||||||
|
username: "joe"
|
||||||
|
|
||||||
|
# validate user can login
|
||||||
|
- do:
|
||||||
|
headers:
|
||||||
|
Authorization: "Basic am9lOnMza3JpdA=="
|
||||||
|
cluster.health: {}
|
||||||
|
- match: { timed_out: false }
|
Loading…
Reference in New Issue