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:
jaymode 2016-09-09 10:13:54 -04:00
parent 5f4e6164e5
commit 2358309f72
6 changed files with 158 additions and 16 deletions

View File

@ -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);
} }
} }

View File

@ -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);
} }

View File

@ -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);
} }

View File

@ -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());
}
} }

View File

@ -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"
}
}

View File

@ -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 }