Răsfoiți Sursa

Remove all "user" related methods from HLRC (#84011)

Removes the following methods from the SecurityClient component
of the High Level Rest Client

- putUser
- deleteUser
- changePassword
- authenticate

As part of this change, I renamed the SecurityClientTestHelper class
to TestSecurityClient and made it a real object rather than a set of
utility methods.

This was needed because different tests need different RequestOptions
objects, but passing it into every method made it cumbersome.
The code is clearer if we use a field in the test client itself. 

Relates: #83423
Tim Vernum 3 ani în urmă
părinte
comite
f9f6ec9ed3
22 a modificat fișierele cu 391 adăugiri și 762 ștergeri
  1. 0 84
      client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java
  2. 0 39
      client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityRequestConverters.java
  3. 0 63
      client/rest-high-level/src/main/java/org/elasticsearch/client/security/ChangePasswordRequest.java
  4. 0 60
      client/rest-high-level/src/main/java/org/elasticsearch/client/security/DeleteUserRequest.java
  5. 0 42
      client/rest-high-level/src/main/java/org/elasticsearch/client/security/DeleteUserResponse.java
  6. 0 192
      client/rest-high-level/src/main/java/org/elasticsearch/client/security/PutUserRequest.java
  7. 0 62
      client/rest-high-level/src/main/java/org/elasticsearch/client/security/PutUserResponse.java
  8. 7 3
      test/framework/src/main/java/org/elasticsearch/test/XContentTestUtils.java
  9. 44 15
      x-pack/plugin/identity-provider/qa/idp-rest-tests/src/javaRestTest/java/org/elasticsearch/xpack/idp/IdpRestTestCase.java
  10. 4 4
      x-pack/plugin/identity-provider/qa/idp-rest-tests/src/javaRestTest/java/org/elasticsearch/xpack/idp/WildcardServiceProviderRestIT.java
  11. 12 12
      x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/SecurityOnTrialLicenseRestTestCase.java
  12. 13 15
      x-pack/plugin/security/qa/smoke-test-all-realms/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/SecurityRealmSmokeTestCase.java
  13. 6 15
      x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/integration/MultipleIndicesPermissionsTests.java
  14. 41 24
      x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java
  15. 21 21
      x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/TokenAuthIntegTests.java
  16. 4 15
      x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmIntegTests.java
  17. 91 53
      x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/pki/PkiAuthDelegationIntegTests.java
  18. 1 10
      x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authz/SnapshotUserRoleIntegTests.java
  19. 0 24
      x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecurityClientTestHelper.java
  20. 13 0
      x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecurityIntegTestCase.java
  21. 119 0
      x-pack/plugin/security/src/test/java/org/elasticsearch/test/TestSecurityClient.java
  22. 15 9
      x-pack/qa/security-example-spi-extension/src/javaRestTest/java/org/elasticsearch/example/role/CustomRolesProviderIT.java

+ 0 - 84
client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java

@@ -8,9 +8,6 @@
 
 package org.elasticsearch.client;
 
-import org.elasticsearch.client.security.AuthenticateRequest;
-import org.elasticsearch.client.security.AuthenticateResponse;
-import org.elasticsearch.client.security.ChangePasswordRequest;
 import org.elasticsearch.client.security.ClearRealmCacheRequest;
 import org.elasticsearch.client.security.ClearRealmCacheResponse;
 import org.elasticsearch.client.security.CreateTokenRequest;
@@ -21,8 +18,6 @@ import org.elasticsearch.client.security.DeleteRoleMappingRequest;
 import org.elasticsearch.client.security.DeleteRoleMappingResponse;
 import org.elasticsearch.client.security.DeleteRoleRequest;
 import org.elasticsearch.client.security.DeleteRoleResponse;
-import org.elasticsearch.client.security.DeleteUserRequest;
-import org.elasticsearch.client.security.DeleteUserResponse;
 import org.elasticsearch.client.security.GetApiKeyRequest;
 import org.elasticsearch.client.security.GetApiKeyResponse;
 import org.elasticsearch.client.security.GetRolesRequest;
@@ -37,8 +32,6 @@ import org.elasticsearch.client.security.PutRoleMappingRequest;
 import org.elasticsearch.client.security.PutRoleMappingResponse;
 import org.elasticsearch.client.security.PutRoleRequest;
 import org.elasticsearch.client.security.PutRoleResponse;
-import org.elasticsearch.client.security.PutUserRequest;
-import org.elasticsearch.client.security.PutUserResponse;
 
 import java.io.IOException;
 
@@ -64,45 +57,6 @@ public final class SecurityClient {
         this.restHighLevelClient = restHighLevelClient;
     }
 
-    /**
-     * Create/update a user in the native realm synchronously.
-     * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-users.html">
-     * the docs</a> for more.
-     *
-     * @param request the request with the user's information
-     * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
-     * @return the response from the put user call
-     * @throws IOException in case there is a problem sending the request or parsing back the response
-     */
-    public PutUserResponse putUser(PutUserRequest request, RequestOptions options) throws IOException {
-        return restHighLevelClient.performRequestAndParseEntity(
-            request,
-            SecurityRequestConverters::putUser,
-            options,
-            PutUserResponse::fromXContent,
-            emptySet()
-        );
-    }
-
-    /**
-     * Removes user from the native realm synchronously.
-     * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-delete-user.html">
-     * the docs</a> for more.
-     * @param request the request with the user to delete
-     * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
-     * @return the response from the delete user call
-     * @throws IOException in case there is a problem sending the request or parsing back the response
-     */
-    public DeleteUserResponse deleteUser(DeleteUserRequest request, RequestOptions options) throws IOException {
-        return restHighLevelClient.performRequestAndParseEntity(
-            request,
-            SecurityRequestConverters::deleteUser,
-            options,
-            DeleteUserResponse::fromXContent,
-            singleton(404)
-        );
-    }
-
     /**
      * Create/Update a role mapping.
      * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-role-mapping.html">
@@ -122,24 +76,6 @@ public final class SecurityClient {
         );
     }
 
-    /**
-     * Authenticate the current user and return all the information about the authenticated user.
-     * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-authenticate.html">
-     * the docs</a> 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()
-        );
-    }
-
     /**
      * Clears the cache in one or more realms.
      * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-clear-cache.html">
@@ -160,26 +96,6 @@ public final class SecurityClient {
         );
     }
 
-    /**
-     * Change the password of a user of a native realm or built-in user synchronously.
-     * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-change-password.html">
-     * the docs</a> for more.
-     *
-     * @param request the request with the user's new password
-     * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
-     * @return {@code true} if the request succeeded (the new password was set)
-     * @throws IOException in case there is a problem sending the request or parsing back the response
-     */
-    public boolean changePassword(ChangePasswordRequest request, RequestOptions options) throws IOException {
-        return restHighLevelClient.performRequest(
-            request,
-            SecurityRequestConverters::changePassword,
-            options,
-            RestHighLevelClient::convertExistsResponse,
-            emptySet()
-        );
-    }
-
     /**
      * Delete a role mapping.
      * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-delete-role-mapping.html">

+ 0 - 39
client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityRequestConverters.java

@@ -12,13 +12,11 @@ import org.apache.http.client.methods.HttpDelete;
 import org.apache.http.client.methods.HttpGet;
 import org.apache.http.client.methods.HttpPost;
 import org.apache.http.client.methods.HttpPut;
-import org.elasticsearch.client.security.ChangePasswordRequest;
 import org.elasticsearch.client.security.ClearRealmCacheRequest;
 import org.elasticsearch.client.security.CreateTokenRequest;
 import org.elasticsearch.client.security.DelegatePkiAuthenticationRequest;
 import org.elasticsearch.client.security.DeleteRoleMappingRequest;
 import org.elasticsearch.client.security.DeleteRoleRequest;
-import org.elasticsearch.client.security.DeleteUserRequest;
 import org.elasticsearch.client.security.GetApiKeyRequest;
 import org.elasticsearch.client.security.GetRolesRequest;
 import org.elasticsearch.client.security.InvalidateApiKeyRequest;
@@ -26,7 +24,6 @@ import org.elasticsearch.client.security.InvalidateTokenRequest;
 import org.elasticsearch.client.security.PutPrivilegesRequest;
 import org.elasticsearch.client.security.PutRoleMappingRequest;
 import org.elasticsearch.client.security.PutRoleRequest;
-import org.elasticsearch.client.security.PutUserRequest;
 import org.elasticsearch.common.Strings;
 
 import java.io.IOException;
@@ -38,42 +35,6 @@ final class SecurityRequestConverters {
 
     private SecurityRequestConverters() {}
 
-    static Request changePassword(ChangePasswordRequest changePasswordRequest) throws IOException {
-        String endpoint = new RequestConverters.EndpointBuilder().addPathPartAsIs("_security/user")
-            .addPathPart(changePasswordRequest.getUsername())
-            .addPathPartAsIs("_password")
-            .build();
-        Request request = new Request(HttpPost.METHOD_NAME, endpoint);
-        request.setEntity(createEntity(changePasswordRequest, REQUEST_BODY_CONTENT_TYPE));
-        RequestConverters.Params params = new RequestConverters.Params();
-        params.withRefreshPolicy(changePasswordRequest.getRefreshPolicy());
-        request.addParameters(params.asMap());
-        return request;
-    }
-
-    static Request putUser(PutUserRequest putUserRequest) throws IOException {
-        String endpoint = new RequestConverters.EndpointBuilder().addPathPartAsIs("_security/user")
-            .addPathPart(putUserRequest.getUser().getUsername())
-            .build();
-        Request request = new Request(HttpPut.METHOD_NAME, endpoint);
-        request.setEntity(createEntity(putUserRequest, REQUEST_BODY_CONTENT_TYPE));
-        RequestConverters.Params params = new RequestConverters.Params();
-        params.withRefreshPolicy(putUserRequest.getRefreshPolicy());
-        request.addParameters(params.asMap());
-        return request;
-    }
-
-    static Request deleteUser(DeleteUserRequest deleteUserRequest) {
-        String endpoint = new RequestConverters.EndpointBuilder().addPathPartAsIs("_security", "user")
-            .addPathPart(deleteUserRequest.getName())
-            .build();
-        Request request = new Request(HttpDelete.METHOD_NAME, endpoint);
-        RequestConverters.Params params = new RequestConverters.Params();
-        params.withRefreshPolicy(deleteUserRequest.getRefreshPolicy());
-        request.addParameters(params.asMap());
-        return request;
-    }
-
     static Request putRoleMapping(final PutRoleMappingRequest putRoleMappingRequest) throws IOException {
         final String endpoint = new RequestConverters.EndpointBuilder().addPathPartAsIs("_security/role_mapping")
             .addPathPart(putRoleMappingRequest.getName())

+ 0 - 63
client/rest-high-level/src/main/java/org/elasticsearch/client/security/ChangePasswordRequest.java

@@ -1,63 +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
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-package org.elasticsearch.client.security;
-
-import org.elasticsearch.client.Validatable;
-import org.elasticsearch.core.CharArrays;
-import org.elasticsearch.core.Nullable;
-import org.elasticsearch.xcontent.ToXContentObject;
-import org.elasticsearch.xcontent.XContentBuilder;
-
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.Objects;
-
-/**
- * Request object to change the password of a user of a native realm or a built-in user.
- */
-public final class ChangePasswordRequest implements Validatable, ToXContentObject {
-
-    private final String username;
-    private final char[] password;
-    private final RefreshPolicy refreshPolicy;
-
-    /**
-     * @param username      The username of the user whose password should be changed or null for the current user.
-     * @param password      The new password. The password array is not cleared by the {@link ChangePasswordRequest} object so the
-     *                      calling code must clear it after receiving the response.
-     * @param refreshPolicy The refresh policy for the request.
-     */
-    public ChangePasswordRequest(@Nullable String username, char[] password, RefreshPolicy refreshPolicy) {
-        this.username = username;
-        this.password = Objects.requireNonNull(password, "password is required");
-        this.refreshPolicy = refreshPolicy == null ? RefreshPolicy.getDefault() : refreshPolicy;
-    }
-
-    public String getUsername() {
-        return username;
-    }
-
-    public char[] getPassword() {
-        return password;
-    }
-
-    public RefreshPolicy getRefreshPolicy() {
-        return refreshPolicy;
-    }
-
-    @Override
-    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
-        byte[] charBytes = CharArrays.toUtf8Bytes(password);
-        try {
-            return builder.startObject().field("password").utf8Value(charBytes, 0, charBytes.length).endObject();
-        } finally {
-            Arrays.fill(charBytes, (byte) 0);
-        }
-    }
-}

