diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java index e604814a3bce5..5203306147f1a 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java @@ -20,6 +20,8 @@ package org.elasticsearch.client; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.client.security.AuthenticateRequest; +import org.elasticsearch.client.security.AuthenticateResponse; import org.elasticsearch.client.security.ChangePasswordRequest; import org.elasticsearch.client.security.ClearRolesCacheRequest; import org.elasticsearch.client.security.ClearRolesCacheResponse; @@ -210,6 +212,32 @@ public void disableUserAsync(DisableUserRequest request, RequestOptions options, EmptyResponse::fromXContent, listener, emptySet()); } + /** + * Authenticate the current user and return all the information about the authenticated user. + * See + * the docs for more. + * + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @return the responsee from the authenticate user call + */ + public AuthenticateResponse authenticate(RequestOptions options) throws IOException { + return restHighLevelClient.performRequestAndParseEntity(AuthenticateRequest.INSTANCE, AuthenticateRequest::getRequest, options, + AuthenticateResponse::fromXContent, emptySet()); + } + + /** + * Authenticate the current user asynchronously and return all the information about the authenticated user. + * See + * the docs for more. + * + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param listener the listener to be notified upon request completion + */ + public void authenticateAsync(RequestOptions options, ActionListener listener) { + restHighLevelClient.performRequestAsyncAndParseEntity(AuthenticateRequest.INSTANCE, AuthenticateRequest::getRequest, options, + AuthenticateResponse::fromXContent, listener, emptySet()); + } + /** * Clears the native roles cache for a set of roles. * See diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/AuthenticateRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/AuthenticateRequest.java new file mode 100644 index 0000000000000..2aefa97cb8bf1 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/AuthenticateRequest.java @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.client.security; + +import org.apache.http.client.methods.HttpGet; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.Validatable; + +/** + * Empty request object required to make the authenticate call. The authenticate call + * retrieves metadata about the authenticated user. + */ +public final class AuthenticateRequest implements Validatable { + + public static final AuthenticateRequest INSTANCE = new AuthenticateRequest(); + + private AuthenticateRequest() { + } + + public Request getRequest() { + return new Request(HttpGet.METHOD_NAME, "/_xpack/security/_authenticate"); + } + +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/AuthenticateResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/AuthenticateResponse.java new file mode 100644 index 0000000000000..62f1cc0955bd1 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/AuthenticateResponse.java @@ -0,0 +1,109 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.client.security; + +import org.elasticsearch.client.security.user.User; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; + +/** + * The response for the authenticate call. The response contains two fields: a + * user field and a boolean flag signaling if the user is enabled or not. The + * user object contains all user metadata which Elasticsearch uses to map roles, + * etc. + */ +public final class AuthenticateResponse { + + static final ParseField USERNAME = new ParseField("username"); + static final ParseField ROLES = new ParseField("roles"); + static final ParseField METADATA = new ParseField("metadata"); + static final ParseField FULL_NAME = new ParseField("full_name"); + static final ParseField EMAIL = new ParseField("email"); + static final ParseField ENABLED = new ParseField("enabled"); + + @SuppressWarnings("unchecked") + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "client_security_authenticate_response", + a -> new AuthenticateResponse(new User((String) a[0], ((List) a[1]), (Map) a[2], + (String) a[3], (String) a[4]), (Boolean) a[5])); + static { + PARSER.declareString(constructorArg(), USERNAME); + PARSER.declareStringArray(constructorArg(), ROLES); + PARSER.>declareObject(constructorArg(), (parser, c) -> parser.map(), METADATA); + PARSER.declareStringOrNull(optionalConstructorArg(), FULL_NAME); + PARSER.declareStringOrNull(optionalConstructorArg(), EMAIL); + PARSER.declareBoolean(constructorArg(), ENABLED); + } + + private final User user; + private final boolean enabled; + + public AuthenticateResponse(User user, boolean enabled) { + this.user = user; + this.enabled = enabled; + } + + /** + * @return The effective user. This is the authenticated user, or, when + * submitting requests on behalf of other users, it is the + * impersonated user. + */ + public User getUser() { + return user; + } + + /** + * @return whether the user is enabled or not + */ + public boolean enabled() { + return enabled; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final AuthenticateResponse that = (AuthenticateResponse) o; + return user.equals(that.user) && enabled == that.enabled; + } + + @Override + public int hashCode() { + return Objects.hash(user, enabled); + } + + public static AuthenticateResponse fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/User.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/User.java new file mode 100644 index 0000000000000..977780b46b79b --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/User.java @@ -0,0 +1,135 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.client.security.user; + +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.Strings; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; + + +/** + * An authenticated user + */ +public final class User { + + private final String username; + private final Collection roles; + private final Map metadata; + @Nullable private final String fullName; + @Nullable private final String email; + + public User(String username, Collection roles, Map metadata, @Nullable String fullName, + @Nullable String email) { + Objects.requireNonNull(username, "`username` cannot be null"); + Objects.requireNonNull(roles, "`roles` cannot be null. Pass an empty collection instead."); + Objects.requireNonNull(roles, "`metadata` cannot be null. Pass an empty map instead."); + this.username = username; + this.roles = roles; + this.metadata = Collections.unmodifiableMap(metadata); + this.fullName = fullName; + this.email = email; + } + + /** + * @return The principal of this user - effectively serving as the + * unique identity of the user. Can never be {@code null}. + */ + public String username() { + return this.username; + } + + /** + * @return The roles this user is associated with. The roles are + * identified by their unique names and each represents as + * set of permissions. Can never be {@code null}. + */ + public Collection roles() { + return this.roles; + } + + /** + * @return The metadata that is associated with this user. Can never be {@code null}. + */ + public Map metadata() { + return metadata; + } + + /** + * @return The full name of this user. May be {@code null}. + */ + public @Nullable String fullName() { + return fullName; + } + + /** + * @return The email of this user. May be {@code null}. + */ + public @Nullable String email() { + return email; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("User[username=").append(username); + sb.append(",roles=[").append(Strings.collectionToCommaDelimitedString(roles)).append("]"); + sb.append(",metadata=").append(metadata); + sb.append(",fullName=").append(fullName); + sb.append(",email=").append(email); + sb.append("]"); + return sb.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o instanceof User == false) { + return false; + } + + final User user = (User) o; + + if (!username.equals(user.username)) { + return false; + } + if (!roles.equals(user.roles)) { + 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() { + return Objects.hash(username, roles, metadata, fullName, email); + } + +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ESRestHighLevelClientTestCase.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ESRestHighLevelClientTestCase.java index d31b9f04dbbb6..af3112ec7e1d8 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ESRestHighLevelClientTestCase.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ESRestHighLevelClientTestCase.java @@ -84,16 +84,42 @@ protected static Resp execute(Req request, SyncMethod syn } } + /** + * Executes the provided request using either the sync method or its async + * variant, both provided as functions. This variant is used when the call does + * not have a request object (only headers and the request path). + */ + protected static Resp execute(SyncMethodNoRequest syncMethodNoRequest, AsyncMethodNoRequest asyncMethodNoRequest, + RequestOptions requestOptions) throws IOException { + if (randomBoolean()) { + return syncMethodNoRequest.execute(requestOptions); + } else { + PlainActionFuture future = PlainActionFuture.newFuture(); + asyncMethodNoRequest.execute(requestOptions, future); + return future.actionGet(); + } + } + @FunctionalInterface protected interface SyncMethod { Response execute(Request request, RequestOptions options) throws IOException; } + @FunctionalInterface + protected interface SyncMethodNoRequest { + Response execute(RequestOptions options) throws IOException; + } + @FunctionalInterface protected interface AsyncMethod { void execute(Request request, RequestOptions options, ActionListener listener); } + @FunctionalInterface + protected interface AsyncMethodNoRequest { + void execute(RequestOptions options, ActionListener listener); + } + private static class HighLevelClient extends RestHighLevelClient { private HighLevelClient(RestClient restClient) { super(restClient, (client) -> {}, Collections.emptyList()); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java index d40c3196e54f4..96c7fdac46e58 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java @@ -719,7 +719,7 @@ public void testApiNamingConventions() throws Exception { methods.containsKey(apiName.substring(0, apiName.length() - 6))); assertThat("async method [" + method + "] should return void", method.getReturnType(), equalTo(Void.TYPE)); assertEquals("async method [" + method + "] should not throw any exceptions", 0, method.getExceptionTypes().length); - if (apiName.equals("security.get_ssl_certificates_async")) { + if (apiName.equals("security.authenticate_async") || apiName.equals("security.get_ssl_certificates_async")) { assertEquals(2, method.getParameterTypes().length); assertThat(method.getParameterTypes()[0], equalTo(RequestOptions.class)); assertThat(method.getParameterTypes()[1], equalTo(ActionListener.class)); @@ -744,7 +744,8 @@ public void testApiNamingConventions() throws Exception { assertEquals("incorrect number of exceptions for method [" + method + "]", 1, method.getExceptionTypes().length); //a few methods don't accept a request object as argument - if (apiName.equals("ping") || apiName.equals("info") || apiName.equals("security.get_ssl_certificates")) { + if (apiName.equals("ping") || apiName.equals("info") || apiName.equals("security.get_ssl_certificates") + || apiName.equals("security.authenticate")) { assertEquals("incorrect number of arguments for method [" + method + "]", 1, method.getParameterTypes().length); assertThat("the parameter to method [" + method + "] is the wrong type", method.getParameterTypes()[0], equalTo(RequestOptions.class)); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityIT.java new file mode 100644 index 0000000000000..74a4d58e2bf77 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityIT.java @@ -0,0 +1,108 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.client; + +import org.apache.http.client.methods.HttpDelete; +import org.elasticsearch.ElasticsearchStatusException; +import org.elasticsearch.client.security.AuthenticateResponse; +import org.elasticsearch.client.security.PutUserRequest; +import org.elasticsearch.client.security.PutUserResponse; +import org.elasticsearch.client.security.RefreshPolicy; +import org.elasticsearch.common.CharArrays; + +import java.util.Arrays; +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.empty; + +public class SecurityIT extends ESRestHighLevelClientTestCase { + + public void testAuthenticate() throws Exception { + final SecurityClient securityClient = highLevelClient().security(); + // test fixture: put enabled user + final PutUserRequest putUserRequest = randomPutUserRequest(true); + final PutUserResponse putUserResponse = execute(putUserRequest, securityClient::putUser, securityClient::putUserAsync); + assertThat(putUserResponse.isCreated(), is(true)); + + // authenticate correctly + final String basicAuthHeader = basicAuthHeader(putUserRequest.getUsername(), putUserRequest.getPassword()); + final AuthenticateResponse authenticateResponse = execute(securityClient::authenticate, securityClient::authenticateAsync, + authorizationRequestOptions(basicAuthHeader)); + + assertThat(authenticateResponse.getUser().username(), is(putUserRequest.getUsername())); + if (putUserRequest.getRoles().isEmpty()) { + assertThat(authenticateResponse.getUser().roles(), is(empty())); + } else { + assertThat(authenticateResponse.getUser().roles(), contains(putUserRequest.getRoles().toArray())); + } + assertThat(authenticateResponse.getUser().metadata(), is(putUserRequest.getMetadata())); + assertThat(authenticateResponse.getUser().fullName(), is(putUserRequest.getFullName())); + assertThat(authenticateResponse.getUser().email(), is(putUserRequest.getEmail())); + assertThat(authenticateResponse.enabled(), is(true)); + + // delete user + final Request deleteUserRequest = new Request(HttpDelete.METHOD_NAME, "/_xpack/security/user/" + putUserRequest.getUsername()); + highLevelClient().getLowLevelClient().performRequest(deleteUserRequest); + + // authentication no longer works + ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> execute(securityClient::authenticate, + securityClient::authenticateAsync, authorizationRequestOptions(basicAuthHeader))); + assertThat(e.getMessage(), containsString("unable to authenticate user [" + putUserRequest.getUsername() + "]")); + } + + private static PutUserRequest randomPutUserRequest(boolean enabled) { + final String username = randomAlphaOfLengthBetween(1, 4); + final char[] password = randomAlphaOfLengthBetween(6, 10).toCharArray(); + final List roles = Arrays.asList(generateRandomStringArray(3, 3, false, true)); + final String fullName = randomFrom(random(), null, randomAlphaOfLengthBetween(0, 3)); + final String email = randomFrom(random(), null, randomAlphaOfLengthBetween(0, 3)); + final Map metadata; + metadata = new HashMap<>(); + if (randomBoolean()) { + metadata.put("string", null); + } else { + metadata.put("string", randomAlphaOfLengthBetween(0, 4)); + } + if (randomBoolean()) { + metadata.put("string_list", null); + } else { + metadata.put("string_list", Arrays.asList(generateRandomStringArray(4, 4, false, true))); + } + return new PutUserRequest(username, password, roles, fullName, email, enabled, metadata, RefreshPolicy.IMMEDIATE); + } + + private static String basicAuthHeader(String username, char[] password) { + final String concat = new StringBuilder().append(username).append(':').append(password).toString(); + final byte[] concatBytes = CharArrays.toUtf8Bytes(concat.toCharArray()); + return "Basic " + Base64.getEncoder().encodeToString(concatBytes); + } + + private static RequestOptions authorizationRequestOptions(String authorizationHeader) { + final RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder(); + builder.addHeader("Authorization", authorizationHeader); + return builder.build(); + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java index 7c37c7ef50a7a..4849228dc529d 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java @@ -29,6 +29,7 @@ import org.elasticsearch.client.Request; import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.RestHighLevelClient; +import org.elasticsearch.client.security.AuthenticateResponse; import org.elasticsearch.client.security.ChangePasswordRequest; import org.elasticsearch.client.security.ClearRolesCacheRequest; import org.elasticsearch.client.security.ClearRolesCacheResponse; @@ -50,10 +51,11 @@ import org.elasticsearch.client.security.PutUserRequest; import org.elasticsearch.client.security.PutUserResponse; import org.elasticsearch.client.security.RefreshPolicy; -import org.elasticsearch.client.security.support.CertificateInfo; import org.elasticsearch.client.security.support.expressiondsl.RoleMapperExpression; -import org.elasticsearch.client.security.support.expressiondsl.expressions.AnyRoleMapperExpression; import org.elasticsearch.client.security.support.expressiondsl.fields.FieldRoleMapperExpression; +import org.elasticsearch.client.security.user.User; +import org.elasticsearch.client.security.support.CertificateInfo; +import org.elasticsearch.client.security.support.expressiondsl.expressions.AnyRoleMapperExpression; import org.elasticsearch.common.Strings; import org.elasticsearch.common.xcontent.XContentBuilder; import org.hamcrest.Matchers; @@ -67,13 +69,14 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import static org.hamcrest.Matchers.empty; -import static org.hamcrest.Matchers.not; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.isIn; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase { @@ -379,6 +382,51 @@ public void onFailure(Exception e) { } } + public void testAuthenticate() throws Exception { + RestHighLevelClient client = highLevelClient(); + { + //tag::authenticate-execute + AuthenticateResponse response = client.security().authenticate(RequestOptions.DEFAULT); + //end::authenticate-execute + + //tag::authenticate-response + User user = response.getUser(); // <1> + boolean enabled = response.enabled(); // <2> + //end::authenticate-response + + assertThat(user.username(), is("test_user")); + assertThat(user.roles(), contains(new String[] {"superuser"})); + assertThat(user.fullName(), nullValue()); + assertThat(user.email(), nullValue()); + assertThat(user.metadata().isEmpty(), is(true)); + assertThat(enabled, is(true)); + } + + { + // tag::authenticate-execute-listener + ActionListener listener = new ActionListener() { + @Override + public void onResponse(AuthenticateResponse response) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + // end::authenticate-execute-listener + + // Replace the empty listener by a blocking listener in test + final CountDownLatch latch = new CountDownLatch(1); + listener = new LatchedActionListener<>(listener, latch); + // tag::authenticate-execute-async + client.security().authenticateAsync(RequestOptions.DEFAULT, listener); // <1> + // end::authenticate-execute-async + + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + } + } public void testClearRolesCache() throws Exception { RestHighLevelClient client = highLevelClient(); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/AuthenticateResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/AuthenticateResponseTests.java new file mode 100644 index 0000000000000..ce813f5ecf59c --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/AuthenticateResponseTests.java @@ -0,0 +1,128 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.client.security; + +import org.elasticsearch.client.security.user.User; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.EqualsHashCodeTestUtils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.elasticsearch.test.AbstractXContentTestCase.xContentTester; + +public class AuthenticateResponseTests extends ESTestCase { + + public void testFromXContent() throws IOException { + xContentTester( + this::createParser, + this::createTestInstance, + this::toXContent, + AuthenticateResponse::fromXContent) + .supportsUnknownFields(false) + .test(); + } + + public void testEqualsAndHashCode() { + final AuthenticateResponse reponse = createTestInstance(); + EqualsHashCodeTestUtils.checkEqualsAndHashCode(reponse, this::copy, + this::mutate); + } + + protected AuthenticateResponse createTestInstance() { + final String username = randomAlphaOfLengthBetween(1, 4); + final List roles = Arrays.asList(generateRandomStringArray(4, 4, false, true)); + final Map metadata; + metadata = new HashMap<>(); + if (randomBoolean()) { + metadata.put("string", null); + } else { + metadata.put("string", randomAlphaOfLengthBetween(0, 4)); + } + if (randomBoolean()) { + metadata.put("string_list", null); + } else { + metadata.put("string_list", Arrays.asList(generateRandomStringArray(4, 4, false, true))); + } + final String fullName = randomFrom(random(), null, randomAlphaOfLengthBetween(0, 4)); + final String email = randomFrom(random(), null, randomAlphaOfLengthBetween(0, 4)); + final boolean enabled = randomBoolean(); + return new AuthenticateResponse(new User(username, roles, metadata, fullName, email), enabled); + } + + private void toXContent(AuthenticateResponse response, XContentBuilder builder) throws IOException { + final User user = response.getUser(); + final boolean enabled = response.enabled(); + builder.startObject(); + builder.field(AuthenticateResponse.USERNAME.getPreferredName(), user.username()); + builder.field(AuthenticateResponse.ROLES.getPreferredName(), user.roles()); + builder.field(AuthenticateResponse.METADATA.getPreferredName(), user.metadata()); + if (user.fullName() != null) { + builder.field(AuthenticateResponse.FULL_NAME.getPreferredName(), user.fullName()); + } + if (user.email() != null) { + builder.field(AuthenticateResponse.EMAIL.getPreferredName(), user.email()); + } + builder.field(AuthenticateResponse.ENABLED.getPreferredName(), enabled); + builder.endObject(); + } + + private AuthenticateResponse copy(AuthenticateResponse response) { + final User originalUser = response.getUser(); + final User copyUser = new User(originalUser.username(), originalUser.roles(), originalUser.metadata(), originalUser.fullName(), + originalUser.email()); + return new AuthenticateResponse(copyUser, response.enabled()); + } + + private AuthenticateResponse mutate(AuthenticateResponse response) { + final User originalUser = response.getUser(); + switch (randomIntBetween(1, 6)) { + case 1: + return new AuthenticateResponse(new User(originalUser.username() + "wrong", originalUser.roles(), originalUser.metadata(), + originalUser.fullName(), originalUser.email()), response.enabled()); + case 2: + final Collection wrongRoles = new ArrayList<>(originalUser.roles()); + wrongRoles.add(randomAlphaOfLengthBetween(1, 4)); + return new AuthenticateResponse(new User(originalUser.username(), wrongRoles, originalUser.metadata(), + originalUser.fullName(), originalUser.email()), response.enabled()); + case 3: + final Map wrongMetadata = new HashMap<>(originalUser.metadata()); + wrongMetadata.put("wrong_string", randomAlphaOfLengthBetween(0, 4)); + return new AuthenticateResponse(new User(originalUser.username(), originalUser.roles(), wrongMetadata, + originalUser.fullName(), originalUser.email()), response.enabled()); + case 4: + return new AuthenticateResponse(new User(originalUser.username(), originalUser.roles(), originalUser.metadata(), + originalUser.fullName() + "wrong", originalUser.email()), response.enabled()); + case 5: + return new AuthenticateResponse(new User(originalUser.username(), originalUser.roles(), originalUser.metadata(), + originalUser.fullName(), originalUser.email() + "wrong"), response.enabled()); + case 6: + return new AuthenticateResponse(new User(originalUser.username(), originalUser.roles(), originalUser.metadata(), + originalUser.fullName(), originalUser.email()), !response.enabled()); + } + throw new IllegalStateException("Bad random number"); + } +} diff --git a/docs/java-rest/high-level/security/authenticate.asciidoc b/docs/java-rest/high-level/security/authenticate.asciidoc new file mode 100644 index 0000000000000..e50c64bf9d0f5 --- /dev/null +++ b/docs/java-rest/high-level/security/authenticate.asciidoc @@ -0,0 +1,66 @@ + +-- +:api: authenticate +:response: AuthenticateResponse +-- + +[id="{upid}-{api}"] +=== Authenticate API + +[id="{upid}-{api}-sync"] +==== Execution + +Authenticating and retrieving information about a user can be performed +using the `security().authenticate()` method: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-execute] +-------------------------------------------------- + +This method does not require a request object. The client waits for the ++{response}+ to be returned before continuing with code execution. + +[id="{upid}-{api}-response"] +==== Response + +The returned +{response}+ contains two fields. Firstly, the `user` field +, accessed with `getUser`, contains all the information about this +authenticated user. The other field, `enabled`, tells if this user is actually +usable or has been temporalily deactivated. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-response] +-------------------------------------------------- +<1> `getUser` retrieves the `User` instance containing the information, +see {javadoc-client}/security/user/User.html. +<2> `enabled` tells if this user is usable or is deactivated. + +[id="{upid}-{api}-async"] +==== Asynchronous Execution + +This request can also be executed asynchronously: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-execute-async] +-------------------------------------------------- +<1> The `ActionListener` to use when the execution completes. This method does +not require a request object. + +The asynchronous method does not block and returns immediately. Once the request +has completed the `ActionListener` is called back using the `onResponse` method +if the execution completed successfully or using the `onFailure` method if +it failed. + +A typical listener for a +{response}+ looks like: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-execute-listener] +-------------------------------------------------- +<1> Called when the execution completed successfully. The response is +provided as an argument. +<2> Called in case of a failure. The exception is provided as an argument. + diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index b7f4cba952083..4411a6b375f91 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -327,6 +327,7 @@ The Java High Level REST Client supports the following Security APIs: * <> * <> * <<{upid}-clear-roles-cache>> +* <<{upid}-authenticate>> * <> * <> * <> @@ -339,6 +340,7 @@ include::security/disable-user.asciidoc[] include::security/change-password.asciidoc[] include::security/delete-role.asciidoc[] include::security/clear-roles-cache.asciidoc[] +include::security/authenticate.asciidoc[] include::security/get-certificates.asciidoc[] include::security/put-role-mapping.asciidoc[] include::security/get-role-mappings.asciidoc[] @@ -386,4 +388,4 @@ don't leak into the rest of the documentation. :response!: :doc-tests-file!: :upid!: --- \ No newline at end of file +--