diff --git a/src/integrationTest/java/org/opensearch/security/api/AccountRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/AccountRestApiIntegrationTest.java new file mode 100644 index 0000000000..ed2e3a5dee --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/api/AccountRestApiIntegrationTest.java @@ -0,0 +1,186 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + */ +package org.opensearch.security.api; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.apache.http.HttpStatus; +import org.junit.Test; + +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.TestRestClient; + +import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; +import static org.opensearch.security.DefaultObjectMapper.objectMapper; +import static org.opensearch.security.dlic.rest.support.Utils.hash; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class AccountRestApiIntegrationTest extends AbstractApiIntegrationTest { + + private final static String TEST_USER = "test-user"; + + private final static String RESERVED_USER = "reserved-user"; + + private final static String HIDDEN_USERS = "hidden-user"; + + public final static String TEST_USER_PASSWORD = randomAlphabetic(10); + + public final static String TEST_USER_NEW_PASSWORD = randomAlphabetic(10); + + static { + testSecurityConfig.user(new TestSecurityConfig.User(TEST_USER).password(TEST_USER_PASSWORD)) + .user(new TestSecurityConfig.User(RESERVED_USER).reserved(true)) + .user(new TestSecurityConfig.User(HIDDEN_USERS).hidden(true)); + } + + private String accountPath() { + return super.apiPath("account"); + } + + @Test + public void accountInfo() throws Exception { + withUser(NEW_USER, client -> { + var response = client.get(accountPath()); + assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); + + final var account = response.bodyAsJsonNode(); + assertEquals(response.getBody(), NEW_USER, account.get("user_name").asText()); + assertFalse(response.getBody(), account.get("is_reserved").asBoolean()); + assertFalse(response.getBody(), account.get("is_hidden").asBoolean()); + assertTrue(response.getBody(), account.get("is_internal_user").asBoolean()); + assertTrue(response.getBody(), account.get("user_requested_tenant").isNull()); + assertTrue(response.getBody(), account.get("backend_roles").isArray()); + assertTrue(response.getBody(), account.get("custom_attribute_names").isArray()); + assertTrue(response.getBody(), account.get("tenants").isObject()); + assertTrue(response.getBody(), account.get("roles").isArray()); + }); + withUser(NEW_USER, "a", client -> { + final var response = client.get(accountPath()); + assertEquals(response.getBody(), HttpStatus.SC_UNAUTHORIZED, response.getStatusCode()); + }); + withUser("a", "b", client -> { + final var response = client.get(accountPath()); + assertEquals(response.getBody(), HttpStatus.SC_UNAUTHORIZED, response.getStatusCode()); + }); + } + + @Test + public void changeAccountPassword() throws Exception { + withUser(TEST_USER, TEST_USER_PASSWORD, this::verifyWrongPayload); + verifyPasswordCanBeChanged(); + + withUser(RESERVED_USER, client -> { + var response = client.get(accountPath()); + assertTrue(response.getBody(), response.getBooleanFromJsonBody("/is_reserved")); + + response = client.putJson(accountPath(), changePasswordPayload(DEFAULT_PASSWORD, randomAlphabetic(10)).toString()); + assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + }); + withUser(HIDDEN_USERS, client -> { + var response = client.get(accountPath()); + assertTrue(response.getBody(), response.getBooleanFromJsonBody("/is_hidden")); + + response = client.putJson(accountPath(), changePasswordPayload(DEFAULT_PASSWORD, randomAlphabetic(10)).toString()); + assertEquals(response.getBody(), HttpStatus.SC_NOT_FOUND, response.getStatusCode()); + }); + withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), client -> { + var response = client.get(accountPath()); + assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); + + response = client.putJson(accountPath(), changePasswordPayload(DEFAULT_PASSWORD, randomAlphabetic(10)).toString()); + assertEquals(response.getBody(), HttpStatus.SC_NOT_FOUND, response.getStatusCode()); + }); + } + + private void verifyWrongPayload(final TestRestClient client) { + var response = client.putJson(accountPath(), EMPTY_BODY); + assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); + + response = client.putJson(accountPath(), changePasswordPayload(null, "new_password").toString()); + assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); + + // test - bad request as current password is incorrect + response = client.putJson(accountPath(), changePasswordPayload("wrong-password", "some_new_pwd").toString()); + assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); + + response = client.putJson(accountPath(), changePasswordPayload(TEST_USER_PASSWORD, null).toString()); + assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); + + response = client.putJson(accountPath(), changePasswordPayload(TEST_USER_PASSWORD, "").toString()); + assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); + + response = client.putJson(accountPath(), changePasswordPayload(TEST_USER_PASSWORD, null).put("hash", "").toString()); + assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); + + response = client.putJson(accountPath(), changePasswordPayload(TEST_USER_PASSWORD, "").put("hash", "").toString()); + assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); + + // test - bad request as invalid parameters are present + response = client.putJson( + accountPath(), + changePasswordPayload(TEST_USER_PASSWORD, "new_password").set("backend_roles", objectMapper.createArrayNode()).toString() + ); + assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); + } + + private void verifyPasswordCanBeChanged() throws Exception { + final var newPassword = randomAlphabetic(10); + withUser(TEST_USER, TEST_USER_PASSWORD, client -> { + final var response = client.putJson( + accountPath(), + changePasswordPayload(TEST_USER_PASSWORD, null).put("hash", hash(newPassword.toCharArray())).toString() + ); + assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); + }); + withUser(TEST_USER, newPassword, client -> { + final var response = client.putJson(accountPath(), changePasswordPayload(newPassword, TEST_USER_NEW_PASSWORD).toString()); + assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); + }); + } + + @Test + public void testPutAccountRetainsAccountInformation() throws Exception { + final var username = "test"; + final String password = randomAlphabetic(10); + final String newPassword = randomAlphabetic(10); + withUser(ADMIN_USER_NAME, client -> { + final var userPayload = objectMapper.createObjectNode() + .put("password", password) + .<ObjectNode>set("backend_roles", objectMapper.createArrayNode().add("test-backend-role-1")) + .<ObjectNode>set("opendistro_security_roles", objectMapper.createArrayNode().add("user_limited-user__limited-role")) + .set("attributes", objectMapper.createObjectNode().put("attribute1", "value1")); + final var response = client.putJson(apiPath("internalusers", username), userPayload.toString()); + assertEquals(response.getBody(), HttpStatus.SC_CREATED, response.getStatusCode()); + }); + withUser(username, password, client -> { + final var response = client.putJson(accountPath(), changePasswordPayload(password, newPassword).toString()); + assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); + }); + withUser(ADMIN_USER_NAME, client -> { + final var response = client.get(apiPath("internalusers", username)); + assertEquals(HttpStatus.SC_OK, response.getStatusCode()); + + final var user = response.bodyAsJsonNode().get(username); + assertEquals(user.toString(), "test-backend-role-1", user.get("backend_roles").get(0).asText()); + assertEquals(user.toString(), "user_limited-user__limited-role", user.get("opendistro_security_roles").get(0).asText()); + assertEquals(user.toString(), "value1", user.get("attributes").get("attribute1").asText()); + + }); + } + + private ObjectNode changePasswordPayload(final String currentPassword, final String newPassword) { + final var changePwdJson = objectMapper.createObjectNode(); + if (currentPassword != null) changePwdJson.put("current_password", currentPassword); + if (newPassword != null) changePwdJson.put("password", newPassword); + return changePwdJson; + } + +} diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java deleted file mode 100644 index 21f68d11df..0000000000 --- a/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java +++ /dev/null @@ -1,227 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.dlic.rest.api; - -import org.apache.hc.core5.http.Header; -import org.apache.http.HttpStatus; -import org.junit.Assert; -import org.junit.Test; - -import org.opensearch.common.settings.Settings; -import org.opensearch.common.xcontent.XContentType; -import org.opensearch.security.securityconf.impl.CType; -import org.opensearch.security.test.helper.rest.RestHelper.HttpResponse; - -import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -public class AccountApiTest extends AbstractRestApiUnitTest { - private final String BASE_ENDPOINT; - private final String ENDPOINT; - - protected String getEndpointPrefix() { - return PLUGINS_PREFIX; - } - - public AccountApiTest() { - BASE_ENDPOINT = getEndpointPrefix() + "/api/"; - ENDPOINT = getEndpointPrefix() + "/api/account"; - } - - @Test - public void testGetAccount() throws Exception { - // arrange - setup(); - final String testUser = "test-user"; - final String testPass = "some password for user"; - addUserWithPassword(testUser, testPass, HttpStatus.SC_CREATED); - - // test - unauthorized access as credentials are missing. - HttpResponse response = rh.executeGetRequest(ENDPOINT, new Header[0]); - assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getStatusCode()); - - // test - incorrect password - response = rh.executeGetRequest(ENDPOINT, encodeBasicHeader(testUser, "wrong-pass")); - assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getStatusCode()); - - // test - incorrect user - response = rh.executeGetRequest(ENDPOINT, encodeBasicHeader("wrong-user", testPass)); - assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getStatusCode()); - - // test - valid request - response = rh.executeGetRequest(ENDPOINT, encodeBasicHeader(testUser, testPass)); - Settings body = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - assertEquals(testUser, body.get("user_name")); - assertFalse(body.getAsBoolean("is_reserved", true)); - assertFalse(body.getAsBoolean("is_hidden", true)); - assertTrue(body.getAsBoolean("is_internal_user", false)); - assertNull(body.get("user_requested_tenant")); - assertNotNull(body.getAsList("backend_roles").size()); - assertNotNull(body.getAsList("custom_attribute_names").size()); - assertNotNull(body.getAsSettings("tenants")); - assertNotNull(body.getAsList("roles")); - } - - @Test - public void testPutAccount() throws Exception { - // arrange - setup(); - final String testUser = "test-user"; - final String testPass = "test-old-pass"; - final String testPassHash = "$2y$12$b7TNPn2hgl0nS7gXJ.beuOd8JGl6Nz5NsTyxofglGCItGNyDdwivK"; // hash for test-old-pass - final String testNewPass = "test-new-pass"; - final String testNewPassHash = "$2y$12$cclJJdVdXMMVzkhqQhEoE.hoERKE8bDzctR0S3aYj2EPHq45Y.GXC"; // hash for test-old-pass - addUserWithPassword(testUser, testPass, HttpStatus.SC_CREATED); - - // test - unauthorized access as credentials are missing. - HttpResponse response = rh.executePutRequest(ENDPOINT, "", new Header[0]); - assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getStatusCode()); - - // test - bad request as body is missing - response = rh.executePutRequest(ENDPOINT, "", encodeBasicHeader(testUser, testPass)); - assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - - // test - bad request as current password is missing - String payload = "{\"password\":\"new-pass\"}"; - response = rh.executePutRequest(ENDPOINT, payload, encodeBasicHeader(testUser, testPass)); - assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - - // test - bad request as current password is incorrect - payload = "{\"password\":\"" + testNewPass + "\", \"current_password\":\"" + "wrong-pass" + "\"}"; - response = rh.executePutRequest(ENDPOINT, payload, encodeBasicHeader(testUser, testPass)); - assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - - // test - bad request as hash/password is missing - payload = "{\"current_password\":\"" + testPass + "\"}"; - response = rh.executePutRequest(ENDPOINT, payload, encodeBasicHeader(testUser, testPass)); - assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - - // test - bad request as password is empty - payload = "{\"password\":\"" + "" + "\", \"current_password\":\"" + testPass + "\"}"; - response = rh.executePutRequest(ENDPOINT, payload, encodeBasicHeader(testUser, testPass)); - assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - - // test - bad request as hash is empty - payload = "{\"hash\":\"" + "" + "\", \"current_password\":\"" + testPass + "\"}"; - response = rh.executePutRequest(ENDPOINT, payload, encodeBasicHeader(testUser, testPass)); - assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - - // test - bad request as hash and password are empty - payload = "{\"hash\": \"\", \"password\": \"\", \"current_password\":\"" + testPass + "\"}"; - response = rh.executePutRequest(ENDPOINT, payload, encodeBasicHeader(testUser, testPass)); - assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - - // test - bad request as invalid parameters are present - payload = "{\"password\":\"new-pass\", \"current_password\":\"" + testPass + "\", \"backend_roles\": []}"; - response = rh.executePutRequest(ENDPOINT, payload, encodeBasicHeader(testUser, testPass)); - assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - - // test - invalid user - payload = "{\"password\":\"" + testNewPass + "\", \"current_password\":\"" + testPass + "\"}"; - response = rh.executePutRequest(ENDPOINT, payload, encodeBasicHeader("wrong-user", testPass)); - assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getStatusCode()); - - // test - valid password change with hash - payload = "{\"hash\":\"" + testNewPassHash + "\", \"current_password\":\"" + testPass + "\"}"; - response = rh.executePutRequest(ENDPOINT, payload, encodeBasicHeader(testUser, testPass)); - assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - - // test - valid password change - payload = "{\"password\":\"" + testPass + "\", \"current_password\":\"" + testNewPass + "\"}"; - response = rh.executePutRequest(ENDPOINT, payload, encodeBasicHeader(testUser, testNewPass)); - assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - - // create users from - resources/restapi/internal_users.yml - rh.keystore = "restapi/kirk-keystore.jks"; - rh.sendAdminCertificate = true; - response = rh.executeGetRequest(BASE_ENDPOINT + CType.INTERNALUSERS.toLCString()); - rh.sendAdminCertificate = false; - Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); - - // test - reserved user - sarek - response = rh.executeGetRequest(ENDPOINT, encodeBasicHeader("sarek", "sarek")); - Settings body = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - // check reserved user exists - assertTrue(body.getAsBoolean("is_reserved", false)); - payload = "{\"password\":\"" + testPass + "\", \"current_password\":\"" + "sarek" + "\"}"; - response = rh.executePutRequest(ENDPOINT, payload, encodeBasicHeader("sarek", "sarek")); - assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - - // test - hidden user - hide - response = rh.executeGetRequest(ENDPOINT, encodeBasicHeader("hide", "hide")); - body = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - // check hidden user exists - assertTrue(body.getAsBoolean("is_hidden", false)); - payload = "{\"password\":\"" + testPass + "\", \"current_password\":\"" + "hide" + "\"}"; - response = rh.executePutRequest(ENDPOINT, payload, encodeBasicHeader("hide", "hide")); - assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); - - // test - admin with admin cert - internal user does not exist - rh.keystore = "restapi/kirk-keystore.jks"; - rh.sendAdminCertificate = true; - response = rh.executeGetRequest(ENDPOINT, encodeBasicHeader("admin", "admin")); - body = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - assertEquals("CN=kirk,OU=client,O=client,L=Test,C=DE", body.get("user_name")); - assertEquals(HttpStatus.SC_OK, response.getStatusCode()); // check admin user exists - payload = "{\"password\":\"" + testPass + "\", \"current_password\":\"" + "admin" + "\"}"; - response = rh.executePutRequest(ENDPOINT, payload, encodeBasicHeader("admin", "admin")); - assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); - } - - @Test - public void testPutAccountRetainsAccountInformation() throws Exception { - // arrange - setup(); - final String testUsername = "test"; - final String testPassword = "test-password"; - final String newPassword = "new-password"; - final String createInternalUserPayload = "{\n" - + " \"password\": \"" - + testPassword - + "\",\n" - + " \"backend_roles\": [\"test-backend-role-1\"],\n" - + " \"opendistro_security_roles\": [\"opendistro_security_all_access\"],\n" - + " \"attributes\": {\n" - + " \"attribute1\": \"value1\"\n" - + " }\n" - + "}"; - final String changePasswordPayload = "{\"password\":\"" + newPassword + "\", \"current_password\":\"" + testPassword + "\"}"; - final String internalUserEndpoint = BASE_ENDPOINT + "internalusers/" + testUsername; - - // create user - rh.sendAdminCertificate = true; - HttpResponse response = rh.executePutRequest(internalUserEndpoint, createInternalUserPayload); - assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - rh.sendAdminCertificate = false; - - // change password to new-password - response = rh.executePutRequest(ENDPOINT, changePasswordPayload, encodeBasicHeader(testUsername, testPassword)); - assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - - // assert account information has not changed - rh.sendAdminCertificate = true; - response = rh.executeGetRequest(internalUserEndpoint); - assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - Settings responseBody = Settings.builder() - .loadFromSource(response.getBody(), XContentType.JSON) - .build() - .getAsSettings(testUsername); - assertTrue(responseBody.getAsList("backend_roles").contains("test-backend-role-1")); - assertTrue(responseBody.getAsList("opendistro_security_roles").contains("opendistro_security_all_access")); - assertEquals(responseBody.getAsSettings("attributes").get("attribute1"), "value1"); - } -} diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacyAccountApiTests.java b/src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacyAccountApiTests.java deleted file mode 100644 index 925d90ccba..0000000000 --- a/src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacyAccountApiTests.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.dlic.rest.api.legacy; - -import org.opensearch.security.dlic.rest.api.AccountApiTest; - -import static org.opensearch.security.OpenSearchSecurityPlugin.LEGACY_OPENDISTRO_PREFIX; - -public class LegacyAccountApiTests extends AccountApiTest { - @Override - protected String getEndpointPrefix() { - return LEGACY_OPENDISTRO_PREFIX; - } -}