+ 0 - 60
client/rest-high-level/src/main/java/org/elasticsearch/client/security/DeleteUserRequest.java

@@ -1,60 +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
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-package org.elasticsearch.client.security;
-
-import org.elasticsearch.client.Validatable;
-
-import java.util.Objects;
-
-/**
- * A request to delete a user from the native realm.
- */
-public final class DeleteUserRequest implements Validatable {
-
-    private final String name;
-    private final RefreshPolicy refreshPolicy;
-
-    public DeleteUserRequest(String name) {
-        this(name, RefreshPolicy.IMMEDIATE);
-    }
-
-    public DeleteUserRequest(String name, RefreshPolicy refreshPolicy) {
-        this.name = Objects.requireNonNull(name, "user name is required");
-        this.refreshPolicy = Objects.requireNonNull(refreshPolicy, "refresh policy is required");
-    }
-
-    public String getName() {
-        return name;
-    }
-
-    public RefreshPolicy getRefreshPolicy() {
-        return refreshPolicy;
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(name, refreshPolicy);
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        final DeleteUserRequest other = (DeleteUserRequest) obj;
-
-        return (refreshPolicy == other.refreshPolicy) && Objects.equals(name, other.name);
-    }
-}

+ 0 - 42
client/rest-high-level/src/main/java/org/elasticsearch/client/security/DeleteUserResponse.java

@@ -1,42 +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
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-package org.elasticsearch.client.security;
-
-import org.elasticsearch.client.core.AcknowledgedResponse;
-import org.elasticsearch.xcontent.ConstructingObjectParser;
-import org.elasticsearch.xcontent.XContentParser;
-
-import java.io.IOException;
-
-/**
- * Response for a user being deleted from the native realm
- */
-public final class DeleteUserResponse extends AcknowledgedResponse {
-
-    private static final String PARSE_FIELD_NAME = "found";
-
-    private static final ConstructingObjectParser<DeleteUserResponse, Void> PARSER = AcknowledgedResponse.generateParser(
-        "delete_user_response",
-        DeleteUserResponse::new,
-        PARSE_FIELD_NAME
-    );
-
-    public DeleteUserResponse(boolean acknowledged) {
-        super(acknowledged);
-    }
-
-    public static DeleteUserResponse fromXContent(final XContentParser parser) throws IOException {
-        return PARSER.parse(parser, null);
-    }
-
-    @Override
-    protected String getFieldName() {
-        return PARSE_FIELD_NAME;
-    }
-}

+ 0 - 192
client/rest-high-level/src/main/java/org/elasticsearch/client/security/PutUserRequest.java

@@ -1,192 +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
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-package org.elasticsearch.client.security;
-
-import org.elasticsearch.client.Validatable;
-import org.elasticsearch.client.ValidationException;
-import org.elasticsearch.client.security.user.User;
-import org.elasticsearch.core.CharArrays;
-import org.elasticsearch.core.Nullable;
-import org.elasticsearch.xcontent.ToXContentObject;
-import org.elasticsearch.xcontent.XContentBuilder;
-
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.Objects;
-import java.util.Optional;
-
-/**
- * Request object to create or update a user in the native realm.
- */
-public final class PutUserRequest implements Validatable, ToXContentObject {
-
-    private final User user;
-    private final @Nullable char[] password;
-    private final @Nullable char[] passwordHash;
-    private final boolean enabled;
-    private final RefreshPolicy refreshPolicy;
-
-    /**
-     * Create or update a user in the native realm, with the user's new or updated password specified in plaintext.
-     * @param user the user to be created or updated
-     * @param password the password of the user. The password array is not modified by this class.
-     *                 It is the responsibility of the caller to clear the password after receiving
-     *                 a response.
-     * @param enabled true if the user is enabled and allowed to access elasticsearch
-     * @param refreshPolicy the refresh policy for the request.
-     */
-    public static PutUserRequest withPassword(User user, char[] password, boolean enabled, RefreshPolicy refreshPolicy) {
-        return new PutUserRequest(user, password, null, enabled, refreshPolicy);
-    }
-
-    /**
-     * Create or update a user in the native realm, with the user's new or updated password specified as a cryptographic hash.
-     * @param user the user to be created or updated
-     * @param passwordHash the hash of the password of the user. It must be in the correct format for the password hashing algorithm in
-     *                     use on this elasticsearch cluster. The array is not modified by this class.
-     *                     It is the responsibility of the caller to clear the hash after receiving a response.
-     * @param enabled true if the user is enabled and allowed to access elasticsearch
-     * @param refreshPolicy the refresh policy for the request.
-     */
-    public static PutUserRequest withPasswordHash(User user, char[] passwordHash, boolean enabled, RefreshPolicy refreshPolicy) {
-        return new PutUserRequest(user, null, passwordHash, enabled, refreshPolicy);
-    }
-
-    /**
-     * Update an existing user in the native realm without modifying their password.
-     * @param user the user to be created or updated
-     * @param enabled true if the user is enabled and allowed to access elasticsearch
-     * @param refreshPolicy the refresh policy for the request.
-     */
-    public static PutUserRequest updateUser(User user, boolean enabled, RefreshPolicy refreshPolicy) {
-        return new PutUserRequest(user, null, null, enabled, refreshPolicy);
-    }
-
-    /**
-     * Creates a new request that is used to create or update a user in the native realm.
-     *
-     * @param user the user to be created or updated
-     * @param password the password of the user. The password array is not modified by this class.
-     *                 It is the responsibility of the caller to clear the password after receiving
-     *                 a response.
-     * @param enabled true if the user is enabled and allowed to access elasticsearch
-     * @param refreshPolicy the refresh policy for the request.
-     * @deprecated Use {@link #withPassword(User, char[], boolean, RefreshPolicy)} or
-     *             {@link #updateUser(User, boolean, RefreshPolicy)} instead.
-     */
-    @Deprecated
-    public PutUserRequest(User user, @Nullable char[] password, boolean enabled, @Nullable RefreshPolicy refreshPolicy) {
-        this(user, password, null, enabled, refreshPolicy);
-    }
-
-    /**
-     * Creates a new request that is used to create or update a user in the native realm.
-     * @param user the user to be created or updated
-     * @param password the password of the user. The password array is not modified by this class.
-     *                 It is the responsibility of the caller to clear the password after receiving
-     *                 a response.
-     * @param passwordHash the hash of the password. Only one of "password" or "passwordHash" may be populated.
-     *                     The other parameter must be {@code null}.
-     * @param enabled true if the user is enabled and allowed to access elasticsearch
-     * @param refreshPolicy the refresh policy for the request.
-     */
-    private PutUserRequest(
-        User user,
-        @Nullable char[] password,
-        @Nullable char[] passwordHash,
-        boolean enabled,
-        RefreshPolicy refreshPolicy
-    ) {
-        this.user = Objects.requireNonNull(user, "user is required, cannot be null");
-        if (password != null && passwordHash != null) {
-            throw new IllegalArgumentException("cannot specify both password and passwordHash");
-        }
-        this.password = password;
-        this.passwordHash = passwordHash;
-        this.enabled = enabled;
-        this.refreshPolicy = refreshPolicy == null ? RefreshPolicy.getDefault() : refreshPolicy;
-    }
-
-    public User getUser() {
-        return user;
-    }
-
-    public @Nullable char[] getPassword() {
-        return password;
-    }
-
-    public boolean isEnabled() {
-        return enabled;
-    }
-
-    public RefreshPolicy getRefreshPolicy() {
-        return refreshPolicy;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        final PutUserRequest that = (PutUserRequest) o;
-        return Objects.equals(user, that.user)
-            && Arrays.equals(password, that.password)
-            && Arrays.equals(passwordHash, that.passwordHash)
-            && enabled == that.enabled
-            && refreshPolicy == that.refreshPolicy;
-    }
-
-    @Override
-    public int hashCode() {
-        int result = Objects.hash(user, enabled, refreshPolicy);
-        result = 31 * result + Arrays.hashCode(password);
-        result = 31 * result + Arrays.hashCode(passwordHash);
-        return result;
-    }
-
-    @Override
-    public Optional<ValidationException> validate() {
-        if (user.getMetadata() != null && user.getMetadata().keySet().stream().anyMatch(s -> s.startsWith("_"))) {
-            ValidationException validationException = new ValidationException();
-            validationException.addValidationError("user metadata keys may not start with [_]");
-            return Optional.of(validationException);
-        }
-        return Optional.empty();
-    }
-
-    @Override
-    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
-        builder.startObject();
-        builder.field("username", user.getUsername());
-        if (password != null) {
-            charField(builder, "password", password);
-        }
-        if (passwordHash != null) {
-            charField(builder, "password_hash", passwordHash);
-        }
-        builder.field("roles", user.getRoles());
-        if (user.getFullName() != null) {
-            builder.field("full_name", user.getFullName());
-        }
-        if (user.getEmail() != null) {
-            builder.field("email", user.getEmail());
-        }
-        builder.field("metadata", user.getMetadata());
-        builder.field("enabled", enabled);
-        return builder.endObject();
-    }
-
-    private void charField(XContentBuilder builder, String fieldName, char[] chars) throws IOException {
-        byte[] charBytes = CharArrays.toUtf8Bytes(chars);
-        try {
-            builder.field(fieldName).utf8Value(charBytes, 0, charBytes.length);
-        } finally {
-            Arrays.fill(charBytes, (byte) 0);
-        }
-    }
-}

