Introducing user full name, email and metadata.

- `full_name` and `email` are optional user fields
- `metadata` is an optional arbitrary meta data that can be associated with the user
- cleaned up the user actions - consistent naming (e.g. `PutUserAction` vs. `AddUserAction`)
- moved source parsing from the `PutUserRequest` to the `PutUserRequestBuilder`
- renamed`WatcherXContentUtils` to `XContentUtils` and moved it to sit under `o.e.xpack.commons.xcontent`

Closes elastic/elasticsearch#412

Original commit: elastic/x-pack-elasticsearch@5460e3caf7
This commit is contained in:
uboness 2016-02-13 03:59:57 +01:00
parent 6d0d09468b
commit 18b08c82ca
39 changed files with 751 additions and 499 deletions

View File

@ -28,7 +28,7 @@ import org.elasticsearch.shield.action.role.TransportAddRoleAction;
import org.elasticsearch.shield.action.role.TransportClearRolesCacheAction;
import org.elasticsearch.shield.action.role.TransportDeleteRoleAction;
import org.elasticsearch.shield.action.role.TransportGetRolesAction;
import org.elasticsearch.shield.action.user.AddUserAction;
import org.elasticsearch.shield.action.user.PutUserAction;
import org.elasticsearch.shield.action.user.DeleteUserAction;
import org.elasticsearch.shield.action.user.GetUsersAction;
import org.elasticsearch.shield.action.user.TransportAddUserAction;
@ -59,7 +59,7 @@ import org.elasticsearch.shield.rest.action.role.RestAddRoleAction;
import org.elasticsearch.shield.rest.action.role.RestClearRolesCacheAction;
import org.elasticsearch.shield.rest.action.role.RestDeleteRoleAction;
import org.elasticsearch.shield.rest.action.role.RestGetRolesAction;
import org.elasticsearch.shield.rest.action.user.RestAddUserAction;
import org.elasticsearch.shield.rest.action.user.RestPutUserAction;
import org.elasticsearch.shield.rest.action.user.RestDeleteUserAction;
import org.elasticsearch.shield.rest.action.user.RestGetUsersAction;
import org.elasticsearch.shield.ssl.SSLModule;
@ -242,7 +242,7 @@ public class Shield {
module.registerAction(ClearRealmCacheAction.INSTANCE, TransportClearRealmCacheAction.class);
module.registerAction(ClearRolesCacheAction.INSTANCE, TransportClearRolesCacheAction.class);
module.registerAction(GetUsersAction.INSTANCE, TransportGetUsersAction.class);
module.registerAction(AddUserAction.INSTANCE, TransportAddUserAction.class);
module.registerAction(PutUserAction.INSTANCE, TransportAddUserAction.class);
module.registerAction(DeleteUserAction.INSTANCE, TransportDeleteUserAction.class);
module.registerAction(GetRolesAction.INSTANCE, TransportGetRolesAction.class);
module.registerAction(AddRoleAction.INSTANCE, TransportAddRoleAction.class);
@ -269,7 +269,7 @@ public class Shield {
module.registerRestHandler(RestClearRealmCacheAction.class);
module.registerRestHandler(RestClearRolesCacheAction.class);
module.registerRestHandler(RestGetUsersAction.class);
module.registerRestHandler(RestAddUserAction.class);
module.registerRestHandler(RestPutUserAction.class);
module.registerRestHandler(RestDeleteUserAction.class);
module.registerRestHandler(RestGetRolesAction.class);
module.registerRestHandler(RestAddRoleAction.class);

View File

@ -6,14 +6,20 @@
package org.elasticsearch.shield;
import org.elasticsearch.ElasticsearchSecurityException;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.inject.internal.Nullable;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import java.io.IOException;
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
/**
* An authenticated user
@ -23,19 +29,37 @@ public class User implements ToXContent {
private final String username;
private final String[] roles;
private final User runAs;
private final Map<String, Object> metadata;
private final @Nullable String fullName;
private final @Nullable String email;
public User(String username, String... roles) {
this.username = username;
this.roles = roles == null ? Strings.EMPTY_ARRAY : roles;
this.runAs = null;
this(username, roles, null, null, null);
}
public User(String username, String[] roles, User runAs) {
this(username, roles, null, null, null, runAs);
}
public User(String username, String[] roles, String fullName, String email, Map<String, Object> metadata) {
this.username = username;
this.roles = roles == null ? Strings.EMPTY_ARRAY : roles;
assert (runAs == null || runAs.runAs() == null) : "the runAs user should not be a user that can run as";
this.metadata = metadata != null ? Collections.unmodifiableMap(metadata) : Collections.emptyMap();
this.fullName = fullName;
this.email = email;
this.runAs = null;
}
public User(String username, String[] roles, String fullName, String email, Map<String, Object> metadata, User runAs) {
this.username = username;
this.roles = roles == null ? Strings.EMPTY_ARRAY : roles;
this.metadata = metadata != null ? Collections.unmodifiableMap(metadata) : Collections.emptyMap();
this.fullName = fullName;
this.email = email;
assert (runAs == null || runAs.runAs() == null) : "the run_as user should not be a user that can run as";
if (runAs == SystemUser.INSTANCE) {
throw new ElasticsearchSecurityException("the runAs user cannot be the internal system user");
throw new ElasticsearchSecurityException("invalid run_as user");
}
this.runAs = runAs;
}
@ -57,6 +81,27 @@ public class User implements ToXContent {
return this.roles;
}
/**
* @return The metadata that is associated with this user. Can never be {@code null}.
*/
public Map<String, Object> metadata() {
return metadata;
}
/**
* @return The full name of this user. May be {@code null}.
*/
public String fullName() {
return fullName;
}
/**
* @return The email of this user. May be {@code null}.
*/
public String email() {
return email;
}
/**
* @return The user that will be used for run as functionality. If run as
* functionality is not being used, then <code>null</code> will be
@ -70,13 +115,11 @@ public class User implements ToXContent {
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("User[username=").append(username);
sb.append(",roles=[");
if (roles != null) {
for (String role : roles) {
sb.append(role).append(",");
}
}
sb.append("]");
sb.append(",roles=[").append(Strings.arrayToCommaDelimitedString(roles)).append("]");
sb.append(",fullName=").append(fullName);
sb.append(",email=").append(email);
sb.append(",metadata=");
append(sb, metadata);
if (runAs != null) {
sb.append(",runAs=[").append(runAs.toString()).append("]");
}
@ -87,31 +130,39 @@ public class User implements ToXContent {
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null) return false;
if (!(o instanceof User)) return false;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
if (!principal().equals(user.principal())) return false;
if (!Arrays.equals(roles(), user.roles())) return false;
if (runAs != null ? !runAs.equals(user.runAs) : user.runAs != null) {
return false;
}
return true;
if (!username.equals(user.username)) return false;
// Probably incorrect - comparing Object[] arrays with Arrays.equals
if (!Arrays.equals(roles, user.roles)) return false;
if (runAs != null ? !runAs.equals(user.runAs) : user.runAs != null) return false;
if (!metadata.equals(user.metadata)) return false;
if (fullName != null ? !fullName.equals(user.fullName) : user.fullName != null) return false;
return !(email != null ? !email.equals(user.email) : user.email != null);
}
@Override
public int hashCode() {
int result = principal().hashCode();
result = 31 * result + Arrays.hashCode(roles());
int result = username.hashCode();
result = 31 * result + Arrays.hashCode(roles);
result = 31 * result + (runAs != null ? runAs.hashCode() : 0);
result = 31 * result + metadata.hashCode();
result = 31 * result + (fullName != null ? fullName.hashCode() : 0);
result = 31 * result + (email != null ? email.hashCode() : 0);
return result;
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
builder.field("username", principal());
builder.array("roles", roles());
builder.field(Fields.USERNAME.getPreferredName(), principal());
builder.array(Fields.ROLES.getPreferredName(), roles());
builder.field(Fields.FULL_NAME.getPreferredName(), fullName);
builder.field(Fields.EMAIL.getPreferredName(), email);
builder.field(Fields.METADATA.getPreferredName(), metadata);
builder.endObject();
return builder;
}
@ -130,12 +181,19 @@ public class User implements ToXContent {
}
String username = input.readString();
String[] roles = input.readStringArray();
Map<String, Object> metadata = input.readMap();
String fullName = input.readOptionalString();
String email = input.readOptionalString();
if (input.readBoolean()) {
String runAsUsername = input.readString();
String[] runAsRoles = input.readStringArray();
return new User(username, roles, new User(runAsUsername, runAsRoles));
Map<String, Object> runAsMetadata = input.readMap();
String runAsFullName = input.readOptionalString();
String runAsEmail = input.readOptionalString();
User runAs = new User(runAsUsername, runAsRoles, runAsFullName, runAsEmail, runAsMetadata);
return new User(username, roles, fullName, email, metadata, runAs);
}
return new User(username, roles);
return new User(username, roles, fullName, email, metadata);
}
public static void writeTo(User user, StreamOutput output) throws IOException {
@ -147,16 +205,67 @@ public class User implements ToXContent {
output.writeString(XPackUser.NAME);
} else {
output.writeBoolean(false);
output.writeString(user.principal());
output.writeStringArray(user.roles());
output.writeString(user.username);
output.writeStringArray(user.roles);
output.writeMap(user.metadata);
output.writeOptionalString(user.fullName);
output.writeOptionalString(user.email);
if (user.runAs == null) {
output.writeBoolean(false);
} else {
output.writeBoolean(true);
output.writeString(user.runAs.principal());
output.writeStringArray(user.runAs.roles());
output.writeString(user.runAs.username);
output.writeStringArray(user.runAs.roles);
output.writeMap(user.runAs.metadata);
output.writeOptionalString(user.runAs.fullName);
output.writeOptionalString(user.runAs.email);
}
}
}
public static void append(StringBuilder sb, Object object) {
if (object == null) {
sb.append((Object) null);
}
if (object instanceof Map) {
sb.append("{");
for (Map.Entry<String, Object> entry : ((Map<String, Object>)object).entrySet()) {
sb.append(entry.getKey()).append("=");
append(sb, entry.getValue());
}
sb.append("}");
} else if (object instanceof Collection) {
sb.append("[");
boolean first = true;
for (Object item : (Collection) object) {
if (!first) {
sb.append(",");
}
append(sb, item);
first = false;
}
sb.append("]");
} else if (object.getClass().isArray()) {
sb.append("[");
for (int i = 0; i < Array.getLength(object); i++) {
if (i != 0) {
sb.append(",");
}
append(sb, Array.get(object, i));
}
sb.append("]");
} else {
sb.append(object);
}
}
public interface Fields {
ParseField USERNAME = new ParseField("username");
ParseField PASSWORD = new ParseField("password");
ParseField ROLES = new ParseField("roles");
ParseField FULL_NAME = new ParseField("full_name");
ParseField EMAIL = new ParseField("email");
ParseField METADATA = new ParseField("metadata");
}
}

View File

@ -1,130 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.action.user;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.XContentHelper;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.shield.authc.support.CharArrays;
import org.elasticsearch.shield.authc.support.Hasher;
import org.elasticsearch.shield.authc.support.SecuredString;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static org.elasticsearch.action.ValidateActions.addValidationError;
/**
* Request object to add a {@code User} to the shield administrative index
*/
public class AddUserRequest extends ActionRequest<AddUserRequest> {
private final Hasher hasher = Hasher.BCRYPT;
private String username;
private String roles[];
private char[] passwordHash;
public AddUserRequest() {
}
@Override
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = null;
if (username == null) {
validationException = addValidationError("user is missing", validationException);
}
if (roles == null) {
validationException = addValidationError("roles are missing", validationException);
}
if (passwordHash == null) {
validationException = addValidationError("passwordHash is missing", validationException);
}
return validationException;
}
public void username(String username) {
this.username = username;
}
public void roles(String... roles) {
this.roles = roles;
}
public void passwordHash(char[] passwordHash) {
this.passwordHash = passwordHash;
}
public String username() {
return username;
}
public String[] roles() {
return roles;
}
public char[] passwordHash() {
return passwordHash;
}
public AddUserRequest source(BytesReference source) throws Exception {
List<String> parsedRoles = new ArrayList<>();
try (XContentParser parser = XContentHelper.createParser(source)) {
XContentParser.Token token;
String currentFieldName = null;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
} else if (token.isValue()) {
if ("username".equals(currentFieldName)) {
username = parser.text();
} else if ("password".equals(currentFieldName)) {
// It's assumed the password is plaintext and needs to be hashed
passwordHash = hasher.hash(new SecuredString(parser.text().toCharArray()));
} else if ("roles".equals(currentFieldName)) {
parsedRoles.add(parser.text());
} else {
throw new ElasticsearchParseException("unexpected field in add user request [{}]", currentFieldName);
}
} else if (token == XContentParser.Token.START_OBJECT) {
// expected
} else if (token == XContentParser.Token.START_ARRAY || token == XContentParser.Token.END_ARRAY) {
if ("roles".equals(currentFieldName) == false) {
throw new ElasticsearchParseException("unexpected array for field [{}]", currentFieldName);
}
} else {
throw new ElasticsearchParseException("failed to parse add user request, got value with wrong type [{}]",
currentFieldName);
}
}
}
roles = parsedRoles.toArray(Strings.EMPTY_ARRAY);
return this;
}
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
username = in.readString();
passwordHash = CharArrays.utf8BytesToChars(in.readByteArray());
roles = in.readStringArray();
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeString(username);
out.writeByteArray(CharArrays.toUtf8Bytes(passwordHash));
out.writeStringArray(roles);
}
}

