From 4e5332c2f1238873e7db80f1084e0f82e20294c8 Mon Sep 17 00:00:00 2001 From: Andrey Pleskach Date: Fri, 5 Jul 2024 09:49:15 +0200 Subject: [PATCH] Refactor InternalUsers REST API test (#4481) Signed-off-by: Andrey Pleskach (cherry picked from commit a588bdd4f09aff813ef0ebfce051246740680ae6) --- .../api/AbstractApiIntegrationTest.java | 33 + ...bstractConfigEntityApiIntegrationTest.java | 27 - ...xpPasswordRulesRestApiIntegrationTest.java | 96 ++ .../InternalUsersRestApiIntegrationTest.java | 793 ++++++++++++ ...edPasswordRulesRestApiIntegrationTest.java | 83 ++ .../dlic/rest/api/InternalUsersApiAction.java | 14 +- .../opensearch/security/user/UserService.java | 22 +- .../security/UserServiceUnitTests.java | 52 +- .../security/dlic/rest/api/UserApiTest.java | 1119 ----------------- .../rest/api/legacy/LegacyUserApiTests.java | 23 - 10 files changed, 1084 insertions(+), 1178 deletions(-) create mode 100644 src/integrationTest/java/org/opensearch/security/api/InternalUsersRegExpPasswordRulesRestApiIntegrationTest.java create mode 100644 src/integrationTest/java/org/opensearch/security/api/InternalUsersRestApiIntegrationTest.java create mode 100644 src/integrationTest/java/org/opensearch/security/api/InternalUsersScoreBasedPasswordRulesRestApiIntegrationTest.java delete mode 100644 src/test/java/org/opensearch/security/dlic/rest/api/UserApiTest.java delete mode 100644 src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacyUserApiTests.java diff --git a/src/integrationTest/java/org/opensearch/security/api/AbstractApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/AbstractApiIntegrationTest.java index 24883780e4..eb93cd8768 100644 --- a/src/integrationTest/java/org/opensearch/security/api/AbstractApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/AbstractApiIntegrationTest.java @@ -373,4 +373,37 @@ void assertResponseBody(final String responseBody) { assertThat(responseBody, not(equalTo(""))); } + static ToXContentObject configJsonArray(final String... values) { + return (builder, params) -> { + builder.startArray(); + if (values != null) { + for (final var v : values) { + if (v == null) { + builder.nullValue(); + } else { + builder.value(v); + } + } + } + return builder.endArray(); + }; + } + + static String[] generateArrayValues(boolean useNulls) { + final var length = randomIntBetween(1, 5); + final var values = new String[length]; + final var nullIndex = randomIntBetween(0, length - 1); + for (var i = 0; i < values.length; i++) { + if (useNulls && i == nullIndex) values[i] = null; + else values[i] = randomAsciiAlphanumOfLength(10); + } + return values; + } + + static ToXContentObject randomConfigArray(final boolean useNulls) { + return useNulls + ? configJsonArray(generateArrayValues(useNulls)) + : randomFrom(List.of(configJsonArray(generateArrayValues(false)), configJsonArray())); + } + } diff --git a/src/integrationTest/java/org/opensearch/security/api/AbstractConfigEntityApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/AbstractConfigEntityApiIntegrationTest.java index a8ca33d539..12b278ec76 100644 --- a/src/integrationTest/java/org/opensearch/security/api/AbstractConfigEntityApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/AbstractConfigEntityApiIntegrationTest.java @@ -82,33 +82,6 @@ public AbstractConfigEntityApiIntegrationTest(final String path, final TestDescr this.testDescriptor = testDescriptor; } - static ToXContentObject configJsonArray(final String... values) { - return (builder, params) -> { - builder.startArray(); - if (values != null) { - for (final var v : values) { - if (v == null) { - builder.nullValue(); - } else { - builder.value(v); - } - } - } - return builder.endArray(); - }; - } - - static String[] generateArrayValues(boolean useNulls) { - final var length = randomIntBetween(1, 5); - final var values = new String[length]; - final var nullIndex = randomIntBetween(0, length - 1); - for (var i = 0; i < values.length; i++) { - if (useNulls && i == nullIndex) values[i] = null; - else values[i] = randomAsciiAlphanumOfLength(10); - } - return values; - } - @Override protected String apiPath(String... paths) { final StringJoiner fullPath = new StringJoiner("/").add(super.apiPath(path)); diff --git a/src/integrationTest/java/org/opensearch/security/api/InternalUsersRegExpPasswordRulesRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/InternalUsersRegExpPasswordRulesRestApiIntegrationTest.java new file mode 100644 index 0000000000..b4a6a8f066 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/api/InternalUsersRegExpPasswordRulesRestApiIntegrationTest.java @@ -0,0 +1,96 @@ +/* + * 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.api; + +import java.util.StringJoiner; + +import org.junit.Test; + +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.security.dlic.rest.validation.PasswordValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator; +import org.opensearch.security.support.ConfigConstants; + +import static org.opensearch.security.api.PatchPayloadHelper.addOp; +import static org.opensearch.security.api.PatchPayloadHelper.patch; + +public class InternalUsersRegExpPasswordRulesRestApiIntegrationTest extends AbstractApiIntegrationTest { + + final static String PASSWORD_VALIDATION_ERROR_MESSAGE = "xxxxxxxx"; + + static { + clusterSettings.put(ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE, PASSWORD_VALIDATION_ERROR_MESSAGE) + .put(ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX, "(?=.*[A-Z])(?=.*[^a-zA-Z\\\\d])(?=.*[0-9])(?=.*[a-z]).{8,}") + .put(ConfigConstants.SECURITY_RESTAPI_PASSWORD_SCORE_BASED_VALIDATION_STRENGTH, PasswordValidator.ScoreStrength.FAIR.name()); + } + + String internalUsers(String... path) { + final var fullPath = new StringJoiner("/").add(super.apiPath("internalusers")); + if (path != null) { + for (final var p : path) + fullPath.add(p); + } + return fullPath.toString(); + } + + ToXContentObject internalUserWithPassword(final String password) { + return (builder, params) -> builder.startObject() + .field("password", password) + .field("backend_roles", randomConfigArray(false)) + .endObject(); + } + + @Test + public void canNotCreateUsersWithPassword() throws Exception { + withUser(ADMIN_USER_NAME, client -> { + // validate short passwords + badRequest(() -> client.putJson(internalUsers("tooshoort"), internalUserWithPassword(""))); + badRequest(() -> client.putJson(internalUsers("tooshoort"), internalUserWithPassword("123"))); + badRequest(() -> client.putJson(internalUsers("tooshoort"), internalUserWithPassword("1234567"))); + badRequest(() -> client.putJson(internalUsers("tooshoort"), internalUserWithPassword("1Aa%"))); + badRequest(() -> client.putJson(internalUsers("tooshoort"), internalUserWithPassword("123456789"))); + badRequest(() -> client.putJson(internalUsers("tooshoort"), internalUserWithPassword("a123456789"))); + badRequest(() -> client.putJson(internalUsers("tooshoort"), internalUserWithPassword("A123456789"))); + // validate that password same as user + badRequest(() -> client.putJson(internalUsers("$1aAAAAAAAAC"), internalUserWithPassword("$1aAAAAAAAAC"))); + badRequest(() -> client.putJson(internalUsers("$1aAAAAAAAac"), internalUserWithPassword("$1aAAAAAAAAC"))); + badRequestWithReason( + () -> client.patch( + internalUsers(), + patch( + addOp("testuser1", internalUserWithPassword("$aA123456789")), + addOp("testuser2", internalUserWithPassword("testpassword2")) + ) + ), + PASSWORD_VALIDATION_ERROR_MESSAGE + ); + // validate similarity + badRequestWithReason( + () -> client.putJson(internalUsers("some_user_name"), internalUserWithPassword("H3235,cc,some_User_Name")), + RequestContentValidator.ValidationError.SIMILAR_PASSWORD.message() + ); + }); + } + + @Test + public void canCreateUsersWithPassword() throws Exception { + withUser(ADMIN_USER_NAME, client -> { + created(() -> client.putJson(internalUsers("ok1"), internalUserWithPassword("$aA123456789"))); + created(() -> client.putJson(internalUsers("ok2"), internalUserWithPassword("$Aa123456789"))); + created(() -> client.putJson(internalUsers("ok3"), internalUserWithPassword("$1aAAAAAAAAA"))); + ok(() -> client.putJson(internalUsers("ok3"), internalUserWithPassword("$1aAAAAAAAAC"))); + ok(() -> client.patch(internalUsers(), patch(addOp("ok3", internalUserWithPassword("$1aAAAAAAAAB"))))); + ok(() -> client.putJson(internalUsers("ok1"), internalUserWithPassword("Admin_123"))); + }); + } + +} diff --git a/src/integrationTest/java/org/opensearch/security/api/InternalUsersRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/InternalUsersRestApiIntegrationTest.java new file mode 100644 index 0000000000..59b501c3c3 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/api/InternalUsersRestApiIntegrationTest.java @@ -0,0 +1,793 @@ +/* + * 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.api; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import com.google.common.collect.ImmutableMap; +import com.fasterxml.jackson.databind.JsonNode; +import org.apache.http.HttpStatus; +import org.junit.Assert; +import org.junit.Test; + +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.common.Strings; +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.security.DefaultObjectMapper; +import org.opensearch.security.dlic.rest.api.Endpoint; +import org.opensearch.security.hasher.BCryptPasswordHasher; +import org.opensearch.security.hasher.PasswordHasher; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.TestRestClient; +import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.opensearch.security.api.PatchPayloadHelper.addOp; +import static org.opensearch.security.api.PatchPayloadHelper.patch; +import static org.opensearch.security.api.PatchPayloadHelper.replaceOp; +import static org.opensearch.security.dlic.rest.api.InternalUsersApiAction.RESTRICTED_FROM_USERNAME; + +public class InternalUsersRestApiIntegrationTest extends AbstractConfigEntityApiIntegrationTest { + + private final static String REST_API_ADMIN_INTERNAL_USERS_ONLY = "rest_api_admin_iternal_users_only"; + + private final static String SERVICE_ACCOUNT_USER = "service_account_user"; + + private final static String HIDDEN_ROLE = "hidden-role"; + + private final static String RESERVED_ROLE = "reserved-role"; + + private final static String SOME_ROLE = "some-role"; + + private final PasswordHasher passwordHasher = new BCryptPasswordHasher(); + + static { + testSecurityConfig.withRestAdminUser(REST_API_ADMIN_INTERNAL_USERS_ONLY, restAdminPermission(Endpoint.INTERNALUSERS)) + .user(new TestSecurityConfig.User(SERVICE_ACCOUNT_USER).attr("service", "true").attr("enabled", "true")) + .roles( + new TestSecurityConfig.Role(HIDDEN_ROLE).hidden(true), + new TestSecurityConfig.Role(RESERVED_ROLE).reserved(true), + new TestSecurityConfig.Role(SOME_ROLE) + ); + } + + public InternalUsersRestApiIntegrationTest() { + super("internalusers", new TestDescriptor() { + + @Override + public ToXContentObject entityPayload(Boolean hidden, Boolean reserved, Boolean _static) { + return internalUser(hidden, reserved, _static, randomAsciiAlphanumOfLength(10), null, null, null); + } + + @Override + public String entityJsonProperty() { + return "backend_roles"; + } + + @Override + public ToXContentObject jsonPropertyPayload() { + return (builder, params) -> builder.startArray().value("a").endArray(); + } + + @Override + public Optional restAdminLimitedUser() { + return Optional.of(REST_API_ADMIN_INTERNAL_USERS_ONLY); + } + }); + } + + static ToXContentObject internalUserWithPassword(final String password) { + return internalUser(null, null, null, password, null, null, null); + } + + static ToXContentObject internalUser( + final Boolean hidden, + final Boolean reserved, + final String password, + final ToXContentObject backendRoles, + final ToXContentObject attributes, + final ToXContentObject securityRoles + ) { + return internalUser(hidden, reserved, null, password, backendRoles, attributes, securityRoles); + } + + static ToXContentObject internalUser( + final String password, + final ToXContentObject backendRoles, + final ToXContentObject attributes, + final ToXContentObject securityRoles + ) { + return internalUser(null, null, null, password, backendRoles, attributes, securityRoles); + } + + static ToXContentObject internalUser( + final Boolean hidden, + final Boolean reserved, + final Boolean _static, + final String password, + final ToXContentObject backendRoles, + final ToXContentObject attributes, + final ToXContentObject securityRoles + ) { + return (builder, params) -> { + builder.startObject(); + if (hidden != null) { + builder.field("hidden", hidden); + } + if (reserved != null) { + builder.field("reserved", reserved); + } + if (_static != null) { + builder.field("static", _static); + } + if (password == null) { + builder.field("password").nullValue(); + } else { + builder.field("password", password); + } + if (backendRoles != null) { + builder.field("backend_roles"); + backendRoles.toXContent(builder, params); + } + if (attributes != null) { + builder.field("attributes", attributes); + } + if (securityRoles != null) { + builder.field("opendistro_security_roles"); + securityRoles.toXContent(builder, params); + } + return builder.endObject(); + }; + } + + static ToXContentObject defaultServiceUser() { + return serviceUser(null, null, null); // default user is disabled + } + + static ToXContentObject serviceUserWithPassword(final Boolean enabled, final String password) { + return serviceUser(enabled, password, null); + } + + static ToXContentObject serviceUserWithHash(final Boolean enabled, final String hash) { + return serviceUser(enabled, null, hash); + } + + static ToXContentObject serviceUser(final Boolean enabled) { + return serviceUser(enabled, null, null); + } + + static ToXContentObject serviceUser(final Boolean enabled, final String password, final String hash) { + return (builder, params) -> { + builder.startObject(); + if (password != null) { + builder.field("password", password); + } + if (hash != null) { + builder.field("hash", hash); + } + final var attributes = ImmutableMap.builder().put("service", "true"); + if (enabled != null) { + attributes.put("enabled", enabled); + } + builder.field("attributes", attributes.build()); + return builder.endObject(); + }; + } + + @Override + void verifyBadRequestOperations(TestRestClient client) throws Exception { + // bad query string parameter name + badRequest(() -> client.get(apiPath() + "?aaaaa=bbbbb")); + final var predefinedUserName = randomAsciiAlphanumOfLength(4); + created( + () -> client.putJson( + apiPath(predefinedUserName), + internalUser(randomAsciiAlphanumOfLength(10), configJsonArray(generateArrayValues(false)), null, null) + ) + ); + invalidJson(client, predefinedUserName); + } + + void invalidJson(final TestRestClient client, final String predefinedUserName) throws Exception { + // put + badRequest(() -> client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), EMPTY_BODY)); + badRequest(() -> client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), (builder, params) -> { + builder.startObject(); + builder.field("backend_roles"); + randomConfigArray(false).toXContent(builder, params); + builder.field("backend_roles"); + randomConfigArray(false).toXContent(builder, params); + return builder.endObject(); + })); + assertInvalidKeys(badRequest(() -> client.putJson(apiPath(randomAsciiAlphanumOfLength(5)), (builder, params) -> { + builder.startObject(); + builder.field("unknown_json_property"); + configJsonArray("a", "b").toXContent(builder, params); + builder.field("backend_roles"); + randomConfigArray(false).toXContent(builder, params); + return builder.endObject(); + })), "unknown_json_property"); + assertWrongDataType( + client.putJson( + apiPath(randomAsciiAlphanumOfLength(10)), + (builder, params) -> builder.startObject() + .field("password", configJsonArray("a", "b")) + .field("hash") + .nullValue() + .field("backend_roles", "c") + .field("attributes", "d") + .field("opendistro_security_roles", "e") + .endObject() + ), + Map.of( + "password", + "String expected", + "hash", + "String expected", + "backend_roles", + "Array expected", + "attributes", + "Object expected", + "opendistro_security_roles", + "Array expected" + ) + ); + assertNullValuesInArray( + client.putJson( + apiPath(randomAsciiAlphanumOfLength(10)), + (builder, params) -> builder.startObject().field("backend_roles", configJsonArray(generateArrayValues(true))).endObject() + ) + ); + // patch + badRequest(() -> client.patch(apiPath(), patch(addOp(randomAsciiAlphanumOfLength(10), EMPTY_BODY)))); + badRequest( + () -> client.patch( + apiPath(predefinedUserName), + patch(replaceOp(randomFrom(List.of("opendistro_security_roles", "backend_roles", "attributes")), EMPTY_BODY)) + ) + ); + badRequest(() -> client.patch(apiPath(), patch(addOp(randomAsciiAlphanumOfLength(5), (ToXContentObject) (builder, params) -> { + builder.startObject(); + builder.field("unknown_json_property"); + configJsonArray("a", "b").toXContent(builder, params); + builder.field("backend_roles"); + randomConfigArray(false).toXContent(builder, params); + return builder.endObject(); + })))); + assertWrongDataType( + client.patch( + apiPath(), + patch( + addOp( + randomAsciiAlphanumOfLength(10), + (ToXContentObject) (builder, params) -> builder.startObject() + .field("password", configJsonArray("a", "b")) + .field("hash") + .nullValue() + .field("backend_roles", "c") + .field("attributes", "d") + .field("opendistro_security_roles", "e") + .endObject() + ) + ) + ), + Map.of( + "password", + "String expected", + "hash", + "String expected", + "backend_roles", + "Array expected", + "attributes", + "Object expected", + "opendistro_security_roles", + "Array expected" + ) + ); + // TODO related to issue #4426 + assertWrongDataType( + client.patch(apiPath(predefinedUserName), patch(replaceOp("backend_roles", "a"))), + Map.of("backend_roles", "Array expected") + ); + assertNullValuesInArray( + client.patch( + apiPath(), + patch( + addOp( + randomAsciiAlphanumOfLength(5), + internalUser(randomAsciiAlphanumOfLength(10), randomConfigArray(true), null, randomConfigArray(true)) + ) + ) + ) + ); + // TODO related to issue #4426 + assertNullValuesInArray(client.patch(apiPath(predefinedUserName), patch(replaceOp("backend_roles", randomConfigArray(true))))); + } + + @Override + void verifyCrudOperations(Boolean hidden, Boolean reserved, TestRestClient client) throws Exception { + // put + final var usernamePut = randomAsciiAlphanumOfLength(10); + final var newUserJsonPut = internalUser( + hidden, + reserved, + randomAsciiAlphanumOfLength(10), + randomConfigArray(false), + randomAttributes(), + randomSecurityRoles() + ); + created(() -> client.putJson(apiPath(usernamePut), newUserJsonPut)); + assertInternalUser( + ok(() -> client.get(apiPath(usernamePut))).bodyAsJsonNode().get(usernamePut), + hidden, + reserved, + Strings.toString(XContentType.JSON, newUserJsonPut) + ); + final var updatedUserJsonPut = internalUser( + hidden, + reserved, + randomAsciiAlphanumOfLength(10), + randomConfigArray(false), + randomAttributes(), + randomSecurityRoles() + ); + ok(() -> client.putJson(apiPath(usernamePut), updatedUserJsonPut)); + assertInternalUser( + ok(() -> client.get(apiPath(usernamePut))).bodyAsJsonNode().get(usernamePut), + hidden, + reserved, + Strings.toString(XContentType.JSON, updatedUserJsonPut) + ); + ok(() -> client.delete(apiPath(usernamePut))); + notFound(() -> client.get(apiPath(usernamePut))); + // patch + // TODO related to issue #4426 + final var usernamePatch = randomAsciiAlphanumOfLength(10); + final var newUserJsonPatch = internalUser( + hidden, + reserved, + randomAsciiAlphanumOfLength(10), + configJsonArray("a", "b"), + (builder, params) -> builder.startObject().endObject(), + configJsonArray() + ); + ok(() -> client.patch(apiPath(), patch(addOp(usernamePatch, newUserJsonPatch)))); + assertInternalUser( + ok(() -> client.get(apiPath(usernamePatch))).bodyAsJsonNode().get(usernamePatch), + hidden, + reserved, + Strings.toString(XContentType.JSON, newUserJsonPatch) + ); + ok(() -> client.patch(apiPath(usernamePatch), patch(replaceOp("backend_roles", configJsonArray("c", "d"))))); + ok( + () -> client.patch( + apiPath(usernamePatch), + patch(addOp("attributes", (ToXContentObject) (builder, params) -> builder.startObject().field("a", "b").endObject())) + ) + ); + ok( + () -> client.patch(apiPath(usernamePatch), patch(addOp("opendistro_security_roles", configJsonArray(RESERVED_ROLE, SOME_ROLE)))) + ); + } + + ToXContentObject randomAttributes() { + return randomFrom( + List.of( + (builder, params) -> builder.startObject().endObject(), + (builder, params) -> builder.startObject().field("a", "b").field("c", "d").endObject() + ) + ); + } + + ToXContentObject randomSecurityRoles() { + return randomFrom(List.of(configJsonArray(), configJsonArray(SOME_ROLE, RESERVED_ROLE))); + } + + void assertInternalUser( + final JsonNode actualObjectNode, + final Boolean hidden, + final Boolean reserved, + final String expectedInternalUserJson + ) throws IOException { + final var expectedObjectNode = DefaultObjectMapper.readTree(expectedInternalUserJson); + final var expectedHidden = hidden != null && hidden; + final var expectedReserved = reserved != null && reserved; + assertThat(actualObjectNode.toPrettyString(), actualObjectNode.get("hidden").asBoolean(), is(expectedHidden)); + assertThat(actualObjectNode.toPrettyString(), actualObjectNode.get("reserved").asBoolean(), is(expectedReserved)); + assertThat(actualObjectNode.toPrettyString(), not(actualObjectNode.has("hash"))); + assertThat(actualObjectNode.toPrettyString(), actualObjectNode.get("backend_roles"), is(expectedObjectNode.get("backend_roles"))); + assertThat(actualObjectNode.toPrettyString(), actualObjectNode.get("attributes"), is(expectedObjectNode.get("attributes"))); + assertThat( + actualObjectNode.toPrettyString(), + actualObjectNode.get("opendistro_security_roles"), + is(expectedObjectNode.get("opendistro_security_roles")) + ); + } + + String filterBy(final String value) { + return apiPath() + "?filterBy=" + value; + } + + @Test + public void filters() throws Exception { + withUser(ADMIN_USER_NAME, client -> { + assertFilterByUsers(ok(() -> client.get(apiPath())), true, true); + assertFilterByUsers(ok(() -> client.get(filterBy("any"))), true, true); + assertFilterByUsers(ok(() -> client.get(filterBy("internal"))), false, true); + assertFilterByUsers(ok(() -> client.get(filterBy("service"))), true, false); + assertFilterByUsers(ok(() -> client.get(filterBy("something"))), true, true); + }); + } + + void assertFilterByUsers(final HttpResponse response, final boolean hasServiceUser, final boolean hasInternalUser) { + assertThat(response.getBody(), response.bodyAsJsonNode().has(SERVICE_ACCOUNT_USER), is(hasServiceUser)); + assertThat(response.getBody(), response.bodyAsJsonNode().has(NEW_USER), is(hasInternalUser)); + } + + @Test + public void userApiWithDotsInName() throws Exception { + withUser(ADMIN_USER_NAME, client -> { + for (final var dottedUserName : List.of(".my.dotuser0", ".my.dot.user0")) { + created( + () -> client.putJson( + apiPath(dottedUserName), + (builder, params) -> builder.startObject().field("password", randomAsciiAlphanumOfLength(10)).endObject() + ) + ); + } + for (final var dottedUserName : List.of(".my.dotuser1", ".my.dot.user1")) { + created( + () -> client.putJson( + apiPath(dottedUserName), + (builder, params) -> builder.startObject() + .field("hash", passwordHasher.hash(randomAsciiAlphanumOfLength(10).toCharArray())) + .endObject() + ) + ); + } + for (final var dottedUserName : List.of(".my.dotuser2", ".my.dot.user2")) { + ok( + () -> client.patch( + apiPath(), + patch( + addOp( + dottedUserName, + (ToXContentObject) (builder, params) -> builder.startObject() + .field("password", randomAsciiAlphanumOfLength(10)) + .endObject() + ) + ) + ) + ); + } + for (final var dottedUserName : List.of(".my.dotuser3", ".my.dot.user3")) { + ok( + () -> client.patch( + apiPath(), + patch( + addOp( + dottedUserName, + (ToXContentObject) (builder, params) -> builder.startObject() + .field("hash", passwordHasher.hash(randomAsciiAlphanumOfLength(10).toCharArray())) + .endObject() + ) + ) + ) + ); + } + }); + } + + @Test + public void noPasswordChange() throws Exception { + withUser(ADMIN_USER_NAME, client -> { + created( + () -> client.putJson( + apiPath("user1"), + (builder, params) -> builder.startObject() + .field("hash", "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m") + .endObject() + ) + ); + badRequest( + () -> client.putJson( + apiPath("user1"), + (builder, params) -> builder.startObject() + .field("hash", "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m") + .field("password", "") + .field("backend_roles", configJsonArray("admin", "role_a")) + .endObject() + ) + ); + ok( + () -> client.putJson( + apiPath("user1"), + (builder, params) -> builder.startObject() + .field("hash", "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m") + .field("password", randomAsciiAlphanumOfLength(10)) + .field("backend_roles", configJsonArray("admin", "role_a")) + .endObject() + ) + ); + created( + () -> client.putJson( + apiPath("user2"), + (builder, params) -> builder.startObject() + .field("hash", "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m") + .field("password", randomAsciiAlphanumOfLength(10)) + .endObject() + ) + ); + badRequest( + () -> client.putJson( + apiPath("user2"), + (builder, params) -> builder.startObject() + .field("password", "") + .field("backend_roles", configJsonArray("admin", "role_b")) + .endObject() + ) + ); + ok( + () -> client.putJson( + apiPath("user2"), + (builder, params) -> builder.startObject() + .field("password", randomAsciiAlphanumOfLength(10)) + .field("backend_roles", configJsonArray("admin", "role_b")) + .endObject() + ) + ); + }); + } + + @Test + public void securityRoles() throws Exception { + final var userWithSecurityRoles = randomAsciiAlphanumOfLength(15); + final var userWithSecurityRolesPassword = randomAsciiAlphanumOfLength(10); + withUser( + ADMIN_USER_NAME, + client -> ok( + () -> client.patch( + apiPath(), + patch(addOp(userWithSecurityRoles, internalUser(userWithSecurityRolesPassword, null, null, null))) + ) + ) + ); + withUser(userWithSecurityRoles, userWithSecurityRolesPassword, client -> forbidden(() -> client.get(apiPath()))); + withUser( + ADMIN_USER_NAME, + client -> ok( + () -> client.patch( + apiPath(), + patch( + replaceOp( + userWithSecurityRoles, + internalUser( + userWithSecurityRolesPassword, + null, + null, + (builder, params) -> builder.startArray().value("user_admin__all_access").endArray() + ) + ) + ) + ) + ) + ); + withUser(userWithSecurityRoles, userWithSecurityRolesPassword, client -> ok(() -> client.get(apiPath()))); + withUser(ADMIN_USER_NAME, client -> impossibleToSetHiddenRoleIsNotAllowed(userWithSecurityRoles, client)); + withUser(ADMIN_USER_NAME, client -> settingOfUnknownRoleIsNotAllowed(userWithSecurityRoles, client)); + + withUser( + ADMIN_USER_NAME, + localCluster.getAdminCertificate(), + client -> settingOfUnknownRoleIsNotAllowed(userWithSecurityRoles, client) + ); + withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), this::canAssignedHiddenRole); + + for (final var restAdminUser : List.of(REST_ADMIN_USER, REST_API_ADMIN_INTERNAL_USERS_ONLY)) { + withUser(restAdminUser, client -> settingOfUnknownRoleIsNotAllowed(userWithSecurityRoles, client)); + withUser(restAdminUser, localCluster.getAdminCertificate(), this::canAssignedHiddenRole); + } + } + + void impossibleToSetHiddenRoleIsNotAllowed(final String predefinedUserName, final TestRestClient client) throws Exception { + // put + notFound( + () -> client.putJson( + apiPath(randomAsciiAlphanumOfLength(10)), + internalUser(randomAsciiAlphanumOfLength(10), null, null, configJsonArray(HIDDEN_ROLE)) + ), + "Resource 'hidden-role' is not available." + ); + // patch + notFound( + () -> client.patch( + apiPath(), + patch( + addOp( + randomAsciiAlphanumOfLength(10), + internalUser(randomAsciiAlphanumOfLength(10), null, null, configJsonArray(HIDDEN_ROLE)) + ) + ) + ) + ); + // TODO related to issue #4426 + notFound( + () -> client.patch(apiPath(predefinedUserName), patch(addOp("opendistro_security_roles", configJsonArray(HIDDEN_ROLE)))) + + ); + } + + void canAssignedHiddenRole(final TestRestClient client) throws Exception { + final var userNamePut = randomAsciiAlphanumOfLength(4); + created( + () -> client.putJson( + apiPath(userNamePut), + internalUser(randomAsciiAlphanumOfLength(10), null, null, configJsonArray(HIDDEN_ROLE)) + ) + ); + } + + void settingOfUnknownRoleIsNotAllowed(final String predefinedUserName, final TestRestClient client) throws Exception { + notFound( + () -> client.putJson( + apiPath(randomAsciiAlphanumOfLength(10)), + internalUser(randomAsciiAlphanumOfLength(10), null, null, configJsonArray("unknown-role")) + ), + "role 'unknown-role' not found." + ); + notFound( + () -> client.patch( + apiPath(), + patch( + addOp( + randomAsciiAlphanumOfLength(4), + internalUser(randomAsciiAlphanumOfLength(10), null, null, configJsonArray("unknown-role")) + ) + ) + ) + ); + notFound( + () -> client.patch(apiPath(predefinedUserName), patch(addOp("opendistro_security_roles", configJsonArray("unknown-role")))) + ); + } + + @Test + public void parallelPutRequests() throws Exception { + withUser(ADMIN_USER_NAME, client -> { + final var userName = randomAsciiAlphanumOfLength(10); + final var httpResponses = new HttpResponse[10]; + + final var executorService = Executors.newFixedThreadPool(httpResponses.length); + try { + final var futures = new ArrayList>(httpResponses.length); + for (int i = 0; i < httpResponses.length; i++) { + futures.add( + executorService.submit( + () -> client.putJson( + apiPath(userName), + (builder, params) -> builder.startObject().field("password", randomAsciiAlphanumOfLength(10)).endObject() + ) + ) + ); + } + for (int i = 0; i < httpResponses.length; i++) { + httpResponses[i] = futures.get(i).get(); + } + } finally { + executorService.shutdown(); + } + boolean created = false; + for (HttpResponse response : httpResponses) { + int sc = response.getStatusCode(); + switch (sc) { + case HttpStatus.SC_CREATED: + Assert.assertFalse(created); + created = true; + break; + case HttpStatus.SC_OK: + break; + default: + Assert.assertEquals(HttpStatus.SC_CONFLICT, sc); + break; + } + } + }); + } + + @Test + public void restrictedUsernameContents() throws Exception { + withUser(ADMIN_USER_NAME, client -> { + { + for (final var restrictedTerm : RESTRICTED_FROM_USERNAME) { + for (final var username : List.of( + randomAsciiAlphanumOfLength(2) + restrictedTerm + randomAsciiAlphanumOfLength(3), + URLEncoder.encode(randomAsciiAlphanumOfLength(4) + ":" + randomAsciiAlphanumOfLength(3), StandardCharsets.UTF_8) + )) { + final var putResponse = badRequest( + () -> client.putJson(apiPath(username), internalUserWithPassword(randomAsciiAlphanumOfLength(10))) + ); + assertThat(putResponse.getBody(), containsString(restrictedTerm)); + final var patchResponse = badRequest( + () -> client.patch(apiPath(), patch(addOp(username, internalUserWithPassword(randomAsciiAlphanumOfLength(10))))) + ); + assertThat(patchResponse.getBody(), containsString(restrictedTerm)); + } + } + } + }); + } + + @Test + public void serviceUsers() throws Exception { + withUser(ADMIN_USER_NAME, client -> { + // Add enabled service account then get it + // TODO related to issue #4426 add default behave when enabled is true + final var happyServiceLiveUserName = randomAsciiAlphanumOfLength(10); + created(() -> client.putJson(apiPath(happyServiceLiveUserName), serviceUser(true))); + final var serviceLiveResponse = ok(() -> client.get(apiPath(happyServiceLiveUserName))); + assertThat( + serviceLiveResponse.getBody(), + serviceLiveResponse.getBooleanFromJsonBody("/" + happyServiceLiveUserName + "/attributes/service") + ); + assertThat( + serviceLiveResponse.getBody(), + serviceLiveResponse.getBooleanFromJsonBody("/" + happyServiceLiveUserName + "/attributes/enabled") + ); + + // Add disabled service account + final var happyServiceDeadUserName = randomAsciiAlphanumOfLength(10); + created(() -> client.putJson(apiPath(happyServiceDeadUserName), serviceUser(false))); + final var serviceDeadResponse = ok(() -> client.get(apiPath(happyServiceDeadUserName))); + assertThat( + serviceDeadResponse.getBody(), + serviceDeadResponse.getBooleanFromJsonBody("/" + happyServiceDeadUserName + "/attributes/service") + ); + assertThat( + serviceDeadResponse.getBody(), + not(serviceDeadResponse.getBooleanFromJsonBody("/" + happyServiceDeadUserName + "/attributes/enabled")) + ); + // Add service account with password -- Should Fail + badRequest( + () -> client.putJson( + apiPath(randomAsciiAlphanumOfLength(10)), + serviceUserWithPassword(true, randomAsciiAlphanumOfLength(10)) + ) + ); + // Add service with hash -- should fail + badRequest( + () -> client.putJson( + apiPath(randomAsciiAlphanumOfLength(10)), + serviceUserWithHash(true, passwordHasher.hash(randomAsciiAlphanumOfLength(10).toCharArray())) + ) + ); + // Add Service account with password & Hash -- should fail + final var password = randomAsciiAlphanumOfLength(10); + badRequest( + () -> client.putJson( + apiPath(randomAsciiAlphanumOfLength(10)), + serviceUser(true, password, passwordHasher.hash(password.toCharArray())) + ) + ); + }); + } +} diff --git a/src/integrationTest/java/org/opensearch/security/api/InternalUsersScoreBasedPasswordRulesRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/InternalUsersScoreBasedPasswordRulesRestApiIntegrationTest.java new file mode 100644 index 0000000000..5b7026b3c3 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/api/InternalUsersScoreBasedPasswordRulesRestApiIntegrationTest.java @@ -0,0 +1,83 @@ +/* + * 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.api; + +import java.util.StringJoiner; + +import org.junit.Test; + +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator; +import org.opensearch.security.support.ConfigConstants; + +import static org.opensearch.security.api.PatchPayloadHelper.addOp; +import static org.opensearch.security.api.PatchPayloadHelper.patch; + +public class InternalUsersScoreBasedPasswordRulesRestApiIntegrationTest extends AbstractApiIntegrationTest { + + static { + clusterSettings.put(ConfigConstants.SECURITY_RESTAPI_PASSWORD_MIN_LENGTH, 9); + } + + String internalUsers(String... path) { + final var fullPath = new StringJoiner("/").add(super.apiPath("internalusers")); + if (path != null) { + for (final var p : path) + fullPath.add(p); + } + return fullPath.toString(); + } + + ToXContentObject internalUserWithPassword(final String password) { + return (builder, params) -> builder.startObject() + .field("password", password) + .field("backend_roles", randomConfigArray(false)) + .endObject(); + } + + @Test + public void canNotCreateUsersWithPassword() throws Exception { + withUser(ADMIN_USER_NAME, client -> { + badRequestWithReason( + () -> client.putJson(internalUsers("admin"), internalUserWithPassword("password89")), + RequestContentValidator.ValidationError.WEAK_PASSWORD.message() + ); + badRequestWithReason( + () -> client.putJson(internalUsers("admin"), internalUserWithPassword("A123456789")), + RequestContentValidator.ValidationError.WEAK_PASSWORD.message() + ); + badRequestWithReason( + () -> client.putJson(internalUsers("admin"), internalUserWithPassword(randomAsciiAlphanumOfLengthBetween(2, 8))), + RequestContentValidator.ValidationError.INVALID_PASSWORD_TOO_SHORT.message() + ); + }); + } + + @Test + public void canCreateUserWithPassword() throws Exception { + withUser(ADMIN_USER_NAME, client -> { + created( + () -> client.putJson( + internalUsers(randomAsciiAlphanumOfLength(10)), + internalUserWithPassword(randomAsciiAlphanumOfLength(9)) + ) + ); + ok( + () -> client.patch( + internalUsers(), + patch(addOp(randomAsciiAlphanumOfLength(10), internalUserWithPassword(randomAsciiAlphanumOfLength(9)))) + ) + ); + }); + } + +} diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java index 3155bdb740..0f4330fb79 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java @@ -59,7 +59,7 @@ protected void consumeParameters(final RestRequest request) { request.param("filterBy"); } - static final List RESTRICTED_FROM_USERNAME = ImmutableList.of( + public static final List RESTRICTED_FROM_USERNAME = ImmutableList.of( ":" // Not allowed in basic auth, see https://stackoverflow.com/a/33391003/533057 ); @@ -264,7 +264,16 @@ public RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator() { @Override public ValidationResult onConfigChange(SecurityConfiguration securityConfiguration) throws IOException { // this method will be called only for PATCH - return EndpointValidator.super.onConfigChange(securityConfiguration).map(this::generateHashForPassword); + return EndpointValidator.super.onConfigChange(securityConfiguration).map(this::validateUserName) + .map(this::generateHashForPassword) + .map(InternalUsersApiAction.this::validateAndUpdatePassword) + .map(InternalUsersApiAction.this::validateSecurityRoles); + } + + private ValidationResult validateUserName(final SecurityConfiguration securityConfiguration) { + return UserService.restrictedFromUsername(securityConfiguration.entityName()) + .>map(m -> ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage(m))) + .orElseGet(() -> ValidationResult.success(securityConfiguration)); } private ValidationResult generateHashForPassword(final SecurityConfiguration securityConfiguration) { @@ -294,6 +303,7 @@ public Settings settings() { public Map allowedKeys() { final ImmutableMap.Builder allowedKeys = ImmutableMap.builder(); if (isCurrentUserAdmin()) { + allowedKeys.put("hidden", DataType.BOOLEAN); allowedKeys.put("reserved", DataType.BOOLEAN); } return allowedKeys.put("backend_roles", DataType.ARRAY) diff --git a/src/main/java/org/opensearch/security/user/UserService.java b/src/main/java/org/opensearch/security/user/UserService.java index b3220e1589..7c5202452b 100644 --- a/src/main/java/org/opensearch/security/user/UserService.java +++ b/src/main/java/org/opensearch/security/user/UserService.java @@ -12,6 +12,7 @@ package org.opensearch.security.user; import java.io.IOException; +import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; @@ -123,6 +124,18 @@ protected final SecurityDynamicConfiguration load(final CType config, boolean return DynamicConfigFactory.addStatics(loaded); } + public static Optional restrictedFromUsername(final String accountName) { + final var foundRestrictedContents = RESTRICTED_FROM_USERNAME.stream() + .filter(r -> URLDecoder.decode(accountName, StandardCharsets.UTF_8).contains(r)) + .collect(Collectors.toList()); + if (!foundRestrictedContents.isEmpty()) { + return Optional.of( + RESTRICTED_CHARACTER_USE_MESSAGE + foundRestrictedContents.stream().map(s -> "'" + s + "'").collect(Collectors.joining(",")) + ); + } + return Optional.empty(); + } + /** * This function will handle the creation or update of a user account. * @@ -156,12 +169,9 @@ public SecurityDynamicConfiguration createOrUpdateAccount(ObjectNode contentA } securityJsonNode = new SecurityJsonNode(contentAsNode); - final List foundRestrictedContents = RESTRICTED_FROM_USERNAME.stream() - .filter(accountName::contains) - .collect(Collectors.toList()); - if (!foundRestrictedContents.isEmpty()) { - final String restrictedContents = foundRestrictedContents.stream().map(s -> "'" + s + "'").collect(Collectors.joining(",")); - throw new UserServiceException(RESTRICTED_CHARACTER_USE_MESSAGE + restrictedContents); + final var foundRestrictedContents = restrictedFromUsername(accountName); + if (foundRestrictedContents.isPresent()) { + throw new UserServiceException(foundRestrictedContents.get()); } // if password is set, it takes precedence over hash diff --git a/src/test/java/org/opensearch/security/UserServiceUnitTests.java b/src/test/java/org/opensearch/security/UserServiceUnitTests.java index ccd3c9848e..f68e1ce380 100644 --- a/src/test/java/org/opensearch/security/UserServiceUnitTests.java +++ b/src/test/java/org/opensearch/security/UserServiceUnitTests.java @@ -14,13 +14,16 @@ import java.io.File; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.util.Optional; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import org.opensearch.client.Client; import org.opensearch.cluster.service.ClusterService; @@ -34,7 +37,18 @@ import org.opensearch.security.user.UserService; import org.mockito.Mock; - +import org.passay.CharacterCharacteristicsRule; +import org.passay.CharacterRule; +import org.passay.EnglishCharacterData; +import org.passay.LengthRule; +import org.passay.PasswordData; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertNotEquals; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class UserServiceUnitTests { SecurityDynamicConfiguration config; @Mock @@ -101,4 +115,40 @@ private SecurityDynamicConfiguration readConfigFromYml(String file, CType cTy return SecurityDynamicConfiguration.fromNode(jsonNode, cType, configVersion, 0, 0); } + @Test + public void restrictedFromUsername() { + assertThat(UserService.restrictedFromUsername("aaaa"), is(Optional.empty())); + assertThat( + UserService.restrictedFromUsername("aaaa:bbb"), + is(Optional.of("A restricted character(s) was detected in the account name. Please remove: ':'")) + ); + } + + @Test + public void testGeneratedPasswordContents() { + String password = UserService.generatePassword(); + PasswordData data = new PasswordData(password); + + LengthRule lengthRule = new LengthRule(8, 16); + + CharacterCharacteristicsRule characteristicsRule = new CharacterCharacteristicsRule(); + + // Define M (3 in this case) + characteristicsRule.setNumberOfCharacteristics(3); + + // Define elements of N (upper, lower, digit, symbol) + characteristicsRule.getRules().add(new CharacterRule(EnglishCharacterData.UpperCase, 1)); + characteristicsRule.getRules().add(new CharacterRule(EnglishCharacterData.LowerCase, 1)); + characteristicsRule.getRules().add(new CharacterRule(EnglishCharacterData.Digit, 1)); + characteristicsRule.getRules().add(new CharacterRule(EnglishCharacterData.Special, 1)); + + org.passay.PasswordValidator validator = new org.passay.PasswordValidator(lengthRule, characteristicsRule); + validator.validate(data); + + String password2 = UserService.generatePassword(); + PasswordData data2 = new PasswordData(password2); + assertNotEquals(password, password2); + assertNotEquals(data, data2); + } + } diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/UserApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/UserApiTest.java deleted file mode 100644 index b8dcb1db88..0000000000 --- a/src/test/java/org/opensearch/security/dlic/rest/api/UserApiTest.java +++ /dev/null @@ -1,1119 +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 java.net.URLEncoder; -import java.util.ArrayList; -import java.util.Base64; -import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; - -import com.fasterxml.jackson.databind.JsonNode; -import org.apache.http.Header; -import org.apache.http.HttpStatus; -import org.apache.http.message.BasicHeader; -import org.hamcrest.Matchers; -import org.junit.Assert; -import org.junit.Test; - -import org.opensearch.common.settings.Settings; -import org.opensearch.common.xcontent.XContentType; -import org.opensearch.security.DefaultObjectMapper; -import org.opensearch.security.dlic.rest.validation.PasswordValidator; -import org.opensearch.security.dlic.rest.validation.RequestContentValidator; -import org.opensearch.security.securityconf.impl.CType; -import org.opensearch.security.support.ConfigConstants; -import org.opensearch.security.test.helper.file.FileHelper; -import org.opensearch.security.test.helper.rest.RestHelper.HttpResponse; -import org.opensearch.security.user.UserService; - -import org.passay.CharacterCharacteristicsRule; -import org.passay.CharacterRule; -import org.passay.EnglishCharacterData; -import org.passay.LengthRule; -import org.passay.PasswordData; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.equalTo; -import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; -import static org.opensearch.security.dlic.rest.api.InternalUsersApiAction.RESTRICTED_FROM_USERNAME; -import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED; -import static org.junit.Assert.assertNotEquals; - -public class UserApiTest extends AbstractRestApiUnitTest { - private final String ENDPOINT; - - protected String getEndpointPrefix() { - return PLUGINS_PREFIX; - } - - final int USER_SETTING_SIZE = 140; // Lines per account entry * number of accounts - - private static final String ENABLED_SERVICE_ACCOUNT_BODY = "{" - + " \"attributes\": { \"service\": \"true\", " - + " \"enabled \": \"true\"}" - + " }\n"; - - private static final String DISABLED_SERVICE_ACCOUNT_BODY = "{" - + " \"attributes\": { \"service\": \"true\", " - + "\"enabled\": \"false\"}" - + " }\n"; - private static final String ENABLED_NOT_SERVICE_ACCOUNT_BODY = "{" - + " \"attributes\": { \"service\": \"false\", " - + "\"enabled\": \"true\"}" - + " }\n"; - private static final String PASSWORD_SERVICE = "{ \"password\" : \"test\"," - + " \"attributes\": { \"service\": \"true\", " - + "\"enabled\": \"true\"}" - + " }\n"; - private static final String HASH_SERVICE = "{ \"owner\" : \"test_owner\"," - + " \"attributes\": { \"service\": \"true\", " - + "\"enabled\": \"true\"}" - + " }\n"; - private static final String PASSWORD_HASH_SERVICE = "{ \"password\" : \"test\", \"hash\" : \"123\"," - + " \"attributes\": { \"service\": \"true\", " - + "\"enabled\": \"true\"}" - + " }\n"; - - public UserApiTest() { - ENDPOINT = getEndpointPrefix() + "/api"; - } - - @Test - public void testSecurityRoles() throws Exception { - - setup(); - - rh.keystore = "restapi/kirk-keystore.jks"; - rh.sendAdminCertificate = true; - - // initial configuration, 6 users - HttpResponse response = rh.executeGetRequest(ENDPOINT + "/" + CType.INTERNALUSERS.toLCString()); - Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); - Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(USER_SETTING_SIZE, settings.size()); - response = rh.executePatchRequest( - ENDPOINT + "/internalusers", - "[{ \"op\": \"add\", \"path\": \"/newuser\", \"value\": {\"password\": \"fair password for the user\", \"opendistro_security_roles\": [\"opendistro_security_all_access\"] } }]", - new Header[0] - ); - Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); - - response = rh.executeGetRequest(ENDPOINT + "/internalusers/newuser", new Header[0]); - Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - Assert.assertTrue(response.getBody().contains("\"opendistro_security_roles\":[\"opendistro_security_all_access\"]")); - - checkGeneralAccess(HttpStatus.SC_OK, "newuser", "fair password for the user"); - } - - @Test - public void testParallelPutRequests() throws Exception { - - setup(); - - rh.keystore = "restapi/kirk-keystore.jks"; - rh.sendAdminCertificate = true; - - HttpResponse[] responses = executeMultipleAsyncPutRequest( - 10, - ENDPOINT + "/internalusers/test1", - "{\"password\":\"test1test1test1test1test1test1\"}" - ); - boolean created = false; - for (HttpResponse response : responses) { - int sc = response.getStatusCode(); - switch (sc) { - case HttpStatus.SC_CREATED: - Assert.assertFalse(created); - created = true; - break; - case HttpStatus.SC_OK: - break; - default: - Assert.assertEquals(HttpStatus.SC_CONFLICT, sc); - break; - } - } - deleteUser("test1"); - } - - private HttpResponse[] executeMultipleAsyncPutRequest(final int numOfRequests, final String request, String body) throws Exception { - final ExecutorService executorService = Executors.newFixedThreadPool(numOfRequests); - try { - List> futures = new ArrayList<>(numOfRequests); - for (int i = 0; i < numOfRequests; i++) { - futures.add(executorService.submit(() -> rh.executePutRequest(request, body))); - } - return futures.stream().map(this::from).toArray(HttpResponse[]::new); - } finally { - executorService.shutdown(); - } - } - - private HttpResponse from(Future future) { - try { - return future.get(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Test - public void testUserFilters() throws Exception { - setup(); - rh.keystore = "restapi/kirk-keystore.jks"; - rh.sendAdminCertificate = true; - final int SERVICE_ACCOUNTS_IN_SETTINGS = 1; - final int INTERNAL_ACCOUNTS_IN_SETTINGS = 20; - final String serviceAccountName = "JohnDoeService"; - HttpResponse response; - - response = rh.executeGetRequest(ENDPOINT + "/internalusers?filterBy=internal"); - - Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); - JsonNode list = DefaultObjectMapper.readTree(response.getBody()); - Assert.assertEquals(INTERNAL_ACCOUNTS_IN_SETTINGS, list.size()); - - response = rh.executeGetRequest(ENDPOINT + "/internalusers?filterBy=service"); - Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); - list = DefaultObjectMapper.readTree(response.getBody()); - assertThat(list, Matchers.emptyIterable()); - - response = rh.executePutRequest(ENDPOINT + "/internalusers/" + serviceAccountName, ENABLED_SERVICE_ACCOUNT_BODY); - - // repeat assertions after adding the service account - - response = rh.executeGetRequest(ENDPOINT + "/internalusers?filterBy=internal"); - - Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); - list = DefaultObjectMapper.readTree(response.getBody()); - Assert.assertEquals(INTERNAL_ACCOUNTS_IN_SETTINGS, list.size()); - - response = rh.executeGetRequest(ENDPOINT + "/internalusers?filterBy=service"); - Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); - list = DefaultObjectMapper.readTree(response.getBody()); - Assert.assertEquals(SERVICE_ACCOUNTS_IN_SETTINGS, list.size()); - assertThat(response.findValueInJson(serviceAccountName + ".attributes.service"), containsString("true")); - - response = rh.executeGetRequest(ENDPOINT + "/internalusers?filterBy=ssas"); - Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); - list = DefaultObjectMapper.readTree(response.getBody()); - Assert.assertEquals(SERVICE_ACCOUNTS_IN_SETTINGS + INTERNAL_ACCOUNTS_IN_SETTINGS, list.size()); - - response = rh.executeGetRequest(ENDPOINT + "/internalusers?wrongparameter=jhondoe"); - Assert.assertEquals(response.getBody(), HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - - response = rh.executePutRequest(ENDPOINT + "/internalusers", "{sample:value"); - Assert.assertEquals(response.getBody(), HttpStatus.SC_METHOD_NOT_ALLOWED, response.getStatusCode()); - } - - @Test - public void testUserApi() throws Exception { - - setup(); - - rh.keystore = "restapi/kirk-keystore.jks"; - rh.sendAdminCertificate = true; - - // initial configuration - HttpResponse response = rh.executeGetRequest(ENDPOINT + "/" + CType.INTERNALUSERS.toLCString()); - Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); - Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(USER_SETTING_SIZE, settings.size()); - verifyGet(); - verifyPut(); - verifyPatch(true); - // create index first - setupStarfleetIndex(); - verifyRoles(true); - } - - private void verifyGet(final Header... header) throws Exception { - // --- GET - // GET, user admin, exists - HttpResponse response = rh.executeGetRequest(ENDPOINT + "/internalusers/admin", header); - Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); - Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(7, settings.size()); - // hash must be filtered - Assert.assertEquals("", settings.get("admin.hash")); - - // GET, user does not exist - response = rh.executeGetRequest(ENDPOINT + "/internalusers/nothinghthere", header); - Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); - - // GET, new URL endpoint in security - response = rh.executeGetRequest(ENDPOINT + "/user/", header); - Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - - // GET, new URL endpoint in security - response = rh.executeGetRequest(ENDPOINT + "/user", header); - Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - } - - private void verifyPut(final Header... header) throws Exception { - // -- PUT - // no username given - HttpResponse response = rh.executePutRequest(ENDPOINT + "/internalusers/", "{\"hash\": \"123\"}", header); - Assert.assertEquals(HttpStatus.SC_METHOD_NOT_ALLOWED, response.getStatusCode()); - - // Faulty JSON payload - response = rh.executePutRequest(ENDPOINT + "/internalusers/nagilum", "{some: \"thing\" asd other: \"thing\"}", header); - Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(settings.get("reason"), RequestContentValidator.ValidationError.BODY_NOT_PARSEABLE.message()); - - // Missing quotes in JSON - parseable in 6.x, but wrong config keys - response = rh.executePutRequest(ENDPOINT + "/internalusers/nagilum", "{some: \"thing\", other: \"thing\"}", header); - Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - // JK: this should be "Could not parse content of request." because JSON is truly invalid - // Assert.assertEquals(settings.get("reason"), ValidationError.INVALID_CONFIGURATION.message()); - // Assert.assertTrue(settings.get(AbstractConfigurationValidator.INVALID_KEYS_KEY + ".keys").contains("some")); - // Assert.assertTrue(settings.get(AbstractConfigurationValidator.INVALID_KEYS_KEY + ".keys").contains("other")); - - // Get hidden role - response = rh.executeGetRequest(ENDPOINT + "/internalusers/hide", header); - Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - Assert.assertTrue(response.getBody().contains("\"hidden\":true")); - - // Associating with hidden role is allowed (for superadmin) - response = rh.executePutRequest( - ENDPOINT + "/internalusers/test", - "{ \"opendistro_security_roles\": " + "[\"opendistro_security_hidden\"]}", - header - ); - Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - - // Associating with reserved role is allowed (for superadmin) - response = rh.executePutRequest( - ENDPOINT + "/internalusers/test", - "{ \"opendistro_security_roles\": [\"opendistro_security_reserved\"], " + "\"hash\": \"123\"}", - header - ); - Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - - // Associating with non-existent role is not allowed - response = rh.executePutRequest( - ENDPOINT + "/internalusers/nagilum", - "{ \"opendistro_security_roles\": [\"non_existent\"]}", - header - ); - Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); - settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(settings.get("message"), "role 'non_existent' not found."); - - // Wrong config keys - response = rh.executePutRequest(ENDPOINT + "/internalusers/nagilum", "{\"some\": \"thing\", \"other\": \"thing\"}", header); - Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(settings.get("reason"), RequestContentValidator.ValidationError.INVALID_CONFIGURATION.message()); - Assert.assertEquals(settings.get("reason"), RequestContentValidator.ValidationError.INVALID_CONFIGURATION.message()); - Assert.assertTrue(settings.get(RequestContentValidator.INVALID_KEYS_KEY + ".keys").contains("some")); - Assert.assertTrue(settings.get(RequestContentValidator.INVALID_KEYS_KEY + ".keys").contains("other")); - - } - - private void verifyPatch(final boolean sendAdminCert, Header... restAdminHeader) throws Exception { - // -- PATCH - // PATCH on non-existing resource - rh.sendAdminCertificate = sendAdminCert; - HttpResponse response = rh.executePatchRequest( - ENDPOINT + "/internalusers/imnothere", - "[{ \"op\": \"add\", \"path\": \"/a/b/c\", \"value\": [ \"foo\", \"bar\" ] }]", - restAdminHeader - ); - Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); - - // PATCH read only resource, must be forbidden, - // but SuperAdmin can PATCH read-only resource - rh.sendAdminCertificate = sendAdminCert; - response = rh.executePatchRequest( - ENDPOINT + "/internalusers/sarek", - "[{ \"op\": \"add\", \"path\": \"/description\", \"value\": \"foo\" }]", - restAdminHeader - ); - Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - - // PATCH hidden resource, must be not found, can be found for super admin - rh.sendAdminCertificate = sendAdminCert; - response = rh.executePatchRequest( - ENDPOINT + "/internalusers/q", - "[{ \"op\": \"add\", \"path\": \"/a/b/c\", \"value\": [ \"foo\", \"bar\" ] }]", - restAdminHeader - ); - Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - - // PATCH value of hidden flag, must fail with validation error - rh.sendAdminCertificate = sendAdminCert; - response = rh.executePatchRequest( - ENDPOINT + "/internalusers/test", - "[{ \"op\": \"add\", \"path\": \"/hidden\", \"value\": true }]", - restAdminHeader - ); - Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - Assert.assertTrue(response.getBody().matches(".*\"invalid_keys\"\\s*:\\s*\\{\\s*\"keys\"\\s*:\\s*\"hidden\"\\s*\\}.*")); - - // PATCH password - rh.sendAdminCertificate = sendAdminCert; - response = rh.executePatchRequest( - ENDPOINT + "/internalusers/test", - "[{ \"op\": \"add\", \"path\": \"/password\", \"value\": \"neu password 42\" }]", - restAdminHeader - ); - Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - response = rh.executeGetRequest(ENDPOINT + "/internalusers/test", restAdminHeader); - Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertFalse(settings.hasValue("test.password")); - Assert.assertTrue(settings.hasValue("test.hash")); - - // -- PATCH on whole config resource - // PATCH on non-existing resource - rh.sendAdminCertificate = sendAdminCert; - response = rh.executePatchRequest( - ENDPOINT + "/internalusers", - "[{ \"op\": \"add\", \"path\": \"/imnothere/a\", \"value\": [ \"foo\", \"bar\" ] }]", - restAdminHeader - ); - Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - - // PATCH read only resource, must be forbidden, - // but SuperAdmin can PATCH read only resouce - rh.sendAdminCertificate = sendAdminCert; - response = rh.executePatchRequest( - ENDPOINT + "/internalusers", - "[{ \"op\": \"add\", \"path\": \"/sarek/description\", \"value\": \"foo\" }]", - restAdminHeader - ); - Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - - rh.sendAdminCertificate = false; - response = rh.executePatchRequest( - ENDPOINT + "/internalusers", - "[{ \"op\": \"add\", \"path\": \"/sarek/a\", \"value\": [ \"foo\", \"bar\" ] }]" - ); - Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getStatusCode()); - - // PATCH hidden resource, must be bad request - rh.sendAdminCertificate = sendAdminCert; - response = rh.executePatchRequest( - ENDPOINT + "/internalusers", - "[{ \"op\": \"add\", \"path\": \"/q/a\", \"value\": [ \"foo\", \"bar\" ] }]", - restAdminHeader - ); - Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - - // PATCH value of hidden flag, must fail with validation error - rh.sendAdminCertificate = sendAdminCert; - response = rh.executePatchRequest( - ENDPOINT + "/internalusers", - "[{ \"op\": \"add\", \"path\": \"/test/hidden\", \"value\": true }]", - restAdminHeader - ); - Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - Assert.assertTrue(response.getBody().matches(".*\"invalid_keys\"\\s*:\\s*\\{\\s*\"keys\"\\s*:\\s*\"hidden\"\\s*\\}.*")); - - // PATCH - rh.sendAdminCertificate = sendAdminCert; - response = rh.executePatchRequest( - ENDPOINT + "/internalusers", - "[{ \"op\": \"add\", \"path\": \"/bulknew1\", \"value\": {\"password\": \"bla bla bla password 42\", \"backend_roles\": [\"vulcan\"] } }]", - restAdminHeader - ); - Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); - response = rh.executeGetRequest(ENDPOINT + "/internalusers/bulknew1", restAdminHeader); - Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertFalse(settings.hasValue("bulknew1.password")); - Assert.assertTrue(settings.hasValue("bulknew1.hash")); - List roles = settings.getAsList("bulknew1.backend_roles"); - Assert.assertEquals(1, roles.size()); - Assert.assertTrue(roles.contains("vulcan")); - - // add user with correct setting. User is in role "opendistro_security_all_access" - - // check access not allowed - checkGeneralAccess(HttpStatus.SC_UNAUTHORIZED, "nagilum", "nagilum"); - - // add/update user, user is read only, forbidden - // SuperAdmin can add read only users - rh.sendAdminCertificate = sendAdminCert; - addUserWithHash("sarek", "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m", HttpStatus.SC_OK); - - // add/update user, user is hidden, forbidden, allowed for super admin - rh.sendAdminCertificate = sendAdminCert; - addUserWithHash("q", "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m", HttpStatus.SC_OK); - - // add users - rh.sendAdminCertificate = sendAdminCert; - addUserWithHash("nagilum", "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m", HttpStatus.SC_CREATED); - - // Add enabled service account then get it - response = rh.executePutRequest(ENDPOINT + "/internalusers/happyServiceLive", ENABLED_SERVICE_ACCOUNT_BODY, restAdminHeader); - Assert.assertEquals(response.getBody(), HttpStatus.SC_CREATED, response.getStatusCode()); - response = rh.executeGetRequest(ENDPOINT + "/internalusers/happyServiceLive", restAdminHeader); - Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - - // Add disabled service account - response = rh.executePutRequest(ENDPOINT + "/internalusers/happyServiceDead", DISABLED_SERVICE_ACCOUNT_BODY, restAdminHeader); - Assert.assertEquals(response.getBody(), HttpStatus.SC_CREATED, response.getStatusCode()); - - // Add service account with password -- Should Fail - response = rh.executePutRequest(ENDPOINT + "/internalusers/passwordService", PASSWORD_SERVICE, restAdminHeader); - Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - - // Add service with hash -- should fail - response = rh.executePutRequest(ENDPOINT + "/internalusers/hashService", HASH_SERVICE, restAdminHeader); - Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - - // Add Service account with password & Hash -- should fail - response = rh.executePutRequest(ENDPOINT + "/internalusers/passwordHashService", PASSWORD_HASH_SERVICE, restAdminHeader); - Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - - // access must be allowed now - checkGeneralAccess(HttpStatus.SC_OK, "nagilum", "nagilum"); - - // try remove user, no username - rh.sendAdminCertificate = sendAdminCert; - response = rh.executeDeleteRequest(ENDPOINT + "/internalusers", restAdminHeader); - Assert.assertEquals(HttpStatus.SC_METHOD_NOT_ALLOWED, response.getStatusCode()); - - // try remove user, nonexisting user - response = rh.executeDeleteRequest(ENDPOINT + "/internalusers/picard", restAdminHeader); - Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); - - // try remove readonly user - response = rh.executeDeleteRequest(ENDPOINT + "/internalusers/sarek", restAdminHeader); - Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - - // try remove hidden user, allowed for super admin - response = rh.executeDeleteRequest(ENDPOINT + "/internalusers/q", restAdminHeader); - Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - Assert.assertTrue(response.getBody().contains("'q' deleted.")); - // now really remove user - deleteUser("nagilum"); - - // Access must be forbidden now - rh.sendAdminCertificate = false; - checkGeneralAccess(HttpStatus.SC_UNAUTHORIZED, "nagilum", "nagilum"); - - // use password instead of hash - rh.sendAdminCertificate = sendAdminCert; - addUserWithPassword("nagilum", "correctpassword", HttpStatus.SC_CREATED); - - rh.sendAdminCertificate = false; - checkGeneralAccess(HttpStatus.SC_UNAUTHORIZED, "nagilum", "wrongpassword"); - checkGeneralAccess(HttpStatus.SC_OK, "nagilum", "correctpassword"); - - deleteUser("nagilum"); - - // Check unchanged password functionality - rh.sendAdminCertificate = sendAdminCert; - - // new user, password or hash is mandatory - addUserWithoutPasswordOrHash("nagilum", new String[] { "starfleet" }, HttpStatus.SC_BAD_REQUEST); - // new user, add hash - addUserWithHash("nagilum", "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m", HttpStatus.SC_CREATED); - // update user, do not specify hash or password, hash must remain the same - addUserWithoutPasswordOrHash("nagilum", new String[] { "starfleet" }, HttpStatus.SC_OK); - // get user, check hash, must be untouched - response = rh.executeGetRequest(ENDPOINT + "/internalusers/nagilum", restAdminHeader); - Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertTrue(settings.get("nagilum.hash").equals("")); - } - - private void verifyAuthToken(final boolean sendAdminCert, Header... restAdminHeader) throws Exception { - - // Add enabled service account then generate auth token - - rh.sendAdminCertificate = sendAdminCert; - HttpResponse response = rh.executePutRequest( - ENDPOINT + "/internalusers/happyServiceLive", - ENABLED_SERVICE_ACCOUNT_BODY, - restAdminHeader - ); - Assert.assertEquals(response.getBody(), HttpStatus.SC_CREATED, response.getStatusCode()); - rh.sendAdminCertificate = sendAdminCert; - response = rh.executeGetRequest(ENDPOINT + "/internalusers/happyServiceLive", restAdminHeader); - Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - - response = rh.executePostRequest( - ENDPOINT + "/internalusers/happyServiceLive/authtoken", - ENABLED_SERVICE_ACCOUNT_BODY, - restAdminHeader - ); - Assert.assertEquals(response.getBody(), HttpStatus.SC_CREATED, response.getStatusCode()); - String tokenFromResponse = response.getBody(); - byte[] decodedResponse = Base64.getUrlDecoder().decode(tokenFromResponse); - String[] decodedResponseString = new String(decodedResponse).split(":", 2); - String username = decodedResponseString[0]; - String password = decodedResponseString[1]; - Assert.assertEquals("Username is: " + username, username, "happyServiceLive"); - - // Add disabled service account then try to get its auth token - rh.sendAdminCertificate = sendAdminCert; - response = rh.executePutRequest(ENDPOINT + "/internalusers/happyServiceDead", DISABLED_SERVICE_ACCOUNT_BODY, restAdminHeader); - Assert.assertEquals(response.getBody(), HttpStatus.SC_CREATED, response.getStatusCode()); - - response = rh.executePostRequest( - ENDPOINT + "/internalusers/happyServiceDead/authtoken", - ENABLED_SERVICE_ACCOUNT_BODY, - restAdminHeader - ); - Assert.assertEquals(response.getBody(), HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - - // Add enabled non-service account - rh.sendAdminCertificate = sendAdminCert; - response = rh.executePutRequest(ENDPOINT + "/internalusers/user_is_owner_1", ENABLED_NOT_SERVICE_ACCOUNT_BODY, restAdminHeader); - Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); - - response = rh.executePostRequest( - ENDPOINT + "/internalusers/user_is_owner_1/authtoken", - ENABLED_SERVICE_ACCOUNT_BODY, - restAdminHeader - ); - Assert.assertEquals(response.getBody(), HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - - } - - private void verifyRoles(final boolean sendAdminCert, Header... header) throws Exception { - - // wrong datatypes in roles file - rh.sendAdminCertificate = sendAdminCert; - HttpResponse response = rh.executePutRequest( - ENDPOINT + "/internalusers/picard", - FileHelper.loadFile("restapi/users_wrong_datatypes.json"), - header - ); - Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - Assert.assertEquals(RequestContentValidator.ValidationError.WRONG_DATATYPE.message(), settings.get("reason")); - Assert.assertTrue(settings.get("backend_roles").equals("Array expected")); - rh.sendAdminCertificate = false; - - rh.sendAdminCertificate = sendAdminCert; - response = rh.executePutRequest( - ENDPOINT + "/internalusers/picard", - FileHelper.loadFile("restapi/users_wrong_datatypes.json"), - header - ); - settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - Assert.assertEquals(RequestContentValidator.ValidationError.WRONG_DATATYPE.message(), settings.get("reason")); - Assert.assertTrue(settings.get("backend_roles").equals("Array expected")); - rh.sendAdminCertificate = false; - - rh.sendAdminCertificate = sendAdminCert; - response = rh.executePutRequest( - ENDPOINT + "/internalusers/picard", - FileHelper.loadFile("restapi/users_wrong_datatypes2.json"), - header - ); - settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - Assert.assertEquals(RequestContentValidator.ValidationError.WRONG_DATATYPE.message(), settings.get("reason")); - Assert.assertTrue(settings.get("password").equals("String expected")); - Assert.assertTrue(settings.get("backend_roles") == null); - rh.sendAdminCertificate = false; - - rh.sendAdminCertificate = sendAdminCert; - response = rh.executePutRequest( - ENDPOINT + "/internalusers/picard", - FileHelper.loadFile("restapi/users_wrong_datatypes3.json"), - header - ); - settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - Assert.assertEquals(RequestContentValidator.ValidationError.WRONG_DATATYPE.message(), settings.get("reason")); - Assert.assertTrue(settings.get("backend_roles").equals("Array expected")); - rh.sendAdminCertificate = false; - - // use backendroles when creating user. User picard does not exist in - // the internal user DB - // and is also not assigned to any role by username - addUserWithPassword("picard", "picardpicardpicardpicardpicard", HttpStatus.SC_CREATED); - // changed in ES5, you now need cluster:monitor/main which pucard does not have - checkGeneralAccess(HttpStatus.SC_FORBIDDEN, "picard", "picardpicardpicardpicardpicard"); - - // check read access to starfleet index and _doc type, must fail - checkReadAccess(HttpStatus.SC_FORBIDDEN, "picard", "picardpicardpicardpicardpicard", "sf", "_doc", 0); - - // overwrite user picard, and give him role "starfleet". - addUserWithPassword("picard", "picardpicardpicardpicardpicard", new String[] { "starfleet" }, HttpStatus.SC_OK); - - checkReadAccess(HttpStatus.SC_OK, "picard", "picardpicardpicardpicardpicard", "sf", "_doc", 0); - checkWriteAccess(HttpStatus.SC_FORBIDDEN, "picard", "picardpicardpicardpicardpicard", "sf", "_doc", 1); - - // overwrite user picard, and give him role "starfleet" plus "captains. Now - // document can be created. - addUserWithPassword("picard", "picardpicardpicardpicardpicard", new String[] { "starfleet", "captains" }, HttpStatus.SC_OK); - checkReadAccess(HttpStatus.SC_OK, "picard", "picardpicardpicardpicardpicard", "sf", "_doc", 0); - checkWriteAccess(HttpStatus.SC_CREATED, "picard", "picardpicardpicardpicardpicard", "sf", "_doc", 1); - - rh.sendAdminCertificate = sendAdminCert; - response = rh.executeGetRequest(ENDPOINT + "/internalusers/picard", header); - Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals("", settings.get("picard.hash")); - List roles = settings.getAsList("picard.backend_roles"); - Assert.assertNotNull(roles); - Assert.assertEquals(2, roles.size()); - Assert.assertTrue(roles.contains("starfleet")); - Assert.assertTrue(roles.contains("captains")); - - addUserWithPassword("some_additional_user", "$1aAAAAAAAAC", HttpStatus.SC_CREATED); - addUserWithPassword("abc", "abcabcabcabc42", HttpStatus.SC_CREATED); - - // check tabs in json - response = rh.executePutRequest(ENDPOINT + "/internalusers/userwithtabs", "\t{\"hash\": \t \"123\"\t} ", header); - Assert.assertEquals(response.getBody(), HttpStatus.SC_CREATED, response.getStatusCode()); - } - - @Test - public void testUserApiWithRestAdminPermissions() throws Exception { - setupWithRestRoles(Settings.builder().put(SECURITY_RESTAPI_ADMIN_ENABLED, true).build()); - rh.sendAdminCertificate = false; - final Header restApiAdminHeader = encodeBasicHeader("rest_api_admin_user", "rest_api_admin_user"); - // initial configuration - HttpResponse response = rh.executeGetRequest(ENDPOINT + "/" + CType.INTERNALUSERS.toLCString(), restApiAdminHeader); - Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); - Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(USER_SETTING_SIZE, settings.size()); - verifyGet(restApiAdminHeader); - verifyPut(restApiAdminHeader); - verifyPatch(false, restApiAdminHeader); - // create index first - setupStarfleetIndex(); - verifyRoles(false, restApiAdminHeader); - } - - @Test - public void testUserApiWithRestInternalUsersAdminPermissions() throws Exception { - setupWithRestRoles(Settings.builder().put(SECURITY_RESTAPI_ADMIN_ENABLED, true).build()); - rh.sendAdminCertificate = false; - final Header restApiInternalUsersAdminHeader = encodeBasicHeader("rest_api_admin_internalusers", "rest_api_admin_internalusers"); - // initial configuration - HttpResponse response = rh.executeGetRequest(ENDPOINT + "/" + CType.INTERNALUSERS.toLCString(), restApiInternalUsersAdminHeader); - Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); - Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(USER_SETTING_SIZE, settings.size()); - verifyGet(restApiInternalUsersAdminHeader); - verifyPut(restApiInternalUsersAdminHeader); - verifyPatch(false, restApiInternalUsersAdminHeader); - // create index first - setupStarfleetIndex(); - verifyRoles(false, restApiInternalUsersAdminHeader); - } - - @Test - public void testRegExpPasswordRules() throws Exception { - Settings nodeSettings = Settings.builder() - .put(ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE, "xxx") - .put(ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX, "(?=.*[A-Z])(?=.*[^a-zA-Z\\\\d])(?=.*[0-9])(?=.*[a-z]).{8,}") - .put(ConfigConstants.SECURITY_RESTAPI_PASSWORD_SCORE_BASED_VALIDATION_STRENGTH, PasswordValidator.ScoreStrength.FAIR.name()) - .build(); - - setup(nodeSettings); - - rh.keystore = "restapi/kirk-keystore.jks"; - rh.sendAdminCertificate = true; - - // initial configuration, 6 users - HttpResponse response = rh.executeGetRequest("_plugins/_security/api/" + CType.INTERNALUSERS.toLCString()); - Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); - Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(USER_SETTING_SIZE, settings.size()); - - verifyCouldNotCreatePasswords(HttpStatus.SC_BAD_REQUEST); - verifyCanCreatePasswords(); - verifySimilarity(RequestContentValidator.ValidationError.SIMILAR_PASSWORD.message()); - - addUserWithPasswordAndHash("empty_password", "", "$%^123", HttpStatus.SC_BAD_REQUEST); - addUserWithPasswordAndHash("null_password", null, "$%^123", HttpStatus.SC_BAD_REQUEST); - - response = rh.executeGetRequest(PLUGINS_PREFIX + "/api/internalusers/nothinghthere?pretty", new Header[0]); - Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); - Assert.assertTrue(response.getBody().contains("NOT_FOUND")); - } - - private void verifyCouldNotCreatePasswords(final int expectedStatus) throws Exception { - addUserWithPassword("tooshoort", "", expectedStatus); - addUserWithPassword("tooshoort", "123", expectedStatus); - addUserWithPassword("tooshoort", "1234567", expectedStatus); - addUserWithPassword("tooshoort", "1Aa%", expectedStatus); - addUserWithPassword("no-nonnumeric", "123456789", expectedStatus); - addUserWithPassword("no-uppercase", "a123456789", expectedStatus); - addUserWithPassword("no-lowercase", "A123456789", expectedStatus); - addUserWithPassword("empty_password_no_hash", "", expectedStatus); - HttpResponse response = rh.executePatchRequest( - PLUGINS_PREFIX + "/api/internalusers", - "[{ \"op\": \"add\", \"path\": \"/ok4\", \"value\": {\"password\": \"bla\", \"backend_roles\": [\"vulcan\"] } }]", - new Header[0] - ); - Assert.assertEquals(response.getBody(), expectedStatus, response.getStatusCode()); - response = rh.executePatchRequest( - PLUGINS_PREFIX + "/api/internalusers", - "[{ \"op\": \"replace\", \"path\": \"/ok4\", \"value\": {\"password\": \"bla\", \"backend_roles\": [\"vulcan\"] } }]", - new Header[0] - ); - Assert.assertEquals(response.getBody(), expectedStatus, response.getStatusCode()); - addUserWithPassword("ok4", "123", expectedStatus); - - // its not allowed to use the username as password (case insensitive) - response = rh.executePatchRequest( - PLUGINS_PREFIX + "/api/internalusers", - "[{ \"op\": \"add\", \"path\": \"/$1aAAAAAAAAB\", \"value\": {\"password\": \"$1aAAAAAAAAB\", \"backend_roles\": [\"vulcan\"] } }]", - new Header[0] - ); - Assert.assertEquals(response.getBody(), expectedStatus, response.getStatusCode()); - addUserWithPassword("$1aAAAAAAAAC", "$1aAAAAAAAAC", expectedStatus); - addUserWithPassword("$1aAAAAAAAac", "$1aAAAAAAAAC", expectedStatus); - addUserWithPassword(URLEncoder.encode("$1aAAAAAAAac%", "UTF-8"), "$1aAAAAAAAAC%", expectedStatus); - addUserWithPassword( - URLEncoder.encode("$1aAAAAAAAac%!=\"/\\;:test&~@^", "UTF-8").replace("+", "%2B"), - "$1aAAAAAAAac%!=\\\"/\\\\;:test&~@^", - expectedStatus - ); - addUserWithPassword( - URLEncoder.encode("$1aAAAAAAAac%!=\"/\\;: test&", "UTF-8"), - "$1aAAAAAAAac%!=\\\"/\\\\;: test&123", - expectedStatus - ); - String patchPayload = "[ " - + "{ \"op\": \"add\", \"path\": \"/testuser1\", \"value\": { \"password\": \"$aA123456789\", \"backend_roles\": [\"testrole1\"] } }," - + "{ \"op\": \"add\", \"path\": \"/testuser2\", \"value\": { \"password\": \"testpassword2\", \"backend_roles\": [\"testrole2\"] } }" - + "]"; - - response = rh.executePatchRequest( - PLUGINS_PREFIX + "/api/internalusers", - patchPayload, - new BasicHeader("Content-Type", "application/json") - ); - Assert.assertEquals(expectedStatus, response.getStatusCode()); - Assert.assertTrue(response.getBody().contains("error")); - Assert.assertTrue(response.getBody().contains("xxx")); - - response = rh.executePutRequest( - PLUGINS_PREFIX + "/api/internalusers/ok1", - "{\"backend_roles\":[\"my-backend-role\"],\"attributes\":{},\"password\":\"\"}", - new Header[0] - ); - Assert.assertEquals(expectedStatus, response.getStatusCode()); - - response = rh.executePutRequest( - PLUGINS_PREFIX + "/api/internalusers/ok1", - "{\"backend_roles\":[\"my-backend-role\"],\"attributes\":{},\"password\":\"bla\"}", - new Header[0] - ); - Assert.assertEquals(expectedStatus, response.getStatusCode()); - } - - private void verifyCanCreatePasswords() throws Exception { - addUserWithPassword("ok1", "a%A123456789", HttpStatus.SC_CREATED); - addUserWithPassword("ok2", "$aA123456789", HttpStatus.SC_CREATED); - addUserWithPassword("ok3", "$Aa123456789", HttpStatus.SC_CREATED); - addUserWithPassword("ok4", "$1aAAAAAAAAA", HttpStatus.SC_CREATED); - addUserWithPassword("ok4", "$1aAAAAAAAAC", HttpStatus.SC_OK); - HttpResponse response = rh.executePatchRequest( - PLUGINS_PREFIX + "/api/internalusers", - "[{ \"op\": \"add\", \"path\": \"/ok4\", \"value\": {\"password\": \"$1aAAAAAAAAB\", \"backend_roles\": [\"vulcan\"] } }]", - new Header[0] - ); - Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); - response = rh.executePutRequest( - PLUGINS_PREFIX + "/api/internalusers/ok1", - "{\"backend_roles\":[\"my-backend-role\"],\"attributes\":{},\"password\":\"Admin_123\"}", - new Header[0] - ); - Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - - response = rh.executePutRequest( - PLUGINS_PREFIX + "/api/internalusers/ok1", - "{\"backend_roles\":[\"my-backend-role\"],\"attributes\":{}}", - new Header[0] - ); - Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - - } - - private void verifySimilarity(final String expectedMessage) throws Exception { - addUserWithPassword("some_user_name", "H3235,cc,some_User_Name", HttpStatus.SC_BAD_REQUEST, expectedMessage); - } - - @Test - public void testScoreBasedPasswordRules() throws Exception { - - Settings nodeSettings = Settings.builder().put(ConfigConstants.SECURITY_RESTAPI_PASSWORD_MIN_LENGTH, 9).build(); - - setup(nodeSettings); - - rh.keystore = "restapi/kirk-keystore.jks"; - rh.sendAdminCertificate = true; - - // initial configuration, 6 users - HttpResponse response = rh.executeGetRequest("_plugins/_security/api/" + CType.INTERNALUSERS.toLCString()); - Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); - Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(USER_SETTING_SIZE, settings.size()); - - addUserWithPassword( - "admin", - "password89", - HttpStatus.SC_BAD_REQUEST, - RequestContentValidator.ValidationError.WEAK_PASSWORD.message() - ); - addUserWithPassword( - "admin", - "A123456789", - HttpStatus.SC_BAD_REQUEST, - RequestContentValidator.ValidationError.WEAK_PASSWORD.message() - ); - - addUserWithPassword( - "admin", - "pas", - HttpStatus.SC_BAD_REQUEST, - RequestContentValidator.ValidationError.INVALID_PASSWORD_TOO_SHORT.message() - ); - - verifySimilarity(RequestContentValidator.ValidationError.SIMILAR_PASSWORD.message()); - - addUserWithPassword("some_user_name", "ASSDsadwe324wadaasdadqwe", HttpStatus.SC_CREATED); - } - - @Test - public void testUserApiWithDots() throws Exception { - setup(); - - rh.keystore = "restapi/kirk-keystore.jks"; - rh.sendAdminCertificate = true; - - // initial configuration, 6 users - HttpResponse response = rh.executeGetRequest(ENDPOINT + "/" + CType.INTERNALUSERS.toLCString()); - Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(USER_SETTING_SIZE, settings.size()); - - addUserWithPassword(".my.dotuser0", "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m", HttpStatus.SC_CREATED); - - addUserWithPassword(".my.dot.user0", "12345678Sd", HttpStatus.SC_CREATED); - - addUserWithHash(".my.dotuser1", "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m", HttpStatus.SC_CREATED); - - addUserWithPassword(".my.dot.user2", "12345678Sd", HttpStatus.SC_CREATED); - - } - - @Test - public void testUserApiNoPasswordChange() throws Exception { - - setup(); - - rh.keystore = "restapi/kirk-keystore.jks"; - rh.sendAdminCertificate = true; - - // initial configuration, 5 users - HttpResponse response; - - addUserWithHash("user1", "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m", HttpStatus.SC_CREATED); - - response = rh.executePutRequest( - ENDPOINT + "/internalusers/user1", - "{\"hash\":\"$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m\",\"password\":\"\",\"backend_roles\":[\"admin\",\"rolea\"]}" - ); - Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - - response = rh.executePutRequest( - ENDPOINT + "/internalusers/user1", - "{\"hash\":\"$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m\",\"password\":\"Admin_123\",\"backend_roles\":[\"admin\",\"rolea\"]}" - ); - Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - - response = rh.executeGetRequest(ENDPOINT + "/internalusers/user1"); - Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - - addUserWithHash("user2", "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m", HttpStatus.SC_CREATED); - - response = rh.executePutRequest(ENDPOINT + "/internalusers/user2", "{\"password\":\"\",\"backend_roles\":[\"admin\",\"rolex\"]}"); - Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - - response = rh.executePutRequest( - ENDPOINT + "/internalusers/user2", - "{\"password\":\"Admin_123\",\"backend_roles\":[\"admin\",\"rolex\"]}" - ); - Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - - response = rh.executeGetRequest(ENDPOINT + "/internalusers/user2"); - Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - } - - @Test - public void testUserApiForNonSuperAdmin() throws Exception { - - setupWithRestRoles(); - - rh.keystore = "restapi/kirk-keystore.jks"; - rh.sendAdminCertificate = false; - rh.sendHTTPClientCredentials = true; - - HttpResponse response; - - // Delete read only user - response = rh.executeDeleteRequest(ENDPOINT + "/internalusers/sarek", new Header[0]); - Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - - // Patch read only users - response = rh.executePatchRequest( - ENDPOINT + "/internalusers/sarek", - "[{ \"op\": \"add\", \"path\": \"/sarek/description\", \"value\": \"foo\" }]", - new Header[0] - ); - Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - - // Put read only users - response = rh.executePutRequest( - ENDPOINT + "/internalusers/sarek", - "{ \"opendistro_security_roles\": [\"opendistro_security_reserved\"]}", - new Header[0] - ); - Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - - // Patch single read only user - response = rh.executePatchRequest( - ENDPOINT + "/internalusers/sarek", - "[{ \"op\": \"add\", \"path\": \"/description\", \"value\": \"foo\" }]", - new Header[0] - ); - Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - - // Patch multiple read only users - response = rh.executePatchRequest( - ENDPOINT + "/internalusers", - "[{ \"op\": \"add\", \"path\": \"/sarek/description\", \"value\": \"foo\" }]", - new Header[0] - ); - Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - - // Get hidden role - response = rh.executeGetRequest(ENDPOINT + "/internalusers/hide", new Header[0]); - Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); - - // Delete hidden user - response = rh.executeDeleteRequest(ENDPOINT + "/internalusers/hide", new Header[0]); - Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); - - // Patch hidden users - response = rh.executePatchRequest( - ENDPOINT + "/internalusers/hide", - "[{ \"op\": \"add\", \"path\": \"/sarek/description\", \"value\": \"foo\" }]", - new Header[0] - ); - Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); - - // Put hidden users - response = rh.executePutRequest( - ENDPOINT + "/internalusers/hide", - "{ \"opendistro_security_roles\": [\"opendistro_security_reserved\"]}", - new Header[0] - ); - Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); - - // Put reserved role is forbidden for non-superadmin - response = rh.executePutRequest( - ENDPOINT + "/internalusers/nagilum", - "{ \"opendistro_security_roles\": [\"opendistro_security_reserved\"]}", - new Header[0] - ); - Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(settings.get("message"), "Resource 'opendistro_security_reserved' is reserved."); - - // Patch single hidden user - response = rh.executePatchRequest( - ENDPOINT + "/internalusers/hide", - "[{ \"op\": \"add\", \"path\": \"/description\", \"value\": \"foo\" }]", - new Header[0] - ); - Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); - - // Patch multiple hidden users - response = rh.executePatchRequest( - ENDPOINT + "/internalusers", - "[{ \"op\": \"add\", \"path\": \"/hide/description\", \"value\": \"foo\" }]", - new Header[0] - ); - Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); - } - - @Test - public void restrictedUsernameContents() throws Exception { - setup(); - - rh.keystore = "restapi/kirk-keystore.jks"; - rh.sendAdminCertificate = true; - - RESTRICTED_FROM_USERNAME.stream().forEach(restrictedTerm -> { - final String username = "nag" + restrictedTerm + "ilum"; - final String url = ENDPOINT + "/internalusers/" + username; - final String bodyWithDefaultPasswordHash = "{\"hash\": \"456\"}"; - final HttpResponse response = rh.executePutRequest(url, bodyWithDefaultPasswordHash); - - assertThat("Expected " + username + " to be rejected", response.getStatusCode(), equalTo(HttpStatus.SC_BAD_REQUEST)); - assertThat(response.getBody(), containsString(restrictedTerm)); - }); - } - - @Test - public void checkNullElementsInArray() throws Exception { - setup(); - rh.keystore = "restapi/kirk-keystore.jks"; - rh.sendAdminCertificate = true; - - String body = FileHelper.loadFile("restapi/users_null_array_element.json"); - HttpResponse response = rh.executePutRequest(ENDPOINT + "/internalusers/picard", body); - Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - Assert.assertEquals(RequestContentValidator.ValidationError.NULL_ARRAY_ELEMENT.message(), settings.get("reason")); - } - - @Test - public void testGeneratedPasswordContents() { - String password = UserService.generatePassword(); - PasswordData data = new PasswordData(password); - - LengthRule lengthRule = new LengthRule(8, 16); - - CharacterCharacteristicsRule characteristicsRule = new CharacterCharacteristicsRule(); - - // Define M (3 in this case) - characteristicsRule.setNumberOfCharacteristics(3); - - // Define elements of N (upper, lower, digit, symbol) - characteristicsRule.getRules().add(new CharacterRule(EnglishCharacterData.UpperCase, 1)); - characteristicsRule.getRules().add(new CharacterRule(EnglishCharacterData.LowerCase, 1)); - characteristicsRule.getRules().add(new CharacterRule(EnglishCharacterData.Digit, 1)); - characteristicsRule.getRules().add(new CharacterRule(EnglishCharacterData.Special, 1)); - - org.passay.PasswordValidator validator = new org.passay.PasswordValidator(lengthRule, characteristicsRule); - validator.validate(data); - - String password2 = UserService.generatePassword(); - PasswordData data2 = new PasswordData(password2); - assertNotEquals(password, password2); - assertNotEquals(data, data2); - } -} diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacyUserApiTests.java b/src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacyUserApiTests.java deleted file mode 100644 index 449bce270d..0000000000 --- a/src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacyUserApiTests.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.UserApiTest; - -import static org.opensearch.security.OpenSearchSecurityPlugin.LEGACY_OPENDISTRO_PREFIX; - -public class LegacyUserApiTests extends UserApiTest { - @Override - protected String getEndpointPrefix() { - return LEGACY_OPENDISTRO_PREFIX; - } -}