+ 0 - 62
client/rest-high-level/src/main/java/org/elasticsearch/client/security/PutUserResponse.java

@@ -1,62 +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
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-package org.elasticsearch.client.security;
-
-import org.elasticsearch.xcontent.ConstructingObjectParser;
-import org.elasticsearch.xcontent.ParseField;
-import org.elasticsearch.xcontent.XContentParser;
-
-import java.io.IOException;
-import java.util.Objects;
-
-import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg;
-
-/**
- * Response when adding a user to the native realm. Returns a
- * single boolean field for whether the user was created or updated.
- */
-public final class PutUserResponse {
-
-    private final boolean created;
-
-    public PutUserResponse(boolean created) {
-        this.created = created;
-    }
-
-    public boolean isCreated() {
-        return created;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        PutUserResponse that = (PutUserResponse) o;
-        return created == that.created;
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(created);
-    }
-
-    private static final ConstructingObjectParser<PutUserResponse, Void> PARSER = new ConstructingObjectParser<>(
-        "put_user_response",
-        true,
-        args -> new PutUserResponse((boolean) args[0])
-    );
-
-    static {
-        PARSER.declareBoolean(constructorArg(), new ParseField("created"));
-    }
-
-    public static PutUserResponse fromXContent(XContentParser parser) throws IOException {
-        return PARSER.parse(parser, null);
-    }
-}

+ 7 - 3
test/framework/src/main/java/org/elasticsearch/test/XContentTestUtils.java