View File

@ -1,39 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.action.user;
import org.elasticsearch.action.ActionRequestBuilder;
import org.elasticsearch.client.ElasticsearchClient;
import org.elasticsearch.shield.authc.support.Hasher;
import org.elasticsearch.shield.authc.support.SecuredString;
public class AddUserRequestBuilder extends ActionRequestBuilder<AddUserRequest, AddUserResponse, AddUserRequestBuilder> {
private final Hasher hasher = Hasher.BCRYPT;
public AddUserRequestBuilder(ElasticsearchClient client) {
this(client, AddUserAction.INSTANCE);
}
public AddUserRequestBuilder(ElasticsearchClient client, AddUserAction action) {
super(client, action, new AddUserRequest());
}
public AddUserRequestBuilder username(String username) {
request.username(username);
return this;
}
public AddUserRequestBuilder roles(String... roles) {
request.roles(roles);
return this;
}
public AddUserRequestBuilder password(String password) {
request.passwordHash(hasher.hash(new SecuredString(password.toCharArray())));
return this;
}
}

View File

@ -9,14 +9,13 @@ import org.elasticsearch.action.Action;
import org.elasticsearch.client.ElasticsearchClient;
/**
* Action for deleting a user from the shield administrative index
* Action for deleting a native user.
*/
public class DeleteUserAction extends Action<DeleteUserRequest, DeleteUserResponse, DeleteUserRequestBuilder> {
public static final DeleteUserAction INSTANCE = new DeleteUserAction();
public static final String NAME = "cluster:admin/shield/user/delete";
protected DeleteUserAction() {
super(NAME);
}

View File

@ -15,7 +15,7 @@ import java.io.IOException;
import static org.elasticsearch.action.ValidateActions.addValidationError;
/**
* A request to delete a user from the shield administrative index by username
* A request to delete a native user.
*/
public class DeleteUserRequest extends ActionRequest<DeleteUserRequest> {
@ -24,24 +24,24 @@ public class DeleteUserRequest extends ActionRequest<DeleteUserRequest> {
public DeleteUserRequest() {
}
public DeleteUserRequest(String user) {
this.username = user;
public DeleteUserRequest(String username) {
this.username = username;
}
@Override
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = null;
if (username == null) {
validationException = addValidationError("user is missing", validationException);
validationException = addValidationError("username is missing", validationException);
}
return validationException;
}
public String user() {
public String username() {
return this.username;
}
public void user(String username) {
public void username(String username) {
this.username = username;
}

View File

@ -18,8 +18,8 @@ public class DeleteUserRequestBuilder extends ActionRequestBuilder<DeleteUserReq
super(client, action, new DeleteUserRequest());
}
public DeleteUserRequestBuilder user(String username) {
request.user(username);
public DeleteUserRequestBuilder username(String username) {
request.username(username);
return this;
}
}

View File

@ -14,16 +14,14 @@ import org.elasticsearch.common.xcontent.XContentBuilder;
import java.io.IOException;
/**
* Response when deleting a user from the shield administrative index. Returns a
* single boolean field for whether the user was found (and deleted) or not
* found.
* Response when deleting a native user. Returns a single boolean field for whether the user was
* found (and deleted) or not found.
*/
public class DeleteUserResponse extends ActionResponse implements ToXContent {
private boolean found;
public DeleteUserResponse() {
}
public DeleteUserResponse(boolean found) {

View File

@ -16,7 +16,6 @@ public class GetUsersAction extends Action<GetUsersRequest, GetUsersResponse, Ge
public static final GetUsersAction INSTANCE = new GetUsersAction();
public static final String NAME = "cluster:admin/shield/user/get";
protected GetUsersAction() {
super(NAME);
}

View File

@ -16,43 +16,43 @@ import java.io.IOException;
import static org.elasticsearch.action.ValidateActions.addValidationError;
/**
* Request to retrieve a user from the shield administrative index from a username
* Request to retrieve a native user.
*/
public class GetUsersRequest extends ActionRequest<GetUsersRequest> {
private String[] users;
private String[] usernames;
public GetUsersRequest() {
users = Strings.EMPTY_ARRAY;
usernames = Strings.EMPTY_ARRAY;
}
@Override
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = null;
if (users == null) {
validationException = addValidationError("users cannot be null", validationException);
if (usernames == null) {
validationException = addValidationError("usernames cannot be null", validationException);
}
return validationException;
}
public void users(String... usernames) {
this.users = usernames;
public void usernames(String... usernames) {
this.usernames = usernames;
}
public String[] users() {
return users;
public String[] usernames() {
return usernames;
}
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
users = in.readStringArray();
usernames = in.readStringArray();
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeStringArray(users);
out.writeStringArray(usernames);
}
}

View File

@ -18,8 +18,8 @@ public class GetUsersRequestBuilder extends ActionRequestBuilder<GetUsersRequest
super(client, action, new GetUsersRequest());
}
public GetUsersRequestBuilder users(String... usernames) {
request.users(usernames);
public GetUsersRequestBuilder usernames(String... usernames) {
request.usernames(usernames);
return this;
}
}

View File

@ -11,52 +11,53 @@ import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.shield.User;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Collection;
/**
* Response containing a User retrieved from the shield administrative index
*/
public class GetUsersResponse extends ActionResponse {
private List<User> users;
public GetUsersResponse() {
this.users = Collections.emptyList();
}
private User[] users;
public GetUsersResponse(User user) {
this.users = Collections.singletonList(user);
}
public GetUsersResponse(List<User> users) {
public GetUsersResponse(User... users) {
this.users = users;
}
public List<User> users() {
public GetUsersResponse(Collection<User> users) {
this(users.toArray(new User[users.size()]));
}
public User[] users() {
return users;
}
public boolean isExists() {
return users != null && users.size() > 0;
public boolean hasUsers() {
return users != null && users.length > 0;
}
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
int size = in.readVInt();
users = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
users.add(User.readFrom(in));
if (size < 0) {
users = null;
} else {
users = new User[size];
for (int i = 0; i < size; i++) {
users[i] = User.readFrom(in);
}
}
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeVInt(users == null ? 0 : users.size());
for (User u : users) {
User.writeTo(u, out);
out.writeVInt(users == null ? -1 : users.length);
if (users != null) {
for (User user : users) {
User.writeTo(user, out);
}
}
}

View File

@ -9,25 +9,24 @@ import org.elasticsearch.action.Action;
import org.elasticsearch.client.ElasticsearchClient;
/**
* Action for adding a user to the shield administrative index
* Action for putting (adding/updating) a native user.
*/
public class AddUserAction extends Action<AddUserRequest, AddUserResponse, AddUserRequestBuilder> {
public class PutUserAction extends Action<PutUserRequest, PutUserResponse, PutUserRequestBuilder> {
public static final AddUserAction INSTANCE = new AddUserAction();
public static final String NAME = "cluster:admin/shield/user/add";
public static final PutUserAction INSTANCE = new PutUserAction();
public static final String NAME = "cluster:admin/shield/user/put";
protected AddUserAction() {
protected PutUserAction() {
super(NAME);
}
@Override
public AddUserRequestBuilder newRequestBuilder(ElasticsearchClient client) {
return new AddUserRequestBuilder(client, this);
public PutUserRequestBuilder newRequestBuilder(ElasticsearchClient client) {
return new PutUserRequestBuilder(client, this);
}
@Override
public AddUserResponse newResponse() {
return new AddUserResponse();
public PutUserResponse newResponse() {
return new PutUserResponse();
}
}

View File

@ -0,0 +1,123 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.action.user;
import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.shield.authc.support.CharArrays;
import java.io.IOException;
import java.util.Map;
import static org.elasticsearch.action.ValidateActions.addValidationError;
/**
* Request object to put a native user.
*/
public class PutUserRequest extends ActionRequest<PutUserRequest> {
private String username;
private String[] roles;
private String fullName;
private String email;
private Map<String, Object> metadata;
private char[] passwordHash;
public PutUserRequest() {
}
@Override
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = null;
if (username == null) {
validationException = addValidationError("user is missing", validationException);
}
if (roles == null) {
validationException = addValidationError("roles are missing", validationException);
}
if (passwordHash == null) {
validationException = addValidationError("passwordHash is missing", validationException);
}
return validationException;
}
public void username(String username) {
this.username = username;
}
public void roles(String... roles) {
this.roles = roles;
}
public void fullName(String fullName) {
this.fullName = fullName;
}
public void email(String email) {
this.email = email;
}
public void metadata(Map<String, Object> metadata) {
this.metadata = metadata;
}
public void passwordHash(char[] passwordHash) {
this.passwordHash = passwordHash;
}
public String username() {
return username;
}
public String[] roles() {
return roles;
}
public String fullName() {
return fullName;
}
public String email() {
return email;
}
public Map<String, Object> metadata() {
return metadata;
}
public char[] passwordHash() {
return passwordHash;
}
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
username = in.readString();
passwordHash = CharArrays.utf8BytesToChars(in.readByteArray());
roles = in.readStringArray();
fullName = in.readOptionalString();
email = in.readOptionalString();
metadata = in.readBoolean() ? in.readMap() : null;
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeString(username);
out.writeByteArray(CharArrays.toUtf8Bytes(passwordHash));
out.writeStringArray(roles);
out.writeOptionalString(fullName);
out.writeOptionalString(email);
if (metadata == null) {
out.writeBoolean(false);
} else {
out.writeBoolean(true);
out.writeMap(metadata);
}
}
}

View File

@ -0,0 +1,128 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.action.user;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.action.ActionRequestBuilder;
import org.elasticsearch.client.ElasticsearchClient;
import org.elasticsearch.common.ParseFieldMatcher;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.xcontent.XContentHelper;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.shield.User;
import org.elasticsearch.shield.authc.support.Hasher;
import org.elasticsearch.shield.authc.support.SecuredString;
import org.elasticsearch.xpack.common.xcontent.XContentUtils;
import java.io.IOException;
import java.util.Arrays;
import java.util.Map;
public class PutUserRequestBuilder extends ActionRequestBuilder<PutUserRequest, PutUserResponse, PutUserRequestBuilder> {
private final Hasher hasher = Hasher.BCRYPT;
public PutUserRequestBuilder(ElasticsearchClient client) {
this(client, PutUserAction.INSTANCE);
}
public PutUserRequestBuilder(ElasticsearchClient client, PutUserAction action) {
super(client, action, new PutUserRequest());
}
public PutUserRequestBuilder username(String username) {
request.username(username);
return this;
}
public PutUserRequestBuilder roles(String... roles) {
request.roles(roles);
return this;
}
public PutUserRequestBuilder password(char[] password) {
request.passwordHash(hasher.hash(new SecuredString(password)));
return this;
}
public PutUserRequestBuilder metadata(Map<String, Object> metadata) {
request.metadata(metadata);
return this;
}
public PutUserRequestBuilder fullName(String fullName) {
request.fullName(fullName);
return this;
}
public PutUserRequestBuilder email(String email) {
request.email(email);
return this;
}
public PutUserRequestBuilder source(BytesReference source) throws IOException {
try (XContentParser parser = XContentHelper.createParser(source)) {
XContentUtils.verifyObject(parser);
XContentParser.Token token;
String currentFieldName = null;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
} else if (ParseFieldMatcher.STRICT.match(currentFieldName, User.Fields.USERNAME)) {
if (token == XContentParser.Token.VALUE_STRING) {
username(parser.text());
} else {
throw new ElasticsearchParseException(
"expected field [{}] to be of type string, but found [{}] instead", currentFieldName, token);
}
} else if (ParseFieldMatcher.STRICT.match(currentFieldName, User.Fields.PASSWORD)) {
if (token == XContentParser.Token.VALUE_STRING) {
String password = parser.text();
char[] passwordChars = password.toCharArray();
password(passwordChars);
password = null;
Arrays.fill(passwordChars, (char) 0);
} else {
throw new ElasticsearchParseException(
"expected field [{}] to be of type string, but found [{}] instead", currentFieldName, token);
}
} else if (ParseFieldMatcher.STRICT.match(currentFieldName, User.Fields.ROLES)) {
if (token == XContentParser.Token.VALUE_STRING) {
roles(Strings.commaDelimitedListToStringArray(parser.text()));
} else {
roles(XContentUtils.readStringArray(parser, false));
}
} else if (ParseFieldMatcher.STRICT.match(currentFieldName, User.Fields.FULL_NAME)) {
if (token == XContentParser.Token.VALUE_STRING) {
fullName(parser.text());
} else {
throw new ElasticsearchParseException(
"expected field [{}] to be of type string, but found [{}] instead", currentFieldName, token);
}
} else if (ParseFieldMatcher.STRICT.match(currentFieldName, User.Fields.EMAIL)) {
if (token == XContentParser.Token.VALUE_STRING) {
email(parser.text());
} else {
throw new ElasticsearchParseException(
"expected field [{}] to be of type string, but found [{}] instead", currentFieldName, token);
}
} else if (ParseFieldMatcher.STRICT.match(currentFieldName, User.Fields.METADATA)) {
if (token == XContentParser.Token.START_OBJECT) {
metadata(parser.map());
} else {
throw new ElasticsearchParseException(
"expected field [{}] to be of type object, but found [{}] instead", currentFieldName, token);
}
} else {
throw new ElasticsearchParseException("failed to parse add user request. unexpected field [{}]", currentFieldName);
}
}
return this;
}
}
}

View File

@ -17,15 +17,14 @@ import java.io.IOException;
* Response when adding a user to the shield administrative index. Returns a
* single boolean field for whether the user was created or updated.
*/
public class AddUserResponse extends ActionResponse implements ToXContent {
public class PutUserResponse extends ActionResponse implements ToXContent {
private boolean created;
public AddUserResponse() {
public PutUserResponse() {
}
public AddUserResponse(boolean created) {
public PutUserResponse(boolean created) {
this.created = created;
}

View File

@ -15,7 +15,7 @@ import org.elasticsearch.shield.authc.esnative.ESNativeUsersStore;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.TransportService;
public class TransportAddUserAction extends HandledTransportAction<AddUserRequest, AddUserResponse> {
public class TransportAddUserAction extends HandledTransportAction<PutUserRequest, PutUserResponse> {
private final ESNativeUsersStore usersStore;
@ -23,13 +23,13 @@ public class TransportAddUserAction extends HandledTransportAction<AddUserReques
public TransportAddUserAction(Settings settings, ThreadPool threadPool, ActionFilters actionFilters,
IndexNameExpressionResolver indexNameExpressionResolver,
ESNativeUsersStore usersStore, TransportService transportService) {
super(settings, AddUserAction.NAME, threadPool, transportService, actionFilters, indexNameExpressionResolver, AddUserRequest::new);
super(settings, PutUserAction.NAME, threadPool, transportService, actionFilters, indexNameExpressionResolver, PutUserRequest::new);
this.usersStore = usersStore;
}
@Override
protected void doExecute(final AddUserRequest request, final ActionListener<AddUserResponse> listener) {
usersStore.addUser(request, new ActionListener<Boolean>() {
protected void doExecute(final PutUserRequest request, final ActionListener<PutUserResponse> listener) {
usersStore.putUser(request, new ActionListener<Boolean>() {
@Override
public void onResponse(Boolean created) {
if (created) {
@ -37,7 +37,7 @@ public class TransportAddUserAction extends HandledTransportAction<AddUserReques
} else {
logger.info("updated user [{}]", request.username());
}
listener.onResponse(new AddUserResponse(created));
listener.onResponse(new PutUserResponse(created));
}
@Override

View File

@ -31,7 +31,7 @@ public class TransportDeleteUserAction extends HandledTransportAction<DeleteUser
@Override
protected void doExecute(DeleteUserRequest request, final ActionListener<DeleteUserResponse> listener) {
try {
usersStore.removeUser(request, new ActionListener<Boolean>() {
usersStore.deleteUser(request, new ActionListener<Boolean>() {
@Override
public void onResponse(Boolean found) {
listener.onResponse(new DeleteUserResponse(found));

View File

@ -34,8 +34,8 @@ public class TransportGetUsersAction extends HandledTransportAction<GetUsersRequ
@Override
protected void doExecute(final GetUsersRequest request, final ActionListener<GetUsersResponse> listener) {
if (request.users().length == 1) {
final String username = request.users()[0];
if (request.usernames().length == 1) {
final String username = request.usernames()[0];
// We can fetch a single user with a get, much cheaper:
usersStore.getUser(username, new ActionListener<User>() {
@Override
@ -54,7 +54,7 @@ public class TransportGetUsersAction extends HandledTransportAction<GetUsersRequ
}
});
} else {
usersStore.getUsers(request.users(), new ActionListener<List<User>>() {
usersStore.getUsers(request.usernames(), new ActionListener<List<User>>() {
@Override
public void onResponse(List<User> users) {
listener.onResponse(new GetUsersResponse(users));
@ -63,7 +63,7 @@ public class TransportGetUsersAction extends HandledTransportAction<GetUsersRequ
@Override
public void onFailure(Throwable e) {
logger.error("failed to retrieve user [{}]", e,
Strings.arrayToDelimitedString(request.users(), ","));
Strings.arrayToDelimitedString(request.usernames(), ","));
listener.onFailure(e);
}
});

View File

@ -49,9 +49,8 @@ import org.elasticsearch.shield.ShieldTemplateService;
import org.elasticsearch.shield.User;
import org.elasticsearch.shield.action.realm.ClearRealmCacheRequest;
import org.elasticsearch.shield.action.realm.ClearRealmCacheResponse;
import org.elasticsearch.shield.action.user.AddUserRequest;
import org.elasticsearch.shield.action.user.DeleteUserRequest;
import org.elasticsearch.shield.authc.AuthenticationService;
import org.elasticsearch.shield.action.user.PutUserRequest;
import org.elasticsearch.shield.authc.support.Hasher;
import org.elasticsearch.shield.authc.support.SecuredString;
import org.elasticsearch.shield.client.SecurityClient;
@ -79,6 +78,15 @@ import java.util.concurrent.atomic.AtomicReference;
*/
public class ESNativeUsersStore extends AbstractComponent implements ClusterStateListener {
public enum State {
INITIALIZED,
STARTING,
STARTED,
STOPPING,
STOPPED,
FAILED
}
// TODO - perhaps separate indices for users/roles instead of types?
public static final String INDEX_USER_TYPE = "user";
@ -93,36 +101,18 @@ public class ESNativeUsersStore extends AbstractComponent implements ClusterStat
private ScheduledFuture<?> versionChecker;
private Client client;
private AuthenticationService authService;
private int scrollSize;
private TimeValue scrollKeepAlive;
private volatile boolean shieldIndexExists = false;
@Inject
public ESNativeUsersStore(Settings settings, Provider<InternalClient> clientProvider,
Provider<AuthenticationService> authProvider, ThreadPool threadPool) {
public ESNativeUsersStore(Settings settings, Provider<InternalClient> clientProvider, ThreadPool threadPool) {
super(settings);
this.clientProvider = clientProvider;
this.threadPool = threadPool;
}
@Nullable
private UserAndPassword transformUser(Map<String, Object> sourceMap) {
if (sourceMap == null) {
return null;
}
try {
String username = (String) sourceMap.get(Fields.USERNAME);
String password = (String) sourceMap.get(Fields.PASSWORD);
String[] roles = ((List<String>) sourceMap.get(Fields.ROLES)).toArray(Strings.EMPTY_ARRAY);
return new UserAndPassword(new User(username, roles), password.toCharArray());
} catch (Exception e) {
logger.error("error in the format of get response for user", e);
return null;
}
}
/**
* Blocking version of {@code getUser} that blocks until the User is returned
*/
@ -296,7 +286,7 @@ public class ESNativeUsersStore extends AbstractComponent implements ClusterStat
}
}
public void addUser(final AddUserRequest addUserRequest, final ActionListener<Boolean> listener) {
public void putUser(final PutUserRequest putUserRequest, final ActionListener<Boolean> listener) {
if (state() != State.STARTED) {
listener.onFailure(new IllegalStateException("user cannot be added as native user service has not been started"));
return;
@ -304,10 +294,13 @@ public class ESNativeUsersStore extends AbstractComponent implements ClusterStat
try {
IndexRequest request = client.prepareIndex(ShieldTemplateService.SHIELD_ADMIN_INDEX_NAME,
INDEX_USER_TYPE, addUserRequest.username())
.setSource("username", addUserRequest.username(),
"password", String.valueOf(addUserRequest.passwordHash()),
"roles", addUserRequest.roles())
INDEX_USER_TYPE, 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())
.request();
client.index(request, new ActionListener<IndexResponse>() {
@ -319,7 +312,7 @@ public class ESNativeUsersStore extends AbstractComponent implements ClusterStat
return;
}
clearRealmCache(addUserRequest.username(), listener, indexResponse.isCreated());
clearRealmCache(putUserRequest.username(), listener, indexResponse.isCreated());
}
@Override
@ -333,7 +326,7 @@ public class ESNativeUsersStore extends AbstractComponent implements ClusterStat
}
}
public void removeUser(final DeleteUserRequest deleteUserRequest, final ActionListener<Boolean> listener) {
public void deleteUser(final DeleteUserRequest deleteUserRequest, final ActionListener<Boolean> listener) {
if (state() != State.STARTED) {
listener.onFailure(new IllegalStateException("user cannot be deleted as native user service has not been started"));
return;
@ -341,12 +334,12 @@ public class ESNativeUsersStore extends AbstractComponent implements ClusterStat
try {
DeleteRequest request = client.prepareDelete(ShieldTemplateService.SHIELD_ADMIN_INDEX_NAME,
INDEX_USER_TYPE, deleteUserRequest.user()).request();
INDEX_USER_TYPE, deleteUserRequest.username()).request();
request.indicesOptions().ignoreUnavailable();
client.delete(request, new ActionListener<DeleteResponse>() {
@Override
public void onResponse(DeleteResponse deleteResponse) {
clearRealmCache(deleteUserRequest.user(), listener, deleteResponse.isFound());
clearRealmCache(deleteUserRequest.username(), listener, deleteResponse.isFound());
}
@Override
@ -504,18 +497,27 @@ public class ESNativeUsersStore extends AbstractComponent implements ClusterStat
this.versionMap.clear();
this.listeners.clear();
this.client = null;
this.authService = null;
this.shieldIndexExists = false;
this.state.set(State.INITIALIZED);
}
public enum State {
INITIALIZED,
STARTING,
STARTED,
STOPPING,
STOPPED,
FAILED
@Nullable
private UserAndPassword transformUser(Map<String, Object> sourceMap) {
if (sourceMap == null) {
return null;
}
try {
String username = (String) sourceMap.get(User.Fields.USERNAME.getPreferredName());
String password = (String) sourceMap.get(User.Fields.PASSWORD.getPreferredName());
String[] roles = ((List<String>) sourceMap.get(User.Fields.ROLES.getPreferredName())).toArray(Strings.EMPTY_ARRAY);
String fullName = (String) sourceMap.get(User.Fields.FULL_NAME.getPreferredName());
String email = (String) sourceMap.get(User.Fields.EMAIL.getPreferredName());
Map<String, Object> metadata = (Map<String, Object>) sourceMap.get(User.Fields.METADATA.getPreferredName());
return new UserAndPassword(new User(username, roles, fullName, email, metadata), password.toCharArray());
} catch (Exception e) {
logger.error("error in the format of get response for user", e);
return null;
}
}
private class UserStorePoller extends AbstractRunnable {
@ -649,12 +651,6 @@ public class ESNativeUsersStore extends AbstractComponent implements ClusterStat
}
}
public static class Fields {
static String USERNAME = "username";
static String PASSWORD = "password";
static String ROLES = "roles";
}
interface ChangeListener {
void onUsersChanged(List<String> username);

View File

@ -8,10 +8,19 @@ package org.elasticsearch.shield.client;
import org.elasticsearch.action.ActionFuture;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.client.ElasticsearchClient;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.shield.action.realm.ClearRealmCacheAction;
import org.elasticsearch.shield.action.realm.ClearRealmCacheRequest;
import org.elasticsearch.shield.action.realm.ClearRealmCacheRequestBuilder;
import org.elasticsearch.shield.action.realm.ClearRealmCacheResponse;
import org.elasticsearch.shield.action.role.AddRoleAction;
import org.elasticsearch.shield.action.role.AddRoleRequest;
import org.elasticsearch.shield.action.role.AddRoleRequestBuilder;
import org.elasticsearch.shield.action.role.AddRoleResponse;
import org.elasticsearch.shield.action.role.ClearRolesCacheAction;
import org.elasticsearch.shield.action.role.ClearRolesCacheRequest;
import org.elasticsearch.shield.action.role.ClearRolesCacheRequestBuilder;
import org.elasticsearch.shield.action.role.ClearRolesCacheResponse;
import org.elasticsearch.shield.action.role.DeleteRoleAction;
import org.elasticsearch.shield.action.role.DeleteRoleRequest;
import org.elasticsearch.shield.action.role.DeleteRoleRequestBuilder;
@ -20,10 +29,6 @@ import org.elasticsearch.shield.action.role.GetRolesAction;
import org.elasticsearch.shield.action.role.GetRolesRequest;
import org.elasticsearch.shield.action.role.GetRolesRequestBuilder;
import org.elasticsearch.shield.action.role.GetRolesResponse;
import org.elasticsearch.shield.action.user.AddUserAction;
import org.elasticsearch.shield.action.user.AddUserRequest;
import org.elasticsearch.shield.action.user.AddUserRequestBuilder;
import org.elasticsearch.shield.action.user.AddUserResponse;
import org.elasticsearch.shield.action.user.DeleteUserAction;
import org.elasticsearch.shield.action.user.DeleteUserRequest;
import org.elasticsearch.shield.action.user.DeleteUserRequestBuilder;
@ -32,14 +37,12 @@ import org.elasticsearch.shield.action.user.GetUsersAction;
import org.elasticsearch.shield.action.user.GetUsersRequest;
import org.elasticsearch.shield.action.user.GetUsersRequestBuilder;
import org.elasticsearch.shield.action.user.GetUsersResponse;
import org.elasticsearch.shield.action.realm.ClearRealmCacheAction;
import org.elasticsearch.shield.action.realm.ClearRealmCacheRequest;
import org.elasticsearch.shield.action.realm.ClearRealmCacheRequestBuilder;
import org.elasticsearch.shield.action.realm.ClearRealmCacheResponse;
import org.elasticsearch.shield.action.role.ClearRolesCacheAction;
import org.elasticsearch.shield.action.role.ClearRolesCacheRequest;
import org.elasticsearch.shield.action.role.ClearRolesCacheRequestBuilder;
import org.elasticsearch.shield.action.role.ClearRolesCacheResponse;
import org.elasticsearch.shield.action.user.PutUserAction;
import org.elasticsearch.shield.action.user.PutUserRequest;
import org.elasticsearch.shield.action.user.PutUserRequestBuilder;
import org.elasticsearch.shield.action.user.PutUserResponse;
import java.io.IOException;
/**
* A wrapper to elasticsearch clients that exposes all Shield related APIs
@ -114,9 +117,7 @@ public class SecurityClient {
return client.execute(ClearRolesCacheAction.INSTANCE, request);
}
/****************
* admin things *
****************/
/** User Management */
public GetUsersRequestBuilder prepareGetUsers() {
return new GetUsersRequestBuilder(client);
@ -126,22 +127,28 @@ public class SecurityClient {
client.execute(GetUsersAction.INSTANCE, request, listener);
}
public DeleteUserRequestBuilder prepareDeleteUser() {
return new DeleteUserRequestBuilder(client);
public DeleteUserRequestBuilder prepareDeleteUser(String username) {
return new DeleteUserRequestBuilder(client).username(username);
}
public void deleteUser(DeleteUserRequest request, ActionListener<DeleteUserResponse> listener) {
client.execute(DeleteUserAction.INSTANCE, request, listener);
}
public AddUserRequestBuilder prepareAddUser() {
return new AddUserRequestBuilder(client);
public PutUserRequestBuilder preparePutUser(String username, BytesReference source) throws IOException {
return new PutUserRequestBuilder(client).username(username).source(source);
}
public void addUser(AddUserRequest request, ActionListener<AddUserResponse> listener) {
client.execute(AddUserAction.INSTANCE, request, listener);
public PutUserRequestBuilder preparePutUser(String username, char[] password, String... roles) {
return new PutUserRequestBuilder(client).username(username).password(password).roles(roles);
}
public void putUser(PutUserRequest request, ActionListener<PutUserResponse> listener) {
client.execute(PutUserAction.INSTANCE, request, listener);
}
/** Role Management */
public GetRolesRequestBuilder prepareGetRoles() {
return new GetRolesRequestBuilder(client);
}

View File

@ -34,10 +34,9 @@ public class RestDeleteUserAction extends BaseRestHandler {
@Override
protected void handleRequest(RestRequest request, final RestChannel channel, Client client) throws Exception {
String user = request.param("username");
DeleteUserRequest delUserRequest = new DeleteUserRequest(user);
String username = request.param("username");
new SecurityClient(client).deleteUser(delUserRequest, new RestBuilderListener<DeleteUserResponse>(channel) {
new SecurityClient(client).prepareDeleteUser(username).execute(new RestBuilderListener<DeleteUserResponse>(channel) {
@Override
public RestResponse buildResponse(DeleteUserResponse response, XContentBuilder builder) throws Exception {
return new BytesRestResponse(response.found() ? RestStatus.OK : RestStatus.NOT_FOUND,

View File

@ -37,13 +37,13 @@ public class RestGetUsersAction extends BaseRestHandler {
@Override
protected void handleRequest(RestRequest request, final RestChannel channel, Client client) throws Exception {
String[] users = Strings.splitStringByCommaToArray(request.param("username"));
String[] usernames = request.paramAsStringArray("username", Strings.EMPTY_ARRAY);
new SecurityClient(client).prepareGetUsers().users(users).execute(new RestBuilderListener<GetUsersResponse>(channel) {
new SecurityClient(client).prepareGetUsers().usernames(usernames).execute(new RestBuilderListener<GetUsersResponse>(channel) {
@Override
public RestResponse buildResponse(GetUsersResponse getUsersResponse, XContentBuilder builder) throws Exception {
builder.startObject();
builder.field("found", getUsersResponse.isExists());
builder.field("found", getUsersResponse.hasUsers());
builder.startArray("users");
for (User user : getUsersResponse.users()) {
user.toXContent(builder, ToXContent.EMPTY_PARAMS);

View File

@ -8,7 +8,6 @@ package org.elasticsearch.shield.rest.action.user;
import org.elasticsearch.client.Client;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.rest.BaseRestHandler;
import org.elasticsearch.rest.BytesRestResponse;
@ -18,17 +17,16 @@ import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.RestResponse;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.rest.action.support.RestBuilderListener;
import org.elasticsearch.shield.action.user.AddUserRequest;
import org.elasticsearch.shield.action.user.AddUserResponse;
import org.elasticsearch.shield.action.user.PutUserResponse;
import org.elasticsearch.shield.client.SecurityClient;
/**
* Rest endpoint to add a User to the shield index
*/
public class RestAddUserAction extends BaseRestHandler {
public class RestPutUserAction extends BaseRestHandler {
@Inject
public RestAddUserAction(Settings settings, RestController controller, Client client) {
public RestPutUserAction(Settings settings, RestController controller, Client client) {
super(settings, client);
controller.registerHandler(RestRequest.Method.POST, "/_shield/user/{username}", this);
controller.registerHandler(RestRequest.Method.PUT, "/_shield/user/{username}", this);
@ -36,18 +34,15 @@ public class RestAddUserAction extends BaseRestHandler {
@Override
protected void handleRequest(RestRequest request, final RestChannel channel, Client client) throws Exception {
AddUserRequest addUserReq = new AddUserRequest();
addUserReq.username(request.param("username"));
addUserReq.source(request.content());
new SecurityClient(client).addUser(addUserReq, new RestBuilderListener<AddUserResponse>(channel) {
@Override
public RestResponse buildResponse(AddUserResponse addUserResponse, XContentBuilder builder) throws Exception {
return new BytesRestResponse(RestStatus.OK,
builder.startObject()
.field("user", (ToXContent) addUserResponse)
.endObject());
}
});
new SecurityClient(client).preparePutUser(request.param("username"), request.content())
.execute(new RestBuilderListener<PutUserResponse>(channel) {
@Override
public RestResponse buildResponse(PutUserResponse putUserResponse, XContentBuilder builder) throws Exception {
return new BytesRestResponse(RestStatus.OK,
builder.startObject()
.field("user", putUserResponse)
.endObject());
}
});
}
}

View File

@ -1,64 +1,101 @@
{
"template": ".shield",
"order": 1000,
"settings": {
"number_of_shards": 1,
"number_of_replicas": 0,
"auto_expand_replicas": "0-all"
"template" : ".shield",
"order" : 1000,
"settings" : {
"number_of_shards" : 1,
"number_of_replicas" : 0,
"auto_expand_replicas" : "0-all",
"analysis" : {
"filter" : {
"email" : {
"type" : "pattern_capture",
"preserve_original" : 1,
"patterns" : [
"([^@]+)",
"(\\p{L}+)",
"(\\d+)",
"@(.+)"
]
}
},
"analyzer" : {
"email" : {
"tokenizer" : "uax_url_email",
"filter" : [
"email",
"lowercase",
"unique"
]
}
}
}
},
"mappings": {
"user": {
"dynamic": "strict",
"properties": {
"username": {
"type": "string",
"index": "not_analyzed"
"mappings" : {
"user" : {
"dynamic" : "strict",
"properties" : {
"username" : {
"type" : "string",
"index" : "not_analyzed"
},
"roles": {
"type": "string",
"index": "not_analyzed"
"roles" : {
"type" : "string",
"index" : "not_analyzed"
},
"password": {
"type": "string",
"index": "no"
"password" : {
"type" : "string",
"index" : "no"
},
"full_name" : {
"type" : "string",
"index" : "analyzed"
},
"email" : {
"type" : "string",
"index" : "analyzed",
"analyzer" : "email"
},
"metadata" : {
"type" : "object",
"dynamic" : true
}
}
},
"role" : {
"dynamic": "strict",
"dynamic" : "strict",
"properties" : {
"cluster" : {
"type" : "string",
"index": "not_analyzed"
"index" : "not_analyzed"
},
"indices" : {
"type": "object",
"type" : "object",
"properties" : {
"fields" : {
"type" : "string",
"index": "not_analyzed"
"index" : "not_analyzed"
},
"names" : {
"type" : "string",
"index": "not_analyzed"
"index" : "not_analyzed"
},
"privileges" : {
"type" : "string",
"index": "not_analyzed"
"index" : "not_analyzed"
},
"query" : {
"type" : "string",
"index": "not_analyzed"
"index" : "not_analyzed"
}
}
},
"name" : {
"type" : "string",
"index": "not_analyzed"
"index" : "not_analyzed"
},
"run_as" : {
"type" : "string",
"index": "not_analyzed"
"index" : "not_analyzed"
}
}
}

View File

@ -41,7 +41,6 @@ public class ShieldF {
settings.put("http.cors.allow-origin", "*");
settings.put("script.inline", "true");
settings.put("xpack.shield.enabled", "true");
settings.put("security.manager.enabled", "false");
// Disable Monitoring to prevent cluster activity
settings.put("xpack.monitoring.enabled", "false");
settings.put(IndexModule.INDEX_QUERY_CACHE_TYPE_SETTING.getKey(), Shield.OPT_OUT_QUERY_CACHE);

View File

@ -11,6 +11,7 @@ import org.elasticsearch.common.io.stream.BytesStreamOutput;
import org.elasticsearch.test.ESTestCase;
import java.util.Arrays;
import java.util.Collections;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
@ -92,15 +93,17 @@ public class UserTests extends ESTestCase {
generateRandomStringArray(16, 30, false), SystemUser.INSTANCE);
fail("should not be able to create a runAs user with the system user");
} catch (ElasticsearchSecurityException e) {
assertThat(e.getMessage(), containsString("system"));
assertThat(e.getMessage(), containsString("invalid run_as user"));
}
}
public void testUserToString() throws Exception {
User sudo = new User("root", new String[]{"r3"});
User u = new User("bob", new String[]{"r1", "r2"}, sudo);
assertEquals("User[username=root,roles=[r3,]]", sudo.toString());
assertEquals("User[username=bob,roles=[r1,r2,],runAs=[User[username=root,roles=[r3,]]]]",
u.toString());
User user = new User("u1", "r1");
assertThat(user.toString(), is("User[username=u1,roles=[r1],fullName=null,email=null,metadata={}]"));
user = new User("u1", new String[] { "r1", "r2" }, "user1", "user1@domain.com", Collections.singletonMap("key", "val"));
assertThat(user.toString(), is("User[username=u1,roles=[r1,r2],fullName=user1,email=user1@domain.com,metadata={key=val}]"));
user = new User("u1", new String[] {"r1", "r2"}, new User("u2", "r3"));
assertThat(user.toString(), is("User[username=u1,roles=[r1,r2],fullName=null,email=null,metadata={},runAs=[User[username=u2," +
"roles=[r3],fullName=null,email=null,metadata={}]]]"));
}
}

View File

@ -44,7 +44,7 @@ public class ESNativeTests extends ShieldIntegTestCase {
public void testDeletingNonexistingUserAndRole() throws Exception {
SecurityClient c = securityClient();
DeleteUserResponse resp = c.prepareDeleteUser().user("joe").get();
DeleteUserResponse resp = c.prepareDeleteUser("joe").get();
assertFalse("user shouldn't be found", resp.found());
DeleteRoleResponse resp2 = c.prepareDeleteRole().role("role").get();
assertFalse("role shouldn't be found", resp2.found());
@ -52,8 +52,8 @@ public class ESNativeTests extends ShieldIntegTestCase {
public void testGettingUserThatDoesntExist() throws Exception {
SecurityClient c = securityClient();
GetUsersResponse resp = c.prepareGetUsers().users("joe").get();
assertFalse("user should not exist", resp.isExists());
GetUsersResponse resp = c.prepareGetUsers().usernames("joe").get();
assertFalse("user should not exist", resp.hasUsers());
GetRolesResponse resp2 = c.prepareGetRoles().roles("role").get();
assertFalse("role should not exist", resp2.isExists());
}
@ -61,46 +61,34 @@ public class ESNativeTests extends ShieldIntegTestCase {
public void testAddAndGetUser() throws Exception {
SecurityClient c = securityClient();
logger.error("--> creating user");
c.prepareAddUser()
.username("joe")
.password("s3kirt")
.roles("role1", "user")
.get();
c.preparePutUser("joe", "s3kirt".toCharArray(), "role1", "user").get();
logger.error("--> waiting for .shield index");
ensureGreen(ShieldTemplateService.SHIELD_ADMIN_INDEX_NAME);
logger.info("--> retrieving user");
GetUsersResponse resp = c.prepareGetUsers().users("joe").get();
assertTrue("user should exist", resp.isExists());
User joe = resp.users().get(0);
GetUsersResponse resp = c.prepareGetUsers().usernames("joe").get();
assertTrue("user should exist", resp.hasUsers());
User joe = resp.users()[0];
assertEquals(joe.principal(), "joe");
assertArrayEquals(joe.roles(), new String[]{"role1", "user"});
logger.info("--> adding two more users");
c.prepareAddUser()
.username("joe2")
.password("s3kirt2")
.roles("role2", "user")
.get();
c.prepareAddUser()
.username("joe3")
.password("s3kirt3")
.roles("role3", "user")
.get();
c.preparePutUser("joe2", "s3kirt2".toCharArray(), "role2", "user").get();
c.preparePutUser("joe3", "s3kirt3".toCharArray(), "role3", "user").get();
// Since getting multiple users relies on them being visible to search, perform a refresh
refresh();
GetUsersResponse allUsersResp = c.prepareGetUsers().get();
assertTrue("users should exist", allUsersResp.isExists());
assertEquals("should be 3 users total", 3, allUsersResp.users().size());
assertTrue("users should exist", allUsersResp.hasUsers());
assertEquals("should be 3 users total", 3, allUsersResp.users().length);
List<String> names = new ArrayList<>(3);
for (User u : allUsersResp.users()) {
names.add(u.principal());
}
CollectionUtil.timSort(names);
assertArrayEquals(new String[]{"joe", "joe2", "joe3"}, names.toArray(Strings.EMPTY_ARRAY));
assertArrayEquals(new String[] { "joe", "joe2", "joe3" }, names.toArray(Strings.EMPTY_ARRAY));
GetUsersResponse someUsersResp = c.prepareGetUsers().users("joe", "joe3").get();
assertTrue("users should exist", someUsersResp.isExists());
assertEquals("should be 2 users returned", 2, someUsersResp.users().size());
GetUsersResponse someUsersResp = c.prepareGetUsers().usernames("joe", "joe3").get();
assertTrue("users should exist", someUsersResp.hasUsers());
assertEquals("should be 2 users returned", 2, someUsersResp.users().length);
names = new ArrayList<>(2);
for (User u : someUsersResp.users()) {
names.add(u.principal());
@ -109,11 +97,11 @@ public class ESNativeTests extends ShieldIntegTestCase {
assertArrayEquals(new String[]{"joe", "joe3"}, names.toArray(Strings.EMPTY_ARRAY));
logger.info("--> deleting user");
DeleteUserResponse delResp = c.prepareDeleteUser().user("joe").get();
DeleteUserResponse delResp = c.prepareDeleteUser("joe").get();
assertTrue(delResp.found());
logger.info("--> retrieving user");
resp = c.prepareGetUsers().users("joe").get();
assertFalse("user should not exist after being deleted", resp.isExists());
resp = c.prepareGetUsers().usernames("joe").get();
assertFalse("user should not exist after being deleted", resp.hasUsers());
}
public void testAddAndGetRole() throws Exception {
@ -180,17 +168,13 @@ public class ESNativeTests extends ShieldIntegTestCase {
new String[]{"body", "title"}, new BytesArray("{\"match_all\": {}}"))
.get();
logger.error("--> creating user");
c.prepareAddUser()
.username("joe")
.password("s3krit")
.roles("test_role")
.get();
c.preparePutUser("joe", "s3krit".toCharArray(), "test_role").get();
refresh();
logger.error("--> waiting for .shield index");
ensureGreen(ShieldTemplateService.SHIELD_ADMIN_INDEX_NAME);
logger.info("--> retrieving user");
GetUsersResponse resp = c.prepareGetUsers().users("joe").get();
assertTrue("user should exist", resp.isExists());
GetUsersResponse resp = c.prepareGetUsers().usernames("joe").get();
assertTrue("user should exist", resp.hasUsers());
createIndex("idx");
ensureGreen("idx");
@ -206,18 +190,14 @@ public class ESNativeTests extends ShieldIntegTestCase {
public void testUpdatingUserAndAuthentication() throws Exception {
SecurityClient c = securityClient();
logger.error("--> creating user");
c.prepareAddUser()
.username("joe")
.password("s3krit")
.roles(ShieldSettingsSource.DEFAULT_ROLE)
.get();
c.preparePutUser("joe", "s3krit".toCharArray(), ShieldSettingsSource.DEFAULT_ROLE).get();
refresh();
logger.error("--> waiting for .shield index");
ensureGreen(ShieldTemplateService.SHIELD_ADMIN_INDEX_NAME);
logger.info("--> retrieving user");
GetUsersResponse resp = c.prepareGetUsers().users("joe").get();
assertTrue("user should exist", resp.isExists());
assertThat(resp.users().get(0).roles(), arrayContaining(ShieldSettingsSource.DEFAULT_ROLE));
GetUsersResponse resp = c.prepareGetUsers().usernames("joe").get();
assertTrue("user should exist", resp.hasUsers());
assertThat(resp.users()[0].roles(), arrayContaining(ShieldSettingsSource.DEFAULT_ROLE));
createIndex("idx");
ensureGreen("idx");
@ -228,11 +208,7 @@ public class ESNativeTests extends ShieldIntegTestCase {
assertEquals(searchResp.getHits().getTotalHits(), 1L);
c.prepareAddUser()
.username("joe")
.password("s3krit2")
.roles(ShieldSettingsSource.DEFAULT_ROLE)
.get();
c.preparePutUser("joe", "s3krit2".toCharArray(), ShieldSettingsSource.DEFAULT_ROLE).get();
try {
client().filterWithHeader(Collections.singletonMap("Authorization", token)).prepareSearch("idx").get();
@ -250,18 +226,14 @@ public class ESNativeTests extends ShieldIntegTestCase {
public void testCreateDeleteAuthenticate() {
SecurityClient c = securityClient();
logger.error("--> creating user");
c.prepareAddUser()
.username("joe")
.password("s3krit")
.roles(ShieldSettingsSource.DEFAULT_ROLE)
.get();
c.preparePutUser("joe", "s3krit".toCharArray(), ShieldSettingsSource.DEFAULT_ROLE).get();
refresh();
logger.error("--> waiting for .shield index");
ensureGreen(ShieldTemplateService.SHIELD_ADMIN_INDEX_NAME);
logger.info("--> retrieving user");
GetUsersResponse resp = c.prepareGetUsers().users("joe").get();
assertTrue("user should exist", resp.isExists());
assertThat(resp.users().get(0).roles(), arrayContaining(ShieldSettingsSource.DEFAULT_ROLE));
GetUsersResponse resp = c.prepareGetUsers().usernames("joe").get();
assertTrue("user should exist", resp.hasUsers());
assertThat(resp.users()[0].roles(), arrayContaining(ShieldSettingsSource.DEFAULT_ROLE));
createIndex("idx");
ensureGreen("idx");
@ -272,7 +244,7 @@ public class ESNativeTests extends ShieldIntegTestCase {
assertEquals(searchResp.getHits().getTotalHits(), 1L);
DeleteUserResponse response = c.prepareDeleteUser().user("joe").get();
DeleteUserResponse response = c.prepareDeleteUser("joe").get();
assertThat(response.found(), is(true));
try {
client().filterWithHeader(Collections.singletonMap("Authorization", token)).prepareSearch("idx").get();
@ -294,11 +266,7 @@ public class ESNativeTests extends ShieldIntegTestCase {
new String[]{"body", "title"}, new BytesArray("{\"match_all\": {}}"))
.get();
logger.error("--> creating user");
c.prepareAddUser()
.username("joe")
.password("s3krit")
.roles("test_role")
.get();
c.preparePutUser("joe", "s3krit".toCharArray(), "test_role").get();
refresh();
logger.error("--> waiting for .shield index");
ensureGreen(ShieldTemplateService.SHIELD_ADMIN_INDEX_NAME);
@ -348,11 +316,7 @@ public class ESNativeTests extends ShieldIntegTestCase {
.addIndices(new String[]{"*"}, new String[]{"read"},
new String[]{"body", "title"}, new BytesArray("{\"match_all\": {}}"))
.get();
c.prepareAddUser()
.username("joe")
.password("s3krit")
.roles("test_role")
.get();
c.preparePutUser("joe", "s3krit".toCharArray(), "test_role").get();
refresh();
logger.error("--> waiting for .shield index");
ensureGreen(ShieldTemplateService.SHIELD_ADMIN_INDEX_NAME);

View File

@ -76,7 +76,7 @@ cluster:admin/plugin/license/delete
cluster:admin/plugin/license/put
cluster:admin/shield/realm/cache/clear
cluster:admin/shield/roles/cache/clear
cluster:admin/shield/user/add
cluster:admin/shield/user/put
cluster:admin/shield/user/delete
cluster:admin/shield/user/get
cluster:admin/shield/role/add

View File

@ -15,7 +15,7 @@ cluster:admin/shield/roles/cache/clear[n]
cluster:admin/shield/role/add
cluster:admin/shield/role/delete
cluster:admin/shield/role/get
cluster:admin/shield/user/add
cluster:admin/shield/user/put
cluster:admin/shield/user/delete
cluster:admin/shield/user/get
indices:admin/analyze[s]

View File

@ -12,9 +12,15 @@
username: "joe"
body: >
{
"username": "joe",
"password": "s3krit",
"roles" : [ "admin" ]
"username" : "joe",
"password" : "s3krit",
"roles" : [ "admin" ],
"full_name" : "Bazooka Joe",
"email" : "joe@bazooka.gum",
"metadata" : {
"key1" : "val1",
"key2" : "val2"
}
}
- match: { user: { created: true } }
@ -30,3 +36,7 @@
- match: { found: true }
- match: { users.0.username: "joe" }
- match: { users.0.roles.0: "admin" }
- match: { users.0.full_name: "Bazooka Joe" }
- match: { users.0.email: "joe@bazooka.gum" }
- match: { users.0.metadata.key1: "val1" }
- match: { users.0.metadata.key2: "val2" }

View File

@ -36,9 +36,15 @@
username: "joe"
body: >
{
"username": "joe",
"password": "s3krit2",
"roles" : [ "admin", "foo" ]
"username" : "joe",
"password" : "s3krit2",
"roles" : [ "admin", "foo" ],
"full_name" : "Bazooka Joe",
"email" : "joe@bazooka.gum",
"metadata" : {
"key1" : "val1",
"key2" : "val2"
}
}
- match: { user: { created: false } }
@ -49,6 +55,10 @@
- match: { users.0.username: "joe" }
- match: { users.0.roles.0: "admin" }
- match: { users.0.roles.1: "foo" }
- match: { users.0.full_name: "Bazooka Joe" }
- match: { users.0.email: "joe@bazooka.gum" }
- match: { users.0.metadata.key1: "val1" }
- match: { users.0.metadata.key2: "val2" }
- do:
headers:

View File

@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.watcher.support.xcontent;
package org.elasticsearch.xpack.common.xcontent;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.common.bytes.BytesReference;
@ -19,9 +19,24 @@ import java.util.List;
/**
*
*/
public class WatcherXContentUtils {
public class XContentUtils {
private WatcherXContentUtils() {
private XContentUtils() {
}
/**
* Ensures that we're currently on the start of an object, or that the next token is a start of an object.
*
* @throws ElasticsearchParseException if the current or the next token is a {@code START_OBJECT}
*/
public static void verifyObject(XContentParser parser) throws IOException, ElasticsearchParseException {
if (parser.currentToken() == XContentParser.Token.START_OBJECT) {
return;
}
XContentParser.Token token = parser.nextToken();
if (token != XContentParser.Token.START_OBJECT) {
throw new ElasticsearchParseException("expected a user object, but found token [{}]", parser.currentToken());
}
}
public static Tuple<XContentType, Object> convertToObject(BytesReference bytes) throws ElasticsearchParseException {

View File

@ -11,7 +11,7 @@ import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.watcher.condition.Condition;
import org.elasticsearch.watcher.support.xcontent.WatcherXContentUtils;
import org.elasticsearch.xpack.common.xcontent.XContentUtils;
import java.io.IOException;
import java.util.Locale;
@ -114,7 +114,7 @@ public class CompareCondition implements Condition {
"operation [{}] must either be a numeric, string, boolean or null value, but found [{}] instead", TYPE,
watchId, path, op.name().toLowerCase(Locale.ROOT), token);
}
value = WatcherXContentUtils.readValue(parser, token);
value = XContentUtils.readValue(parser, token);
token = parser.nextToken();
if (token != XContentParser.Token.END_OBJECT) {
throw new ElasticsearchParseException("could not parse [{}] condition for watch [{}]. expected end of path object, " +

View File

@ -13,7 +13,7 @@ import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.watcher.condition.Condition;
import org.elasticsearch.watcher.condition.compare.LenientCompare;
import org.elasticsearch.watcher.support.xcontent.WatcherXContentUtils;
import org.elasticsearch.xpack.common.xcontent.XContentUtils;
import java.io.IOException;
import java.util.List;
@ -149,7 +149,7 @@ public class ArrayCompareCondition implements Condition {
"boolean or null value, but found [{}] instead", TYPE, watchId, path,
op.name().toLowerCase(Locale.ROOT), token);
}
value = WatcherXContentUtils.readValue(parser, token);
value = XContentUtils.readValue(parser, token);
haveValue = true;
} else if (ParseFieldMatcher.STRICT.match(parser.currentName(), Field.QUANTIFIER)) {
if (quantifier != null) {

View File

@ -15,6 +15,7 @@ import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentHelper;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.xpack.common.xcontent.XContentUtils;
import java.io.IOException;
import java.util.List;
@ -117,7 +118,7 @@ public class XContentSource implements ToXContent {
private Object data() {
if (data == null) {
try (XContentParser parser = parser()) {
data = WatcherXContentUtils.readValue(parser, parser.nextToken());
data = XContentUtils.readValue(parser, parser.nextToken());
} catch (IOException ex) {
throw new ElasticsearchException("failed to read value", ex);
}

View File

@ -12,7 +12,7 @@ import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.watcher.support.text.TextTemplate;
import org.elasticsearch.watcher.support.xcontent.WatcherXContentUtils;
import org.elasticsearch.xpack.common.xcontent.XContentUtils;
import java.util.ArrayList;
import java.util.Arrays;
@ -68,9 +68,9 @@ public class HipChatMessageTests extends ESTestCase {
} else if ("body".equals(currentFieldName)) {
message = parser.text();
} else if ("room".equals(currentFieldName)) {
rooms = WatcherXContentUtils.readStringArray(parser, false);
rooms = XContentUtils.readStringArray(parser, false);
} else if ("user".equals(currentFieldName)) {
users = WatcherXContentUtils.readStringArray(parser, false);
users = XContentUtils.readStringArray(parser, false);
} else if ("from".equals(currentFieldName)) {
from = parser.text();
} else if ("format".equals(currentFieldName)) {

View File

@ -10,6 +10,7 @@ import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.env.Environment;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.junit.annotations.Network;
import org.elasticsearch.watcher.support.http.auth.HttpAuthRegistry;
import static org.hamcrest.Matchers.greaterThan;
@ -22,6 +23,7 @@ public class HttpConnectionTimeoutTests extends ESTestCase {
// setting an unroutable IP to simulate a connection timeout
private static final String UNROUTABLE_IP = "192.168.255.255";
@Network
public void testDefaultTimeout() throws Exception {
Environment environment = new Environment(Settings.builder().put("path.home", createTempDir()).build());
HttpClient httpClient = new HttpClient(Settings.EMPTY, mock(HttpAuthRegistry.class), environment).start();
@ -45,6 +47,7 @@ public class HttpConnectionTimeoutTests extends ESTestCase {
}
}
@Network
public void testDefaultTimeoutCustom() throws Exception {
Environment environment = new Environment(Settings.builder().put("path.home", createTempDir()).build());
HttpClient httpClient = new HttpClient(Settings.builder()
@ -71,6 +74,7 @@ public class HttpConnectionTimeoutTests extends ESTestCase {
}
}
@Network
public void testTimeoutCustomPerRequest() throws Exception {
Environment environment = new Environment(Settings.builder().put("path.home", createTempDir()).build());
HttpClient httpClient = new HttpClient(Settings.builder()

View File

@ -5,12 +5,15 @@
*/
package org.elasticsearch.watcher.support.http;
import com.squareup.okhttp.mockwebserver.Dispatcher;
import com.squareup.okhttp.mockwebserver.MockResponse;
import com.squareup.okhttp.mockwebserver.MockWebServer;
import com.squareup.okhttp.mockwebserver.RecordedRequest;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.ElasticsearchTimeoutException;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.util.concurrent.CountDown;
import org.elasticsearch.env.Environment;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.watcher.support.http.auth.HttpAuthRegistry;
@ -18,6 +21,8 @@ import org.junit.After;
import org.junit.Before;
import java.net.BindException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.lessThan;
@ -45,8 +50,9 @@ public class HttpReadTimeoutTests extends ESTestCase {
}
@After
public void after() throws Exception {
public void cleanup() throws Exception {
webServer.shutdown();
}
public void testDefaultTimeout() throws Exception {
@ -83,11 +89,19 @@ public class HttpReadTimeoutTests extends ESTestCase {
Environment environment = new Environment(Settings.builder().put("path.home", createTempDir()).build());
HttpClient httpClient = new HttpClient(Settings.builder()
.put("watcher.http.default_read_timeout", "5s")
.put("watcher.http.default_read_timeout", "3s")
.build()
, mock(HttpAuthRegistry.class), environment).start();
// we're not going to enqueue an response... so the server will just hang
final CountDownLatch latch = new CountDownLatch(1);
webServer.setDispatcher(new Dispatcher() {
@Override
public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
Thread.sleep(5000);
latch.countDown();
return new MockResponse().setStatus("200");
}
});
HttpRequest request = HttpRequest.builder("localhost", webPort)
.method(HttpMethod.POST)
@ -104,12 +118,14 @@ public class HttpReadTimeoutTests extends ESTestCase {
TimeValue timeout = TimeValue.timeValueNanos(System.nanoTime() - start);
logger.info("http connection timed out after {}", timeout.format());
// it's supposed to be 5, but we'll give it an error margin of 2 seconds
assertThat(timeout.seconds(), greaterThan(3L));
assertThat(timeout.seconds(), lessThan(7L));
// it's supposed to be 3, but we'll give it an error margin of 2 seconds
assertThat(timeout.seconds(), greaterThan(1L));
assertThat(timeout.seconds(), lessThan(5L));
}
// lets enqueue a response to relese the server.
webServer.enqueue(new MockResponse());
if (!latch.await(10, TimeUnit.SECONDS)) {
// should never happen
fail("waited too long for the response to be returned");
}
}
@ -121,7 +137,15 @@ public class HttpReadTimeoutTests extends ESTestCase {
.build()
, mock(HttpAuthRegistry.class), environment).start();
// we're not going to enqueue an response... so the server will just hang
final CountDownLatch latch = new CountDownLatch(1);
webServer.setDispatcher(new Dispatcher() {
@Override
public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
Thread.sleep(5000);
latch.countDown();
return new MockResponse().setStatus("200");
}
});
HttpRequest request = HttpRequest.builder("localhost", webPort)
.readTimeout(TimeValue.timeValueSeconds(5))
@ -142,9 +166,11 @@ public class HttpReadTimeoutTests extends ESTestCase {
// it's supposed to be 5, but we'll give it an error margin of 2 seconds
assertThat(timeout.seconds(), greaterThan(3L));
assertThat(timeout.seconds(), lessThan(7L));
}
// lets enqueue a response to relese the server.
webServer.enqueue(new MockResponse());
if (!latch.await(7, TimeUnit.SECONDS)) {
// should never happen
fail("waited too long for the response to be returned");
}
}
}