From 576c81a4e90f783d1589778904cc28bcc500d254 Mon Sep 17 00:00:00 2001 From: Andrey Pleskach Date: Thu, 27 Jul 2023 17:26:38 +0200 Subject: [PATCH] Clean up REST API (Part 1) (#2900) The aim of this PR is to start cleaning code in REST API since with the current implementation is difficult to understand and support. Changes: - Implemented new `RequestConetnValidator` class which uses the same validation logic as `AbstractConfigurationValidator` - Removed all redundant `AbstractConfigurationValidator` extensions [Please provide details of testing done: unit testing, integration testing and manual testing] - [ ] New functionality includes testing - [ ] New functionality has been documented - [ ] Commits are signed per the DCO using --signoff By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. For more information on following Developer Certificate of Origin and signing off your commits, please check [here](https://github.com/opensearch-project/OpenSearch/blob/main/CONTRIBUTING.md#developer-certificate-of-origin). --------- Signed-off-by: Andrey Pleskach (cherry picked from commit 6bac470df67b16a22f496b052776e7afdd50e900) --- .../security/api/DashboardsInfoTest.java | 1 - .../dlic/rest/api/AbstractApiAction.java | 105 ++--- .../dlic/rest/api/AccountApiAction.java | 39 +- .../dlic/rest/api/ActionGroupsApiAction.java | 50 ++- .../dlic/rest/api/AllowlistApiAction.java | 35 +- .../dlic/rest/api/AuditApiAction.java | 96 ++++- .../rest/api/AuthTokenProcessorAction.java | 8 +- .../dlic/rest/api/FlushCacheApiAction.java | 21 +- .../dlic/rest/api/InternalUsersApiAction.java | 60 ++- .../dlic/rest/api/MigrateApiAction.java | 27 +- .../rest/api/MultiTenancyConfigApiAction.java | 68 +++- .../dlic/rest/api/NodesDnApiAction.java | 33 +- .../rest/api/PatchableResourceApiAction.java | 46 +-- .../dlic/rest/api/RolesApiAction.java | 91 ++++- .../dlic/rest/api/RolesMappingApiAction.java | 40 +- .../dlic/rest/api/SecurityConfigAction.java | 26 +- .../dlic/rest/api/SecuritySSLCertsAction.java | 5 +- .../dlic/rest/api/TenantsApiAction.java | 30 +- .../dlic/rest/api/ValidateApiAction.java | 18 +- .../AbstractConfigurationValidator.java | 353 ---------------- .../rest/validation/AccountValidator.java | 27 -- .../rest/validation/ActionGroupValidator.java | 37 -- .../rest/validation/AllowlistValidator.java | 26 -- .../dlic/rest/validation/AuditValidator.java | 83 ---- .../rest/validation/CredentialsValidator.java | 83 ---- .../validation/InternalUsersValidator.java | 37 -- .../MultiTenancyConfigValidator.java | 31 -- .../dlic/rest/validation/NoOpValidator.java | 24 -- .../rest/validation/NodesDnValidator.java | 27 -- .../rest/validation/PasswordValidator.java | 30 +- .../validation/RequestContentValidator.java | 382 ++++++++++++++++++ .../validation/RolesMappingValidator.java | 41 -- .../dlic/rest/validation/RolesValidator.java | 85 ---- .../validation/SecurityConfigValidator.java | 26 -- .../dlic/rest/validation/TenantValidator.java | 49 --- .../rest/validation/ValidationResult.java | 70 ++++ .../rest/validation/WhitelistValidator.java | 26 -- .../dlic/rest/api/ActionGroupsApiTest.java | 8 +- .../dlic/rest/api/AllowlistApiTest.java | 4 +- .../dlic/rest/api/NodesDnApiTest.java | 4 +- .../security/dlic/rest/api/RolesApiTest.java | 37 +- .../dlic/rest/api/RolesMappingApiTest.java | 26 +- .../security/dlic/rest/api/UserApiTest.java | 30 +- .../dlic/rest/api/WhitelistApiTest.java | 4 +- .../validation/PasswordValidatorTest.java | 46 +-- .../RequestContentValidatorTest.java | 318 +++++++++++++++ 46 files changed, 1405 insertions(+), 1308 deletions(-) delete mode 100644 src/main/java/org/opensearch/security/dlic/rest/validation/AbstractConfigurationValidator.java delete mode 100644 src/main/java/org/opensearch/security/dlic/rest/validation/AccountValidator.java delete mode 100644 src/main/java/org/opensearch/security/dlic/rest/validation/ActionGroupValidator.java delete mode 100644 src/main/java/org/opensearch/security/dlic/rest/validation/AllowlistValidator.java delete mode 100644 src/main/java/org/opensearch/security/dlic/rest/validation/AuditValidator.java delete mode 100644 src/main/java/org/opensearch/security/dlic/rest/validation/CredentialsValidator.java delete mode 100644 src/main/java/org/opensearch/security/dlic/rest/validation/InternalUsersValidator.java delete mode 100644 src/main/java/org/opensearch/security/dlic/rest/validation/MultiTenancyConfigValidator.java delete mode 100644 src/main/java/org/opensearch/security/dlic/rest/validation/NoOpValidator.java delete mode 100644 src/main/java/org/opensearch/security/dlic/rest/validation/NodesDnValidator.java create mode 100644 src/main/java/org/opensearch/security/dlic/rest/validation/RequestContentValidator.java delete mode 100644 src/main/java/org/opensearch/security/dlic/rest/validation/RolesMappingValidator.java delete mode 100644 src/main/java/org/opensearch/security/dlic/rest/validation/RolesValidator.java delete mode 100644 src/main/java/org/opensearch/security/dlic/rest/validation/SecurityConfigValidator.java delete mode 100644 src/main/java/org/opensearch/security/dlic/rest/validation/TenantValidator.java create mode 100644 src/main/java/org/opensearch/security/dlic/rest/validation/ValidationResult.java delete mode 100644 src/main/java/org/opensearch/security/dlic/rest/validation/WhitelistValidator.java create mode 100644 src/test/java/org/opensearch/security/dlic/rest/validation/RequestContentValidatorTest.java diff --git a/src/integrationTest/java/org/opensearch/security/api/DashboardsInfoTest.java b/src/integrationTest/java/org/opensearch/security/api/DashboardsInfoTest.java index a1dbc611a3..daf66cdc90 100644 --- a/src/integrationTest/java/org/opensearch/security/api/DashboardsInfoTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/DashboardsInfoTest.java @@ -12,7 +12,6 @@ package org.opensearch.security.api; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; -import org.apache.hc.core5.http.HttpStatus; import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/AbstractApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/AbstractApiAction.java index ba1d5ec0cc..8838e98170 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/AbstractApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/AbstractApiAction.java @@ -13,7 +13,6 @@ import java.io.IOException; import java.nio.file.Path; -import java.util.Arrays; import java.util.Collections; import java.util.Objects; @@ -30,7 +29,6 @@ import org.opensearch.client.Client; import org.opensearch.client.node.NodeClient; import org.opensearch.cluster.service.ClusterService; -import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext.StoredContext; import org.opensearch.common.xcontent.XContentHelper; @@ -47,14 +45,12 @@ import org.opensearch.core.rest.RestStatus; import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.action.configupdate.ConfigUpdateAction; -import org.opensearch.security.action.configupdate.ConfigUpdateNodeResponse; import org.opensearch.security.action.configupdate.ConfigUpdateRequest; import org.opensearch.security.action.configupdate.ConfigUpdateResponse; import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; -import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator.ErrorType; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.securityconf.DynamicConfigFactory; import org.opensearch.security.securityconf.impl.CType; @@ -68,7 +64,7 @@ public abstract class AbstractApiAction extends BaseRestHandler { - protected final Logger log = LogManager.getLogger(this.getClass()); + private final static Logger LOGGER = LogManager.getLogger(AbstractApiAction.class); protected final ConfigurationRepository cl; protected final ClusterService cs; @@ -119,7 +115,7 @@ protected AbstractApiAction( this.auditLog = auditLog; } - protected abstract AbstractConfigurationValidator getValidator(RestRequest request, BytesReference ref, Object... params); + protected abstract RequestContentValidator createValidator(final Object... params); protected abstract String getResourceName(); @@ -128,25 +124,22 @@ protected AbstractApiAction( protected void handleApiRequest(final RestChannel channel, final RestRequest request, final Client client) throws IOException { try { - // validate additional settings, if any - AbstractConfigurationValidator validator = getValidator(request, request.content()); - if (!validator.validate()) { - request.params().clear(); - badRequestResponse(channel, validator); - return; - } switch (request.method()) { case DELETE: - handleDelete(channel, request, client, validator.getContentAsNode()); + handleDelete(channel, request, client, null); break; case POST: - handlePost(channel, request, client, validator.getContentAsNode()); + createValidator().validate(request) + .valid(jsonContent -> handlePost(channel, request, client, jsonContent)) + .error(toXContent -> requestContentInvalid(request, channel, toXContent)); break; case PUT: - handlePut(channel, request, client, validator.getContentAsNode()); + createValidator().validate(request) + .valid(jsonContent -> handlePut(channel, request, client, jsonContent)) + .error(toXContent -> requestContentInvalid(request, channel, toXContent)); break; case GET: - handleGet(channel, request, client, validator.getContentAsNode()); + handleGet(channel, request, client, null); break; default: throw new IllegalArgumentException(request.method() + " not supported"); @@ -160,6 +153,11 @@ protected void handleApiRequest(final RestChannel channel, final RestRequest req } } + protected void requestContentInvalid(final RestRequest request, final RestChannel channel, final ToXContent toXContent) { + request.params().clear(); + badRequestResponse(channel, toXContent); + } + protected void handleDelete(final RestChannel channel, final RestRequest request, final Client client, final JsonNode content) throws IOException { final String name = request.param("name"); @@ -200,16 +198,12 @@ public void onResponse(IndexResponse response) { protected void handlePut(final RestChannel channel, final RestRequest request, final Client client, final JsonNode content) throws IOException { - final String name = request.param("name"); - if (name == null || name.length() == 0) { badRequestResponse(channel, "No " + getResourceName() + " specified."); return; } - final SecurityDynamicConfiguration existingConfiguration = load(getConfigName(), false); - if (existingConfiguration.getSeqNo() < 0) { forbidden( channel, @@ -227,8 +221,8 @@ protected void handlePut(final RestChannel channel, final RestRequest request, f return; } - if (log.isTraceEnabled() && content != null) { - log.trace(content.toString()); + if (LOGGER.isTraceEnabled() && content != null) { + LOGGER.trace(content.toString()); } boolean existed = existingConfiguration.exists(name); @@ -274,28 +268,21 @@ protected boolean hasPermissionsToCreate( } protected void handleGet(final RestChannel channel, RestRequest request, Client client, final JsonNode content) throws IOException { - final String resourcename = request.param("name"); - final SecurityDynamicConfiguration configuration = load(getConfigName(), true); filter(configuration); - // no specific resource requested, return complete config if (resourcename == null || resourcename.length() == 0) { successResponse(channel, configuration); return; } - if (!configuration.exists(resourcename)) { notFound(channel, "Resource '" + resourcename + "' not found."); return; } - configuration.removeOthers(resourcename); successResponse(channel, configuration); - - return; } protected final SecurityDynamicConfiguration load(final CType config, boolean logComplianceEvent) { @@ -305,15 +292,6 @@ protected final SecurityDynamicConfiguration load(final CType config, boolean return DynamicConfigFactory.addStatics(loaded); } - protected final SecurityDynamicConfiguration load(final CType config, boolean logComplianceEvent, boolean acceptInvalid) { - SecurityDynamicConfiguration loaded = cl.getConfigurationsFromIndex( - Collections.singleton(config), - logComplianceEvent, - acceptInvalid - ).get(config).deepClone(); - return DynamicConfigFactory.addStatics(loaded); - } - protected boolean ensureIndexExists() { if (!cs.state().metadata().hasConcreteIndex(this.securityIndexName)) { return false; @@ -434,7 +412,7 @@ protected final RestChannelConsumer prepareRequest(RestRequest request, NodeClie // check if .opendistro_security index has been initialized if (!ensureIndexExists()) { - return channel -> internalErrorResponse(channel, ErrorType.SECURITY_NOT_INITIALIZED.getMessage()); + return channel -> internalErrorResponse(channel, RequestContentValidator.ValidationError.SECURITY_NOT_INITIALIZED.message()); } // check if request is authorized @@ -443,7 +421,7 @@ protected final RestChannelConsumer prepareRequest(RestRequest request, NodeClie final User user = (User) threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); final String userName = user == null ? null : user.getName(); if (authError != null) { - log.error("No permission to access REST API: " + authError); + LOGGER.error("No permission to access REST API: " + authError); auditLog.logMissingPrivileges(authError, userName, request); // for rest request request.params().clear(); @@ -465,7 +443,7 @@ protected final RestChannelConsumer prepareRequest(RestRequest request, NodeClie handleApiRequest(channel, request, client); } catch (Exception e) { - log.error("Error processing request {}", request, e); + LOGGER.error("Error processing request {}", request, e); try { channel.sendResponse(new BytesRestResponse(channel, e)); } catch (IOException ioe) { @@ -475,37 +453,6 @@ protected final RestChannelConsumer prepareRequest(RestRequest request, NodeClie }); } - protected boolean checkConfigUpdateResponse(final ConfigUpdateResponse response) { - - final int nodeCount = cs.state().getNodes().getNodes().size(); - final int expectedConfigCount = 1; - - boolean success = response.getNodes().size() == nodeCount; - if (!success) { - log.error("Expected " + nodeCount + " nodes to return response, but got only " + response.getNodes().size()); - } - - for (final String nodeId : response.getNodesMap().keySet()) { - final ConfigUpdateNodeResponse node = response.getNodesMap().get(nodeId); - final boolean successNode = node.getUpdatedConfigTypes() != null && node.getUpdatedConfigTypes().length == expectedConfigCount; - - if (!successNode) { - log.error( - "Expected " - + expectedConfigCount - + " config types for node " - + nodeId - + " but got only " - + Arrays.toString(node.getUpdatedConfigTypes()) - ); - } - - success = success && successNode; - } - - return success; - } - protected static XContentBuilder convertToJson(RestChannel channel, ToXContent toxContent) { try { XContentBuilder builder = channel.newBuilder(); @@ -541,12 +488,12 @@ protected void successResponse(RestChannel channel) { channel.sendResponse(new BytesRestResponse(RestStatus.OK, builder)); } catch (IOException e) { internalErrorResponse(channel, "Unable to fetch license: " + e.getMessage()); - log.error("Cannot fetch convert license to XContent due to", e); + LOGGER.error("Cannot fetch convert license to XContent due to", e); } } - protected void badRequestResponse(RestChannel channel, AbstractConfigurationValidator validator) { - channel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, validator.errorsAsXContent(channel))); + protected void badRequestResponse(RestChannel channel, ToXContent validationResult) { + channel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, convertToJson(channel, validationResult))); } protected void successResponse(RestChannel channel, String message) { @@ -573,10 +520,6 @@ protected void internalErrorResponse(RestChannel channel, String message) { response(channel, RestStatus.INTERNAL_SERVER_ERROR, message); } - protected void unprocessable(RestChannel channel, String message) { - response(channel, RestStatus.UNPROCESSABLE_ENTITY, message); - } - protected void conflict(RestChannel channel, String message) { response(channel, RestStatus.CONFLICT, message); } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/AccountApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/AccountApiAction.java index 0ee9224194..b5d210df5c 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/AccountApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/AccountApiAction.java @@ -14,16 +14,20 @@ import java.io.IOException; import java.nio.file.Path; import java.util.List; +import java.util.Map; import java.util.Set; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.bouncycastle.crypto.generators.OpenBSDBCrypt; import org.opensearch.action.index.IndexResponse; import org.opensearch.client.Client; import org.opensearch.cluster.service.ClusterService; -import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.common.settings.Settings; import org.opensearch.core.common.transport.TransportAddress; import org.opensearch.common.util.concurrent.ThreadContext; @@ -38,8 +42,8 @@ import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; -import org.opensearch.security.dlic.rest.validation.AccountValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator.DataType; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.securityconf.Hashed; import org.opensearch.security.securityconf.impl.CType; @@ -58,6 +62,9 @@ * Currently this action serves GET and PUT request for /_opendistro/_security/api/account endpoint */ public class AccountApiAction extends AbstractApiAction { + + private final static Logger LOGGER = LogManager.getLogger(AccountApiAction.class); + private static final String RESOURCE_NAME = "account"; private static final List routes = addRoutesPrefix( ImmutableList.of(new Route(Method.GET, "/account"), new Route(Method.PUT, "/account")) @@ -154,7 +161,7 @@ protected void handleGet(RestChannel channel, RestRequest request, Client client response = new BytesRestResponse(RestStatus.OK, builder); } catch (final Exception exception) { - log.error(exception.toString()); + LOGGER.error(exception.toString()); builder.startObject().field("error", exception.toString()).endObject(); @@ -241,9 +248,29 @@ public void onResponse(IndexResponse response) { } @Override - protected AbstractConfigurationValidator getValidator(RestRequest request, BytesReference ref, Object... params) { + protected RequestContentValidator createValidator(final Object... params) { final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - return new AccountValidator(request, ref, this.settings, user.getName()); + return RequestContentValidator.of(new RequestContentValidator.ValidationContext() { + @Override + public Object[] params() { + return new Object[] { user.getName() }; + } + + @Override + public Settings settings() { + return settings; + } + + @Override + public Set mandatoryKeys() { + return ImmutableSet.of("current_password"); + } + + @Override + public Map allowedKeys() { + return ImmutableMap.of("hash", DataType.STRING, "password", DataType.STRING, "current_password", DataType.STRING); + } + }); } @Override diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiAction.java index 3b3e772eda..dee97dfa39 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiAction.java @@ -11,17 +11,12 @@ package org.opensearch.security.dlic.rest.api; -import java.io.IOException; -import java.nio.file.Path; -import java.util.List; -import java.util.Set; - import com.fasterxml.jackson.databind.JsonNode; import com.google.common.collect.ImmutableList; - +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import org.opensearch.client.Client; import org.opensearch.cluster.service.ClusterService; -import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.common.inject.Inject; import org.opensearch.common.settings.Settings; import org.opensearch.rest.RestChannel; @@ -32,14 +27,20 @@ import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; -import org.opensearch.security.dlic.rest.validation.ActionGroupValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator.DataType; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.threadpool.ThreadPool; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Set; + import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; public class ActionGroupsApiAction extends PatchableResourceApiAction { @@ -92,8 +93,35 @@ public List routes() { } @Override - protected AbstractConfigurationValidator getValidator(final RestRequest request, BytesReference ref, Object... param) { - return new ActionGroupValidator(request, isSuperAdmin(), ref, this.settings, param); + protected RequestContentValidator createValidator(final Object... params) { + return RequestContentValidator.of(new RequestContentValidator.ValidationContext() { + @Override + public Object[] params() { + return params; + } + + @Override + public Settings settings() { + return settings; + } + + @Override + public Map allowedKeys() { + final ImmutableMap.Builder allowedKeys = ImmutableMap.builder(); + if (isSuperAdmin()) { + allowedKeys.put("reserved", DataType.BOOLEAN); + } + allowedKeys.put("allowed_actions", DataType.ARRAY); + allowedKeys.put("description", DataType.STRING); + allowedKeys.put("type", DataType.STRING); + return allowedKeys.build(); + } + + @Override + public Set mandatoryKeys() { + return ImmutableSet.of("allowed_actions"); + } + }); } @Override diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/AllowlistApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/AllowlistApiAction.java index 054ac1ea46..a6b93f07dd 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/AllowlistApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/AllowlistApiAction.java @@ -11,17 +11,12 @@ package org.opensearch.security.dlic.rest.api; -import java.io.IOException; -import java.nio.file.Path; -import java.util.List; - import com.fasterxml.jackson.databind.JsonNode; import com.google.common.collect.ImmutableList; - +import com.google.common.collect.ImmutableMap; import org.opensearch.action.index.IndexResponse; import org.opensearch.client.Client; import org.opensearch.cluster.service.ClusterService; -import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.common.inject.Inject; import org.opensearch.common.settings.Settings; import org.opensearch.rest.RestChannel; @@ -31,8 +26,8 @@ import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; -import org.opensearch.security.dlic.rest.validation.AllowlistValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator.DataType; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; @@ -41,6 +36,11 @@ import org.opensearch.security.tools.SecurityAdmin; import org.opensearch.threadpool.ThreadPool; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + /** * This class implements GET and PUT operations to manage dynamic AllowlistingSettings. *

@@ -185,8 +185,23 @@ protected Endpoint getEndpoint() { } @Override - protected AbstractConfigurationValidator getValidator(RestRequest request, BytesReference ref, Object... param) { - return new AllowlistValidator(request, ref, this.settings, param); + protected RequestContentValidator createValidator(Object... params) { + return RequestContentValidator.of(new RequestContentValidator.ValidationContext() { + @Override + public Object[] params() { + return params; + } + + @Override + public Settings settings() { + return settings; + } + + @Override + public Map allowedKeys() { + return ImmutableMap.of("enabled", DataType.BOOLEAN, "requests", DataType.OBJECT); + } + }); } @Override diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/AuditApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/AuditApiAction.java index 078bfcc62a..61982eb296 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/AuditApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/AuditApiAction.java @@ -11,19 +11,14 @@ package org.opensearch.security.dlic.rest.api; -import java.io.IOException; -import java.nio.file.Path; -import java.util.List; -import java.util.Map; - import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; - +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import org.opensearch.client.Client; import org.opensearch.cluster.service.ClusterService; -import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.rest.RestChannel; @@ -32,18 +27,26 @@ import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.auditlog.config.AuditConfig; +import org.opensearch.security.auditlog.impl.AuditCategory; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ConfigurationRepository; import org.opensearch.security.configuration.StaticResourceException; import org.opensearch.security.dlic.rest.support.Utils; -import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; -import org.opensearch.security.dlic.rest.validation.AuditValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator.DataType; +import org.opensearch.security.dlic.rest.validation.ValidationResult; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.threadpool.ThreadPool; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Set; + import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; /** @@ -141,6 +144,62 @@ public class AuditApiAction extends PatchableResourceApiAction { private final PrivilegesEvaluator privilegesEvaluator; private final ThreadContext threadContext; + public static class AuditRequestContentValidator extends RequestContentValidator { + private static final Set DISABLED_REST_CATEGORIES = ImmutableSet.of( + AuditCategory.BAD_HEADERS, + AuditCategory.SSL_EXCEPTION, + AuditCategory.AUTHENTICATED, + AuditCategory.FAILED_LOGIN, + AuditCategory.GRANTED_PRIVILEGES, + AuditCategory.MISSING_PRIVILEGES + ); + + private static final Set DISABLED_TRANSPORT_CATEGORIES = ImmutableSet.of( + AuditCategory.BAD_HEADERS, + AuditCategory.SSL_EXCEPTION, + AuditCategory.AUTHENTICATED, + AuditCategory.FAILED_LOGIN, + AuditCategory.GRANTED_PRIVILEGES, + AuditCategory.MISSING_PRIVILEGES, + AuditCategory.INDEX_EVENT, + AuditCategory.OPENDISTRO_SECURITY_INDEX_ATTEMPT + ); + + protected AuditRequestContentValidator(ValidationContext validationContext) { + super(validationContext); + } + + @Override + public ValidationResult validate(RestRequest request) throws IOException { + return super.validate(request).map(this::validateAuditPayload); + } + + @Override + public ValidationResult validate(RestRequest request, JsonNode jsonContent) throws IOException { + return super.validate(request, jsonContent).map(this::validateAuditPayload); + } + + private ValidationResult validateAuditPayload(final JsonNode jsonContent) { + try { + // try parsing to target type + final AuditConfig auditConfig = DefaultObjectMapper.readTree(jsonContent, AuditConfig.class); + final AuditConfig.Filter filter = auditConfig.getFilter(); + if (!DISABLED_REST_CATEGORIES.containsAll(filter.getDisabledRestCategories())) { + throw new IllegalArgumentException("Invalid REST categories passed in the request"); + } + if (!DISABLED_TRANSPORT_CATEGORIES.containsAll(filter.getDisabledTransportCategories())) { + throw new IllegalArgumentException("Invalid transport categories passed in the request"); + } + return ValidationResult.success(jsonContent); + } catch (final Exception e) { + // this.content is not valid json + this.validationError = ValidationError.BODY_NOT_PARSEABLE; + LOGGER.error("Invalid content passed in the request", e); + return ValidationResult.error(this); + } + } + } + public AuditApiAction( final Settings settings, final Path configPath, @@ -232,8 +291,23 @@ protected void handleDelete(RestChannel channel, final RestRequest request, fina } @Override - protected AbstractConfigurationValidator getValidator(RestRequest request, BytesReference ref, Object... params) { - return new AuditValidator(request, ref, this.settings, params); + protected RequestContentValidator createValidator(final Object... params) { + return new AuditRequestContentValidator(new RequestContentValidator.ValidationContext() { + @Override + public Object[] params() { + return params; + } + + @Override + public Settings settings() { + return settings; + } + + @Override + public Map allowedKeys() { + return ImmutableMap.of("enabled", DataType.BOOLEAN, "audit", DataType.OBJECT, "compliance", DataType.OBJECT); + } + }); } @Override diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/AuthTokenProcessorAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/AuthTokenProcessorAction.java index 87c82cf77c..dd9591f22e 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/AuthTokenProcessorAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/AuthTokenProcessorAction.java @@ -20,7 +20,6 @@ import org.opensearch.client.Client; import org.opensearch.cluster.service.ClusterService; -import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.common.inject.Inject; import org.opensearch.common.settings.Settings; import org.opensearch.rest.RestChannel; @@ -30,8 +29,7 @@ import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; -import org.opensearch.security.dlic.rest.validation.NoOpValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; @@ -84,8 +82,8 @@ protected void handlePost(RestChannel channel, final RestRequest request, final } @Override - protected AbstractConfigurationValidator getValidator(RestRequest request, BytesReference ref, Object... param) { - return new NoOpValidator(request, ref, this.settings, param); + protected RequestContentValidator createValidator(Object... params) { + return RequestContentValidator.NOOP_VALIDATOR; } @Override diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/FlushCacheApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/FlushCacheApiAction.java index 5b645f93c6..834ba09996 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/FlushCacheApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/FlushCacheApiAction.java @@ -18,10 +18,11 @@ import com.fasterxml.jackson.databind.JsonNode; import com.google.common.collect.ImmutableList; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.opensearch.core.action.ActionListener; import org.opensearch.client.Client; import org.opensearch.cluster.service.ClusterService; -import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.common.inject.Inject; import org.opensearch.common.settings.Settings; import org.opensearch.rest.RestChannel; @@ -34,8 +35,7 @@ import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; -import org.opensearch.security.dlic.rest.validation.NoOpValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; @@ -45,6 +45,9 @@ import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; public class FlushCacheApiAction extends AbstractApiAction { + + private final static Logger LOGGER = LogManager.getLogger(FlushCacheApiAction.class); + private static final List routes = addRoutesPrefix( ImmutableList.of( new Route(Method.DELETE, "/cache"), @@ -101,19 +104,19 @@ protected void handleDelete(RestChannel channel, RestRequest request, Client cli @Override public void onResponse(ConfigUpdateResponse ur) { if (ur.hasFailures()) { - log.error("Cannot flush cache due to", ur.failures().get(0)); + LOGGER.error("Cannot flush cache due to", ur.failures().get(0)); internalErrorResponse(channel, "Cannot flush cache due to " + ur.failures().get(0).getMessage() + "."); return; } successResponse(channel, "Cache flushed successfully."); - if (log.isDebugEnabled()) { - log.debug("cache flushed successfully"); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("cache flushed successfully"); } } @Override public void onFailure(Exception e) { - log.error("Cannot flush cache due to", e); + LOGGER.error("Cannot flush cache due to", e); internalErrorResponse(channel, "Cannot flush cache due to " + e.getMessage() + "."); } @@ -140,8 +143,8 @@ protected void handlePut(RestChannel channel, final RestRequest request, final C } @Override - protected AbstractConfigurationValidator getValidator(RestRequest request, BytesReference ref, Object... param) { - return new NoOpValidator(request, ref, this.settings, param); + protected RequestContentValidator createValidator(final Object... params) { + return RequestContentValidator.NOOP_VALIDATOR; } @Override 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 358693d9a1..95ab20a30b 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 @@ -14,19 +14,19 @@ import java.io.IOException; import java.nio.file.Path; import java.util.List; +import java.util.Map; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import org.opensearch.action.index.IndexResponse; import org.opensearch.client.Client; import org.opensearch.cluster.service.ClusterService; -import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.common.inject.Inject; import org.opensearch.common.settings.Settings; -import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestController; import org.opensearch.rest.RestRequest; @@ -35,8 +35,9 @@ import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; -import org.opensearch.security.dlic.rest.validation.InternalUsersValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator.DataType; +import org.opensearch.security.dlic.rest.validation.ValidationResult; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.securityconf.Hashed; import org.opensearch.security.securityconf.impl.CType; @@ -269,33 +270,23 @@ protected void filter(SecurityDynamicConfiguration builder) { } @Override - protected AbstractConfigurationValidator postProcessApplyPatchResult( + protected ValidationResult postProcessApplyPatchResult( RestChannel channel, RestRequest request, JsonNode existingResourceAsJsonNode, JsonNode updatedResourceAsJsonNode, String resourceName - ) { - AbstractConfigurationValidator retVal = null; + ) throws IOException { + RequestContentValidator retVal = null; JsonNode passwordNode = updatedResourceAsJsonNode.get("password"); - if (passwordNode != null) { String plainTextPassword = passwordNode.asText(); - try { - XContentBuilder builder = channel.newBuilder(); - builder.startObject(); - builder.field("password", plainTextPassword); - builder.endObject(); - retVal = getValidator(request, BytesReference.bytes(builder), resourceName); - } catch (IOException e) { - log.error(e.toString()); - } - + final JsonNode passwordObject = DefaultObjectMapper.objectMapper.createObjectNode().put("password", plainTextPassword); + final ValidationResult validationResult = createValidator(resourceName).validate(request, passwordObject); ((ObjectNode) updatedResourceAsJsonNode).remove("password"); ((ObjectNode) updatedResourceAsJsonNode).set("hash", new TextNode(hash(plainTextPassword.toCharArray()))); - return retVal; + return validationResult; } - return null; } @@ -310,7 +301,32 @@ protected CType getConfigName() { } @Override - protected AbstractConfigurationValidator getValidator(RestRequest request, BytesReference ref, Object... params) { - return new InternalUsersValidator(request, isSuperAdmin(), ref, this.settings, params); + protected RequestContentValidator createValidator(final Object... params) { + return RequestContentValidator.of(new RequestContentValidator.ValidationContext() { + @Override + public Object[] params() { + return params; + } + + @Override + public Settings settings() { + return settings; + } + + @Override + public Map allowedKeys() { + final ImmutableMap.Builder allowedKeys = ImmutableMap.builder(); + if (isSuperAdmin()) { + allowedKeys.put("reserved", DataType.BOOLEAN); + } + return allowedKeys.put("backend_roles", DataType.ARRAY) + .put("attributes", DataType.OBJECT) + .put("description", DataType.STRING) + .put("opendistro_security_roles", DataType.ARRAY) + .put("hash", DataType.STRING) + .put("password", DataType.STRING) + .build(); + } + }); } } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/MigrateApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/MigrateApiAction.java index 6223b6b59d..6dc46b850a 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/MigrateApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/MigrateApiAction.java @@ -23,6 +23,8 @@ import org.opensearch.LegacyESVersion; import org.opensearch.Version; import org.opensearch.core.action.ActionListener; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.opensearch.action.admin.indices.create.CreateIndexResponse; import org.opensearch.action.bulk.BulkRequestBuilder; import org.opensearch.action.bulk.BulkResponse; @@ -47,8 +49,7 @@ import org.opensearch.security.auditlog.config.AuditConfig; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; -import org.opensearch.security.dlic.rest.validation.NoOpValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.securityconf.Migration; import org.opensearch.security.securityconf.impl.CType; @@ -73,6 +74,8 @@ // CS-ENFORCE-SINGLE public class MigrateApiAction extends AbstractApiAction { + private final static Logger LOGGER = LogManager.getLogger(MigrateApiAction.class); + private static final List routes = addRoutesPrefix(Collections.singletonList(new Route(Method.POST, "/migrate"))); @Inject @@ -196,7 +199,7 @@ protected void handlePost(RestChannel channel, RestRequest request, Client clien public void onResponse(AcknowledgedResponse response) { if (response.isAcknowledged()) { - log.debug("opendistro_security index deleted successfully"); + LOGGER.debug("opendistro_security index deleted successfully"); client.admin() .indices() @@ -224,7 +227,7 @@ public void onResponse(CreateIndexResponse response) { cTypes.add(id); } } catch (final IOException e1) { - log.error("Unable to create bulk request " + e1, e1); + LOGGER.error("Unable to create bulk request " + e1, e1); internalErrorResponse(channel, "Unable to create bulk request."); return; } @@ -238,7 +241,7 @@ public void onResponse(CreateIndexResponse response) { @Override public void onResponse(BulkResponse response) { if (response.hasFailures()) { - log.error( + LOGGER.error( "Unable to upload migrated configuration because of " + response.buildFailureMessage() ); @@ -247,7 +250,7 @@ public void onResponse(BulkResponse response) { "Unable to upload migrated configuration (bulk index failed)." ); } else { - log.debug("Migration completed"); + LOGGER.debug("Migration completed"); successResponse(channel, "Migration completed."); } @@ -255,7 +258,7 @@ public void onResponse(BulkResponse response) { @Override public void onFailure(Exception e) { - log.error("Unable to upload migrated configuration because of " + e, e); + LOGGER.error("Unable to upload migrated configuration because of " + e, e); internalErrorResponse(channel, "Unable to upload migrated configuration."); } } @@ -266,19 +269,19 @@ public void onFailure(Exception e) { @Override public void onFailure(Exception e) { - log.error("Unable to create opendistro_security index because of " + e, e); + LOGGER.error("Unable to create opendistro_security index because of " + e, e); internalErrorResponse(channel, "Unable to create opendistro_security index."); } }); } else { - log.error("Unable to create opendistro_security index."); + LOGGER.error("Unable to create opendistro_security index."); } } @Override public void onFailure(Exception e) { - log.error("Unable to delete opendistro_security index because of " + e, e); + LOGGER.error("Unable to delete opendistro_security index because of " + e, e); internalErrorResponse(channel, "Unable to delete opendistro_security index."); } }); @@ -304,8 +307,8 @@ protected void handlePut(RestChannel channel, final RestRequest request, final C } @Override - protected AbstractConfigurationValidator getValidator(RestRequest request, BytesReference ref, Object... param) { - return new NoOpValidator(request, ref, this.settings, param); + protected RequestContentValidator createValidator(final Object... params) { + return RequestContentValidator.NOOP_VALIDATOR; } @Override diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/MultiTenancyConfigApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/MultiTenancyConfigApiAction.java index bef77ecb2b..4e36101692 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/MultiTenancyConfigApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/MultiTenancyConfigApiAction.java @@ -14,6 +14,7 @@ import java.io.IOException; import java.nio.file.Path; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -21,12 +22,14 @@ import com.fasterxml.jackson.databind.JsonNode; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.opensearch.action.index.IndexResponse; import org.opensearch.client.Client; import org.opensearch.cluster.service.ClusterService; -import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.common.settings.Settings; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.rest.BytesRestResponse; @@ -37,8 +40,8 @@ import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; -import org.opensearch.security.dlic.rest.validation.MultiTenancyConfigValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator.DataType; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; @@ -53,6 +56,12 @@ public class MultiTenancyConfigApiAction extends AbstractApiAction { + private final static Logger LOGGER = LogManager.getLogger(MultiTenancyConfigApiAction.class); + + public static final String DEFAULT_TENANT_JSON_PROPERTY = "default_tenant"; + public static final String PRIVATE_TENANT_ENABLED_JSON_PROPERTY = "private_tenant_enabled"; + public static final String MULTITENANCY_ENABLED_JSON_PROPERTY = "multitenancy_enabled"; + private static final List ROUTES = addRoutesPrefix( ImmutableList.of(new Route(GET, "/tenancy/config"), new Route(PUT, "/tenancy/config")) ); @@ -90,8 +99,30 @@ public MultiTenancyConfigApiAction( } @Override - protected AbstractConfigurationValidator getValidator(RestRequest request, BytesReference ref, Object... params) { - return new MultiTenancyConfigValidator(request, ref, settings, params); + protected RequestContentValidator createValidator(final Object... params) { + return RequestContentValidator.of(new RequestContentValidator.ValidationContext() { + @Override + public Object[] params() { + return params; + } + + @Override + public Settings settings() { + return settings; + } + + @Override + public Map allowedKeys() { + return ImmutableMap.of( + DEFAULT_TENANT_JSON_PROPERTY, + DataType.STRING, + PRIVATE_TENANT_ENABLED_JSON_PROPERTY, + DataType.BOOLEAN, + MULTITENANCY_ENABLED_JSON_PROPERTY, + DataType.BOOLEAN + ); + } + }); } @Override @@ -121,18 +152,15 @@ private void multitenancyResponse(final ConfigV7 config, final RestChannel chann new BytesRestResponse( RestStatus.OK, contentBuilder.startObject() - .field(MultiTenancyConfigValidator.DEFAULT_TENANT_JSON_PROPERTY, config.dynamic.kibana.default_tenant) - .field( - MultiTenancyConfigValidator.PRIVATE_TENANT_ENABLED_JSON_PROPERTY, - config.dynamic.kibana.private_tenant_enabled - ) - .field(MultiTenancyConfigValidator.MULTITENANCY_ENABLED_JSON_PROPERTY, config.dynamic.kibana.multitenancy_enabled) + .field(DEFAULT_TENANT_JSON_PROPERTY, config.dynamic.kibana.default_tenant) + .field(PRIVATE_TENANT_ENABLED_JSON_PROPERTY, config.dynamic.kibana.private_tenant_enabled) + .field(MULTITENANCY_ENABLED_JSON_PROPERTY, config.dynamic.kibana.multitenancy_enabled) .endObject() ) ); } catch (final Exception e) { internalErrorResponse(channel, e.getMessage()); - log.error("Error handle request ", e); + LOGGER.error("Error handle request ", e); } } @@ -163,18 +191,14 @@ public void onResponse(IndexResponse response) { } private void updateAndValidatesValues(final ConfigV7 config, final JsonNode jsonContent) { - if (Objects.nonNull(jsonContent.findValue(MultiTenancyConfigValidator.DEFAULT_TENANT_JSON_PROPERTY))) { - config.dynamic.kibana.default_tenant = jsonContent.findValue(MultiTenancyConfigValidator.DEFAULT_TENANT_JSON_PROPERTY).asText(); + if (Objects.nonNull(jsonContent.findValue(DEFAULT_TENANT_JSON_PROPERTY))) { + config.dynamic.kibana.default_tenant = jsonContent.findValue(DEFAULT_TENANT_JSON_PROPERTY).asText(); } - if (Objects.nonNull(jsonContent.findValue(MultiTenancyConfigValidator.PRIVATE_TENANT_ENABLED_JSON_PROPERTY))) { - config.dynamic.kibana.private_tenant_enabled = jsonContent.findValue( - MultiTenancyConfigValidator.PRIVATE_TENANT_ENABLED_JSON_PROPERTY - ).booleanValue(); + if (Objects.nonNull(jsonContent.findValue(PRIVATE_TENANT_ENABLED_JSON_PROPERTY))) { + config.dynamic.kibana.private_tenant_enabled = jsonContent.findValue(PRIVATE_TENANT_ENABLED_JSON_PROPERTY).booleanValue(); } - if (Objects.nonNull(jsonContent.findValue(MultiTenancyConfigValidator.MULTITENANCY_ENABLED_JSON_PROPERTY))) { - config.dynamic.kibana.multitenancy_enabled = jsonContent.findValue( - MultiTenancyConfigValidator.MULTITENANCY_ENABLED_JSON_PROPERTY - ).asBoolean(); + if (Objects.nonNull(jsonContent.findValue(MULTITENANCY_ENABLED_JSON_PROPERTY))) { + config.dynamic.kibana.multitenancy_enabled = jsonContent.findValue(MULTITENANCY_ENABLED_JSON_PROPERTY).asBoolean(); } final String defaultTenant = Optional.ofNullable(config.dynamic.kibana.default_tenant).map(String::toLowerCase).orElse(""); diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/NodesDnApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/NodesDnApiAction.java index c7392e4441..8d09705799 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/NodesDnApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/NodesDnApiAction.java @@ -15,13 +15,16 @@ import java.nio.file.Path; import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.Set; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import org.opensearch.client.Client; import org.opensearch.cluster.service.ClusterService; -import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.common.inject.Inject; import org.opensearch.common.settings.Settings; import org.opensearch.rest.RestChannel; @@ -31,8 +34,8 @@ import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; -import org.opensearch.security.dlic.rest.validation.NodesDnValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator.DataType; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.NodesDn; @@ -180,7 +183,27 @@ protected CType getConfigName() { } @Override - protected AbstractConfigurationValidator getValidator(RestRequest request, BytesReference ref, Object... params) { - return new NodesDnValidator(request, ref, this.settings, params); + protected RequestContentValidator createValidator(final Object... params) { + return RequestContentValidator.of(new RequestContentValidator.ValidationContext() { + @Override + public Object[] params() { + return params; + } + + @Override + public Settings settings() { + return settings; + } + + @Override + public Set mandatoryKeys() { + return ImmutableSet.of("nodes_dn"); + } + + @Override + public Map allowedKeys() { + return ImmutableMap.of("nodes_dn", DataType.ARRAY); + } + }); } } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/PatchableResourceApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/PatchableResourceApiAction.java index f630a6bab7..401110c9b1 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/PatchableResourceApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/PatchableResourceApiAction.java @@ -12,12 +12,10 @@ package org.opensearch.security.dlic.rest.api; import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.Iterator; import java.util.List; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.flipkart.zjsonpatch.JsonPatch; @@ -28,8 +26,6 @@ import org.opensearch.action.index.IndexResponse; import org.opensearch.client.Client; import org.opensearch.cluster.service.ClusterService; -import org.opensearch.core.common.bytes.BytesArray; -import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.common.settings.Settings; import org.opensearch.common.xcontent.XContentType; import org.opensearch.core.common.Strings; @@ -42,7 +38,8 @@ import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ConfigurationRepository; import org.opensearch.security.dlic.rest.support.Utils; -import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator; +import org.opensearch.security.dlic.rest.validation.ValidationResult; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; @@ -140,7 +137,7 @@ private void handleSinglePatch( return; } - AbstractConfigurationValidator originalValidator = postProcessApplyPatchResult( + ValidationResult originalValidationResult = postProcessApplyPatchResult( channel, request, existingResourceAsJsonNode, @@ -148,10 +145,10 @@ private void handleSinglePatch( name ); - if (originalValidator != null) { - if (!originalValidator.validate()) { + if (originalValidationResult != null) { + if (!originalValidationResult.isValid()) { request.params().clear(); - badRequestResponse(channel, originalValidator); + badRequestResponse(channel, originalValidationResult.errorMessage()); return; } } @@ -162,9 +159,9 @@ private void handleSinglePatch( return; } - AbstractConfigurationValidator validator = getValidator(request, patchedResourceAsJsonNode); - - if (!validator.validate()) { + RequestContentValidator validator = createValidator(); + final ValidationResult validationResult = validator.validate(request, patchedResourceAsJsonNode); + if (!validationResult.isValid()) { request.params().clear(); badRequestResponse(channel, validator); return; @@ -230,7 +227,7 @@ private void handleBulkPatch( JsonNode oldResource = existingAsObjectNode.get(resourceName); JsonNode patchedResource = patchedAsJsonNode.get(resourceName); - AbstractConfigurationValidator originalValidator = postProcessApplyPatchResult( + ValidationResult originalValidationResult = postProcessApplyPatchResult( channel, request, oldResource, @@ -238,10 +235,10 @@ private void handleBulkPatch( resourceName ); - if (originalValidator != null) { - if (!originalValidator.validate()) { + if (originalValidationResult != null) { + if (!originalValidationResult.isValid()) { request.params().clear(); - badRequestResponse(channel, originalValidator); + badRequestResponse(channel, originalValidationResult.errorMessage()); return; } } @@ -253,9 +250,9 @@ private void handleBulkPatch( } if (oldResource == null || !oldResource.equals(patchedResource)) { - AbstractConfigurationValidator validator = getValidator(request, patchedResource); - - if (!validator.validate()) { + RequestContentValidator validator = createValidator(); + final ValidationResult validationResult = validator.validate(request, patchedResource); + if (!validationResult.isValid()) { request.params().clear(); badRequestResponse(channel, validator); return; @@ -299,13 +296,13 @@ private JsonNode applyPatch(JsonNode jsonPatch, JsonNode existingResourceAsJsonN return JsonPatch.apply(jsonPatch, existingResourceAsJsonNode); } - protected AbstractConfigurationValidator postProcessApplyPatchResult( + protected ValidationResult postProcessApplyPatchResult( RestChannel channel, RestRequest request, JsonNode existingResourceAsJsonNode, JsonNode updatedResourceAsJsonNode, String resourceName - ) { + ) throws IOException { // do nothing by default return null; } @@ -320,13 +317,6 @@ protected void handleApiRequest(RestChannel channel, final RestRequest request, } } - private AbstractConfigurationValidator getValidator(RestRequest request, JsonNode patchedResource) throws JsonProcessingException { - BytesReference patchedResourceAsByteReference = new BytesArray( - DefaultObjectMapper.objectMapper.writeValueAsString(patchedResource).getBytes(StandardCharsets.UTF_8) - ); - return getValidator(request, patchedResourceAsByteReference); - } - // Prevent the case where action group references to itself in the allowed_actions. protected Boolean hasActionGroupSelfReference(SecurityDynamicConfiguration mdc, String name) { List allowedActions = ((ActionGroupsV7) mdc.getCEntry(name)).getAllowed_actions(); diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/RolesApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/RolesApiAction.java index ea1ca791ba..5523bc08a7 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/RolesApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/RolesApiAction.java @@ -11,15 +11,13 @@ package org.opensearch.security.dlic.rest.api; -import java.io.IOException; -import java.nio.file.Path; -import java.util.List; - +import com.fasterxml.jackson.databind.JsonNode; import com.google.common.collect.ImmutableList; - +import com.google.common.collect.ImmutableMap; +import com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.ReadContext; import org.opensearch.client.Client; import org.opensearch.cluster.service.ClusterService; -import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.common.inject.Inject; import org.opensearch.common.settings.Settings; import org.opensearch.rest.RestController; @@ -28,14 +26,22 @@ import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; -import org.opensearch.security.dlic.rest.validation.RolesValidator; +import org.opensearch.security.configuration.MaskedField; +import org.opensearch.security.configuration.Salt; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator.DataType; +import org.opensearch.security.dlic.rest.validation.ValidationResult; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.threadpool.ThreadPool; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; public class RolesApiAction extends PatchableResourceApiAction { @@ -50,6 +56,50 @@ public class RolesApiAction extends PatchableResourceApiAction { ) ); + public static class RoleValidator extends RequestContentValidator { + + private static final Salt SALT = new Salt(new byte[] { 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 6 }); + + protected RoleValidator(ValidationContext validationContext) { + super(validationContext); + } + + @Override + public ValidationResult validate(RestRequest request) throws IOException { + return super.validate(request).map(this::validateMaskedFields); + } + + @Override + public ValidationResult validate(RestRequest request, JsonNode jsonContent) throws IOException { + return super.validate(request, jsonContent).map(this::validateMaskedFields); + } + + private ValidationResult validateMaskedFields(final JsonNode content) { + final ReadContext ctx = JsonPath.parse(content.toString()); + final List maskedFields = ctx.read("$..masked_fields[*]"); + if (maskedFields != null) { + for (String mf : maskedFields) { + if (!validateMaskedFieldSyntax(mf)) { + this.validationError = ValidationError.WRONG_DATATYPE; + return ValidationResult.error(this); + } + } + } + return ValidationResult.success(content); + } + + private boolean validateMaskedFieldSyntax(String mf) { + try { + new MaskedField(mf, SALT).isValid(); + } catch (Exception e) { + wrongDataTypes.put("Masked field not valid: " + mf, e.getMessage()); + return false; + } + return true; + } + + } + @Inject public RolesApiAction( Settings settings, @@ -78,8 +128,29 @@ protected Endpoint getEndpoint() { } @Override - protected AbstractConfigurationValidator getValidator(RestRequest request, BytesReference ref, Object... param) { - return new RolesValidator(request, isSuperAdmin(), ref, this.settings, param); + protected RequestContentValidator createValidator(final Object... params) { + return new RoleValidator(new RequestContentValidator.ValidationContext() { + @Override + public Object[] params() { + return params; + } + + @Override + public Settings settings() { + return settings; + } + + @Override + public Map allowedKeys() { + final ImmutableMap.Builder allowedKeys = ImmutableMap.builder(); + if (isSuperAdmin()) allowedKeys.put("reserved", DataType.BOOLEAN); + return allowedKeys.put("cluster_permissions", DataType.ARRAY) + .put("tenant_permissions", DataType.ARRAY) + .put("index_permissions", DataType.ARRAY) + .put("description", DataType.STRING) + .build(); + } + }); } @Override diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/RolesMappingApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/RolesMappingApiAction.java index e2909edd6c..7fba7e897c 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/RolesMappingApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/RolesMappingApiAction.java @@ -14,14 +14,17 @@ import java.io.IOException; import java.nio.file.Path; import java.util.List; +import java.util.Map; +import java.util.Set; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import org.opensearch.action.index.IndexResponse; import org.opensearch.client.Client; import org.opensearch.cluster.service.ClusterService; -import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.common.inject.Inject; import org.opensearch.common.settings.Settings; import org.opensearch.rest.RestChannel; @@ -32,8 +35,8 @@ import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; -import org.opensearch.security.dlic.rest.validation.RolesMappingValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator.DataType; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; @@ -150,8 +153,35 @@ protected Endpoint getEndpoint() { } @Override - protected AbstractConfigurationValidator getValidator(RestRequest request, BytesReference ref, Object... param) { - return new RolesMappingValidator(request, isSuperAdmin(), ref, this.settings, param); + protected RequestContentValidator createValidator(final Object... params) { + return RequestContentValidator.of(new RequestContentValidator.ValidationContext() { + @Override + public Object[] params() { + return params; + } + + @Override + public Settings settings() { + return settings; + } + + @Override + public Set mandatoryOrKeys() { + return ImmutableSet.of("backend_roles", "and_backend_roles", "hosts", "users"); + } + + @Override + public Map allowedKeys() { + final ImmutableMap.Builder allowedKeys = ImmutableMap.builder(); + if (isSuperAdmin()) allowedKeys.put("reserved", DataType.BOOLEAN); + return allowedKeys.put("backend_roles", DataType.ARRAY) + .put("and_backend_roles", DataType.ARRAY) + .put("hosts", DataType.ARRAY) + .put("users", DataType.ARRAY) + .put("description", DataType.STRING) + .build(); + } + }); } @Override diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfigAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfigAction.java index c4abc45cee..963d64a4df 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfigAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfigAction.java @@ -15,13 +15,14 @@ import java.nio.file.Path; import java.util.Collections; import java.util.List; +import java.util.Map; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import org.opensearch.client.Client; import org.opensearch.cluster.service.ClusterService; -import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.common.inject.Inject; import org.opensearch.common.settings.Settings; import org.opensearch.rest.RestChannel; @@ -31,8 +32,8 @@ import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; -import org.opensearch.security.dlic.rest.validation.SecurityConfigValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator.DataType; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; @@ -134,8 +135,23 @@ protected void handleDelete(RestChannel channel, final RestRequest request, fina } @Override - protected AbstractConfigurationValidator getValidator(RestRequest request, BytesReference ref, Object... param) { - return new SecurityConfigValidator(request, ref, this.settings, param); + protected RequestContentValidator createValidator(final Object... params) { + return RequestContentValidator.of(new RequestContentValidator.ValidationContext() { + @Override + public Object[] params() { + return params; + } + + @Override + public Settings settings() { + return settings; + } + + @Override + public Map allowedKeys() { + return ImmutableMap.of("dynamic", DataType.OBJECT); + } + }); } @Override diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsAction.java index 0b576bfd68..0e454eb8f6 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsAction.java @@ -27,7 +27,6 @@ import org.opensearch.client.Client; import org.opensearch.cluster.service.ClusterService; -import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.common.settings.Settings; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.rest.BytesRestResponse; @@ -39,7 +38,7 @@ import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; @@ -306,7 +305,7 @@ public String getName() { } @Override - protected AbstractConfigurationValidator getValidator(RestRequest request, BytesReference ref, Object... params) { + protected RequestContentValidator createValidator(final Object... params) { return null; } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/TenantsApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/TenantsApiAction.java index b011325c86..f0c9be3ee9 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/TenantsApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/TenantsApiAction.java @@ -29,12 +29,13 @@ import java.nio.file.Path; import java.util.List; +import java.util.Map; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import org.opensearch.client.Client; import org.opensearch.cluster.service.ClusterService; -import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.common.inject.Inject; import org.opensearch.common.settings.Settings; import org.opensearch.rest.RestController; @@ -43,8 +44,8 @@ import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; -import org.opensearch.security.dlic.rest.validation.TenantValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator.DataType; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; @@ -102,8 +103,27 @@ protected Endpoint getEndpoint() { } @Override - protected AbstractConfigurationValidator getValidator(final RestRequest request, BytesReference ref, Object... param) { - return new TenantValidator(request, isSuperAdmin(), ref, this.settings, param); + protected RequestContentValidator createValidator(final Object... params) { + return RequestContentValidator.of(new RequestContentValidator.ValidationContext() { + @Override + public Object[] params() { + return params; + } + + @Override + public Settings settings() { + return settings; + } + + @Override + public Map allowedKeys() { + final ImmutableMap.Builder allowedKeys = ImmutableMap.builder(); + if (isSuperAdmin()) { + allowedKeys.put("reserved", DataType.BOOLEAN); + } + return allowedKeys.put("description", DataType.STRING).build(); + } + }); } @Override diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/ValidateApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/ValidateApiAction.java index f9612d3b40..f3f882cc19 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/ValidateApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/ValidateApiAction.java @@ -20,7 +20,6 @@ import org.opensearch.client.Client; import org.opensearch.cluster.service.ClusterService; -import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.common.collect.Tuple; import org.opensearch.common.inject.Inject; import org.opensearch.common.settings.Settings; @@ -32,9 +31,9 @@ import org.opensearch.security.auditlog.config.AuditConfig; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; -import org.opensearch.security.dlic.rest.validation.NoOpValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator; import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.securityconf.DynamicConfigFactory; import org.opensearch.security.securityconf.Migration; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; @@ -166,8 +165,8 @@ protected void handlePut(RestChannel channel, final RestRequest request, final C } @Override - protected AbstractConfigurationValidator getValidator(RestRequest request, BytesReference ref, Object... param) { - return new NoOpValidator(request, ref, this.settings, param); + protected RequestContentValidator createValidator(final Object... params) { + return RequestContentValidator.NOOP_VALIDATOR; } @Override @@ -186,4 +185,13 @@ protected void consumeParameters(final RestRequest request) { request.paramAsBoolean("accept_invalid", false); } + private final SecurityDynamicConfiguration load(final CType config, boolean logComplianceEvent, boolean acceptInvalid) { + SecurityDynamicConfiguration loaded = cl.getConfigurationsFromIndex( + Collections.singleton(config), + logComplianceEvent, + acceptInvalid + ).get(config).deepClone(); + return DynamicConfigFactory.addStatics(loaded); + } + } diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/AbstractConfigurationValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/AbstractConfigurationValidator.java deleted file mode 100644 index 543ad4b4a7..0000000000 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/AbstractConfigurationValidator.java +++ /dev/null @@ -1,353 +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.validation; - -import java.io.IOException; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; - -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.base.Joiner; -import com.google.common.collect.ImmutableList; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.core.common.bytes.BytesReference; -import org.opensearch.common.settings.Settings; -import org.opensearch.common.xcontent.XContentHelper; -import org.opensearch.common.xcontent.XContentType; -import org.opensearch.core.xcontent.XContentBuilder; -import org.opensearch.rest.RestChannel; -import org.opensearch.rest.RestRequest; -import org.opensearch.rest.RestRequest.Method; -import org.opensearch.security.DefaultObjectMapper; -import org.opensearch.security.support.ConfigConstants; - -public abstract class AbstractConfigurationValidator { - - JsonFactory factory = new JsonFactory(); - - /* public for testing */ - public final static String INVALID_KEYS_KEY = "invalid_keys"; - - /* public for testing */ - public final static String MISSING_MANDATORY_KEYS_KEY = "missing_mandatory_keys"; - - /* public for testing */ - public final static String MISSING_MANDATORY_OR_KEYS_KEY = "specify_one_of"; - - protected final Logger log = LogManager.getLogger(this.getClass()); - - /** Define the various keys for this validator */ - protected final Map allowedKeys = new HashMap<>(); - - protected final Set mandatoryKeys = new HashSet<>(); - - protected final Set mandatoryOrKeys = new HashSet<>(); - - protected final Map wrongDatatypes = new HashMap<>(); - - /** Contain errorneous keys */ - protected final Set missingMandatoryKeys = new HashSet<>(); - - protected final Set invalidKeys = new HashSet<>(); - - protected final Set missingMandatoryOrKeys = new HashSet<>(); - - /** The error type */ - protected ErrorType errorType = ErrorType.NONE; - - /** Behaviour regarding payload */ - protected boolean payloadMandatory = false; - - protected boolean payloadAllowed = true; - - protected final Method method; - - protected final BytesReference content; - - protected final Settings opensearchSettings; - - protected final RestRequest request; - - protected final Object[] param; - - private JsonNode contentAsNode; - - public AbstractConfigurationValidator( - final RestRequest request, - final BytesReference ref, - final Settings opensearchSettings, - Object... param - ) { - this.content = ref; - this.method = request.method(); - this.opensearchSettings = opensearchSettings; - this.request = request; - this.param = param; - } - - public JsonNode getContentAsNode() { - return contentAsNode; - } - - /** - * - * @return false if validation fails - */ - public boolean validate() { - // no payload for DELETE and GET requests - if (method.equals(Method.DELETE) || method.equals(Method.GET)) { - return true; - } - - if (this.payloadMandatory && content.length() == 0) { - this.errorType = ErrorType.PAYLOAD_MANDATORY; - return false; - } - - if (!this.payloadMandatory && content.length() == 0) { - return true; - } - - if (this.payloadMandatory && content.length() > 0) { - - try { - if (DefaultObjectMapper.readTree(content.utf8ToString()).size() == 0) { - this.errorType = ErrorType.PAYLOAD_MANDATORY; - return false; - } - - } catch (IOException e) { - log.error(errorType.BODY_NOT_PARSEABLE.toString(), e); - this.errorType = ErrorType.BODY_NOT_PARSEABLE; - return false; - } - } - - if (!this.payloadAllowed && content.length() > 0) { - this.errorType = ErrorType.PAYLOAD_NOT_ALLOWED; - return false; - } - - // try to parse payload - Set requested = new HashSet(); - try { - contentAsNode = DefaultObjectMapper.readTree(content.utf8ToString()); - requested.addAll(ImmutableList.copyOf(contentAsNode.fieldNames())); - } catch (Exception e) { - log.error(errorType.BODY_NOT_PARSEABLE.toString(), e); - this.errorType = ErrorType.BODY_NOT_PARSEABLE; - return false; - } - - // mandatory settings, one of ... - if (Collections.disjoint(requested, mandatoryOrKeys)) { - this.missingMandatoryOrKeys.addAll(mandatoryOrKeys); - } - - // mandatory settings - Set mandatory = new HashSet<>(mandatoryKeys); - mandatory.removeAll(requested); - missingMandatoryKeys.addAll(mandatory); - - // invalid settings - Set allowed = new HashSet<>(allowedKeys.keySet()); - requested.removeAll(allowed); - this.invalidKeys.addAll(requested); - boolean valid = missingMandatoryKeys.isEmpty() && invalidKeys.isEmpty() && missingMandatoryOrKeys.isEmpty(); - if (!valid) { - this.errorType = ErrorType.INVALID_CONFIGURATION; - } - - // check types - try { - if (!checkDatatypes()) { - this.errorType = ErrorType.WRONG_DATATYPE; - return false; - } - } catch (Exception e) { - log.error(errorType.BODY_NOT_PARSEABLE.toString(), e); - this.errorType = ErrorType.BODY_NOT_PARSEABLE; - return false; - } - - // null element in the values of all the possible keys with DataType as ARRAY - for (Entry allowedKey : allowedKeys.entrySet()) { - JsonNode value = contentAsNode.get(allowedKey.getKey()); - if (value != null) { - if (hasNullArrayElement(value)) { - this.errorType = ErrorType.NULL_ARRAY_ELEMENT; - return false; - } - } - } - return valid; - } - - private boolean checkDatatypes() throws Exception { - String contentAsJson = XContentHelper.convertToJson(content, false, XContentType.JSON); - try (JsonParser parser = factory.createParser(contentAsJson)) { - JsonToken token = null; - while ((token = parser.nextToken()) != null) { - if (token.equals(JsonToken.FIELD_NAME)) { - String currentName = parser.getCurrentName(); - DataType dataType = allowedKeys.get(currentName); - if (dataType != null) { - JsonToken valueToken = parser.nextToken(); - switch (dataType) { - case STRING: - if (!valueToken.equals(JsonToken.VALUE_STRING)) { - wrongDatatypes.put(currentName, "String expected"); - } - break; - case ARRAY: - if (!valueToken.equals(JsonToken.START_ARRAY) && !valueToken.equals(JsonToken.END_ARRAY)) { - wrongDatatypes.put(currentName, "Array expected"); - } - break; - case OBJECT: - if (!valueToken.equals(JsonToken.START_OBJECT) && !valueToken.equals(JsonToken.END_OBJECT)) { - wrongDatatypes.put(currentName, "Object expected"); - } - break; - } - } - } - } - return wrongDatatypes.isEmpty(); - } - } - - public XContentBuilder errorsAsXContent(RestChannel channel) { - try { - final XContentBuilder builder = channel.newBuilder(); - builder.startObject(); - switch (this.errorType) { - case NONE: - builder.field("status", "error"); - builder.field("reason", errorType.getMessage()); - break; - case INVALID_CONFIGURATION: - builder.field("status", "error"); - builder.field("reason", ErrorType.INVALID_CONFIGURATION.getMessage()); - addErrorMessage(builder, INVALID_KEYS_KEY, invalidKeys); - addErrorMessage(builder, MISSING_MANDATORY_KEYS_KEY, missingMandatoryKeys); - addErrorMessage(builder, MISSING_MANDATORY_OR_KEYS_KEY, missingMandatoryKeys); - break; - case INVALID_PASSWORD: - builder.field("status", "error"); - builder.field( - "reason", - opensearchSettings.get( - ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE, - "Password does not match minimum criteria" - ) - ); - break; - case WEAK_PASSWORD: - case SIMILAR_PASSWORD: - builder.field("status", "error"); - builder.field( - "reason", - opensearchSettings.get(ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE, errorType.message) - ); - break; - case WRONG_DATATYPE: - builder.field("status", "error"); - builder.field("reason", ErrorType.WRONG_DATATYPE.getMessage()); - for (Entry entry : wrongDatatypes.entrySet()) { - builder.field(entry.getKey(), entry.getValue()); - } - break; - case NULL_ARRAY_ELEMENT: - builder.field("status", "error"); - builder.field("reason", ErrorType.NULL_ARRAY_ELEMENT.getMessage()); - break; - default: - builder.field("status", "error"); - builder.field("reason", errorType.getMessage()); - - } - builder.endObject(); - return builder; - } catch (IOException ex) { - log.error("Cannot build error settings", ex); - return null; - } - } - - private void addErrorMessage(final XContentBuilder builder, final String message, final Set keys) throws IOException { - if (!keys.isEmpty()) { - builder.startObject(message); - builder.field("keys", Joiner.on(",").join(keys.toArray(new String[0]))); - builder.endObject(); - } - } - - public static enum DataType { - STRING, - ARRAY, - OBJECT, - BOOLEAN; - } - - public static enum ErrorType { - NONE("ok"), - INVALID_CONFIGURATION("Invalid configuration"), - INVALID_PASSWORD("Invalid password"), - WEAK_PASSWORD("Weak password"), - SIMILAR_PASSWORD("Password is similar to user name"), - WRONG_DATATYPE("Wrong datatype"), - BODY_NOT_PARSEABLE("Could not parse content of request."), - PAYLOAD_NOT_ALLOWED("Request body not allowed for this action."), - PAYLOAD_MANDATORY("Request body required for this action."), - SECURITY_NOT_INITIALIZED("Security index not initialized"), - NULL_ARRAY_ELEMENT("`null` is not allowed as json array element"); - - private String message; - - private ErrorType(String message) { - this.message = message; - } - - public String getMessage() { - return message; - } - } - - protected final boolean hasParams() { - return param != null && param.length > 0; - } - - private boolean hasNullArrayElement(JsonNode node) { - for (JsonNode element : node) { - if (element.isNull()) { - if (node.isArray()) { - return true; - } - } else if (element.isContainerNode()) { - if (hasNullArrayElement(element)) { - return true; - } - } - } - return false; - } -} diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/AccountValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/AccountValidator.java deleted file mode 100644 index a2f085f05b..0000000000 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/AccountValidator.java +++ /dev/null @@ -1,27 +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.validation; - -import org.opensearch.core.common.bytes.BytesReference; -import org.opensearch.common.settings.Settings; -import org.opensearch.rest.RestRequest; - -/** - * Validator for Account Api Action. - */ -public class AccountValidator extends CredentialsValidator { - public AccountValidator(RestRequest request, BytesReference ref, Settings opensearchSettings, Object... param) { - super(request, ref, opensearchSettings, param); - allowedKeys.put("current_password", DataType.STRING); - mandatoryKeys.add("current_password"); - } -} diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/ActionGroupValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/ActionGroupValidator.java deleted file mode 100644 index 7c65ff4567..0000000000 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/ActionGroupValidator.java +++ /dev/null @@ -1,37 +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.validation; - -import org.opensearch.core.common.bytes.BytesReference; -import org.opensearch.common.settings.Settings; -import org.opensearch.rest.RestRequest; - -public class ActionGroupValidator extends AbstractConfigurationValidator { - - public ActionGroupValidator( - final RestRequest request, - boolean isSuperAdmin, - BytesReference ref, - final Settings opensearchSettings, - Object... param - ) { - super(request, ref, opensearchSettings, param); - this.payloadMandatory = true; - allowedKeys.put("allowed_actions", DataType.ARRAY); - allowedKeys.put("description", DataType.STRING); - allowedKeys.put("type", DataType.STRING); - if (isSuperAdmin) allowedKeys.put("reserved", DataType.BOOLEAN); - - mandatoryKeys.add("allowed_actions"); - } - -} diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/AllowlistValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/AllowlistValidator.java deleted file mode 100644 index 9fc7465642..0000000000 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/AllowlistValidator.java +++ /dev/null @@ -1,26 +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.validation; - -import org.opensearch.core.common.bytes.BytesReference; -import org.opensearch.common.settings.Settings; -import org.opensearch.rest.RestRequest; - -public class AllowlistValidator extends AbstractConfigurationValidator { - - public AllowlistValidator(final RestRequest request, final BytesReference ref, final Settings opensearchSettings, Object... param) { - super(request, ref, opensearchSettings, param); - this.payloadMandatory = true; - allowedKeys.put("enabled", DataType.BOOLEAN); - allowedKeys.put("requests", DataType.OBJECT); - } -} diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/AuditValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/AuditValidator.java deleted file mode 100644 index b57cd1e715..0000000000 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/AuditValidator.java +++ /dev/null @@ -1,83 +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.validation; - -import java.util.Set; - -import com.google.common.collect.ImmutableSet; - -import org.opensearch.core.common.bytes.BytesReference; -import org.opensearch.common.settings.Settings; -import org.opensearch.rest.RestRequest; -import org.opensearch.security.DefaultObjectMapper; -import org.opensearch.security.auditlog.config.AuditConfig; -import org.opensearch.security.auditlog.impl.AuditCategory; - -public class AuditValidator extends AbstractConfigurationValidator { - - private static final Set DISABLED_REST_CATEGORIES = ImmutableSet.of( - AuditCategory.BAD_HEADERS, - AuditCategory.SSL_EXCEPTION, - AuditCategory.AUTHENTICATED, - AuditCategory.FAILED_LOGIN, - AuditCategory.GRANTED_PRIVILEGES, - AuditCategory.MISSING_PRIVILEGES - ); - - private static final Set DISABLED_TRANSPORT_CATEGORIES = ImmutableSet.of( - AuditCategory.BAD_HEADERS, - AuditCategory.SSL_EXCEPTION, - AuditCategory.AUTHENTICATED, - AuditCategory.FAILED_LOGIN, - AuditCategory.GRANTED_PRIVILEGES, - AuditCategory.MISSING_PRIVILEGES, - AuditCategory.INDEX_EVENT, - AuditCategory.OPENDISTRO_SECURITY_INDEX_ATTEMPT - ); - - public AuditValidator(final RestRequest request, final BytesReference ref, final Settings opensearchSettings, final Object... param) { - super(request, ref, opensearchSettings, param); - this.payloadMandatory = true; - this.allowedKeys.put("enabled", DataType.BOOLEAN); - this.allowedKeys.put("audit", DataType.OBJECT); - this.allowedKeys.put("compliance", DataType.OBJECT); - } - - @Override - public boolean validate() { - if (!super.validate()) { - return false; - } - - if ((request.method() == RestRequest.Method.PUT || request.method() == RestRequest.Method.PATCH) - && this.content != null - && this.content.length() > 0) { - try { - // try parsing to target type - final AuditConfig auditConfig = DefaultObjectMapper.readTree(getContentAsNode(), AuditConfig.class); - final AuditConfig.Filter filter = auditConfig.getFilter(); - if (!DISABLED_REST_CATEGORIES.containsAll(filter.getDisabledRestCategories())) { - throw new IllegalArgumentException("Invalid REST categories passed in the request"); - } - if (!DISABLED_TRANSPORT_CATEGORIES.containsAll(filter.getDisabledTransportCategories())) { - throw new IllegalArgumentException("Invalid transport categories passed in the request"); - } - } catch (Exception e) { - // this.content is not valid json - this.errorType = ErrorType.BODY_NOT_PARSEABLE; - log.error("Invalid content passed in the request", e); - return false; - } - } - return true; - } -} diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/CredentialsValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/CredentialsValidator.java deleted file mode 100644 index 283af8dd00..0000000000 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/CredentialsValidator.java +++ /dev/null @@ -1,83 +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.validation; - -import java.util.Map; - -import org.opensearch.core.common.bytes.BytesReference; -import org.opensearch.core.common.compress.NotXContentException; -import org.opensearch.common.settings.Settings; -import org.opensearch.common.xcontent.XContentHelper; -import org.opensearch.common.xcontent.XContentType; -import org.opensearch.core.common.Strings; -import org.opensearch.rest.RestRequest; -import org.opensearch.security.ssl.util.Utils; - -/** - * Validator for validating password and hash present in the payload - */ -public class CredentialsValidator extends AbstractConfigurationValidator { - - private final PasswordValidator passwordValidator; - - public CredentialsValidator(final RestRequest request, final BytesReference ref, final Settings opensearchSettings, Object... param) { - super(request, ref, opensearchSettings, param); - this.payloadMandatory = true; - this.passwordValidator = PasswordValidator.of(opensearchSettings); - allowedKeys.put("hash", DataType.STRING); - allowedKeys.put("password", DataType.STRING); - } - - /** - * Function to validate password in the content body. - * @return true if validation is successful else false - */ - @Override - public boolean validate() { - if (!super.validate()) { - return false; - } - if ((request.method() == RestRequest.Method.PUT || request.method() == RestRequest.Method.PATCH) - && this.content != null - && this.content.length() > 1) { - try { - final Map contentAsMap = XContentHelper.convertToMap(this.content, false, XContentType.JSON).v2(); - final String password = (String) contentAsMap.get("password"); - if (password != null) { - // Password is not allowed to be empty if present. - if (password.isEmpty()) { - this.errorType = ErrorType.INVALID_PASSWORD; - return false; - } - final String username = Utils.coalesce(request.param("name"), hasParams() ? (String) param[0] : null); - if (Strings.isNullOrEmpty(username)) { - if (log.isDebugEnabled()) { - log.debug("Unable to validate username because no user is given"); - } - return false; - } - final ErrorType passwordValidationResult = passwordValidator.validate(username, password); - if (passwordValidationResult != ErrorType.NONE) { - this.errorType = passwordValidationResult; - return false; - } - } - } catch (NotXContentException e) { - // this.content is not valid json/yaml - log.error("Invalid xContent: " + e, e); - return false; - } - } - return true; - } - -} diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/InternalUsersValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/InternalUsersValidator.java deleted file mode 100644 index 87423e3912..0000000000 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/InternalUsersValidator.java +++ /dev/null @@ -1,37 +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.validation; - -import org.opensearch.core.common.bytes.BytesReference; -import org.opensearch.common.settings.Settings; -import org.opensearch.rest.RestRequest; - -/** - * Validator for Internal Users Api Action. - */ -public class InternalUsersValidator extends CredentialsValidator { - - public InternalUsersValidator( - final RestRequest request, - boolean isSuperAdmin, - BytesReference ref, - final Settings opensearchSettings, - Object... param - ) { - super(request, ref, opensearchSettings, param); - allowedKeys.put("backend_roles", DataType.ARRAY); - allowedKeys.put("attributes", DataType.OBJECT); - allowedKeys.put("description", DataType.STRING); - allowedKeys.put("opendistro_security_roles", DataType.ARRAY); - if (isSuperAdmin) allowedKeys.put("reserved", DataType.BOOLEAN); - } -} diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/MultiTenancyConfigValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/MultiTenancyConfigValidator.java deleted file mode 100644 index dd07e9ac2e..0000000000 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/MultiTenancyConfigValidator.java +++ /dev/null @@ -1,31 +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.validation; - -import org.opensearch.core.common.bytes.BytesReference; -import org.opensearch.common.settings.Settings; -import org.opensearch.rest.RestRequest; - -public class MultiTenancyConfigValidator extends AbstractConfigurationValidator { - - public static final String DEFAULT_TENANT_JSON_PROPERTY = "default_tenant"; - public static final String PRIVATE_TENANT_ENABLED_JSON_PROPERTY = "private_tenant_enabled"; - public static final String MULTITENANCY_ENABLED_JSON_PROPERTY = "multitenancy_enabled"; - - public MultiTenancyConfigValidator(RestRequest request, BytesReference ref, Settings opensearchSettings, Object... param) { - super(request, ref, opensearchSettings, param); - this.payloadMandatory = true; - allowedKeys.put(DEFAULT_TENANT_JSON_PROPERTY, DataType.STRING); - allowedKeys.put(PRIVATE_TENANT_ENABLED_JSON_PROPERTY, DataType.BOOLEAN); - allowedKeys.put(MULTITENANCY_ENABLED_JSON_PROPERTY, DataType.BOOLEAN); - } - -} diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/NoOpValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/NoOpValidator.java deleted file mode 100644 index d29ec91561..0000000000 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/NoOpValidator.java +++ /dev/null @@ -1,24 +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.validation; - -import org.opensearch.core.common.bytes.BytesReference; -import org.opensearch.common.settings.Settings; -import org.opensearch.rest.RestRequest; - -public class NoOpValidator extends AbstractConfigurationValidator { - - public NoOpValidator(final RestRequest request, BytesReference ref, final Settings opensearchSettings, Object... param) { - super(request, ref, opensearchSettings, param); - } - -} diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/NodesDnValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/NodesDnValidator.java deleted file mode 100644 index a2abb06f7b..0000000000 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/NodesDnValidator.java +++ /dev/null @@ -1,27 +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.validation; - -import org.opensearch.core.common.bytes.BytesReference; -import org.opensearch.common.settings.Settings; -import org.opensearch.rest.RestRequest; - -public class NodesDnValidator extends AbstractConfigurationValidator { - - public NodesDnValidator(final RestRequest request, final BytesReference ref, final Settings opensearchSettings, Object... param) { - super(request, ref, opensearchSettings, param); - this.payloadMandatory = true; - - allowedKeys.put("nodes_dn", DataType.ARRAY); - mandatoryKeys.add("nodes_dn"); - } -} diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/PasswordValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/PasswordValidator.java index ac521dee8a..767f823e45 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/PasswordValidator.java +++ b/src/main/java/org/opensearch/security/dlic/rest/validation/PasswordValidator.java @@ -11,23 +11,21 @@ package org.opensearch.security.dlic.rest.validation; -import java.util.List; -import java.util.Locale; -import java.util.Objects; -import java.util.StringJoiner; -import java.util.function.Predicate; -import java.util.regex.Pattern; - import com.google.common.collect.ImmutableList; import com.nulabinc.zxcvbn.Strength; import com.nulabinc.zxcvbn.Zxcvbn; import com.nulabinc.zxcvbn.matchers.Match; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; - import org.opensearch.common.settings.Settings; import org.opensearch.core.common.Strings; -import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator.ErrorType; + +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.StringJoiner; +import java.util.function.Predicate; +import java.util.regex.Pattern; import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_PASSWORD_MIN_LENGTH; import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_PASSWORD_SCORE_BASED_VALIDATION_STRENGTH; @@ -79,14 +77,14 @@ public static PasswordValidator of(final Settings settings) { ); } - ErrorType validate(final String username, final String password) { + public RequestContentValidator.ValidationError validate(final String username, final String password) { if (minPasswordLength > 0 && password.length() < minPasswordLength) { logger.debug( "Password is too short, the minimum required length is {}, but current length is {}", minPasswordLength, password.length() ); - return ErrorType.INVALID_PASSWORD; + return RequestContentValidator.ValidationError.INVALID_PASSWORD; } if (password.length() > MAX_LENGTH) { logger.debug( @@ -94,11 +92,11 @@ ErrorType validate(final String username, final String password) { MAX_LENGTH, password.length() ); - return ErrorType.INVALID_PASSWORD; + return RequestContentValidator.ValidationError.INVALID_PASSWORD; } if (Objects.nonNull(passwordRegexpPattern) && !passwordRegexpPattern.matcher(password).matches()) { logger.debug("Regex does not match password"); - return ErrorType.INVALID_PASSWORD; + return RequestContentValidator.ValidationError.INVALID_PASSWORD; } final Strength strength = zxcvbn.measure(password, ImmutableList.of(username)); if (strength.getScore() < scoreStrength.score()) { @@ -107,14 +105,14 @@ ErrorType validate(final String username, final String password) { scoreStrength, ScoreStrength.fromScore(strength.getScore()) ); - return ErrorType.WEAK_PASSWORD; + return RequestContentValidator.ValidationError.WEAK_PASSWORD; } final boolean similar = strength.getSequence().stream().anyMatch(USERNAME_SIMILARITY_CHECK); if (similar) { logger.debug("Password is too similar to the user name {}", username); - return ErrorType.SIMILAR_PASSWORD; + return RequestContentValidator.ValidationError.SIMILAR_PASSWORD; } - return ErrorType.NONE; + return RequestContentValidator.ValidationError.NONE; } public enum ScoreStrength { diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/RequestContentValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/RequestContentValidator.java new file mode 100644 index 0000000000..4c1ef559fd --- /dev/null +++ b/src/main/java/org/opensearch/security/dlic/rest/validation/RequestContentValidator.java @@ -0,0 +1,382 @@ +/* + * 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.validation; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.JsonNode; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.common.settings.Settings; +import org.opensearch.core.common.Strings; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.rest.RestRequest; +import org.opensearch.security.DefaultObjectMapper; +import org.opensearch.security.ssl.util.Utils; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE; + +public class RequestContentValidator implements ToXContent { + + public final static RequestContentValidator NOOP_VALIDATOR = new RequestContentValidator(new ValidationContext() { + @Override + public Object[] params() { + return new Object[0]; + } + + @Override + public Settings settings() { + return Settings.EMPTY; + } + + @Override + public Map allowedKeys() { + return Collections.emptyMap(); + } + }) { + @Override + public ValidationResult validate(RestRequest request) { + return ValidationResult.success(DefaultObjectMapper.objectMapper.createObjectNode()); + } + + @Override + public ValidationResult validate(RestRequest request, JsonNode jsonNode) { + return ValidationResult.success(DefaultObjectMapper.objectMapper.createObjectNode()); + } + }; + + public final static String INVALID_KEYS_KEY = "invalid_keys"; + + /* public for testing */ + public final static String MISSING_MANDATORY_KEYS_KEY = "missing_mandatory_keys"; + + /* public for testing */ + public final static String MISSING_MANDATORY_OR_KEYS_KEY = "specify_one_of"; + + protected final static Logger LOGGER = LogManager.getLogger(RequestContentValidator.class); + + public static enum DataType { + STRING, + ARRAY, + OBJECT, + BOOLEAN; + } + + public interface ValidationContext { + + default boolean hasParams() { + return params() != null && params().length > 0; + } + + default Set mandatoryKeys() { + return Collections.emptySet(); + } + + default Set mandatoryOrKeys() { + return Collections.emptySet(); + } + + Object[] params(); + + Settings settings(); + + Map allowedKeys(); + + } + + protected ValidationError validationError; + + protected final ValidationContext validationContext; + + protected final Map wrongDataTypes = new HashMap<>(); + + /** Contain errorneous keys */ + private final Set missingMandatoryKeys = new HashSet<>(); + + private final Set invalidKeys = new HashSet<>(); + + private final Set missingMandatoryOrKeys = new HashSet<>(); + + protected RequestContentValidator(final ValidationContext validationContext) { + this.validationError = ValidationError.NONE; + this.validationContext = validationContext; + } + + public ValidationResult validate(final RestRequest request) throws IOException { + return parseRequestContent(request).map(this::validateContentSize).map(jsonContent -> validate(request, jsonContent)); + } + + public ValidationResult validate(final RestRequest request, final JsonNode jsonContent) throws IOException { + return validateContentSize(jsonContent).map(this::validateJsonKeys) + .map(this::validateDataType) + .map(this::nullValuesInArrayValidator) + .map(ignored -> validatePassword(request, jsonContent)); + } + + private ValidationResult parseRequestContent(final RestRequest request) { + try { + final JsonNode jsonContent = DefaultObjectMapper.readTree(request.content().utf8ToString()); + return ValidationResult.success(jsonContent); + } catch (final IOException ioe) { + LOGGER.error(ValidationError.BODY_NOT_PARSEABLE.message(), ioe); + this.validationError = ValidationError.BODY_NOT_PARSEABLE; + return ValidationResult.error(this); + } + } + + private ValidationResult validateContentSize(final JsonNode jsonContent) { + if (jsonContent.size() == 0) { + this.validationError = ValidationError.PAYLOAD_MANDATORY; + return ValidationResult.error(this); + } + return ValidationResult.success(jsonContent); + } + + private ValidationResult validateJsonKeys(final JsonNode jsonContent) { + final Set requestedKeys = new HashSet<>(); + jsonContent.fieldNames().forEachRemaining(requestedKeys::add); + // mandatory settings, one of ... + if (Collections.disjoint(requestedKeys, validationContext.mandatoryOrKeys())) { + missingMandatoryOrKeys.addAll(validationContext.mandatoryOrKeys()); + } + final Set mandatory = new HashSet<>(validationContext.mandatoryKeys()); + mandatory.removeAll(requestedKeys); + missingMandatoryKeys.addAll(mandatory); + + final Set allowed = new HashSet<>(validationContext.allowedKeys().keySet()); + requestedKeys.removeAll(allowed); + invalidKeys.addAll(requestedKeys); + if (!missingMandatoryKeys.isEmpty() || !invalidKeys.isEmpty() || !missingMandatoryOrKeys.isEmpty()) { + this.validationError = ValidationError.INVALID_CONFIGURATION; + return ValidationResult.error(this); + } + return ValidationResult.success(jsonContent); + } + + private ValidationResult validateDataType(final JsonNode jsonContent) { + try (final JsonParser parser = new JsonFactory().createParser(jsonContent.toString())) { + JsonToken token; + while ((token = parser.nextToken()) != null) { + if (token.equals(JsonToken.FIELD_NAME)) { + String currentName = parser.getCurrentName(); + final DataType dataType = validationContext.allowedKeys().get(currentName); + if (dataType != null) { + JsonToken valueToken = parser.nextToken(); + switch (dataType) { + case STRING: + if (valueToken != JsonToken.VALUE_STRING) { + wrongDataTypes.put(currentName, "String expected"); + } + break; + case ARRAY: + if (valueToken != JsonToken.START_ARRAY && valueToken != JsonToken.END_ARRAY) { + wrongDataTypes.put(currentName, "Array expected"); + } + break; + case OBJECT: + if (!valueToken.equals(JsonToken.START_OBJECT) && !valueToken.equals(JsonToken.END_OBJECT)) { + wrongDataTypes.put(currentName, "Object expected"); + } + break; + } + } + } + } + } catch (final IOException ioe) { + LOGGER.error("Couldn't create JSON for payload {}", jsonContent, ioe); + this.validationError = ValidationError.BODY_NOT_PARSEABLE; + return ValidationResult.error(this); + } + if (!wrongDataTypes.isEmpty()) { + this.validationError = ValidationError.WRONG_DATATYPE; + return ValidationResult.error(this); + } + return ValidationResult.success(jsonContent); + } + + private ValidationResult nullValuesInArrayValidator(final JsonNode jsonContent) { + for (final Map.Entry allowedKey : validationContext.allowedKeys().entrySet()) { + JsonNode value = jsonContent.get(allowedKey.getKey()); + if (value != null) { + if (hasNullArrayElement(value)) { + this.validationError = ValidationError.NULL_ARRAY_ELEMENT; + return ValidationResult.error(this); + } + } + } + return ValidationResult.success(jsonContent); + } + + private boolean hasNullArrayElement(final JsonNode node) { + for (final JsonNode element : node) { + if (element.isNull()) { + if (node.isArray()) { + return true; + } + } else if (element.isContainerNode()) { + if (hasNullArrayElement(element)) { + return true; + } + } + } + return false; + } + + private ValidationResult validatePassword(final RestRequest request, final JsonNode jsonContent) { + if (jsonContent.has("password")) { + final PasswordValidator passwordValidator = PasswordValidator.of(validationContext.settings()); + final String password = jsonContent.get("password").asText(); + if (Strings.isNullOrEmpty(password)) { + this.validationError = ValidationError.INVALID_PASSWORD; + return ValidationResult.error(this); + } + final String username = Utils.coalesce( + request.param("name"), + validationContext.hasParams() ? (String) validationContext.params()[0] : null + ); + if (Strings.isNullOrEmpty(username)) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Unable to validate username because no user is given"); + } + this.validationError = ValidationError.NO_USERNAME; + return ValidationResult.error(this); + } + this.validationError = passwordValidator.validate(username, password); + if (this.validationError != ValidationError.NONE) { + return ValidationResult.error(this); + } + } + return ValidationResult.success(jsonContent); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException { + builder.startObject(); + switch (this.validationError) { + case INVALID_CONFIGURATION: + builder.field("status", "error"); + builder.field("reason", ValidationError.INVALID_CONFIGURATION.message()); + addErrorMessage(builder, INVALID_KEYS_KEY, invalidKeys); + addErrorMessage(builder, MISSING_MANDATORY_KEYS_KEY, missingMandatoryKeys); + addErrorMessage(builder, MISSING_MANDATORY_OR_KEYS_KEY, missingMandatoryOrKeys); + break; + case INVALID_PASSWORD: + builder.field("status", "error"); + builder.field( + "reason", + validationContext.settings() + .get(SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE, "Password does not match minimum criteria") + ); + break; + case WEAK_PASSWORD: + case SIMILAR_PASSWORD: + builder.field("status", "error"); + builder.field( + "reason", + validationContext.settings().get(SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE, validationError.message()) + ); + break; + case WRONG_DATATYPE: + builder.field("status", "error"); + builder.field("reason", ValidationError.WRONG_DATATYPE.message()); + for (Map.Entry entry : wrongDataTypes.entrySet()) { + builder.field(entry.getKey(), entry.getValue()); + } + break; + default: + builder.field("status", "error"); + builder.field("reason", validationError.message()); + break; + } + builder.endObject(); + return builder; + } + + private void addErrorMessage(final XContentBuilder builder, final String message, final Set keys) throws IOException { + if (!keys.isEmpty()) { + builder.startObject(message).field("keys", String.join(",", keys)).endObject(); + } + } + + public static RequestContentValidator of(final ValidationContext validationContext) { + return new RequestContentValidator(new ValidationContext() { + + private final Object[] params = validationContext.params(); + + private final Set mandatoryKeys = validationContext.mandatoryKeys(); + + private final Set mandatoryOrKeys = validationContext.mandatoryOrKeys(); + + private final Map allowedKeys = validationContext.allowedKeys(); + + @Override + public Settings settings() { + return validationContext.settings(); + } + + @Override + public Object[] params() { + return params; + } + + @Override + public Set mandatoryKeys() { + return mandatoryKeys; + } + + @Override + public Set mandatoryOrKeys() { + return mandatoryOrKeys; + } + + @Override + public Map allowedKeys() { + return allowedKeys; + } + }); + } + + public enum ValidationError { + NONE("ok"), + INVALID_CONFIGURATION("Invalid configuration"), + INVALID_PASSWORD("Invalid password"), + NO_USERNAME("No username is given"), + WEAK_PASSWORD("Weak password"), + SIMILAR_PASSWORD("Password is similar to user name"), + WRONG_DATATYPE("Wrong datatype"), + BODY_NOT_PARSEABLE("Could not parse content of request."), + PAYLOAD_NOT_ALLOWED("Request body not allowed for this action."), + PAYLOAD_MANDATORY("Request body required for this action."), + SECURITY_NOT_INITIALIZED("Security index not initialized"), + NULL_ARRAY_ELEMENT("`null` is not allowed as json array element"); + + private final String message; + + private ValidationError(String message) { + this.message = message; + } + + public String message() { + return message; + } + + } +} diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/RolesMappingValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/RolesMappingValidator.java deleted file mode 100644 index 10c630c771..0000000000 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/RolesMappingValidator.java +++ /dev/null @@ -1,41 +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.validation; - -import org.opensearch.core.common.bytes.BytesReference; -import org.opensearch.common.settings.Settings; -import org.opensearch.rest.RestRequest; - -public class RolesMappingValidator extends AbstractConfigurationValidator { - - public RolesMappingValidator( - final RestRequest request, - boolean isSuperAdmin, - final BytesReference ref, - final Settings opensearchSettings, - Object... param - ) { - super(request, ref, opensearchSettings, param); - this.payloadMandatory = true; - allowedKeys.put("backend_roles", DataType.ARRAY); - allowedKeys.put("and_backend_roles", DataType.ARRAY); - allowedKeys.put("hosts", DataType.ARRAY); - allowedKeys.put("users", DataType.ARRAY); - allowedKeys.put("description", DataType.STRING); - if (isSuperAdmin) allowedKeys.put("reserved", DataType.BOOLEAN); - - mandatoryOrKeys.add("backend_roles"); - mandatoryOrKeys.add("and_backend_roles"); - mandatoryOrKeys.add("hosts"); - mandatoryOrKeys.add("users"); - } -} diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/RolesValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/RolesValidator.java deleted file mode 100644 index 019a21c73b..0000000000 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/RolesValidator.java +++ /dev/null @@ -1,85 +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.validation; - -import java.util.List; - -import com.jayway.jsonpath.JsonPath; -import com.jayway.jsonpath.ReadContext; - -import org.opensearch.core.common.bytes.BytesReference; -import org.opensearch.common.settings.Settings; -import org.opensearch.rest.RestRequest; -import org.opensearch.security.configuration.MaskedField; -import org.opensearch.security.configuration.Salt; - -public class RolesValidator extends AbstractConfigurationValidator { - - private static final Salt SALT = new Salt(new byte[] { 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 6 }); - - public RolesValidator( - final RestRequest request, - boolean isSuperAdmin, - final BytesReference ref, - final Settings opensearchSettings, - Object... param - ) { - super(request, ref, opensearchSettings, param); - this.payloadMandatory = true; - allowedKeys.put("cluster_permissions", DataType.ARRAY); - allowedKeys.put("tenant_permissions", DataType.ARRAY); - allowedKeys.put("index_permissions", DataType.ARRAY); - allowedKeys.put("description", DataType.STRING); - if (isSuperAdmin) allowedKeys.put("reserved", DataType.BOOLEAN); - } - - @Override - public boolean validate() { - - if (!super.validate()) { - return false; - } - - boolean valid = true; - - if (this.content != null && this.content.length() > 0) { - - final ReadContext ctx = JsonPath.parse(this.content.utf8ToString()); - final List maskedFields = ctx.read("$..masked_fields[*]"); - - if (maskedFields != null) { - - for (String mf : maskedFields) { - if (!validateMaskedFieldSyntax(mf)) { - valid = false; - } - } - } - } - - if (!valid) { - this.errorType = ErrorType.WRONG_DATATYPE; - } - - return valid; - } - - private boolean validateMaskedFieldSyntax(String mf) { - try { - new MaskedField(mf, SALT).isValid(); - } catch (Exception e) { - wrongDatatypes.put("Masked field not valid: " + mf, e.getMessage()); - return false; - } - return true; - } -} diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/SecurityConfigValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/SecurityConfigValidator.java deleted file mode 100644 index 30f3c91965..0000000000 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/SecurityConfigValidator.java +++ /dev/null @@ -1,26 +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.validation; - -import org.opensearch.core.common.bytes.BytesReference; -import org.opensearch.common.settings.Settings; -import org.opensearch.rest.RestRequest; - -public class SecurityConfigValidator extends AbstractConfigurationValidator { - - public SecurityConfigValidator(final RestRequest request, BytesReference ref, final Settings opensearchSettings, Object... param) { - super(request, ref, opensearchSettings, param); - this.payloadMandatory = true; - allowedKeys.put("dynamic", DataType.OBJECT); - } - -} diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/TenantValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/TenantValidator.java deleted file mode 100644 index 07a0ec9cf3..0000000000 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/TenantValidator.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2015-2017 floragunn GmbH - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -/* - * 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.validation; - -import org.opensearch.core.common.bytes.BytesReference; -import org.opensearch.common.settings.Settings; -import org.opensearch.rest.RestRequest; - -public class TenantValidator extends AbstractConfigurationValidator { - - public TenantValidator( - final RestRequest request, - boolean isSuperAdmin, - BytesReference ref, - final Settings opensearchSettings, - Object... param - ) { - super(request, ref, opensearchSettings, param); - this.payloadMandatory = true; - allowedKeys.put("description", DataType.STRING); - if (isSuperAdmin) allowedKeys.put("reserved", DataType.BOOLEAN); - } - -} diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/ValidationResult.java b/src/main/java/org/opensearch/security/dlic/rest/validation/ValidationResult.java new file mode 100644 index 0000000000..0247742a17 --- /dev/null +++ b/src/main/java/org/opensearch/security/dlic/rest/validation/ValidationResult.java @@ -0,0 +1,70 @@ +/* + * 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.validation; + +import com.fasterxml.jackson.databind.JsonNode; +import org.opensearch.common.CheckedConsumer; +import org.opensearch.common.CheckedFunction; +import org.opensearch.core.xcontent.ToXContent; + +import java.io.IOException; +import java.util.Objects; + +public class ValidationResult { + + private final JsonNode jsonContent; + + private final ToXContent errorMessage; + + private ValidationResult(final JsonNode jsonContent, final ToXContent errorMessage) { + this.jsonContent = jsonContent; + this.errorMessage = errorMessage; + } + + public static ValidationResult success(final JsonNode jsonContent) { + return new ValidationResult(jsonContent, null); + } + + public static ValidationResult error(final ToXContent errorMessage) { + return new ValidationResult(null, errorMessage); + } + + public ValidationResult map(final CheckedFunction validation) throws IOException { + if (jsonContent != null) { + return Objects.requireNonNull(validation).apply(jsonContent); + } else { + return this; + } + } + + public void error(final CheckedConsumer invalid) throws IOException { + if (errorMessage != null) { + Objects.requireNonNull(invalid).accept(errorMessage); + } + } + + public ValidationResult valid(final CheckedConsumer contentHandler) throws IOException { + if (jsonContent != null) { + Objects.requireNonNull(contentHandler).accept(jsonContent); + } + return this; + } + + public boolean isValid() { + return errorMessage == null; + } + + public ToXContent errorMessage() { + return errorMessage; + } + +} diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/WhitelistValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/WhitelistValidator.java deleted file mode 100644 index 91a283b4c5..0000000000 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/WhitelistValidator.java +++ /dev/null @@ -1,26 +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.validation; - -import org.opensearch.core.common.bytes.BytesReference; -import org.opensearch.common.settings.Settings; -import org.opensearch.rest.RestRequest; - -public class WhitelistValidator extends AbstractConfigurationValidator { - - public WhitelistValidator(final RestRequest request, final BytesReference ref, final Settings opensearchSettings, Object... param) { - super(request, ref, opensearchSettings, param); - this.payloadMandatory = true; - allowedKeys.put("enabled", DataType.BOOLEAN); - allowedKeys.put("requests", DataType.OBJECT); - } -} diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiTest.java index 611109e9fa..910a0ab657 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiTest.java @@ -24,7 +24,7 @@ import org.opensearch.common.settings.Settings; import org.opensearch.common.xcontent.XContentType; import org.opensearch.security.DefaultObjectMapper; -import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator; import org.opensearch.security.test.helper.file.FileHelper; import org.opensearch.security.test.helper.rest.RestHelper.HttpResponse; @@ -148,13 +148,13 @@ void verifyPutForSuperAdmin(final Header[] header, final boolean userAdminCert) HttpResponse response = rh.executePutRequest(ENDPOINT + "/SOMEGROUP", "", header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(AbstractConfigurationValidator.ErrorType.PAYLOAD_MANDATORY.getMessage(), settings.get("reason")); + Assert.assertEquals(RequestContentValidator.ValidationError.PAYLOAD_MANDATORY.message(), settings.get("reason")); // put new configuration with invalid payload, must fail response = rh.executePutRequest(ENDPOINT + "/SOMEGROUP", FileHelper.loadFile("restapi/actiongroup_not_parseable.json"), header); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - Assert.assertEquals(AbstractConfigurationValidator.ErrorType.BODY_NOT_PARSEABLE.getMessage(), settings.get("reason")); + Assert.assertEquals(RequestContentValidator.ValidationError.BODY_NOT_PARSEABLE.message(), settings.get("reason")); response = rh.executePutRequest(ENDPOINT + "/CRUD_UT", FileHelper.loadFile("restapi/actiongroup_crud.json"), header); Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); @@ -554,7 +554,7 @@ public void checkNullElementsInArray() throws Exception { HttpResponse response = rh.executePutRequest(ENDPOINT + "/CRUD_UT", body, new Header[0]); Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - Assert.assertEquals(AbstractConfigurationValidator.ErrorType.NULL_ARRAY_ELEMENT.getMessage(), settings.get("reason")); + Assert.assertEquals(RequestContentValidator.ValidationError.NULL_ARRAY_ELEMENT.message(), settings.get("reason")); } } diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/AllowlistApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/AllowlistApiTest.java index e0343301b1..6accc0d336 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/AllowlistApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/AllowlistApiTest.java @@ -26,7 +26,7 @@ import org.opensearch.security.auditlog.impl.AuditCategory; import org.opensearch.security.auditlog.impl.AuditMessage; import org.opensearch.security.auditlog.integration.TestAuditlogImpl; -import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator; import org.opensearch.security.filter.SecurityRestFilter; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.test.helper.file.FileHelper; @@ -142,7 +142,7 @@ public void testPayloadMandatory() throws Exception { response = rh.executePutRequest(ENDPOINT, "", new Header[0]); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); JsonNode settings = DefaultObjectMapper.readTree(response.getBody()); - Assert.assertEquals(AbstractConfigurationValidator.ErrorType.PAYLOAD_MANDATORY.getMessage(), settings.get("reason").asText()); + Assert.assertEquals(RequestContentValidator.ValidationError.PAYLOAD_MANDATORY.message(), settings.get("reason").asText()); } /** diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/NodesDnApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/NodesDnApiTest.java index 50294c3c1a..f655c60fae 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/NodesDnApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/NodesDnApiTest.java @@ -29,7 +29,7 @@ import org.opensearch.security.auditlog.impl.AuditCategory; import org.opensearch.security.auditlog.impl.AuditMessage; import org.opensearch.security.auditlog.integration.TestAuditlogImpl; -import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.test.helper.file.FileHelper; import org.opensearch.security.test.helper.rest.RestHelper.HttpResponse; @@ -124,7 +124,7 @@ private void checkNullElementsInArray(final Header headers) throws Exception { HttpResponse response = rh.executePutRequest(ENDPOINT + "/nodesdn/cluster1", body, headers); Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - Assert.assertEquals(AbstractConfigurationValidator.ErrorType.NULL_ARRAY_ELEMENT.getMessage(), settings.get("reason")); + Assert.assertEquals(RequestContentValidator.ValidationError.NULL_ARRAY_ELEMENT.message(), settings.get("reason")); } @Test diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/RolesApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/RolesApiTest.java index 53a8d8764c..211417d84e 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/RolesApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/RolesApiTest.java @@ -11,8 +11,6 @@ package org.opensearch.security.dlic.rest.api; -import java.util.List; - import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -21,15 +19,16 @@ import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; - import org.opensearch.common.settings.Settings; import org.opensearch.common.xcontent.XContentType; import org.opensearch.security.DefaultObjectMapper; -import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator; import org.opensearch.security.support.SecurityJsonNode; import org.opensearch.security.test.helper.file.FileHelper; import org.opensearch.security.test.helper.rest.RestHelper.HttpResponse; +import java.util.List; + import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED; @@ -248,7 +247,7 @@ void verifyPutForSuperAdmin(final Header[] header, final boolean sendAdminCert) HttpResponse response = rh.executePutRequest(ENDPOINT + "/roles/opendistro_security_role_starfleet", "", header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); JsonNode settings = DefaultObjectMapper.readTree(response.getBody()); - Assert.assertEquals(AbstractConfigurationValidator.ErrorType.PAYLOAD_MANDATORY.getMessage(), settings.get("reason").asText()); + Assert.assertEquals(RequestContentValidator.ValidationError.PAYLOAD_MANDATORY.message(), settings.get("reason").asText()); // put new configuration with invalid payload, must fail response = rh.executePutRequest( @@ -258,7 +257,7 @@ void verifyPutForSuperAdmin(final Header[] header, final boolean sendAdminCert) ); settings = DefaultObjectMapper.readTree(response.getBody()); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - Assert.assertEquals(AbstractConfigurationValidator.ErrorType.BODY_NOT_PARSEABLE.getMessage(), settings.get("reason").asText()); + Assert.assertEquals(RequestContentValidator.ValidationError.BODY_NOT_PARSEABLE.message(), settings.get("reason").asText()); // put new configuration with invalid keys, must fail response = rh.executePutRequest( @@ -268,13 +267,9 @@ void verifyPutForSuperAdmin(final Header[] header, final boolean sendAdminCert) ); settings = DefaultObjectMapper.readTree(response.getBody()); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - Assert.assertEquals(AbstractConfigurationValidator.ErrorType.INVALID_CONFIGURATION.getMessage(), settings.get("reason").asText()); - Assert.assertTrue( - settings.get(AbstractConfigurationValidator.INVALID_KEYS_KEY).get("keys").asText().contains("indexx_permissions") - ); - Assert.assertTrue( - settings.get(AbstractConfigurationValidator.INVALID_KEYS_KEY).get("keys").asText().contains("kluster_permissions") - ); + Assert.assertEquals(RequestContentValidator.ValidationError.INVALID_CONFIGURATION.message(), settings.get("reason").asText()); + Assert.assertTrue(settings.get(RequestContentValidator.INVALID_KEYS_KEY).get("keys").asText().contains("indexx_permissions")); + Assert.assertTrue(settings.get(RequestContentValidator.INVALID_KEYS_KEY).get("keys").asText().contains("kluster_permissions")); // put new configuration with wrong datatypes, must fail response = rh.executePutRequest( @@ -284,7 +279,7 @@ void verifyPutForSuperAdmin(final Header[] header, final boolean sendAdminCert) ); settings = DefaultObjectMapper.readTree(response.getBody()); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - Assert.assertEquals(AbstractConfigurationValidator.ErrorType.WRONG_DATATYPE.getMessage(), settings.get("reason").asText()); + Assert.assertEquals(RequestContentValidator.ValidationError.WRONG_DATATYPE.message(), settings.get("reason").asText()); Assert.assertTrue(settings.get("cluster_permissions").asText().equals("Array expected")); // put read only role, must be forbidden @@ -501,7 +496,7 @@ void verifyPutForSuperAdmin(final Header[] header, final boolean sendAdminCert) Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); settings = DefaultObjectMapper.readTree(response.getBody()); Assert.assertEquals(settings.get("status").asText(), "error"); - Assert.assertEquals(settings.get("reason").asText(), AbstractConfigurationValidator.ErrorType.INVALID_CONFIGURATION.getMessage()); + Assert.assertEquals(settings.get("reason").asText(), RequestContentValidator.ValidationError.INVALID_CONFIGURATION.message()); } void verifyPatchForSuperAdmin(final Header[] header, final boolean sendAdminCert) throws Exception { @@ -893,37 +888,37 @@ public void checkNullElementsInArray() throws Exception { HttpResponse response = rh.executePutRequest(ENDPOINT + "/roles/opendistro_security_role_starfleet", body, new Header[0]); Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - Assert.assertEquals(AbstractConfigurationValidator.ErrorType.NULL_ARRAY_ELEMENT.getMessage(), settings.get("reason")); + Assert.assertEquals(RequestContentValidator.ValidationError.NULL_ARRAY_ELEMENT.message(), settings.get("reason")); body = FileHelper.loadFile("restapi/roles_null_array_element_index_permissions.json"); response = rh.executePutRequest(ENDPOINT + "/roles/opendistro_security_role_starfleet", body, new Header[0]); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - Assert.assertEquals(AbstractConfigurationValidator.ErrorType.NULL_ARRAY_ELEMENT.getMessage(), settings.get("reason")); + Assert.assertEquals(RequestContentValidator.ValidationError.NULL_ARRAY_ELEMENT.message(), settings.get("reason")); body = FileHelper.loadFile("restapi/roles_null_array_element_tenant_permissions.json"); response = rh.executePutRequest(ENDPOINT + "/roles/opendistro_security_role_starfleet", body, new Header[0]); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - Assert.assertEquals(AbstractConfigurationValidator.ErrorType.NULL_ARRAY_ELEMENT.getMessage(), settings.get("reason")); + Assert.assertEquals(RequestContentValidator.ValidationError.NULL_ARRAY_ELEMENT.message(), settings.get("reason")); body = FileHelper.loadFile("restapi/roles_null_array_element_index_patterns.json"); response = rh.executePutRequest(ENDPOINT + "/roles/opendistro_security_role_starfleet", body, new Header[0]); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - Assert.assertEquals(AbstractConfigurationValidator.ErrorType.NULL_ARRAY_ELEMENT.getMessage(), settings.get("reason")); + Assert.assertEquals(RequestContentValidator.ValidationError.NULL_ARRAY_ELEMENT.message(), settings.get("reason")); body = FileHelper.loadFile("restapi/roles_null_array_element_masked_fields.json"); response = rh.executePutRequest(ENDPOINT + "/roles/opendistro_security_role_starfleet", body, new Header[0]); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - Assert.assertEquals(AbstractConfigurationValidator.ErrorType.NULL_ARRAY_ELEMENT.getMessage(), settings.get("reason")); + Assert.assertEquals(RequestContentValidator.ValidationError.NULL_ARRAY_ELEMENT.message(), settings.get("reason")); body = FileHelper.loadFile("restapi/roles_null_array_element_allowed_actions.json"); response = rh.executePutRequest(ENDPOINT + "/roles/opendistro_security_role_starfleet", body, new Header[0]); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - Assert.assertEquals(AbstractConfigurationValidator.ErrorType.NULL_ARRAY_ELEMENT.getMessage(), settings.get("reason")); + Assert.assertEquals(RequestContentValidator.ValidationError.NULL_ARRAY_ELEMENT.message(), settings.get("reason")); } } diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/RolesMappingApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/RolesMappingApiTest.java index 5274e34b48..bafbcf0d0f 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/RolesMappingApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/RolesMappingApiTest.java @@ -24,7 +24,7 @@ import org.opensearch.common.settings.Settings; import org.opensearch.common.xcontent.XContentType; import org.opensearch.security.DefaultObjectMapper; -import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator; import org.opensearch.security.test.helper.file.FileHelper; import org.opensearch.security.test.helper.rest.RestHelper.HttpResponse; @@ -239,7 +239,7 @@ void verifyPutForSuperAdmin(final Header[] header) throws Exception { HttpResponse response = rh.executePutRequest(ENDPOINT + "/rolesmapping/opendistro_security_role_starfleet_captains", "", header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(AbstractConfigurationValidator.ErrorType.PAYLOAD_MANDATORY.getMessage(), settings.get("reason")); + Assert.assertEquals(RequestContentValidator.ValidationError.PAYLOAD_MANDATORY.message(), settings.get("reason")); // put new configuration with invalid payload, must fail response = rh.executePutRequest( @@ -249,7 +249,7 @@ void verifyPutForSuperAdmin(final Header[] header) throws Exception { ); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - Assert.assertEquals(AbstractConfigurationValidator.ErrorType.BODY_NOT_PARSEABLE.getMessage(), settings.get("reason")); + Assert.assertEquals(RequestContentValidator.ValidationError.BODY_NOT_PARSEABLE.message(), settings.get("reason")); // put new configuration with invalid keys, must fail response = rh.executePutRequest( @@ -259,10 +259,10 @@ void verifyPutForSuperAdmin(final Header[] header) throws Exception { ); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - Assert.assertEquals(AbstractConfigurationValidator.ErrorType.INVALID_CONFIGURATION.getMessage(), settings.get("reason")); - Assert.assertTrue(settings.get(AbstractConfigurationValidator.INVALID_KEYS_KEY + ".keys").contains("theusers")); - Assert.assertTrue(settings.get(AbstractConfigurationValidator.INVALID_KEYS_KEY + ".keys").contains("thebackendroles")); - Assert.assertTrue(settings.get(AbstractConfigurationValidator.INVALID_KEYS_KEY + ".keys").contains("thehosts")); + Assert.assertEquals(RequestContentValidator.ValidationError.INVALID_CONFIGURATION.message(), settings.get("reason")); + Assert.assertTrue(settings.get(RequestContentValidator.INVALID_KEYS_KEY + ".keys").contains("theusers")); + Assert.assertTrue(settings.get(RequestContentValidator.INVALID_KEYS_KEY + ".keys").contains("thebackendroles")); + Assert.assertTrue(settings.get(RequestContentValidator.INVALID_KEYS_KEY + ".keys").contains("thehosts")); // wrong datatypes response = rh.executePutRequest( @@ -272,7 +272,7 @@ void verifyPutForSuperAdmin(final Header[] header) throws Exception { ); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - Assert.assertEquals(AbstractConfigurationValidator.ErrorType.WRONG_DATATYPE.getMessage(), settings.get("reason")); + Assert.assertEquals(RequestContentValidator.ValidationError.WRONG_DATATYPE.message(), settings.get("reason")); Assert.assertTrue(settings.get("backend_roles").equals("Array expected")); Assert.assertTrue(settings.get("hosts") == null); Assert.assertTrue(settings.get("users") == null); @@ -284,7 +284,7 @@ void verifyPutForSuperAdmin(final Header[] header) throws Exception { ); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - Assert.assertEquals(AbstractConfigurationValidator.ErrorType.WRONG_DATATYPE.getMessage(), settings.get("reason")); + Assert.assertEquals(RequestContentValidator.ValidationError.WRONG_DATATYPE.message(), settings.get("reason")); Assert.assertTrue(settings.get("hosts").equals("Array expected")); Assert.assertTrue(settings.get("backend_roles") == null); Assert.assertTrue(settings.get("users") == null); @@ -296,7 +296,7 @@ void verifyPutForSuperAdmin(final Header[] header) throws Exception { ); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - Assert.assertEquals(AbstractConfigurationValidator.ErrorType.WRONG_DATATYPE.getMessage(), settings.get("reason")); + Assert.assertEquals(RequestContentValidator.ValidationError.WRONG_DATATYPE.message(), settings.get("reason")); Assert.assertTrue(settings.get("hosts").equals("Array expected")); Assert.assertTrue(settings.get("users").equals("Array expected")); Assert.assertTrue(settings.get("backend_roles").equals("Array expected")); @@ -653,18 +653,18 @@ public void checkNullElementsInArray() throws Exception { ); Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - Assert.assertEquals(AbstractConfigurationValidator.ErrorType.NULL_ARRAY_ELEMENT.getMessage(), settings.get("reason")); + Assert.assertEquals(RequestContentValidator.ValidationError.NULL_ARRAY_ELEMENT.message(), settings.get("reason")); body = FileHelper.loadFile("restapi/rolesmapping_null_array_element_backend_roles.json"); response = rh.executePutRequest(ENDPOINT + "/rolesmapping/opendistro_security_role_starfleet_captains", body, new Header[0]); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - Assert.assertEquals(AbstractConfigurationValidator.ErrorType.NULL_ARRAY_ELEMENT.getMessage(), settings.get("reason")); + Assert.assertEquals(RequestContentValidator.ValidationError.NULL_ARRAY_ELEMENT.message(), settings.get("reason")); body = FileHelper.loadFile("restapi/rolesmapping_null_array_element_hosts.json"); response = rh.executePutRequest(ENDPOINT + "/rolesmapping/opendistro_security_role_starfleet_captains", body, new Header[0]); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - Assert.assertEquals(AbstractConfigurationValidator.ErrorType.NULL_ARRAY_ELEMENT.getMessage(), settings.get("reason")); + Assert.assertEquals(RequestContentValidator.ValidationError.NULL_ARRAY_ELEMENT.message(), settings.get("reason")); } } 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 index 23f6065f34..682a5f4c37 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/UserApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/UserApiTest.java @@ -23,8 +23,8 @@ import org.opensearch.common.settings.Settings; import org.opensearch.common.xcontent.XContentType; -import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; 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; @@ -195,14 +195,14 @@ private void verifyPut(final Header... header) throws Exception { 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"), AbstractConfigurationValidator.ErrorType.BODY_NOT_PARSEABLE.getMessage()); + 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"), AbstractConfigurationValidator.ErrorType.INVALID_CONFIGURATION.getMessage()); + // 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")); @@ -241,9 +241,11 @@ private void verifyPut(final Header... header) throws Exception { 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"), AbstractConfigurationValidator.ErrorType.INVALID_CONFIGURATION.getMessage()); - Assert.assertTrue(settings.get(AbstractConfigurationValidator.INVALID_KEYS_KEY + ".keys").contains("some")); - Assert.assertTrue(settings.get(AbstractConfigurationValidator.INVALID_KEYS_KEY + ".keys").contains("other")); + 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 { @@ -521,7 +523,7 @@ private void verifyRoles(final boolean sendAdminCert, Header... header) throws E ); Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - Assert.assertEquals(AbstractConfigurationValidator.ErrorType.WRONG_DATATYPE.getMessage(), settings.get("reason")); + Assert.assertEquals(RequestContentValidator.ValidationError.WRONG_DATATYPE.message(), settings.get("reason")); Assert.assertTrue(settings.get("backend_roles").equals("Array expected")); rh.sendAdminCertificate = false; @@ -533,7 +535,7 @@ private void verifyRoles(final boolean sendAdminCert, Header... header) throws E ); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - Assert.assertEquals(AbstractConfigurationValidator.ErrorType.WRONG_DATATYPE.getMessage(), settings.get("reason")); + Assert.assertEquals(RequestContentValidator.ValidationError.WRONG_DATATYPE.message(), settings.get("reason")); Assert.assertTrue(settings.get("backend_roles").equals("Array expected")); rh.sendAdminCertificate = false; @@ -545,7 +547,7 @@ private void verifyRoles(final boolean sendAdminCert, Header... header) throws E ); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - Assert.assertEquals(AbstractConfigurationValidator.ErrorType.WRONG_DATATYPE.getMessage(), settings.get("reason")); + 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; @@ -558,7 +560,7 @@ private void verifyRoles(final boolean sendAdminCert, Header... header) throws E ); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - Assert.assertEquals(AbstractConfigurationValidator.ErrorType.WRONG_DATATYPE.getMessage(), settings.get("reason")); + Assert.assertEquals(RequestContentValidator.ValidationError.WRONG_DATATYPE.message(), settings.get("reason")); Assert.assertTrue(settings.get("backend_roles").equals("Array expected")); rh.sendAdminCertificate = false; @@ -794,18 +796,18 @@ public void testScoreBasedPasswordRules() throws Exception { "admin", "password89", HttpStatus.SC_BAD_REQUEST, - AbstractConfigurationValidator.ErrorType.WEAK_PASSWORD.getMessage() + RequestContentValidator.ValidationError.WEAK_PASSWORD.message() ); addUserWithPassword( "admin", "A123456789", HttpStatus.SC_BAD_REQUEST, - AbstractConfigurationValidator.ErrorType.WEAK_PASSWORD.getMessage() + RequestContentValidator.ValidationError.WEAK_PASSWORD.message() ); addUserWithPassword("admin", "pas", HttpStatus.SC_BAD_REQUEST, "Password does not match minimum criteria"); - verifySimilarity(AbstractConfigurationValidator.ErrorType.SIMILAR_PASSWORD.getMessage()); + verifySimilarity(RequestContentValidator.ValidationError.SIMILAR_PASSWORD.message()); addUserWithPassword("some_user_name", "ASSDsadwe324wadaasdadqwe", HttpStatus.SC_CREATED); } @@ -1002,7 +1004,7 @@ public void checkNullElementsInArray() throws Exception { 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(AbstractConfigurationValidator.ErrorType.NULL_ARRAY_ELEMENT.getMessage(), settings.get("reason")); + Assert.assertEquals(RequestContentValidator.ValidationError.NULL_ARRAY_ELEMENT.message(), settings.get("reason")); } @Test diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/WhitelistApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/WhitelistApiTest.java index 0ae49efeef..f8130fd455 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/WhitelistApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/WhitelistApiTest.java @@ -26,7 +26,7 @@ import org.opensearch.security.auditlog.impl.AuditCategory; import org.opensearch.security.auditlog.impl.AuditMessage; import org.opensearch.security.auditlog.integration.TestAuditlogImpl; -import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator; import org.opensearch.security.filter.SecurityRestFilter; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.test.helper.file.FileHelper; @@ -149,7 +149,7 @@ public void testPayloadMandatory() throws Exception { response = rh.executePutRequest(ENDPOINT + "/whitelist", "", new Header[0]); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); JsonNode settings = DefaultObjectMapper.readTree(response.getBody()); - Assert.assertEquals(AbstractConfigurationValidator.ErrorType.PAYLOAD_MANDATORY.getMessage(), settings.get("reason").asText()); + Assert.assertEquals(RequestContentValidator.ValidationError.PAYLOAD_MANDATORY.message(), settings.get("reason").asText()); } /** diff --git a/src/test/java/org/opensearch/security/dlic/rest/validation/PasswordValidatorTest.java b/src/test/java/org/opensearch/security/dlic/rest/validation/PasswordValidatorTest.java index 7ea6f23898..6f2648d5dd 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/validation/PasswordValidatorTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/validation/PasswordValidatorTest.java @@ -66,7 +66,7 @@ public class PasswordValidatorTest { public void verifyWeakPasswords( final PasswordValidator passwordValidator, - final AbstractConfigurationValidator.ErrorType expectedValidationResult + final RequestContentValidator.ValidationError expectedValidationResult ) { for (final String password : WEAK_PASSWORDS) assertEquals(password, expectedValidationResult, passwordValidator.validate("some_user_name", password)); @@ -75,7 +75,7 @@ public void verifyWeakPasswords( public void verifyFairPasswords( final PasswordValidator passwordValidator, - final AbstractConfigurationValidator.ErrorType expectedValidationResult + final RequestContentValidator.ValidationError expectedValidationResult ) { for (final String password : FAIR_PASSWORDS) assertEquals(password, expectedValidationResult, passwordValidator.validate("some_user_name", password)); @@ -84,7 +84,7 @@ public void verifyFairPasswords( public void verifyGoodPasswords( final PasswordValidator passwordValidator, - final AbstractConfigurationValidator.ErrorType expectedValidationResult + final RequestContentValidator.ValidationError expectedValidationResult ) { for (final String password : GOOD_PASSWORDS) assertEquals(password, expectedValidationResult, passwordValidator.validate("some_user_name", password)); @@ -93,7 +93,7 @@ public void verifyGoodPasswords( public void verifyStrongPasswords( final PasswordValidator passwordValidator, - final AbstractConfigurationValidator.ErrorType expectedValidationResult + final RequestContentValidator.ValidationError expectedValidationResult ) { for (final String password : STRONG_PASSWORDS) assertEquals(password, expectedValidationResult, passwordValidator.validate("some_user_name", password)); @@ -102,7 +102,7 @@ public void verifyStrongPasswords( public void verifyVeryStrongPasswords( final PasswordValidator passwordValidator, - final AbstractConfigurationValidator.ErrorType expectedValidationResult + final RequestContentValidator.ValidationError expectedValidationResult ) { for (final String password : VERY_STRONG_PASSWORDS) assertEquals(password, expectedValidationResult, passwordValidator.validate("some_user_name", password)); @@ -113,7 +113,7 @@ public void verifySimilarPasswords(final PasswordValidator passwordValidator) { for (final String password : SIMILAR_PASSWORDS) assertEquals( password, - AbstractConfigurationValidator.ErrorType.SIMILAR_PASSWORD, + RequestContentValidator.ValidationError.SIMILAR_PASSWORD, passwordValidator.validate("some_user_name", password) ); @@ -126,22 +126,22 @@ public void testRegExpBasedValidation() { .put(SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX, "(?=.*[A-Z])(?=.*[^a-zA-Z\\\\d])(?=.*[0-9])(?=.*[a-z]).{8,}") .build() ); - verifyWeakPasswords(passwordValidator, AbstractConfigurationValidator.ErrorType.INVALID_PASSWORD); - verifyFairPasswords(passwordValidator, AbstractConfigurationValidator.ErrorType.INVALID_PASSWORD); + verifyWeakPasswords(passwordValidator, RequestContentValidator.ValidationError.INVALID_PASSWORD); + verifyFairPasswords(passwordValidator, RequestContentValidator.ValidationError.INVALID_PASSWORD); for (final String password : GOOD_PASSWORDS.subList(0, GOOD_PASSWORDS.size() - 2)) assertEquals( password, - AbstractConfigurationValidator.ErrorType.INVALID_PASSWORD, + RequestContentValidator.ValidationError.INVALID_PASSWORD, passwordValidator.validate("some_user_name", password) ); for (final String password : GOOD_PASSWORDS.subList(GOOD_PASSWORDS.size() - 2, GOOD_PASSWORDS.size())) assertEquals( password, - AbstractConfigurationValidator.ErrorType.WEAK_PASSWORD, + RequestContentValidator.ValidationError.WEAK_PASSWORD, passwordValidator.validate("some_user_name", password) ); - verifyStrongPasswords(passwordValidator, AbstractConfigurationValidator.ErrorType.NONE); - verifyVeryStrongPasswords(passwordValidator, AbstractConfigurationValidator.ErrorType.NONE); + verifyStrongPasswords(passwordValidator, RequestContentValidator.ValidationError.NONE); + verifyVeryStrongPasswords(passwordValidator, RequestContentValidator.ValidationError.NONE); verifySimilarPasswords(passwordValidator); } @@ -151,7 +151,7 @@ public void testMinLength() { Settings.builder().put(SECURITY_RESTAPI_PASSWORD_MIN_LENGTH, 15).build() ); for (final String password : STRONG_PASSWORDS) { - assertEquals(AbstractConfigurationValidator.ErrorType.INVALID_PASSWORD, passwordValidator.validate(password, "some_user_name")); + assertEquals(RequestContentValidator.ValidationError.INVALID_PASSWORD, passwordValidator.validate(password, "some_user_name")); } } @@ -159,11 +159,11 @@ public void testMinLength() { @Test public void testScoreBasedValidation() { PasswordValidator passwordValidator = PasswordValidator.of(Settings.EMPTY); - verifyWeakPasswords(passwordValidator, AbstractConfigurationValidator.ErrorType.WEAK_PASSWORD); - verifyFairPasswords(passwordValidator, AbstractConfigurationValidator.ErrorType.WEAK_PASSWORD); - verifyGoodPasswords(passwordValidator, AbstractConfigurationValidator.ErrorType.WEAK_PASSWORD); - verifyStrongPasswords(passwordValidator, AbstractConfigurationValidator.ErrorType.NONE); - verifyVeryStrongPasswords(passwordValidator, AbstractConfigurationValidator.ErrorType.NONE); + verifyWeakPasswords(passwordValidator, RequestContentValidator.ValidationError.WEAK_PASSWORD); + verifyFairPasswords(passwordValidator, RequestContentValidator.ValidationError.WEAK_PASSWORD); + verifyGoodPasswords(passwordValidator, RequestContentValidator.ValidationError.WEAK_PASSWORD); + verifyStrongPasswords(passwordValidator, RequestContentValidator.ValidationError.NONE); + verifyVeryStrongPasswords(passwordValidator, RequestContentValidator.ValidationError.NONE); verifySimilarPasswords(passwordValidator); passwordValidator = PasswordValidator.of( @@ -172,11 +172,11 @@ public void testScoreBasedValidation() { .build() ); - verifyWeakPasswords(passwordValidator, AbstractConfigurationValidator.ErrorType.WEAK_PASSWORD); - verifyFairPasswords(passwordValidator, AbstractConfigurationValidator.ErrorType.NONE); - verifyGoodPasswords(passwordValidator, AbstractConfigurationValidator.ErrorType.NONE); - verifyStrongPasswords(passwordValidator, AbstractConfigurationValidator.ErrorType.NONE); - verifyVeryStrongPasswords(passwordValidator, AbstractConfigurationValidator.ErrorType.NONE); + verifyWeakPasswords(passwordValidator, RequestContentValidator.ValidationError.WEAK_PASSWORD); + verifyFairPasswords(passwordValidator, RequestContentValidator.ValidationError.NONE); + verifyGoodPasswords(passwordValidator, RequestContentValidator.ValidationError.NONE); + verifyStrongPasswords(passwordValidator, RequestContentValidator.ValidationError.NONE); + verifyVeryStrongPasswords(passwordValidator, RequestContentValidator.ValidationError.NONE); verifySimilarPasswords(passwordValidator); } diff --git a/src/test/java/org/opensearch/security/dlic/rest/validation/RequestContentValidatorTest.java b/src/test/java/org/opensearch/security/dlic/rest/validation/RequestContentValidatorTest.java new file mode 100644 index 0000000000..a64de6ddeb --- /dev/null +++ b/src/test/java/org/opensearch/security/dlic/rest/validation/RequestContentValidatorTest.java @@ -0,0 +1,318 @@ +/* + * 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.validation; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.NullNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.core.common.bytes.BytesArray; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.http.HttpChannel; +import org.opensearch.http.HttpRequest; +import org.opensearch.rest.RestRequest; +import org.opensearch.security.DefaultObjectMapper; + +import java.io.IOException; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class RequestContentValidatorTest { + + @Mock + private HttpRequest httpRequest; + + @Mock + private NamedXContentRegistry xContentRegistry; + + @Mock + private HttpChannel httpChannel; + + private RestRequest request; + + @Before + public void setUpRequest() { + when(httpRequest.uri()).thenReturn(""); + when(httpRequest.content()).thenReturn(new BytesArray(new byte[1])); + when(httpRequest.getHeaders()).thenReturn( + Collections.singletonMap("Content-Type", Collections.singletonList("application/json")) + ); + request = RestRequest.request(xContentRegistry, httpRequest, httpChannel); + } + + @Test + public void testParseRequestContent() throws Exception { + final RequestContentValidator validator = RequestContentValidator.of(new RequestContentValidator.ValidationContext() { + @Override + public Object[] params() { + return new Object[0]; + } + + @Override + public Settings settings() { + return Settings.EMPTY; + } + + @Override + public Map allowedKeys() { + return Collections.emptyMap(); + } + }); + when(httpRequest.content()).thenReturn(new BytesArray("{`a`: `b`}")); + final ValidationResult validationResult = validator.validate(request); + assertFalse(validationResult.isValid()); + assertErrorMessage(validationResult.errorMessage(), RequestContentValidator.ValidationError.BODY_NOT_PARSEABLE); + } + + @Test + public void testValidateContentSize() throws Exception { + final RequestContentValidator validator = RequestContentValidator.of(new RequestContentValidator.ValidationContext() { + @Override + public Object[] params() { + return new Object[0]; + } + + @Override + public Settings settings() { + return Settings.EMPTY; + } + + @Override + public Map allowedKeys() { + return Collections.emptyMap(); + } + }); + when(httpRequest.content()).thenReturn(new BytesArray("")); + ValidationResult validationResult = validator.validate(request); + assertFalse(validationResult.isValid()); + assertErrorMessage(validationResult.errorMessage(), RequestContentValidator.ValidationError.PAYLOAD_MANDATORY); + + when(httpRequest.content()).thenReturn(new BytesArray("{}")); + validationResult = validator.validate(request); + assertFalse(validationResult.isValid()); + assertErrorMessage(validationResult.errorMessage(), RequestContentValidator.ValidationError.PAYLOAD_MANDATORY); + } + + @Test + public void testValidateDataTypes() throws Exception { + final RequestContentValidator validator = RequestContentValidator.of(new RequestContentValidator.ValidationContext() { + @Override + public Object[] params() { + return new Object[0]; + } + + @Override + public Settings settings() { + return Settings.EMPTY; + } + + @Override + public Map allowedKeys() { + return ImmutableMap.of( + "a", + RequestContentValidator.DataType.STRING, + "b", + RequestContentValidator.DataType.OBJECT, + "c", + RequestContentValidator.DataType.ARRAY + ); + } + }); + + final JsonNode payload = DefaultObjectMapper.objectMapper.createObjectNode().put("a", 1).put("b", "[]").put("c", "{}"); + when(httpRequest.content()).thenReturn(new BytesArray(payload.toString())); + final ValidationResult validationResult = validator.validate(request); + + final JsonNode errorMessage = xContentToJsonNode(validationResult.errorMessage()); + assertFalse(validationResult.isValid()); + assertErrorMessage(errorMessage, RequestContentValidator.ValidationError.WRONG_DATATYPE); + + assertEquals("String expected", errorMessage.get("a").asText()); + assertEquals("Object expected", errorMessage.get("b").asText()); + assertEquals("Array expected", errorMessage.get("c").asText()); + } + + @Test + public void testValidateJsonKeys() throws Exception { + final RequestContentValidator validator = RequestContentValidator.of(new RequestContentValidator.ValidationContext() { + @Override + public Object[] params() { + return new Object[0]; + } + + @Override + public Settings settings() { + return Settings.EMPTY; + } + + @Override + public Set mandatoryKeys() { + return Set.of("a"); + } + + @Override + public Map allowedKeys() { + return Map.of("a", RequestContentValidator.DataType.STRING, "b", RequestContentValidator.DataType.STRING); + } + }); + + final JsonNode payload = DefaultObjectMapper.objectMapper.createObjectNode().put("c", "aaa").put("d", "aaa"); + when(httpRequest.content()).thenReturn(new BytesArray(payload.toString())); + final ValidationResult validationResult = validator.validate(request); + final JsonNode errorMessage = xContentToJsonNode(validationResult.errorMessage()); + assertErrorMessage(errorMessage, RequestContentValidator.ValidationError.INVALID_CONFIGURATION); + + assertEquals("{\"keys\":\"c,d\"}", errorMessage.get("invalid_keys").toString()); + assertEquals("{\"keys\":\"a\"}", errorMessage.get("missing_mandatory_keys").toString()); + } + + @Test + public void testNullValuesInArray() throws Exception { + final RequestContentValidator validator = RequestContentValidator.of(new RequestContentValidator.ValidationContext() { + @Override + public Object[] params() { + return new Object[0]; + } + + @Override + public Settings settings() { + return Settings.EMPTY; + } + + @Override + public Set mandatoryKeys() { + return ImmutableSet.of("a"); + } + + @Override + public Map allowedKeys() { + return ImmutableMap.of("a", RequestContentValidator.DataType.ARRAY); + } + }); + final ObjectNode payload = DefaultObjectMapper.objectMapper.createObjectNode().putObject("a"); + payload.putArray("a").add(NullNode.getInstance()).add("b").add("c"); + when(request.content()).thenReturn(new BytesArray(payload.toString())); + final ValidationResult validationResult = validator.validate(request); + assertErrorMessage(validationResult.errorMessage(), RequestContentValidator.ValidationError.NULL_ARRAY_ELEMENT); + } + + @Test + public void testValidatePassword() throws Exception { + final RequestContentValidator validator = RequestContentValidator.of(new RequestContentValidator.ValidationContext() { + @Override + public Object[] params() { + return new Object[0]; + } + + @Override + public Settings settings() { + return Settings.EMPTY; + } + + @Override + public Set mandatoryKeys() { + return ImmutableSet.of("password"); + } + + @Override + public Map allowedKeys() { + return ImmutableMap.of("password", RequestContentValidator.DataType.STRING); + } + }); + ObjectNode payload = DefaultObjectMapper.objectMapper.createObjectNode().put("password", "a"); + when(httpRequest.content()).thenReturn(new BytesArray(payload.toString())); + ValidationResult validationResult = validator.validate(request); + assertErrorMessage(validationResult.errorMessage(), RequestContentValidator.ValidationError.NO_USERNAME); + + when(httpRequest.uri()).thenReturn("/aaaa?name=a"); + when(request.content()).thenReturn(new BytesArray(payload.toString())); + validationResult = validator.validate(RestRequest.request(xContentRegistry, httpRequest, httpChannel)); + assertErrorMessage(validationResult.errorMessage(), RequestContentValidator.ValidationError.WEAK_PASSWORD); + } + + @Test + public void testValidationSuccess() throws Exception { + final RequestContentValidator validator = RequestContentValidator.of(new RequestContentValidator.ValidationContext() { + @Override + public Object[] params() { + return new Object[0]; + } + + @Override + public Settings settings() { + return Settings.EMPTY; + } + + @Override + public Map allowedKeys() { + return ImmutableMap.of( + "a", + RequestContentValidator.DataType.ARRAY, + "b", + RequestContentValidator.DataType.BOOLEAN, + "c", + RequestContentValidator.DataType.OBJECT, + "d", + RequestContentValidator.DataType.STRING, + "e", + RequestContentValidator.DataType.BOOLEAN + ); + } + }); + + ObjectNode payload = DefaultObjectMapper.objectMapper.createObjectNode().putObject("a"); + payload.putArray("a").add("arrray"); + payload.put("b", true).put("d", "some_string").put("e", "true"); + payload.putObject("c"); + + when(httpRequest.content()).thenReturn(new BytesArray(payload.toString())); + final ValidationResult validationResult = validator.validate(request); + assertTrue(validationResult.isValid()); + assertNull(validationResult.errorMessage()); + } + + private JsonNode xContentToJsonNode(final ToXContent toXContent) throws IOException { + try (final var xContentBuilder = XContentFactory.jsonBuilder()) { + toXContent.toXContent(xContentBuilder, ToXContent.EMPTY_PARAMS); + return DefaultObjectMapper.readTree(xContentBuilder.toString()); + } + } + + private void assertErrorMessage(final ToXContent toXContent, final RequestContentValidator.ValidationError expectedValidationError) + throws IOException { + final var jsonNode = xContentToJsonNode(toXContent); + assertErrorMessage(jsonNode, expectedValidationError); + } + + private void assertErrorMessage(final JsonNode jsonNode, final RequestContentValidator.ValidationError expectedValidationError) { + assertEquals("error", jsonNode.get("status").asText()); + assertEquals(expectedValidationError.message(), jsonNode.get("reason").asText()); + } + +}