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 727624e4e4..aeeb4f1c92 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 a93929f57b..4763d312dd 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.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 4898897df7..ad7e035d7c 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.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 6db8963ab5..201d599117 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; @@ -268,33 +269,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; } @@ -309,7 +300,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 ff82af9426..73066666b9 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 @@ -20,6 +20,8 @@ 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.action.ActionListener; import org.opensearch.action.admin.indices.create.CreateIndexResponse; import org.opensearch.action.bulk.BulkRequestBuilder; @@ -45,8 +47,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; @@ -71,6 +72,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 @@ -187,7 +190,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() @@ -215,7 +218,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; } @@ -229,7 +232,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() ); @@ -238,7 +241,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."); } @@ -246,7 +249,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."); } } @@ -257,19 +260,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."); } }); @@ -295,8 +298,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 6050b9785e..116dc82d06 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/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/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/test/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiTest.java index c87698155f..46b730abac 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; @@ -147,13 +147,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()); @@ -553,7 +553,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 79ce591723..ab0cf53237 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 a549ad4dd9..8a429022a4 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 bf703fea35..95c9c432ab 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.hc.core5.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; @@ -247,7 +246,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( @@ -257,7 +256,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( @@ -267,13 +266,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( @@ -283,7 +278,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 @@ -495,7 +490,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 { @@ -887,37 +882,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 42fb111281..4cfe057388 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")); @@ -652,18 +652,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 895f65bc81..707dbe614e 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; @@ -192,14 +192,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")); @@ -238,9 +238,9 @@ 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.assertTrue(settings.get(RequestContentValidator.INVALID_KEYS_KEY + ".keys").contains("some")); + Assert.assertTrue(settings.get(RequestContentValidator.INVALID_KEYS_KEY + ".keys").contains("other")); } @@ -518,7 +518,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; @@ -530,7 +530,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; @@ -542,7 +542,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; @@ -555,7 +555,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; @@ -792,18 +792,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); } @@ -1001,7 +1001,7 @@ public void checkNullElementsInArray() throws Exception { HttpResponse response = rh.executePutRequest(ENDPOINT + "/internalusers/picard", 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/WhitelistApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/WhitelistApiTest.java index 371341147e..8a1d8ce83f 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..ee98d2a10b --- /dev/null +++ b/src/test/java/org/opensearch/security/dlic/rest/validation/RequestContentValidatorTest.java @@ -0,0 +1,320 @@ +/* + * 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.Strings; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.common.xcontent.XContentType; +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.contentBuilder(XContentType.JSON)) { + toXContent.toXContent(xContentBuilder, ToXContent.EMPTY_PARAMS); + return DefaultObjectMapper.readTree(Strings.toString(xContentBuilder)); + } + } + + 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()); + } + +}