@@ -43,9 +43,13 @@ public final class XContentTestUtils {
 
     public static Map<String, Object> convertToMap(ToXContent part) throws IOException {
         XContentBuilder builder = XContentFactory.jsonBuilder();
-        builder.startObject();
-        part.toXContent(builder, EMPTY_PARAMS);
-        builder.endObject();
+        if (part.isFragment()) {
+            builder.startObject();
+            part.toXContent(builder, EMPTY_PARAMS);
+            builder.endObject();
+        } else {
+            part.toXContent(builder, EMPTY_PARAMS);
+        }
         return XContentHelper.convertToMap(BytesReference.bytes(builder), false, builder.contentType()).v2();
     }
 

+ 44 - 15
x-pack/plugin/identity-provider/qa/idp-rest-tests/src/javaRestTest/java/org/elasticsearch/xpack/idp/IdpRestTestCase.java

@@ -6,18 +6,17 @@
  */
 package org.elasticsearch.xpack.idp;
 
+import org.apache.http.client.methods.HttpDelete;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
 import org.elasticsearch.client.Request;
 import org.elasticsearch.client.RequestOptions;
 import org.elasticsearch.client.Response;
 import org.elasticsearch.client.RestHighLevelClient;
-import org.elasticsearch.client.security.ChangePasswordRequest;
 import org.elasticsearch.client.security.DeleteRoleRequest;
-import org.elasticsearch.client.security.DeleteUserRequest;
 import org.elasticsearch.client.security.PutPrivilegesRequest;
 import org.elasticsearch.client.security.PutRoleRequest;
-import org.elasticsearch.client.security.PutUserRequest;
 import org.elasticsearch.client.security.RefreshPolicy;
-import org.elasticsearch.client.security.user.User;
 import org.elasticsearch.client.security.user.privileges.ApplicationPrivilege;
 import org.elasticsearch.client.security.user.privileges.ApplicationResourcePrivileges;
 import org.elasticsearch.client.security.user.privileges.IndicesPrivileges;
@@ -29,6 +28,7 @@ import org.elasticsearch.common.util.concurrent.ThreadContext;
 import org.elasticsearch.test.rest.ESRestTestCase;
 import org.elasticsearch.xcontent.ObjectPath;
 import org.elasticsearch.xcontent.json.JsonXContent;
+import org.elasticsearch.xpack.core.security.user.User;
 import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProviderIndex;
 
 import java.io.IOException;
@@ -67,18 +67,40 @@ public abstract class IdpRestTestCase extends ESRestTestCase {
         return highLevelAdminClient;
     }
 
-    protected User createUser(String username, SecureString password, String... roles) throws IOException {
-        final RestHighLevelClient client = getHighLevelAdminClient();
-        final User user = new User(username, List.of(roles), Map.of(), username + " in " + getTestName(), username + "@test.example.com");
-        final PutUserRequest request = PutUserRequest.withPassword(user, password.getChars(), true, RefreshPolicy.IMMEDIATE);
-        client.security().putUser(request, RequestOptions.DEFAULT);
+    protected User createUser(String username, SecureString password, String role) throws IOException {
+        final User user = new User(
+            username,
+            new String[] { role },
+            username + " in " + getTestName(),
+            username + "@test.example.com",
+            Map.of(),
+            true
+        );
+        final String endpoint = "/_security/user/" + username;
+        final Request request = new Request(HttpPut.METHOD_NAME, endpoint);
+        final String body = """
+            {
+                "username": "%s",
+                "full_name": "%s",
+                "email": "%s",
+                "password": "%s",
+                "roles": [ "%s" ]
+            }
+            """.formatted(user.principal(), user.fullName(), user.email(), password.toString(), role);
+        request.setJsonEntity(body);
+        request.addParameters(Map.of("refresh", "true"));
+        request.setOptions(RequestOptions.DEFAULT);
+        adminClient().performRequest(request);
+
         return user;
     }
 
     protected void deleteUser(String username) throws IOException {
-        final RestHighLevelClient client = getHighLevelAdminClient();
-        final DeleteUserRequest request = new DeleteUserRequest(username, RefreshPolicy.WAIT_UNTIL);
-        client.security().deleteUser(request, RequestOptions.DEFAULT);
+        final String endpoint = "/_security/user/" + username;
+        final Request request = new Request(HttpDelete.METHOD_NAME, endpoint);
+        request.addParameters(Map.of("refresh", "true"));
+        request.setOptions(RequestOptions.DEFAULT);
+        adminClient().performRequest(request);
     }
 
     protected void createRole(
@@ -114,9 +136,16 @@ public abstract class IdpRestTestCase extends ESRestTestCase {
     }
 
     protected void setUserPassword(String username, SecureString password) throws IOException {
-        final RestHighLevelClient client = getHighLevelAdminClient();
-        final ChangePasswordRequest request = new ChangePasswordRequest(username, password.getChars(), RefreshPolicy.NONE);
-        client.security().changePassword(request, RequestOptions.DEFAULT);
+        final String endpoint = "/_security/user/" + username + "/_password";
+        final Request request = new Request(HttpPost.METHOD_NAME, endpoint);
+        final String body = """
+            {
+                "password": "%s"
+            }
+            """.formatted(password.toString());
+        request.setJsonEntity(body);
+        request.setOptions(RequestOptions.DEFAULT);
+        adminClient().performRequest(request);
     }
 
     protected SamlServiceProviderIndex.DocumentVersion createServiceProvider(String entityId, Map<String, Object> body) throws IOException {

+ 4 - 4
x-pack/plugin/identity-provider/qa/idp-rest-tests/src/javaRestTest/java/org/elasticsearch/xpack/idp/WildcardServiceProviderRestIT.java

@@ -8,13 +8,13 @@ package org.elasticsearch.xpack.idp;
 
 import org.elasticsearch.client.Request;
 import org.elasticsearch.client.Response;
-import org.elasticsearch.client.security.user.User;
 import org.elasticsearch.client.security.user.privileges.ApplicationResourcePrivileges;
 import org.elasticsearch.common.bytes.BytesReference;
 import org.elasticsearch.common.settings.SecureString;
 import org.elasticsearch.xcontent.XContentBuilder;
 import org.elasticsearch.xcontent.XContentType;
 import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken;
+import org.elasticsearch.xpack.core.security.user.User;
 import org.junit.Before;
 
 import java.io.IOException;
@@ -78,9 +78,9 @@ public class WildcardServiceProviderRestIT extends IdpRestTestCase {
             assertThat(samlResponse, containsString("FriendlyName=\"" + attr + "\""));
         }
 
-        assertThat(samlResponse, containsString(user.getUsername()));
-        assertThat(samlResponse, containsString(user.getEmail()));
-        assertThat(samlResponse, containsString(user.getFullName()));
+        assertThat(samlResponse, containsString(user.principal()));
+        assertThat(samlResponse, containsString(user.email()));
+        assertThat(samlResponse, containsString(user.fullName()));
         assertThat(samlResponse, containsString(">admin<"));
 
         deleteUser(username);

+ 12 - 12
x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/SecurityOnTrialLicenseRestTestCase.java

@@ -12,21 +12,19 @@ import org.elasticsearch.client.RestHighLevelClient;
 import org.elasticsearch.client.security.CreateTokenRequest;
 import org.elasticsearch.client.security.CreateTokenResponse;
 import org.elasticsearch.client.security.DeleteRoleRequest;
-import org.elasticsearch.client.security.DeleteUserRequest;
 import org.elasticsearch.client.security.GetApiKeyRequest;
 import org.elasticsearch.client.security.GetApiKeyResponse;
 import org.elasticsearch.client.security.InvalidateApiKeyRequest;
 import org.elasticsearch.client.security.PutRoleRequest;
-import org.elasticsearch.client.security.PutUserRequest;
-import org.elasticsearch.client.security.RefreshPolicy;
 import org.elasticsearch.client.security.support.ApiKey;
-import org.elasticsearch.client.security.user.User;
 import org.elasticsearch.client.security.user.privileges.Role;
 import org.elasticsearch.common.settings.SecureString;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.util.concurrent.ThreadContext;
 import org.elasticsearch.core.Tuple;
+import org.elasticsearch.test.TestSecurityClient;
 import org.elasticsearch.test.rest.ESRestTestCase;
+import org.elasticsearch.xpack.core.security.user.User;
 import org.hamcrest.Matchers;
 
 import java.io.IOException;
@@ -36,6 +34,7 @@ import java.util.List;
 @SuppressWarnings("removal")
 public abstract class SecurityOnTrialLicenseRestTestCase extends ESRestTestCase {
     private RestHighLevelClient highLevelAdminClient;
+    private TestSecurityClient securityClient;
 
     @Override
     protected Settings restAdminSettings() {
@@ -49,13 +48,15 @@ public abstract class SecurityOnTrialLicenseRestTestCase extends ESRestTestCase
         return Settings.builder().put(ThreadContext.PREFIX + ".Authorization", token).build();
     }
 
+    protected TestSecurityClient getSecurityClient() {
+        if (securityClient == null) {
+            securityClient = new TestSecurityClient(adminClient());
+        }
+        return securityClient;
+    }
+
     protected void createUser(String username, SecureString password, List<String> roles) throws IOException {
-        final RestHighLevelClient client = getHighLevelAdminClient();
-        client.security()
-            .putUser(
-                PutUserRequest.withPassword(new User(username, roles), password.getChars(), true, RefreshPolicy.WAIT_UNTIL),
-                RequestOptions.DEFAULT
-            );
+        getSecurityClient().putUser(new User(username, roles.toArray(String[]::new)), password);
     }
 
     protected void createRole(String name, Collection<String> clusterPrivileges) throws IOException {
@@ -75,8 +76,7 @@ public abstract class SecurityOnTrialLicenseRestTestCase extends ESRestTestCase
     }
 
     protected void deleteUser(String username) throws IOException {
-        final RestHighLevelClient client = getHighLevelAdminClient();
-        client.security().deleteUser(new DeleteUserRequest(username), RequestOptions.DEFAULT);
+        getSecurityClient().deleteUser(username);
     }
 
     protected void deleteRole(String name) throws IOException {

+ 13 - 15
x-pack/plugin/security/qa/smoke-test-all-realms/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/SecurityRealmSmokeTestCase.java

@@ -11,21 +11,19 @@ import org.elasticsearch.client.Request;
 import org.elasticsearch.client.RequestOptions;
 import org.elasticsearch.client.Response;
 import org.elasticsearch.client.RestHighLevelClient;
-import org.elasticsearch.client.security.ChangePasswordRequest;
 import org.elasticsearch.client.security.DeleteRoleRequest;
-import org.elasticsearch.client.security.DeleteUserRequest;
 import org.elasticsearch.client.security.PutRoleRequest;
-import org.elasticsearch.client.security.PutUserRequest;
 import org.elasticsearch.client.security.RefreshPolicy;
-import org.elasticsearch.client.security.user.User;
 import org.elasticsearch.client.security.user.privileges.Role;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.settings.SecureString;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.util.concurrent.ThreadContext;
 import org.elasticsearch.core.PathUtils;
+import org.elasticsearch.test.TestSecurityClient;
 import org.elasticsearch.test.rest.ESRestTestCase;
 import org.elasticsearch.xpack.core.security.authc.Authentication.AuthenticationType;
+import org.elasticsearch.xpack.core.security.user.User;
 import org.junit.BeforeClass;
 
 import java.io.FileNotFoundException;
@@ -48,6 +46,7 @@ public abstract class SecurityRealmSmokeTestCase extends ESRestTestCase {
 
     private static Path httpCAPath;
     private RestHighLevelClient highLevelAdminClient;
+    private TestSecurityClient securityClient;
 
     @BeforeClass
     public static void findHttpCertificateAuthority() throws Exception {
@@ -111,18 +110,11 @@ public abstract class SecurityRealmSmokeTestCase extends ESRestTestCase {
     }
 
     protected void createUser(String username, SecureString password, List<String> roles) throws IOException {
-        final RestHighLevelClient client = getHighLevelAdminClient();
-        client.security()
-            .putUser(
-                PutUserRequest.withPassword(new User(username, roles), password.getChars(), true, RefreshPolicy.WAIT_UNTIL),
-                RequestOptions.DEFAULT
-            );
+        getSecurityClient().putUser(new User(username, roles.toArray(String[]::new)), password);
     }
 
     protected void changePassword(String username, SecureString password) throws IOException {
-        final RestHighLevelClient client = getHighLevelAdminClient();
-        client.security()
-            .changePassword(new ChangePasswordRequest(username, password.getChars(), RefreshPolicy.WAIT_UNTIL), RequestOptions.DEFAULT);
+        getSecurityClient().changePassword(username, password);
     }
 
     protected void createRole(String name, Collection<String> clusterPrivileges) throws IOException {
@@ -132,8 +124,7 @@ public abstract class SecurityRealmSmokeTestCase extends ESRestTestCase {
     }
 
     protected void deleteUser(String username) throws IOException {
-        final RestHighLevelClient client = getHighLevelAdminClient();
-        client.security().deleteUser(new DeleteUserRequest(username), RequestOptions.DEFAULT);
+        getSecurityClient().deleteUser(username);
     }
 
     protected void deleteRole(String name) throws IOException {
@@ -148,4 +139,11 @@ public abstract class SecurityRealmSmokeTestCase extends ESRestTestCase {
         }
         return highLevelAdminClient;
     }
+
+    protected TestSecurityClient getSecurityClient() {
+        if (securityClient == null) {
+            securityClient = new TestSecurityClient(adminClient());
+        }
+        return securityClient;
+    }
 }

+ 6 - 15
x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/integration/MultipleIndicesPermissionsTests.java

@@ -21,25 +21,22 @@ import org.elasticsearch.client.Request;
 import org.elasticsearch.client.RequestOptions;
 import org.elasticsearch.client.Response;
 import org.elasticsearch.client.internal.Client;
-import org.elasticsearch.client.security.PutUserRequest;
-import org.elasticsearch.client.security.RefreshPolicy;
-import org.elasticsearch.client.security.user.User;
 import org.elasticsearch.common.settings.SecureString;
 import org.elasticsearch.index.query.QueryBuilders;
 import org.elasticsearch.rest.RestStatus;
 import org.elasticsearch.test.SecurityIntegTestCase;
 import org.elasticsearch.test.SecuritySettingsSource;
 import org.elasticsearch.test.SecuritySettingsSourceField;
+import org.elasticsearch.test.TestSecurityClient;
 import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken;
+import org.elasticsearch.xpack.core.security.user.User;
 import org.junit.After;
 import org.junit.Before;
 
 import java.util.Collections;
-import java.util.List;
 
 import static org.elasticsearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE;
 import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery;
-import static org.elasticsearch.test.SecuritySettingsSource.SECURITY_REQUEST_OPTIONS;
 import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
 import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount;
 import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoFailures;
@@ -58,16 +55,10 @@ public class MultipleIndicesPermissionsTests extends SecurityIntegTestCase {
     @Before
     public void waitForSecurityIndexWritable() throws Exception {
         // adds a dummy user to the native realm to force .security index creation
-        new TestRestHighLevelClient().security()
-            .putUser(
-                PutUserRequest.withPassword(
-                    new User("dummy_user", List.of("missing_role")),
-                    "password".toCharArray(),
-                    true,
-                    RefreshPolicy.IMMEDIATE
-                ),
-                SECURITY_REQUEST_OPTIONS
-            );
+        new TestSecurityClient(getRestClient(), SecuritySettingsSource.SECURITY_REQUEST_OPTIONS).putUser(
+            new User("dummy_user", "missing_role"),
+            SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING
+        );
         assertSecurityIndexActive();
     }
 

+ 41 - 24
x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java

@@ -8,7 +8,6 @@
 package org.elasticsearch.xpack.security.authc;
 
 import org.elasticsearch.ElasticsearchSecurityException;
-import org.elasticsearch.ElasticsearchStatusException;
 import org.elasticsearch.action.ActionRequestValidationException;
 import org.elasticsearch.action.DocWriteResponse;
 import org.elasticsearch.action.admin.cluster.node.info.NodeInfo;
@@ -24,10 +23,9 @@ import org.elasticsearch.client.Request;
 import org.elasticsearch.client.RequestOptions;
 import org.elasticsearch.client.ResponseException;
 import org.elasticsearch.client.RestClient;
-import org.elasticsearch.client.RestHighLevelClient;
 import org.elasticsearch.client.internal.Client;
-import org.elasticsearch.client.security.AuthenticateResponse;
 import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.settings.SecureString;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.util.concurrent.EsExecutors;
 import org.elasticsearch.common.util.concurrent.EsRejectedExecutionException;
@@ -37,6 +35,8 @@ import org.elasticsearch.core.Tuple;
 import org.elasticsearch.rest.RestStatus;
 import org.elasticsearch.test.SecurityIntegTestCase;
 import org.elasticsearch.test.SecuritySettingsSource;
+import org.elasticsearch.test.TestSecurityClient;
+import org.elasticsearch.test.rest.yaml.ObjectPath;
 import org.elasticsearch.threadpool.ThreadPool;
 import org.elasticsearch.xpack.core.XPackSettings;
 import org.elasticsearch.xpack.core.security.action.ClearSecurityCacheAction;
@@ -55,7 +55,9 @@ import org.elasticsearch.xpack.core.security.action.apikey.InvalidateApiKeyRespo
 import org.elasticsearch.xpack.core.security.action.user.PutUserAction;
 import org.elasticsearch.xpack.core.security.action.user.PutUserRequest;
 import org.elasticsearch.xpack.core.security.action.user.PutUserResponse;
+import org.elasticsearch.xpack.core.security.authc.Authentication;
 import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
+import org.elasticsearch.xpack.core.security.user.User;
 import org.elasticsearch.xpack.security.transport.filter.IPFilter;
 import org.junit.After;
 import org.junit.Before;
@@ -70,6 +72,7 @@ import java.util.Base64;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.CountDownLatch;
@@ -206,20 +209,18 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
         assertThat(simple.getId(), not(containsString(new String(simple.getKey().getChars()))));
         assertNull(simple.getExpiration());
 
-        // use the first ApiKey for authorized action
-        final String base64ApiKeyKeyValue = Base64.getEncoder()
-            .encodeToString((response.getId() + ":" + response.getKey().toString()).getBytes(StandardCharsets.UTF_8));
         // Assert that we can authenticate with the API KEY
-        final RestHighLevelClient restClient = new TestRestHighLevelClient();
-        AuthenticateResponse authResponse = restClient.security()
-            .authenticate(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", "ApiKey " + base64ApiKeyKeyValue).build());
-        assertThat(authResponse.getUser().getUsername(), equalTo(ES_TEST_ROOT_USER));
-        assertThat(authResponse.getAuthenticationType(), equalTo("api_key"));
+        final Map<String, Object> authResponse = authenticateWithApiKey(response.getId(), response.getKey());
+        assertThat(authResponse.get(User.Fields.USERNAME.getPreferredName()), equalTo(ES_TEST_ROOT_USER));
 
         // use the first ApiKey for an unauthorized action
+        final Map<String, String> authorizationHeaders = Collections.singletonMap(
+            "Authorization",
+            "ApiKey " + getBase64EncodedApiKeyValue(response.getId(), response.getKey())
+        );
         ElasticsearchSecurityException e = expectThrows(
             ElasticsearchSecurityException.class,
-            () -> client().filterWithHeader(Collections.singletonMap("Authorization", "ApiKey " + base64ApiKeyKeyValue))
+            () -> client().filterWithHeader(authorizationHeaders)
                 .admin()
                 .cluster()
                 .prepareUpdateSettings()
@@ -373,15 +374,12 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
         }
 
         // Authentication with the first key should fail
-        final String base64ApiKeyKeyValue = Base64.getEncoder()
-            .encodeToString((apiKey1.v1() + ":" + apiKey1.v2()).getBytes(StandardCharsets.UTF_8));
-        ElasticsearchStatusException e = expectThrows(
-            ElasticsearchStatusException.class,
-            () -> new TestRestHighLevelClient().security()
-                .authenticate(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", "ApiKey " + base64ApiKeyKeyValue).build())
+        ResponseException e = expectThrows(
+            ResponseException.class,
+            () -> authenticateWithApiKey(apiKey1.v1(), new SecureString(apiKey1.v2().toCharArray()))
         );
         assertThat(e.getMessage(), containsString("security_exception"));
-        assertThat(e.status(), is(RestStatus.UNAUTHORIZED));
+        assertThat(e.getResponse().getStatusLine().getStatusCode(), is(RestStatus.UNAUTHORIZED.getStatus()));
     }
 
     private void verifyInvalidateResponse(
@@ -1400,14 +1398,33 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
             .setMetadata(ApiKeyTests.randomMetadata())
             .get();
         final String docId = createApiKeyResponse.getId();
-        final String base64ApiKeyKeyValue = Base64.getEncoder()
-            .encodeToString((docId + ":" + createApiKeyResponse.getKey().toString()).getBytes(StandardCharsets.UTF_8));
-        AuthenticateResponse authResponse = new TestRestHighLevelClient().security()
-            .authenticate(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", "ApiKey " + base64ApiKeyKeyValue).build());
-        assertEquals("api_key", authResponse.getAuthenticationType());
+        authenticateWithApiKey(docId, createApiKeyResponse.getKey());
         return Tuple.tuple(docId, createApiKeyResponse.getKey().toString());
     }
 
+    private Map<String, Object> authenticateWithApiKey(String id, SecureString key) throws IOException {
+        final RequestOptions requestOptions = RequestOptions.DEFAULT.toBuilder()
+            .addHeader("Authorization", "ApiKey " + getBase64EncodedApiKeyValue(id, key))
+            .build();
+        final TestSecurityClient securityClient = getSecurityClient(requestOptions);
+        final Map<String, Object> response = securityClient.authenticate();
+
+        final String authenticationTypeString = String.valueOf(response.get(User.Fields.AUTHENTICATION_TYPE.getPreferredName()));
+        final Authentication.AuthenticationType authenticationType = Authentication.AuthenticationType.valueOf(
+            authenticationTypeString.toUpperCase(Locale.ROOT)
+        );
+        assertThat(authenticationType, is(Authentication.AuthenticationType.API_KEY));
+
+        assertThat(ObjectPath.evaluate(response, "api_key.id"), is(id));
+
+        return response;
+    }
+
+    private String getBase64EncodedApiKeyValue(String id, SecureString key) {
+        final String base64ApiKeyKeyValue = Base64.getEncoder().encodeToString((id + ":" + key).getBytes(StandardCharsets.UTF_8));
+        return base64ApiKeyKeyValue;
+    }
+
     private void assertApiKeyNotCreated(Client client, String keyName) throws ExecutionException, InterruptedException {
         new RefreshRequestBuilder(client, RefreshAction.INSTANCE).setIndices(SECURITY_MAIN_ALIAS).execute().get();
         assertEquals(

+ 21 - 21
x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/TokenAuthIntegTests.java

@@ -19,8 +19,8 @@ import org.elasticsearch.action.support.master.AcknowledgedResponse;
 import org.elasticsearch.action.update.UpdateRequest;
 import org.elasticsearch.action.update.UpdateResponse;
 import org.elasticsearch.client.RequestOptions;
+import org.elasticsearch.client.ResponseException;
 import org.elasticsearch.client.RestHighLevelClient;
-import org.elasticsearch.client.security.AuthenticateResponse;
 import org.elasticsearch.client.security.CreateTokenRequest;
 import org.elasticsearch.client.security.CreateTokenResponse;
 import org.elasticsearch.client.security.InvalidateTokenRequest;
@@ -34,10 +34,12 @@ import org.elasticsearch.search.builder.SearchSourceBuilder;
 import org.elasticsearch.test.SecurityIntegTestCase;
 import org.elasticsearch.test.SecuritySettingsSource;
 import org.elasticsearch.test.SecuritySettingsSourceField;
+import org.elasticsearch.test.TestSecurityClient;
 import org.elasticsearch.xpack.core.XPackSettings;
 import org.elasticsearch.xpack.core.security.authc.TokenMetadata;
 import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken;
 import org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames;
+import org.elasticsearch.xpack.core.security.user.User;
 import org.junit.After;
 import org.junit.Before;
 
@@ -58,6 +60,7 @@ import static org.elasticsearch.test.SecuritySettingsSource.SECURITY_REQUEST_OPT
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.empty;
 import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasEntry;
 import static org.hamcrest.Matchers.hasItem;
 
 @SuppressWarnings("removal")
@@ -749,10 +752,10 @@ public class TokenAuthIntegTests extends SecurityIntegTestCase {
         assertNotEquals(refreshResponse.getAccessToken(), createTokenResponse.getAccessToken());
         assertNotEquals(refreshResponse.getRefreshToken(), createTokenResponse.getRefreshToken());
 
-        AuthenticateResponse response = restClient.security().authenticate(superuserOptions);
+        final Map<String, Object> authenticateResponse = getSecurityClient(superuserOptions).authenticate();
 
-        assertEquals(SecuritySettingsSource.ES_TEST_ROOT_USER, response.getUser().getUsername());
-        assertEquals("realm", response.getAuthenticationType());
+        assertThat(authenticateResponse, hasEntry(User.Fields.USERNAME.getPreferredName(), SecuritySettingsSource.ES_TEST_ROOT_USER));
+        assertThat(authenticateResponse, hasEntry(User.Fields.AUTHENTICATION_TYPE.getPreferredName(), "realm"));
 
         assertAuthenticateWithToken(createTokenResponse.getAccessToken(), SecuritySettingsSource.TEST_USER_NAME);
         assertAuthenticateWithToken(refreshResponse.getAccessToken(), SecuritySettingsSource.TEST_USER_NAME);
@@ -838,31 +841,28 @@ public class TokenAuthIntegTests extends SecurityIntegTestCase {
     }
 
     private void assertAuthenticateWithToken(String accessToken, String expectedUser) throws IOException {
-        final RestHighLevelClient restClient = new TestRestHighLevelClient();
-        AuthenticateResponse authResponse = restClient.security()
-            .authenticate(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", "Bearer " + accessToken).build());
-        assertThat(authResponse.getUser().getUsername(), equalTo(expectedUser));
-        assertThat(authResponse.getAuthenticationType(), equalTo("token"));
+        final TestSecurityClient securityClient = getSecurityClient(accessToken);
+        final Map<String, Object> authResponse = securityClient.authenticate();
+        assertThat(authResponse, hasEntry(User.Fields.USERNAME.getPreferredName(), expectedUser));
+        assertThat(authResponse, hasEntry(User.Fields.AUTHENTICATION_TYPE.getPreferredName(), "token"));
     }
 
     private void assertUnauthorizedToken(String accessToken) {
-        final RestHighLevelClient restClient = new TestRestHighLevelClient();
-        ElasticsearchStatusException e = expectThrows(
-            ElasticsearchStatusException.class,
-            () -> restClient.security()
-                .authenticate(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", "Bearer " + accessToken).build())
-        );
-        assertThat(e.status(), equalTo(RestStatus.UNAUTHORIZED));
+        final TestSecurityClient securityClient = getSecurityClient(accessToken);
+        ResponseException e = expectThrows(ResponseException.class, securityClient::authenticate);
+        assertThat(e.getResponse().getStatusLine().getStatusCode(), equalTo(RestStatus.UNAUTHORIZED.getStatus()));
+    }
+
+    private TestSecurityClient getSecurityClient(String accessToken) {
+        return getSecurityClient(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", "Bearer " + accessToken).build());
     }
 
     private RestStatus getAuthenticationResponseCode(String accessToken) throws IOException {
-        final RestHighLevelClient restClient = new TestRestHighLevelClient();
         try {
-            restClient.security()
-                .authenticate(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", "Bearer " + accessToken).build());
+            getSecurityClient(accessToken).authenticate();
             return RestStatus.OK;
-        } catch (ElasticsearchStatusException esse) {
-            return esse.status();
+        } catch (ResponseException esse) {
+            return RestStatus.fromCode(esse.getResponse().getStatusLine().getStatusCode());
         }
     }
 }

+ 4 - 15
x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmIntegTests.java

@@ -9,12 +9,9 @@ package org.elasticsearch.xpack.security.authc.esnative;
 import org.elasticsearch.ElasticsearchSecurityException;
 import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse;
 import org.elasticsearch.client.RestHighLevelClient;
-import org.elasticsearch.client.security.ChangePasswordRequest;
-import org.elasticsearch.client.security.RefreshPolicy;
 import org.elasticsearch.common.settings.SecureString;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.test.NativeRealmIntegTestCase;
-import org.elasticsearch.test.SecurityClientTestHelper;
 import org.elasticsearch.xpack.core.security.authc.support.Hasher;
 import org.elasticsearch.xpack.core.security.user.APMSystemUser;
 import org.elasticsearch.xpack.core.security.user.BeatsSystemUser;
@@ -30,7 +27,6 @@ import java.util.Arrays;
 import java.util.List;
 
 import static java.util.Collections.singletonMap;
-import static org.elasticsearch.test.SecuritySettingsSource.SECURITY_REQUEST_OPTIONS;
 import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.is;
@@ -86,7 +82,6 @@ public class ReservedRealmIntegTests extends NativeRealmIntegTestCase {
      * the reserved realm.
      */
     public void testAuthenticateAfterEnablingUser() throws IOException {
-        final RestHighLevelClient restClient = new TestRestHighLevelClient();
         final List<String> usernames = Arrays.asList(
             ElasticUser.NAME,
             KibanaUser.NAME,
@@ -97,7 +92,7 @@ public class ReservedRealmIntegTests extends NativeRealmIntegTestCase {
             RemoteMonitoringUser.NAME
         );
         for (String username : usernames) {
-            SecurityClientTestHelper.setUserEnabled(getRestClient(), username, true);
+            getSecurityClient().setUserEnabled(username, true);
 
             ClusterHealthResponse response = client().filterWithHeader(
                 singletonMap("Authorization", basicAuthHeaderValue(username, getReservedPassword()))
@@ -126,13 +121,7 @@ public class ReservedRealmIntegTests extends NativeRealmIntegTestCase {
             assertThat(response.getClusterName(), is(cluster().getClusterName()));
         }
 
-        final RestHighLevelClient restClient = new TestRestHighLevelClient();
-        final boolean changed = restClient.security()
-            .changePassword(
-                new ChangePasswordRequest(username, Arrays.copyOf(newPassword, newPassword.length), RefreshPolicy.IMMEDIATE),
-                SECURITY_REQUEST_OPTIONS
-            );
-        assertTrue(changed);
+        getSecurityClient().changePassword(username, new SecureString(Arrays.copyOf(newPassword, newPassword.length)));
 
         ElasticsearchSecurityException elasticsearchSecurityException = expectThrows(
             ElasticsearchSecurityException.class,
@@ -159,7 +148,7 @@ public class ReservedRealmIntegTests extends NativeRealmIntegTestCase {
         assertThat(response.getClusterName(), is(cluster().getClusterName()));
 
         // disable user
-        SecurityClientTestHelper.setUserEnabled(getRestClient(), ElasticUser.NAME, false);
+        getSecurityClient().setUserEnabled(ElasticUser.NAME, false);
         ElasticsearchSecurityException elasticsearchSecurityException = expectThrows(
             ElasticsearchSecurityException.class,
             () -> client().filterWithHeader(singletonMap("Authorization", basicAuthHeaderValue(ElasticUser.NAME, getReservedPassword())))
@@ -171,7 +160,7 @@ public class ReservedRealmIntegTests extends NativeRealmIntegTestCase {
         assertThat(elasticsearchSecurityException.getMessage(), containsString("authenticate"));
 
         // enable
-        SecurityClientTestHelper.setUserEnabled(getRestClient(), ElasticUser.NAME, true);
+        getSecurityClient().setUserEnabled(ElasticUser.NAME, true);
         response = client().filterWithHeader(singletonMap("Authorization", basicAuthHeaderValue(ElasticUser.NAME, getReservedPassword())))
             .admin()
             .cluster()

+ 91 - 53
x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/pki/PkiAuthDelegationIntegTests.java

@@ -9,10 +9,9 @@ package org.elasticsearch.xpack.security.authc.pki;
 
 import org.elasticsearch.ElasticsearchStatusException;
 import org.elasticsearch.client.RequestOptions;
+import org.elasticsearch.client.ResponseException;
 import org.elasticsearch.client.RestHighLevelClient;
 import org.elasticsearch.client.ValidationException;
-import org.elasticsearch.client.security.AuthenticateResponse;
-import org.elasticsearch.client.security.AuthenticateResponse.RealmInfo;
 import org.elasticsearch.client.security.DelegatePkiAuthenticationRequest;
 import org.elasticsearch.client.security.DelegatePkiAuthenticationResponse;
 import org.elasticsearch.client.security.DeleteRoleMappingRequest;
@@ -21,15 +20,20 @@ import org.elasticsearch.client.security.InvalidateTokenResponse;
 import org.elasticsearch.client.security.PutRoleMappingRequest;
 import org.elasticsearch.client.security.RefreshPolicy;
 import org.elasticsearch.client.security.support.expressiondsl.fields.FieldRoleMapperExpression;
-import org.elasticsearch.client.security.user.User;
 import org.elasticsearch.common.settings.SecureString;
 import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.rest.RestStatus;
 import org.elasticsearch.test.SecurityIntegTestCase;
 import org.elasticsearch.test.SecuritySettingsSource;
 import org.elasticsearch.test.SecuritySettingsSourceField;
+import org.elasticsearch.test.TestSecurityClient;
+import org.elasticsearch.test.rest.ESRestTestCase;
+import org.elasticsearch.xcontent.ObjectPath;
+import org.elasticsearch.xcontent.ParseField;
 import org.elasticsearch.xpack.core.XPackSettings;
 import org.elasticsearch.xpack.core.security.action.realm.ClearRealmCacheRequestBuilder;
 import org.elasticsearch.xpack.core.security.authc.support.Hasher;
+import org.elasticsearch.xpack.core.security.user.User.Fields;
 import org.junit.Before;
 
 import java.io.InputStream;
@@ -39,10 +43,14 @@ import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
+import java.util.Map;
 
 import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue;
 import static org.hamcrest.Matchers.containsInAnyOrder;
-import static org.hamcrest.Matchers.emptyCollectionOf;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.hasEntry;
+import static org.hamcrest.Matchers.instanceOf;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.notNullValue;
 import static org.hamcrest.Matchers.startsWith;
@@ -179,15 +187,16 @@ public class PkiAuthDelegationIntegTests extends SecurityIntegTestCase {
                 // authenticate
                 optionsBuilder = RequestOptions.DEFAULT.toBuilder();
                 optionsBuilder.addHeader("Authorization", "Bearer " + token);
-                AuthenticateResponse resp = restClient.security().authenticate(optionsBuilder.build());
-                User user = resp.getUser();
-                assertThat(user, is(notNullValue()));
-                assertThat(user.getUsername(), is("Elasticsearch Test Client"));
-                RealmInfo authnRealm = resp.getAuthenticationRealm();
-                assertThat(authnRealm, is(notNullValue()));
-                assertThat(authnRealm.getName(), is("pki3"));
-                assertThat(authnRealm.getType(), is("pki"));
-                assertThat(resp.getAuthenticationType(), is("token"));
+
+                final TestSecurityClient securityClient = getSecurityClient(optionsBuilder.build());
+                final Map<String, Object> authenticateResponse = securityClient.authenticate();
+                assertThat(authenticateResponse, hasEntry(Fields.USERNAME.getPreferredName(), "Elasticsearch Test Client"));
+
+                Map<String, Object> realm = assertMap(authenticateResponse, Fields.AUTHENTICATION_REALM);
+                assertThat(realm, hasEntry(Fields.REALM_NAME.getPreferredName(), "pki3"));
+                assertThat(realm, hasEntry(Fields.REALM_TYPE.getPreferredName(), "pki"));
+
+                assertThat(authenticateResponse, hasEntry(Fields.AUTHENTICATION_TYPE.getPreferredName(), "token"));
             }
         }
     }
@@ -220,23 +229,25 @@ public class PkiAuthDelegationIntegTests extends SecurityIntegTestCase {
             // authenticate
             optionsBuilder = RequestOptions.DEFAULT.toBuilder();
             optionsBuilder.addHeader("Authorization", "Bearer " + token);
-            AuthenticateResponse resp = restClient.security().authenticate(optionsBuilder.build());
-            User user = resp.getUser();
-            assertThat(user, is(notNullValue()));
-            assertThat(user.getUsername(), is("Elasticsearch Test Client"));
-            assertThat(user.getMetadata().get("pki_dn"), is(notNullValue()));
-            assertThat(user.getMetadata().get("pki_dn"), is("O=org, OU=Elasticsearch, CN=Elasticsearch Test Client"));
-            assertThat(user.getMetadata().get("pki_delegated_by_user"), is(notNullValue()));
-            assertThat(user.getMetadata().get("pki_delegated_by_user"), is(delegateeUsername));
-            assertThat(user.getMetadata().get("pki_delegated_by_realm"), is(notNullValue()));
-            assertThat(user.getMetadata().get("pki_delegated_by_realm"), is("file"));
+            final TestSecurityClient securityClient = getSecurityClient(optionsBuilder.build());
+            final Map<String, Object> authenticateResponse = securityClient.authenticate();
+            assertThat(authenticateResponse, hasEntry(Fields.USERNAME.getPreferredName(), "Elasticsearch Test Client"));
+
+            final Map<String, Object> metadata = assertMap(authenticateResponse, Fields.METADATA);
+            assertThat(metadata, hasEntry("pki_dn", "O=org, OU=Elasticsearch, CN=Elasticsearch Test Client"));
+            assertThat(metadata, hasEntry("pki_delegated_by_user", delegateeUsername));
+            assertThat(metadata, hasEntry("pki_delegated_by_realm", "file"));
+
             // no roles because no role mappings
-            assertThat(user.getRoles(), is(emptyCollectionOf(String.class)));
-            RealmInfo authnRealm = resp.getAuthenticationRealm();
-            assertThat(authnRealm, is(notNullValue()));
-            assertThat(authnRealm.getName(), is("pki3"));
-            assertThat(authnRealm.getType(), is("pki"));
-            assertThat(resp.getAuthenticationType(), is("token"));
+            List<?> roles = assertList(authenticateResponse, Fields.ROLES);
+            assertThat(roles, empty());
+
+            Map<String, Object> realm = assertMap(authenticateResponse, Fields.AUTHENTICATION_REALM);
+            assertThat(realm, hasEntry(Fields.REALM_NAME.getPreferredName(), "pki3"));
+            assertThat(realm, hasEntry(Fields.REALM_TYPE.getPreferredName(), "pki"));
+
+            assertThat(authenticateResponse, hasEntry(Fields.AUTHENTICATION_TYPE.getPreferredName(), "token"));
+
             // invalidate
             InvalidateTokenRequest invalidateRequest = InvalidateTokenRequest.accessToken(token);
             optionsBuilder = RequestOptions.DEFAULT.toBuilder();
@@ -248,12 +259,19 @@ public class PkiAuthDelegationIntegTests extends SecurityIntegTestCase {
             assertThat(invalidateResponse.getInvalidatedTokens(), is(1));
             assertThat(invalidateResponse.getErrorsCount(), is(0));
             // failed authenticate
-            ElasticsearchStatusException e1 = expectThrows(
-                ElasticsearchStatusException.class,
-                () -> restClient.security()
-                    .authenticate(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", "Bearer " + token).build())
+            ResponseException ex = expectThrows(
+                ResponseException.class,
+                () -> new TestSecurityClient(
+                    getRestClient(),
+                    RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", "Bearer " + token).build()
+                ).authenticate()
             );
-            assertThat(e1.getMessage(), is("Elasticsearch exception [type=security_exception, reason=token expired]"));
+
+            assertThat(ex.getResponse().getStatusLine().getStatusCode(), is(RestStatus.UNAUTHORIZED.getStatus()));
+
+            final Map<String, Object> response = ESRestTestCase.entityAsMap(ex.getResponse());
+            assertThat(ObjectPath.eval("error.type", response), is("security_exception"));
+            assertThat(ObjectPath.eval("error.reason", response), is("token expired"));
         }
     }
 
@@ -336,26 +354,27 @@ public class PkiAuthDelegationIntegTests extends SecurityIntegTestCase {
             DelegatePkiAuthenticationResponse delegatePkiResponse = restClient.security()
                 .delegatePkiAuthentication(delegatePkiRequest, testUserOptions);
             // authenticate
-            AuthenticateResponse resp = restClient.security()
-                .authenticate(
-                    RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", "Bearer " + delegatePkiResponse.getAccessToken()).build()
-                );
-            User user = resp.getUser();
-            assertThat(user, is(notNullValue()));
-            assertThat(user.getUsername(), is("Elasticsearch Test Client"));
-            assertThat(user.getMetadata().get("pki_dn"), is(notNullValue()));
-            assertThat(user.getMetadata().get("pki_dn"), is("O=org, OU=Elasticsearch, CN=Elasticsearch Test Client"));
-            assertThat(user.getMetadata().get("pki_delegated_by_user"), is(notNullValue()));
-            assertThat(user.getMetadata().get("pki_delegated_by_user"), is("test_user"));
-            assertThat(user.getMetadata().get("pki_delegated_by_realm"), is(notNullValue()));
-            assertThat(user.getMetadata().get("pki_delegated_by_realm"), is("file"));
+            TestSecurityClient securityClient = getSecurityClient(
+                RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", "Bearer " + delegatePkiResponse.getAccessToken()).build()
+            );
+            final Map<String, Object> authenticateResponse = securityClient.authenticate();
+            assertThat(authenticateResponse, hasEntry(Fields.USERNAME.getPreferredName(), "Elasticsearch Test Client"));
+
+            final Map<String, Object> metadata = assertMap(authenticateResponse, Fields.METADATA);
+            assertThat(metadata, hasEntry("pki_dn", "O=org, OU=Elasticsearch, CN=Elasticsearch Test Client"));
+            assertThat(metadata, hasEntry("pki_delegated_by_user", "test_user"));
+            assertThat(metadata, hasEntry("pki_delegated_by_realm", "file"));
+
             // assert roles
-            assertThat(user.getRoles(), containsInAnyOrder("role_by_delegated_user", "role_by_delegated_realm"));
-            RealmInfo authnRealm = resp.getAuthenticationRealm();
-            assertThat(authnRealm, is(notNullValue()));
-            assertThat(authnRealm.getName(), is("pki3"));
-            assertThat(authnRealm.getType(), is("pki"));
-            assertThat(resp.getAuthenticationType(), is("token"));
+            List<?> roles = assertList(authenticateResponse, Fields.ROLES);
+            assertThat(roles, containsInAnyOrder("role_by_delegated_user", "role_by_delegated_realm"));
+
+            Map<String, Object> realm = assertMap(authenticateResponse, Fields.AUTHENTICATION_REALM);
+            assertThat(realm, hasEntry(Fields.REALM_NAME.getPreferredName(), "pki3"));
+            assertThat(realm, hasEntry(Fields.REALM_TYPE.getPreferredName(), "pki"));
+
+            assertThat(authenticateResponse, hasEntry(Fields.AUTHENTICATION_TYPE.getPreferredName(), "token"));
+
             // delete role mappings for delegated PKI
             restClient.security()
                 .deleteRoleMapping(new DeleteRoleMappingRequest("role_by_delegated_user", RefreshPolicy.IMMEDIATE), testUserOptions);
@@ -364,6 +383,13 @@ public class PkiAuthDelegationIntegTests extends SecurityIntegTestCase {
         }
     }
 
+    private Object evaluate(Map<String, Object> map, ParseField... fields) {
+        for (int i = 0; i < fields.length - 1; i++) {
+            map = assertMap(map, fields[i]);
+        }
+        return map.get(fields[fields.length - 1]);
+    }
+
     public void testIncorrectCertChain() throws Exception {
         X509Certificate clientCertificate = readCertForPkiDelegation("testClient.crt");
         X509Certificate intermediateCA = readCertForPkiDelegation("testIntermediateCA.crt");
@@ -417,4 +443,16 @@ public class PkiAuthDelegationIntegTests extends SecurityIntegTestCase {
         }
     }
 
+    @SuppressWarnings("unchecked")
+    private Map<String, Object> assertMap(Map<String, Object> map, ParseField field) {
+        final Object val = map.get(field.getPreferredName());
+        assertThat("Field " + field + " of " + map, val, instanceOf(Map.class));
+        return (Map<String, Object>) val;
+    }
+
+    private List<?> assertList(Map<String, Object> map, ParseField field) {
+        final Object val = map.get(field.getPreferredName());
+        assertThat("Field " + field + " of " + map, val, instanceOf(List.class));
+        return (List<?>) val;
+    }
 }

+ 1 - 10
x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authz/SnapshotUserRoleIntegTests.java

@@ -17,10 +17,7 @@ import org.elasticsearch.client.RestHighLevelClient;
 import org.elasticsearch.client.internal.Client;
 import org.elasticsearch.client.security.DeleteRoleRequest;
 import org.elasticsearch.client.security.PutRoleRequest;
-import org.elasticsearch.client.security.PutUserRequest;
-import org.elasticsearch.client.security.PutUserResponse;
 import org.elasticsearch.client.security.RefreshPolicy;
-import org.elasticsearch.client.security.user.User;
 import org.elasticsearch.client.security.user.privileges.Role;
 import org.elasticsearch.common.settings.SecureString;
 import org.elasticsearch.common.settings.Settings;
@@ -31,7 +28,6 @@ import org.junit.Before;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.List;
 import java.util.Locale;
 
 import static org.elasticsearch.test.SecuritySettingsSource.SECURITY_REQUEST_OPTIONS;
@@ -72,12 +68,7 @@ public class SnapshotUserRoleIntegTests extends NativeRealmIntegTestCase {
         final char[] password = new char[] { 'p', 'a', 's', 's', 'w', 'o', 'r', 'd' };
         final String snapshotUserToken = basicAuthHeaderValue(user, new SecureString(password));
         client = client().filterWithHeader(Collections.singletonMap("Authorization", snapshotUserToken));
-        PutUserResponse response = new TestRestHighLevelClient().security()
-            .putUser(
-                PutUserRequest.withPassword(new User(user, List.of("snapshot_user")), password, true, RefreshPolicy.IMMEDIATE),
-                SECURITY_REQUEST_OPTIONS
-            );
-        assertTrue(response.isCreated());
+        getSecurityClient().putUser(new org.elasticsearch.xpack.core.security.user.User(user, "snapshot_user"), new SecureString(password));
         ensureGreen(INTERNAL_SECURITY_MAIN_INDEX_7);
     }
 

+ 0 - 24
x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecurityClientTestHelper.java

@@ -1,24 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-package org.elasticsearch.test;
-
-import org.apache.http.client.methods.HttpPut;
-import org.elasticsearch.client.Request;
-import org.elasticsearch.client.RestClient;
-
-import java.io.IOException;
-
-public class SecurityClientTestHelper {
-
-    public static void setUserEnabled(RestClient client, String username, boolean enabled) throws IOException {
-        final String endpoint = "/_security/user/" + username + "/" + (enabled ? "_enable" : "_disable");
-        final Request request = new Request(HttpPut.METHOD_NAME, endpoint);
-        request.setOptions(SecuritySettingsSource.SECURITY_REQUEST_OPTIONS);
-        client.performRequest(request);
-    }
-}

+ 13 - 0
x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecurityIntegTestCase.java

@@ -17,6 +17,7 @@ import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
 import org.elasticsearch.action.admin.indices.get.GetIndexRequest;
 import org.elasticsearch.action.admin.indices.get.GetIndexResponse;
 import org.elasticsearch.action.support.IndicesOptions;
+import org.elasticsearch.client.RequestOptions;
 import org.elasticsearch.client.RestHighLevelClient;
 import org.elasticsearch.client.internal.Client;
 import org.elasticsearch.client.internal.node.NodeClient;
@@ -75,6 +76,7 @@ public abstract class SecurityIntegTestCase extends ESIntegTestCase {
      * to how {@link ESIntegTestCase#nodeSettings(int, Settings)} works.
      */
     private static CustomSecuritySettingsSource customSecuritySettingsSource = null;
+    private TestSecurityClient securityClient;
 
     @BeforeClass
     public static void generateBootstrapPassword() {
@@ -469,4 +471,15 @@ public abstract class SecurityIntegTestCase extends ESIntegTestCase {
             super(getRestClient(), client -> {}, List.of());
         }
     }
+
+    protected TestSecurityClient getSecurityClient(RequestOptions requestOptions) {
+        return new TestSecurityClient(getRestClient(), requestOptions);
+    }
+
+    protected TestSecurityClient getSecurityClient() {
+        if (securityClient == null) {
+            securityClient = getSecurityClient(SecuritySettingsSource.SECURITY_REQUEST_OPTIONS);
+        }
+        return securityClient;
+    }
 }

+ 119 - 0
x-pack/plugin/security/src/test/java/org/elasticsearch/test/TestSecurityClient.java

@@ -0,0 +1,119 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.test;
+
+import org.apache.http.client.methods.HttpDelete;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
+import org.elasticsearch.client.Request;
+import org.elasticsearch.client.RequestOptions;
+import org.elasticsearch.client.Response;
+import org.elasticsearch.client.RestClient;
+import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.settings.SecureString;
+import org.elasticsearch.xcontent.XContentBuilder;
+import org.elasticsearch.xcontent.XContentFactory;
+import org.elasticsearch.xpack.core.security.user.User;
+
+import java.io.IOException;
+import java.util.Map;
+
+import static org.elasticsearch.test.rest.ESRestTestCase.entityAsMap;
+
+public class TestSecurityClient {
+
+    private final RestClient client;
+    private final RequestOptions options;
+
+    public TestSecurityClient(RestClient client) {
+        this(client, RequestOptions.DEFAULT);
+    }
+
+    public TestSecurityClient(RestClient client, RequestOptions options) {
+        this.client = client;
+        this.options = options;
+    }
+
+    /**
+     * Uses the REST API to retrieve the currently authenticated user.
+     * @see User.Fields
+     * @see org.elasticsearch.xpack.security.rest.action.RestAuthenticateAction
+     */
+    public Map<String, Object> authenticate() throws IOException {
+        final String endpoint = "/_security/_authenticate";
+        final Request request = new Request(HttpGet.METHOD_NAME, endpoint);
+        return entityAsMap(execute(request));
+    }
+
+    /**
+     * Uses the REST API to create a new user in the native realm.
+     * @see org.elasticsearch.xpack.security.rest.action.user.RestPutUserAction
+     */
+    public void putUser(User user, SecureString password) throws IOException {
+        final String endpoint = "/_security/user/" + user.principal();
+        final Request request = new Request(HttpPut.METHOD_NAME, endpoint);
+        final Map<String, Object> map = XContentTestUtils.convertToMap(user);
+        if (password != null) {
+            map.put("password", password.toString());
+        }
+        final String body = toJson(map);
+        request.setJsonEntity(body);
+        request.addParameters(Map.of("refresh", "true"));
+        execute(request);
+    }
+
+    /**
+     * Uses the REST API to delete a user from the native realm.
+     * @see org.elasticsearch.xpack.security.rest.action.user.RestDeleteUserAction
+     */
+    public void deleteUser(String username) throws IOException {
+        final String endpoint = "/_security/user/" + username;
+        final Request request = new Request(HttpDelete.METHOD_NAME, endpoint);
+        request.addParameters(Map.of("refresh", "true"));
+        execute(request);
+    }
+
+    /**
+     * Uses the REST API to change the password of a user in the native/reserverd realms.
+     * @see org.elasticsearch.xpack.security.rest.action.user.RestChangePasswordAction
+     */
+    public void changePassword(String username, SecureString password) throws IOException {
+        final String endpoint = "/_security/user/" + username + "/_password";
+        final Request request = new Request(HttpPost.METHOD_NAME, endpoint);
+        final String body = """
+            {
+                "password": "%s"
+            }
+            """.formatted(password.toString());
+        request.setJsonEntity(body);
+        execute(request);
+    }
+
+    /**
+     * Uses the REST API to enable or disable a user in the native/reserved realm.
+     * @see org.elasticsearch.xpack.security.rest.action.user.RestSetEnabledAction
+     */
+    public void setUserEnabled(String username, boolean enabled) throws IOException {
+        final String endpoint = "/_security/user/" + username + "/" + (enabled ? "_enable" : "_disable");
+        final Request request = new Request(HttpPut.METHOD_NAME, endpoint);
+        execute(request);
+    }
+
+    private static String toJson(Map<String, Object> map) throws IOException {
+        final XContentBuilder builder = XContentFactory.jsonBuilder().map(map);
+        final BytesReference bytes = BytesReference.bytes(builder);
+        return bytes.utf8ToString();
+    }
+
+    private Response execute(Request request) throws IOException {
+        request.setOptions(options);
+        return this.client.performRequest(request);
+    }
+
+}

+ 15 - 9
x-pack/qa/security-example-spi-extension/src/javaRestTest/java/org/elasticsearch/example/role/CustomRolesProviderIT.java

@@ -6,14 +6,12 @@
  */
 package org.elasticsearch.example.role;
 
+import org.apache.http.client.methods.HttpPut;
 import org.elasticsearch.client.Request;
 import org.elasticsearch.client.RequestOptions;
 import org.elasticsearch.client.Response;
 import org.elasticsearch.client.ResponseException;
 import org.elasticsearch.client.RestHighLevelClient;
-import org.elasticsearch.client.security.PutUserRequest;
-import org.elasticsearch.client.security.RefreshPolicy;
-import org.elasticsearch.client.security.user.User;
 import org.elasticsearch.common.settings.SecureString;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.util.concurrent.ThreadContext;
@@ -24,7 +22,7 @@ import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken
 
 import java.io.IOException;
 import java.util.Collections;
-import java.util.List;
+import java.util.Map;
 
 import static org.elasticsearch.example.role.CustomInMemoryRolesProvider.INDEX;
 import static org.elasticsearch.example.role.CustomInMemoryRolesProvider.ROLE_A;
@@ -58,11 +56,19 @@ public class CustomRolesProviderIT extends ESRestTestCase {
     }
 
     public void setupTestUser(String role) throws IOException {
-        new TestRestHighLevelClient().security()
-            .putUser(
-                PutUserRequest.withPassword(new User(TEST_USER, List.of(role)), TEST_PWD.toCharArray(), true, RefreshPolicy.IMMEDIATE),
-                RequestOptions.DEFAULT
-            );
+        final String endpoint = "/_security/user/" + TEST_USER;
+        Request request = new Request(HttpPut.METHOD_NAME, endpoint);
+        final String body = """
+            {
+                "username": "%s",
+                "password": "%s",
+                "roles": [ "%s" ]
+            }
+            """.formatted(TEST_USER, TEST_PWD, role);
+        request.setJsonEntity(body);
+        request.addParameters(Map.of("refresh", "true"));
+        request.setOptions(RequestOptions.DEFAULT);
+        adminClient().performRequest(request);
     }
 
     public void testAuthorizedCustomRoleSucceeds() throws Exception {