diff --git a/src/main/java/org/opensearch/security/DefaultObjectMapper.java b/src/main/java/org/opensearch/security/DefaultObjectMapper.java index 64bcd95fc5..27aae2b5e4 100644 --- a/src/main/java/org/opensearch/security/DefaultObjectMapper.java +++ b/src/main/java/org/opensearch/security/DefaultObjectMapper.java @@ -102,12 +102,12 @@ public static boolean getOrDefault(Map properties, String key, b ); } + @SuppressWarnings("unchecked") public static T getOrDefault(Map properties, String key, T defaultValue) { T value = (T) properties.get(key); return value != null ? value : defaultValue; } - @SuppressWarnings("removal") public static T readTree(JsonNode node, Class clazz) throws IOException { final SecurityManager sm = System.getSecurityManager(); @@ -117,12 +117,7 @@ public static T readTree(JsonNode node, Class clazz) throws IOException { } try { - return AccessController.doPrivileged(new PrivilegedExceptionAction() { - @Override - public T run() throws Exception { - return objectMapper.treeToValue(node, clazz); - } - }); + return AccessController.doPrivileged((PrivilegedExceptionAction) () -> objectMapper.treeToValue(node, clazz)); } catch (final PrivilegedActionException e) { throw (IOException) e.getCause(); } @@ -138,12 +133,7 @@ public static T readValue(String string, Class clazz) throws IOException } try { - return AccessController.doPrivileged(new PrivilegedExceptionAction() { - @Override - public T run() throws Exception { - return objectMapper.readValue(string, clazz); - } - }); + return AccessController.doPrivileged((PrivilegedExceptionAction) () -> objectMapper.readValue(string, clazz)); } catch (final PrivilegedActionException e) { throw (IOException) e.getCause(); } @@ -159,12 +149,7 @@ public static JsonNode readTree(String string) throws IOException { } try { - return AccessController.doPrivileged(new PrivilegedExceptionAction() { - @Override - public JsonNode run() throws Exception { - return objectMapper.readTree(string); - } - }); + return AccessController.doPrivileged((PrivilegedExceptionAction) () -> objectMapper.readTree(string)); } catch (final PrivilegedActionException e) { throw (IOException) e.getCause(); } @@ -180,12 +165,11 @@ public static String writeValueAsString(Object value, boolean omitDefaults) thro } try { - return AccessController.doPrivileged(new PrivilegedExceptionAction() { - @Override - public String run() throws Exception { - return (omitDefaults ? defaulOmittingObjectMapper : objectMapper).writeValueAsString(value); - } - }); + return AccessController.doPrivileged( + (PrivilegedExceptionAction) () -> (omitDefaults ? defaulOmittingObjectMapper : objectMapper).writeValueAsString( + value + ) + ); } catch (final PrivilegedActionException e) { throw (JsonProcessingException) e.getCause(); } @@ -224,12 +208,7 @@ public static T readValue(String string, JavaType jt) throws IOException { } try { - return AccessController.doPrivileged(new PrivilegedExceptionAction() { - @Override - public T run() throws Exception { - return objectMapper.readValue(string, jt); - } - }); + return AccessController.doPrivileged((PrivilegedExceptionAction) () -> objectMapper.readValue(string, jt)); } catch (final PrivilegedActionException e) { throw (IOException) e.getCause(); } 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 6a1d011fd3..6d3710871b 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 @@ -11,16 +11,16 @@ package org.opensearch.security.dlic.rest.api; -import java.io.IOException; -import java.nio.file.Path; -import java.util.Collections; -import java.util.Objects; - -import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.core.JsonPointer; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.flipkart.zjsonpatch.JsonPatch; +import com.flipkart.zjsonpatch.JsonPatchApplicationException; +import com.google.common.collect.ImmutableSet; +import org.apache.commons.lang3.tuple.Pair; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; - import org.opensearch.ExceptionsHelper; import org.opensearch.action.index.IndexRequest; import org.opensearch.action.index.IndexResponse; @@ -28,295 +28,419 @@ import org.opensearch.client.Client; import org.opensearch.client.node.NodeClient; import org.opensearch.cluster.service.ClusterService; -import org.opensearch.common.settings.Settings; +import org.opensearch.common.CheckedSupplier; import org.opensearch.common.util.concurrent.ThreadContext.StoredContext; import org.opensearch.common.xcontent.XContentHelper; import org.opensearch.common.xcontent.XContentType; -import org.opensearch.core.xcontent.ToXContent; -import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.common.Strings; +import org.opensearch.core.common.transport.TransportAddress; +import org.opensearch.core.rest.RestStatus; import org.opensearch.index.engine.VersionConflictEngineException; import org.opensearch.rest.BaseRestHandler; import org.opensearch.rest.BytesRestResponse; import org.opensearch.rest.RestChannel; -import org.opensearch.rest.RestController; import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestRequest.Method; -import org.opensearch.core.action.ActionListener; -import org.opensearch.core.rest.RestStatus; -import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.action.configupdate.ConfigUpdateAction; 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.support.Utils; +import org.opensearch.security.dlic.rest.validation.EndpointValidator; import org.opensearch.security.dlic.rest.validation.RequestContentValidator; -import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.dlic.rest.validation.ValidationResult; import org.opensearch.security.securityconf.DynamicConfigFactory; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; -import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; -import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import static org.opensearch.security.dlic.rest.api.RequestHandler.methodNotImplementedHandler; +import static org.opensearch.security.dlic.rest.api.Responses.badRequestMessage; +import static org.opensearch.security.dlic.rest.api.Responses.conflict; +import static org.opensearch.security.dlic.rest.api.Responses.forbidden; +import static org.opensearch.security.dlic.rest.api.Responses.forbiddenMessage; +import static org.opensearch.security.dlic.rest.api.Responses.internalSeverError; +import static org.opensearch.security.dlic.rest.api.Responses.payload; +import static org.opensearch.security.dlic.rest.support.Utils.withIOException; public abstract class AbstractApiAction extends BaseRestHandler { private final static Logger LOGGER = LogManager.getLogger(AbstractApiAction.class); - protected final ConfigurationRepository cl; - protected final ClusterService cs; - final ThreadPool threadPool; - protected String securityIndexName; - private final RestApiPrivilegesEvaluator restApiPrivilegesEvaluator; - protected final RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator; - protected final AuditLog auditLog; - protected final Settings settings; + private final static Set supportedPatchOperations = Set.of("add", "replace", "remove"); + + private final static String supportedPatchOperationsAsString = String.join(",", supportedPatchOperations); + + protected final ClusterService clusterService; + + protected final ThreadPool threadPool; + + private Map requestHandlers; + + protected final RequestHandler.RequestHandlersBuilder requestHandlersBuilder; + + protected final EndpointValidator endpointValidator; + + protected final Endpoint endpoint; + + protected final SecurityApiDependencies securityApiDependencies; protected AbstractApiAction( - final Settings settings, - final Path configPath, - final RestController controller, - final Client client, - final AdminDNs adminDNs, - final ConfigurationRepository cl, - final ClusterService cs, - final PrincipalExtractor principalExtractor, - final PrivilegesEvaluator evaluator, - ThreadPool threadPool, - AuditLog auditLog + final Endpoint endpoint, + final ClusterService clusterService, + final ThreadPool threadPool, + final SecurityApiDependencies securityApiDependencies ) { super(); - this.settings = settings; - this.securityIndexName = settings.get( - ConfigConstants.SECURITY_CONFIG_INDEX_NAME, - ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX - ); - - this.cl = cl; - this.cs = cs; + this.endpoint = endpoint; + this.clusterService = clusterService; this.threadPool = threadPool; - this.restApiPrivilegesEvaluator = new RestApiPrivilegesEvaluator( - settings, - adminDNs, - evaluator, - principalExtractor, - configPath, - threadPool - ); - this.restApiAdminPrivilegesEvaluator = new RestApiAdminPrivilegesEvaluator( - threadPool.getThreadContext(), - evaluator, - adminDNs, - settings.getAsBoolean(SECURITY_RESTAPI_ADMIN_ENABLED, false) - ); - this.auditLog = auditLog; + this.securityApiDependencies = securityApiDependencies; + this.requestHandlersBuilder = new RequestHandler.RequestHandlersBuilder(); + this.requestHandlersBuilder.configureRequestHandlers(this::buildDefaultRequestHandlers); + this.endpointValidator = createEndpointValidator(); } - protected abstract RequestContentValidator createValidator(final Object... params); + private void buildDefaultRequestHandlers(final RequestHandler.RequestHandlersBuilder builder) { + builder.withAccessHandler(request -> securityApiDependencies.restApiAdminPrivilegesEvaluator().isCurrentUserAdminFor(endpoint)) + .withSaveOrUpdateConfigurationHandler(this::saveOrUpdateConfiguration) + .add(Method.POST, methodNotImplementedHandler) + .add(Method.PATCH, methodNotImplementedHandler) + .onGetRequest(this::processGetRequest) + .onChangeRequest(Method.DELETE, this::processDeleteRequest) + .onChangeRequest(Method.PUT, this::processPutRequest); + } - protected abstract String getResourceName(); + protected final ValidationResult processDeleteRequest(final RestRequest request) throws IOException { + return endpointValidator.withRequiredEntityName(nameParam(request)) + .map(entityName -> loadConfiguration(entityName, false)) + .map(endpointValidator::onConfigDelete) + .map(this::removeEntityFromConfig); + } - protected abstract CType getConfigName(); + protected final ValidationResult removeEntityFromConfig(final SecurityConfiguration securityConfiguration) { + final var configuration = securityConfiguration.configuration(); + configuration.remove(securityConfiguration.entityName()); + return ValidationResult.success(securityConfiguration); + } - protected void handleApiRequest(final RestChannel channel, final RestRequest request, final Client client) throws IOException { + protected final ValidationResult processGetRequest(final RestRequest request) throws IOException { + return loadConfiguration(getConfigType(), true, true).map( + configuration -> ValidationResult.success(SecurityConfiguration.of(nameParam(request), configuration)) + ).map(endpointValidator::onConfigLoad).map(securityConfiguration -> securityConfiguration.maybeEntityName().map(entityName -> { + securityConfiguration.configuration().removeOthers(entityName); + return ValidationResult.success(securityConfiguration); + }).orElse(ValidationResult.success(securityConfiguration))); + } + /** + * Process patch requests for all types of configuration, which can be one entity in the URI or a list of entities in the request body. + **/ + protected final ValidationResult processPatchRequest(final RestRequest request) throws IOException { + return loadConfiguration(nameParam(request), false).map( + securityConfiguration -> withPatchRequestContent(request).map( + patchContent -> securityConfiguration.maybeEntityName() + .map(entityName -> patchEntity(request, patchContent, securityConfiguration)) + .orElseGet(() -> patchEntities(request, patchContent, securityConfiguration)) + ) + ); + } + + protected final ValidationResult withPatchRequestContent(final RestRequest request) { try { - switch (request.method()) { - case DELETE: - handleDelete(channel, request, client, null); - break; - case POST: - createValidator().validate(request) - .valid(jsonContent -> handlePost(channel, request, client, jsonContent)) - .error(toXContent -> requestContentInvalid(request, channel, toXContent)); - break; - case PUT: - createValidator().validate(request) - .valid(jsonContent -> handlePut(channel, request, client, jsonContent)) - .error(toXContent -> requestContentInvalid(request, channel, toXContent)); - break; - case GET: - handleGet(channel, request, client, null); - break; - default: - throw new IllegalArgumentException(request.method() + " not supported"); + final var parsedPatchRequestContent = Utils.toJsonNode(request.content().utf8ToString()); + if (!(parsedPatchRequestContent instanceof ArrayNode)) { + return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage("Wrong request body")); } - } catch (JsonMappingException jme) { - throw jme; - // TODO strip source - // if(jme.getLocation() == null || jme.getLocation().getSourceRef() == null) { - // throw jme; - // } else throw new JsonMappingException(null, jme.getMessage()); + final var operations = patchOperations(parsedPatchRequestContent); + if (operations.isEmpty()) { + return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage("Wrong request body")); + } + for (final var patchOperation : operations) { + if (!supportedPatchOperations.contains(patchOperation)) { + return ValidationResult.error( + RestStatus.BAD_REQUEST, + badRequestMessage( + "Unsupported patch operation: " + patchOperation + ". Supported are: " + supportedPatchOperationsAsString + ) + ); + } + } + return ValidationResult.success(parsedPatchRequestContent); + } catch (final IOException e) { + LOGGER.debug("Error while parsing JSON patch", e); + return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage("Error in JSON patch: " + e.getMessage())); } } - protected void requestContentInvalid(final RestRequest request, final RestChannel channel, final ToXContent toXContent) { - request.params().clear(); - badRequestResponse(channel, toXContent); + protected final ValidationResult patchEntity( + final RestRequest request, + final JsonNode patchContent, + final SecurityConfiguration securityConfiguration + ) { + final var entityName = securityConfiguration.entityName(); + final var configuration = securityConfiguration.configuration(); + return withIOException( + () -> endpointValidator.isAllowedToChangeImmutableEntity(securityConfiguration) + .map(endpointValidator::entityExists) + .map(ignore -> { + final var configurationAsJson = (ObjectNode) Utils.convertJsonToJackson(configuration, true); + final var entityAsJson = (ObjectNode) configurationAsJson.get(entityName); + return withJsonPatchException( + () -> endpointValidator.createRequestContentValidator(entityName) + .validate(request, JsonPatch.apply(patchContent, entityAsJson)) + .map( + patchedEntity -> endpointValidator.onConfigChange( + SecurityConfiguration.of(patchedEntity, entityName, configuration) + ).map(sc -> ValidationResult.success(patchedEntity)) + ) + .map(patchedEntity -> { + final var updatedConfigurationAsJson = configurationAsJson.deepCopy().set(entityName, patchedEntity); + return ValidationResult.success( + SecurityConfiguration.of( + entityName, + SecurityDynamicConfiguration.fromNode( + updatedConfigurationAsJson, + configuration.getCType(), + configuration.getVersion(), + configuration.getSeqNo(), + configuration.getPrimaryTerm() + ) + ) + ); + }) + ); + }) + ); } - protected void handleDelete(final RestChannel channel, final RestRequest request, final Client client, final JsonNode content) - throws IOException { - final String name = request.param("name"); + protected final ValidationResult patchEntities( + final RestRequest request, + final JsonNode patchContent, + final SecurityConfiguration securityConfiguration + ) { + final var configuration = securityConfiguration.configuration(); + final var configurationAsJson = (ObjectNode) Utils.convertJsonToJackson(configuration, true); + return withIOException(() -> withJsonPatchException(() -> { + final var patchedConfigurationAsJson = JsonPatch.apply(patchContent, configurationAsJson); + for (final var entityName : patchEntityNames(patchContent)) { + final var beforePatchEntity = configurationAsJson.get(entityName); + final var patchedEntity = patchedConfigurationAsJson.get(entityName); + // verify we can process exising or updated entities + if (beforePatchEntity != null && !Objects.equals(beforePatchEntity, patchedEntity)) { + final var checkEntityCanBeProcess = endpointValidator.isAllowedToChangeImmutableEntity( + SecurityConfiguration.of(entityName, configuration) + ); + if (!checkEntityCanBeProcess.isValid()) { + return checkEntityCanBeProcess; + } + } + // entity removed no need to process patched content + if (patchedEntity == null) { + continue; + } + // create or update case of the entity. we need to verify new JSON configuration for them + if ((beforePatchEntity == null) || !Objects.equals(beforePatchEntity, patchedEntity)) { + final var requestCheck = endpointValidator.createRequestContentValidator(entityName).validate(request, patchedEntity); + if (!requestCheck.isValid()) { + return ValidationResult.error(requestCheck.status(), requestCheck.errorMessage()); + } + } + // verify new JSON content for each entity using same set of validator we use for PUT, PATCH and DELETE + final var additionalValidatorCheck = endpointValidator.onConfigChange( + SecurityConfiguration.of(patchedEntity, entityName, configuration) + ); + if (!additionalValidatorCheck.isValid()) { + return additionalValidatorCheck; + } + } + return ValidationResult.success( + SecurityConfiguration.of( + null,// there is no entity name in case of patch, since there could be more the one diff entity within configuration + SecurityDynamicConfiguration.fromNode( + patchedConfigurationAsJson, + configuration.getCType(), + configuration.getVersion(), + configuration.getSeqNo(), + configuration.getPrimaryTerm() + ) + ) + ); + })); + } - if (name == null || name.length() == 0) { - badRequestResponse(channel, "No " + getResourceName() + " specified."); - return; + private ValidationResult withJsonPatchException( + final CheckedSupplier, IOException> action + ) throws IOException { + try { + return action.get(); + } catch (final JsonPatchApplicationException e) { + LOGGER.debug("Error while applying JSON patch", e); + return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage(e.getMessage())); } + } - final SecurityDynamicConfiguration existingConfiguration = load(getConfigName(), false); - - if (!isWriteable(channel, existingConfiguration, name)) { - return; + protected final Set patchOperations(final JsonNode patchRequestContent) { + final var operations = ImmutableSet.builder(); + for (final JsonNode node : patchRequestContent) { + if (node.has("op")) operations.add(node.get("op").asText()); } + return operations.build(); + } - boolean existed = existingConfiguration.exists(name); - existingConfiguration.remove(name); + protected final Set patchEntityNames(final JsonNode patchRequestContent) { + final var patchedResourceNames = ImmutableSet.builder(); + for (final JsonNode node : patchRequestContent) { + if (node.has("path")) { + final var s = JsonPointer.compile(node.get("path").asText()); + patchedResourceNames.add(s.getMatchingProperty()); + } + } + return patchedResourceNames.build(); + } - if (existed) { - AbstractApiAction.saveAndUpdateConfigs( - this.securityIndexName, - client, - getConfigName(), - existingConfiguration, - new OnSucessActionListener(channel) { + protected final ValidationResult processPutRequest(final RestRequest request) throws IOException { + return endpointValidator.withRequiredEntityName(nameParam(request)) + .map(entityName -> loadConfigurationWithRequestContent(entityName, request)) + .map(endpointValidator::onConfigChange) + .map(this::addEntityToConfig); + } - @Override - public void onResponse(IndexResponse response) { - successResponse(channel, "'" + name + "' deleted."); - } - } - ); + protected final ValidationResult addEntityToConfig(final SecurityConfiguration securityConfiguration) + throws IOException { + final var configuration = securityConfiguration.configuration(); + final var entityObjectConfig = Utils.toConfigObject(securityConfiguration.requestContent(), configuration.getImplementingClass()); + configuration.putCObject(securityConfiguration.entityName(), entityObjectConfig); + return ValidationResult.success(securityConfiguration); + } - } else { - notFound(channel, getResourceName() + " " + name + " not found."); - } + final void saveOrUpdateConfiguration( + final Client client, + final SecurityDynamicConfiguration configuration, + final OnSucessActionListener onSucessActionListener + ) { + saveAndUpdateConfigs(securityApiDependencies.securityIndexName(), client, getConfigType(), configuration, onSucessActionListener); } - protected void handlePut(final RestChannel channel, final RestRequest request, final Client client, final JsonNode content) - throws IOException { + protected final String nameParam(final RestRequest request) { final String name = request.param("name"); - if (name == null || name.length() == 0) { - badRequestResponse(channel, "No " + getResourceName() + " specified."); - return; + if (Strings.isNullOrEmpty(name)) { + return null; } - final SecurityDynamicConfiguration existingConfiguration = load(getConfigName(), false); - if (existingConfiguration.getSeqNo() < 0) { - forbidden( - channel, - "Security index need to be updated to support '" + getConfigName().toLCString() + "'. Use SecurityAdmin to populate." + return name; + } + + protected final ValidationResult loadConfigurationWithRequestContent( + final String entityName, + final RestRequest request + ) throws IOException { + return endpointValidator.createRequestContentValidator() + .validate(request) + .map( + content -> loadConfiguration(getConfigType(), false, false).map( + configuration -> ValidationResult.success(SecurityConfiguration.of(content, entityName, configuration)) + ) ); - return; - } + } - if (!isWriteable(channel, existingConfiguration, name)) { - return; - } + protected final ValidationResult loadConfiguration(final String entityName, final boolean logComplianceEvent) + throws IOException { + return loadConfiguration(getConfigType(), false, logComplianceEvent).map( + configuration -> ValidationResult.success(SecurityConfiguration.of(entityName, configuration)) + ); + } - if (isReadonlyFieldUpdated(existingConfiguration, content)) { - conflict(channel, "Attempted to update read-only property."); - return; + protected final ValidationResult> loadConfiguration( + final CType cType, + boolean omitSensitiveData, + final boolean logComplianceEvent + ) { + final var configuration = load(cType, logComplianceEvent); + if (configuration.getSeqNo() < 0) { + return ValidationResult.error( + RestStatus.FORBIDDEN, + forbiddenMessage( + "Security index need to be updated to support '" + getConfigType().toLCString() + "'. Use SecurityAdmin to populate." + ) + ); } - - if (LOGGER.isTraceEnabled() && content != null) { - LOGGER.trace(content.toString()); + if (omitSensitiveData) { + if (!securityApiDependencies.restApiAdminPrivilegesEvaluator().isCurrentUserAdminFor(endpoint)) { + configuration.removeHidden(); + } + configuration.clearHashes(); + configuration.set_meta(null); } + return ValidationResult.success(configuration); + } - boolean existed = existingConfiguration.exists(name); - final Object newContent = DefaultObjectMapper.readTree(content, existingConfiguration.getImplementingClass()); - if (!hasPermissionsToCreate(existingConfiguration, newContent, getResourceName())) { - forbidden(channel, "No permissions"); - return; + protected final ValidationResult> withUserAndRemoteAddress() { + final var userAndRemoteAddress = Utils.userAndRemoteAddressFrom(threadPool.getThreadContext()); + if (userAndRemoteAddress.getLeft() == null) { + return ValidationResult.error(RestStatus.UNAUTHORIZED, payload(RestStatus.UNAUTHORIZED, "Unauthorized")); } - existingConfiguration.putCObject(name, newContent); + return ValidationResult.success(userAndRemoteAddress); + } - AbstractApiAction.saveAndUpdateConfigs( - this.securityIndexName, - client, - getConfigName(), - existingConfiguration, - new OnSucessActionListener(channel) { + protected EndpointValidator createEndpointValidator() { + // Pessimistic Validator. All CRUD actions are forbidden + return new EndpointValidator() { + @Override + public Endpoint endpoint() { + return endpoint; + } - @Override - public void onResponse(IndexResponse response) { - if (existed) { - successResponse(channel, "'" + name + "' updated."); - } else { - createdResponse(channel, "'" + name + "' created."); - } + @Override + public RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator() { + return securityApiDependencies.restApiAdminPrivilegesEvaluator(); + } - } + @Override + public ValidationResult onConfigDelete(SecurityConfiguration securityConfiguration) throws IOException { + return ValidationResult.error(RestStatus.FORBIDDEN, forbiddenMessage("Access denied")); } - ); - } + @Override + public ValidationResult onConfigLoad(SecurityConfiguration securityConfiguration) throws IOException { + return ValidationResult.error(RestStatus.FORBIDDEN, forbiddenMessage("Access denied")); + } - protected void handlePost(final RestChannel channel, final RestRequest request, final Client client, final JsonNode content) - throws IOException { - notImplemented(channel, Method.POST); - } + @Override + public ValidationResult onConfigChange(SecurityConfiguration securityConfiguration) throws IOException { + return ValidationResult.error(RestStatus.FORBIDDEN, forbiddenMessage("Access denied")); + } - protected boolean hasPermissionsToCreate( - final SecurityDynamicConfiguration dynamicConfigFactory, - final Object content, - final String resourceName - ) throws IOException { - return false; + @Override + public RequestContentValidator createRequestContentValidator(Object... params) { + return RequestContentValidator.NOOP_VALIDATOR; + } + }; } - 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); - } + protected abstract CType getConfigType(); protected final SecurityDynamicConfiguration load(final CType config, boolean logComplianceEvent) { - SecurityDynamicConfiguration loaded = cl.getConfigurationsFromIndex(Collections.singleton(config), logComplianceEvent) + SecurityDynamicConfiguration loaded = securityApiDependencies.configurationRepository() + .getConfigurationsFromIndex(List.of(config), logComplianceEvent) .get(config) .deepClone(); return DynamicConfigFactory.addStatics(loaded); } protected boolean ensureIndexExists() { - if (!cs.state().metadata().hasConcreteIndex(this.securityIndexName)) { - return false; - } - return true; + return clusterService.state().metadata().hasConcreteIndex(securityApiDependencies.securityIndexName()); } - protected void filter(SecurityDynamicConfiguration builder) { - if (!isSuperAdmin()) { - builder.removeHidden(); - } - builder.set_meta(null); - } - - protected boolean isReadonlyFieldUpdated(final JsonNode existingResource, final JsonNode targetResource) { - // Default is false. Override function for additional logic - return false; - } - - protected boolean isReadonlyFieldUpdated(final SecurityDynamicConfiguration configuration, final JsonNode targetResource) { - // Default is false. Override function for additional logic - return false; - } - - abstract class OnSucessActionListener implements ActionListener { + abstract static class OnSucessActionListener implements ActionListener { private final RestChannel channel; @@ -330,7 +454,7 @@ public final void onFailure(Exception e) { if (ExceptionsHelper.unwrapCause(e) instanceof VersionConflictEngineException) { conflict(channel, e.getMessage()); } else { - internalErrorResponse(channel, "Error " + e.getMessage()); + internalSeverError(channel, "Error " + e.getMessage()); } } @@ -412,36 +536,39 @@ protected final RestChannelConsumer prepareRequest(RestRequest request, NodeClie // check if .opendistro_security index has been initialized if (!ensureIndexExists()) { - return channel -> internalErrorResponse(channel, RequestContentValidator.ValidationError.SECURITY_NOT_INITIALIZED.message()); + return channel -> internalSeverError(channel, RequestContentValidator.ValidationError.SECURITY_NOT_INITIALIZED.message()); } // check if request is authorized - String authError = restApiPrivilegesEvaluator.checkAccessPermissions(request, getEndpoint()); + final String authError = securityApiDependencies.restApiPrivilegesEvaluator().checkAccessPermissions(request, endpoint); - final User user = (User) threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + final User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); final String userName = user == null ? null : user.getName(); if (authError != null) { LOGGER.error("No permission to access REST API: " + authError); - auditLog.logMissingPrivileges(authError, userName, request); + securityApiDependencies.auditLog().logMissingPrivileges(authError, userName, request); // for rest request request.params().clear(); return channel -> forbidden(channel, "No permission to access REST API: " + authError); } else { - auditLog.logGrantedPrivileges(userName, request); + securityApiDependencies.auditLog().logGrantedPrivileges(userName, request); } - final Object originalUser = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - final Object originalRemoteAddress = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); + final var originalUserAndRemoteAddress = Utils.userAndRemoteAddressFrom(threadPool.getThreadContext()); final Object originalOrigin = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN); return channel -> threadPool.generic().submit(() -> { try (StoredContext ignore = threadPool.getThreadContext().stashContext()) { threadPool.getThreadContext().putHeader(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER, "true"); - threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, originalUser); - threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS, originalRemoteAddress); + threadPool.getThreadContext() + .putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, originalUserAndRemoteAddress.getLeft()); + threadPool.getThreadContext() + .putTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS, originalUserAndRemoteAddress.getRight()); threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN, originalOrigin); - handleApiRequest(channel, request, client); + requestHandlers = Optional.ofNullable(requestHandlers).orElseGet(requestHandlersBuilder::build); + final var requestHandler = requestHandlers.getOrDefault(request.method(), methodNotImplementedHandler); + requestHandler.handle(channel, request, client); } catch (Exception e) { LOGGER.error("Error processing request {}", request, e); try { @@ -453,89 +580,6 @@ protected final RestChannelConsumer prepareRequest(RestRequest request, NodeClie }); } - protected static XContentBuilder convertToJson(RestChannel channel, ToXContent toxContent) { - try { - XContentBuilder builder = channel.newBuilder(); - toxContent.toXContent(builder, ToXContent.EMPTY_PARAMS); - return builder; - } catch (IOException e) { - throw ExceptionsHelper.convertToOpenSearchException(e); - } - } - - protected void response(RestChannel channel, RestStatus status, String message) { - try { - final XContentBuilder builder = channel.newBuilder(); - builder.startObject(); - builder.field("status", status.name()); - builder.field("message", message); - builder.endObject(); - channel.sendResponse(new BytesRestResponse(status, builder)); - } catch (IOException e) { - throw ExceptionsHelper.convertToOpenSearchException(e); - } - } - - protected void successResponse(RestChannel channel, SecurityDynamicConfiguration response) { - channel.sendResponse(new BytesRestResponse(RestStatus.OK, convertToJson(channel, response))); - } - - protected void successResponse(RestChannel channel) { - try { - final XContentBuilder builder = channel.newBuilder(); - builder.startObject(); - builder.endObject(); - channel.sendResponse(new BytesRestResponse(RestStatus.OK, builder)); - } catch (IOException e) { - internalErrorResponse(channel, "Unable to fetch license: " + e.getMessage()); - LOGGER.error("Cannot fetch convert license to XContent due to", e); - } - } - - protected void badRequestResponse(RestChannel channel, ToXContent validationResult) { - channel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, convertToJson(channel, validationResult))); - } - - protected void successResponse(RestChannel channel, String message) { - response(channel, RestStatus.OK, message); - } - - protected void createdResponse(RestChannel channel, String message) { - response(channel, RestStatus.CREATED, message); - } - - protected void badRequestResponse(RestChannel channel, String message) { - response(channel, RestStatus.BAD_REQUEST, message); - } - - protected void notFound(RestChannel channel, String message) { - response(channel, RestStatus.NOT_FOUND, message); - } - - protected void forbidden(RestChannel channel, String message) { - response(channel, RestStatus.FORBIDDEN, message); - } - - protected void internalErrorResponse(RestChannel channel, String message) { - response(channel, RestStatus.INTERNAL_SERVER_ERROR, message); - } - - protected void conflict(RestChannel channel, String message) { - response(channel, RestStatus.CONFLICT, message); - } - - protected void notImplemented(RestChannel channel, Method method) { - response(channel, RestStatus.NOT_IMPLEMENTED, "Method " + method.name() + " not supported for this action."); - } - - protected final boolean isReserved(SecurityDynamicConfiguration configuration, String resourceName) { - return configuration.isStatic(resourceName) || configuration.isReserved(resourceName); - } - - protected final boolean isHidden(SecurityDynamicConfiguration configuration, String resourceName) { - return configuration.isHidden(resourceName) && !isSuperAdmin(); - } - /** * Consume all defined parameters for the request. Before we handle the * request in subclasses where we actually need the parameter, some global @@ -554,57 +598,4 @@ public String getName() { return getClass().getSimpleName(); } - protected abstract Endpoint getEndpoint(); - - protected boolean isSuperAdmin() { - return restApiAdminPrivilegesEvaluator.isCurrentUserRestApiAdminFor(getEndpoint()); - } - - /** - * Resource is readonly if it is reserved and user is not super admin. - * @param existingConfiguration Configuration - * @param name - * @return True if resource readonly - */ - protected boolean isReadOnly(final SecurityDynamicConfiguration existingConfiguration, String name) { - return !isSuperAdmin() && isReserved(existingConfiguration, name); - } - - /** - * Checks if it is valid to add role to opendistro_security_roles or rolesmapping. - * Role can be mapped to user if it exists. Only superadmin can add hidden or reserved roles. - * - * @param channel Rest Channel for response - * @param role Name of the role - * @return True if role can be mapped - */ - protected boolean isValidRolesMapping(final RestChannel channel, final String role) { - final SecurityDynamicConfiguration rolesConfiguration = load(CType.ROLES, false); - final SecurityDynamicConfiguration rolesMappingConfiguration = load(CType.ROLESMAPPING, false); - - if (!rolesConfiguration.exists(role)) { - notFound(channel, "Role '" + role + "' is not available for role-mapping."); - return false; - } - - if (isHidden(rolesConfiguration, role)) { - notFound(channel, "Role '" + role + "' is not available for role-mapping."); - return false; - } - - return isWriteable(channel, rolesMappingConfiguration, role); - } - - boolean isWriteable(final RestChannel channel, final SecurityDynamicConfiguration configuration, final String resourceName) { - if (isHidden(configuration, resourceName)) { - notFound(channel, "Resource '" + resourceName + "' is not available."); - return false; - } - - if (isReadOnly(configuration, resourceName)) { - forbidden(channel, "Resource '" + resourceName + "' is read-only."); - return false; - } - return true; - } } 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 b5d210df5c..e4a1c0d05a 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 @@ -11,49 +11,37 @@ 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 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.apache.commons.lang3.tuple.Triple; 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.common.settings.Settings; -import org.opensearch.core.common.transport.TransportAddress; -import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.core.common.Strings; -import org.opensearch.core.xcontent.XContentBuilder; -import org.opensearch.rest.BytesRestResponse; +import org.opensearch.core.common.transport.TransportAddress; +import org.opensearch.core.rest.RestStatus; import org.opensearch.rest.RestChannel; -import org.opensearch.rest.RestController; -import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestRequest.Method; -import org.opensearch.core.rest.RestStatus; -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.EndpointValidator; 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.dlic.rest.validation.ValidationResult; import org.opensearch.security.securityconf.Hashed; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; -import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.SecurityJsonNode; import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.opensearch.security.dlic.rest.api.Responses.badRequestMessage; +import static org.opensearch.security.dlic.rest.api.Responses.ok; +import static org.opensearch.security.dlic.rest.api.Responses.response; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; import static org.opensearch.security.dlic.rest.support.Utils.hash; @@ -62,42 +50,17 @@ * 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")) ); - private final PrivilegesEvaluator privilegesEvaluator; - private final ThreadContext threadContext; - public AccountApiAction( - Settings settings, - Path configPath, - RestController controller, - Client client, - AdminDNs adminDNs, - ConfigurationRepository cl, - ClusterService cs, - PrincipalExtractor principalExtractor, - PrivilegesEvaluator privilegesEvaluator, - ThreadPool threadPool, - AuditLog auditLog + final ClusterService clusterService, + final ThreadPool threadPool, + final SecurityApiDependencies securityApiDependencies ) { - super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, privilegesEvaluator, threadPool, auditLog); - this.privilegesEvaluator = privilegesEvaluator; - this.threadContext = threadPool.getThreadContext(); - } - - @Override - protected boolean hasPermissionsToCreate( - final SecurityDynamicConfiguration dynamicConfigFactory, - final Object content, - final String resourceName - ) { - return true; + super(Endpoint.ACCOUNT, clusterService, threadPool, securityApiDependencies); + this.requestHandlersBuilder.configureRequestHandlers(this::accountApiRequestHandlers); } @Override @@ -105,121 +68,81 @@ public List routes() { return routes; } - /** - * GET request to fetch account details - * - * Sample request: - * GET _opendistro/_security/api/account - * - * Sample response: - * { - * "user_name" : "test", - * "is_reserved" : false, - * "is_hidden" : false, - * "is_internal_user" : true, - * "user_requested_tenant" : "__user__", - * "backend_roles" : [ ], - * "custom_attribute_names" : [ ], - * "tenants" : { - * "test" : true - * }, - * "roles" : [ - * "own_index" - * ] - * } - * - * @param channel channel to return response - * @param request request to be served - * @param client client - * @param content content body - * @throws IOException - */ @Override - protected void handleGet(RestChannel channel, RestRequest request, Client client, final JsonNode content) throws IOException { - final XContentBuilder builder = channel.newBuilder(); - BytesRestResponse response; - - try { - builder.startObject(); - final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - if (user != null) { - final TransportAddress remoteAddress = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); - final Set securityRoles = privilegesEvaluator.mapRoles(user, remoteAddress); - final SecurityDynamicConfiguration configuration = load(getConfigName(), false); - - builder.field("user_name", user.getName()) - .field("is_reserved", isReserved(configuration, user.getName())) - .field("is_hidden", configuration.isHidden(user.getName())) - .field("is_internal_user", configuration.exists(user.getName())) - .field("user_requested_tenant", user.getRequestedTenant()) - .field("backend_roles", user.getRoles()) - .field("custom_attribute_names", user.getCustomAttributesMap().keySet()) - .field("tenants", privilegesEvaluator.mapTenants(user, securityRoles)) - .field("roles", securityRoles); - } - builder.endObject(); - - response = new BytesRestResponse(RestStatus.OK, builder); - } catch (final Exception exception) { - LOGGER.error(exception.toString()); - - builder.startObject().field("error", exception.toString()).endObject(); - - response = new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, builder); - } - channel.sendResponse(response); + protected CType getConfigType() { + return CType.INTERNALUSERS; } - /** - * PUT request to update account password. - * - * Sample request: - * PUT _opendistro/_security/api/account - * { - * "current_password": "old-pass", - * "password": "new-pass" - * } - * - * Sample response: - * { - * "status":"OK", - * "message":"'test' updated." - * } - * - * @param channel channel to return response - * @param request request to be served - * @param client client - * @param content content body - * @throws IOException - */ - @Override - protected void handlePut(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) - throws IOException { - final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - final String username = user.getName(); - final SecurityDynamicConfiguration internalUser = load(CType.INTERNALUSERS, false); - - if (!internalUser.exists(username)) { - notFound(channel, "Could not find user."); - return; - } - - if (!isWriteable(channel, internalUser, username)) { - return; - } + private void accountApiRequestHandlers(RequestHandler.RequestHandlersBuilder requestHandlersBuilder) { + requestHandlersBuilder.allMethodsNotImplemented() + .override( + Method.GET, + (channel, request, client) -> withUserAndRemoteAddress().map( + userAndRemoteAddress -> loadConfiguration(getConfigType(), false, false).map( + configuration -> ValidationResult.success( + Triple.of(userAndRemoteAddress.getLeft(), userAndRemoteAddress.getRight(), configuration) + ) + ) + ).valid(userRemoteAddressAndConfig -> { + final var user = userRemoteAddressAndConfig.getLeft(); + final var remoteAddress = userRemoteAddressAndConfig.getMiddle(); + final var configuration = userRemoteAddressAndConfig.getRight(); + userAccount(channel, user, remoteAddress, configuration); + }).error((status, toXContent) -> response(channel, status, toXContent)) + ) + .onChangeRequest( + Method.PUT, + request -> withUserAndRemoteAddress().map( + userAndRemoteAddress -> loadConfigurationWithRequestContent(userAndRemoteAddress.getLeft().getName(), request) + ) + .map(endpointValidator::entityExists) + .map(endpointValidator::onConfigChange) + .map(this::validCurrentPassword) + .map(this::updatePassword) + ); + } - final SecurityJsonNode securityJsonNode = new SecurityJsonNode(content); - final String currentPassword = content.get("current_password").asText(); - final Hashed internalUserEntry = (Hashed) internalUser.getCEntry(username); - final String currentHash = internalUserEntry.getHash(); + private void userAccount( + final RestChannel channel, + final User user, + final TransportAddress remoteAddress, + final SecurityDynamicConfiguration configuration + ) { + final var securityRoles = securityApiDependencies.privilegesEvaluator().mapRoles(user, remoteAddress); + ok( + channel, + (builder, params) -> builder.startObject() + .field("user_name", user.getName()) + .field("is_reserved", configuration.isReserved(user.getName())) + .field("is_hidden", configuration.isHidden(user.getName())) + .field("is_internal_user", configuration.exists(user.getName())) + .field("user_requested_tenant", user.getRequestedTenant()) + .field("backend_roles", user.getRoles()) + .field("custom_attribute_names", user.getCustomAttributesMap().keySet()) + .field("tenants", securityApiDependencies.privilegesEvaluator().mapTenants(user, securityRoles)) + .field("roles", securityRoles) + .endObject() + ); + } + ValidationResult validCurrentPassword(final SecurityConfiguration securityConfiguration) { + final var username = securityConfiguration.entityName(); + final var content = securityConfiguration.requestContent(); + final var currentPassword = content.get("current_password").asText(); + final var internalUserEntry = (Hashed) securityConfiguration.configuration().getCEntry(username); + final var currentHash = internalUserEntry.getHash(); if (currentHash == null || !OpenBSDBCrypt.checkPassword(currentHash, currentPassword.toCharArray())) { - badRequestResponse(channel, "Could not validate your current password."); - return; + return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage("Could not validate your current password.")); } + return ValidationResult.success(securityConfiguration); + } + ValidationResult updatePassword(final SecurityConfiguration securityConfiguration) { + final var username = securityConfiguration.entityName(); + final var securityJsonNode = new SecurityJsonNode(securityConfiguration.requestContent()); + final var internalUserEntry = (Hashed) securityConfiguration.configuration().getCEntry(username); // if password is set, it takes precedence over hash - final String password = securityJsonNode.get("password").asString(); + final var password = securityJsonNode.get("password").asString(); final String hash; if (Strings.isNullOrEmpty(password)) { hash = securityJsonNode.get("hash").asString(); @@ -227,73 +150,55 @@ protected void handlePut(RestChannel channel, final RestRequest request, final C hash = hash(password.toCharArray()); } if (Strings.isNullOrEmpty(hash)) { - badRequestResponse(channel, "Both provided password and hash cannot be null/empty."); - return; + return ValidationResult.error( + RestStatus.BAD_REQUEST, + badRequestMessage("Both provided password and hash cannot be null/empty.") + ); } - internalUserEntry.setHash(hash); - - AccountApiAction.saveAndUpdateConfigs( - this.securityIndexName, - client, - CType.INTERNALUSERS, - internalUser, - new OnSucessActionListener(channel) { - @Override - public void onResponse(IndexResponse response) { - successResponse(channel, "'" + username + "' updated."); - } - } - ); + return ValidationResult.success(securityConfiguration); } @Override - protected RequestContentValidator createValidator(final Object... params) { - final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - return RequestContentValidator.of(new RequestContentValidator.ValidationContext() { - @Override - public Object[] params() { - return new Object[] { user.getName() }; - } + protected EndpointValidator createEndpointValidator() { + return new EndpointValidator() { @Override - public Settings settings() { - return settings; + public Endpoint endpoint() { + return endpoint; } @Override - public Set mandatoryKeys() { - return ImmutableSet.of("current_password"); + public RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator() { + return securityApiDependencies.restApiAdminPrivilegesEvaluator(); } @Override - public Map allowedKeys() { - return ImmutableMap.of("hash", DataType.STRING, "password", DataType.STRING, "current_password", DataType.STRING); + public RequestContentValidator createRequestContentValidator(Object... params) { + final User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + return RequestContentValidator.of(new RequestContentValidator.ValidationContext() { + @Override + public Object[] params() { + return new Object[] { user.getName() }; + } + + @Override + public Settings settings() { + return securityApiDependencies.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 - protected String getResourceName() { - return RESOURCE_NAME; - } - - @Override - protected Endpoint getEndpoint() { - return Endpoint.ACCOUNT; - } - - @Override - protected void filter(SecurityDynamicConfiguration builder) { - super.filter(builder); - // replace password hashes in addition. We must not remove them from the - // Builder since this would remove users completely if they - // do not have any addition properties like roles or attributes - builder.clearHashes(); + }; } - @Override - protected CType getConfigName() { - return CType.INTERNALUSERS; - } } 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 dee97dfa39..5eb4f202bf 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,39 +11,34 @@ package org.opensearch.security.dlic.rest.api; -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.common.inject.Inject; import org.opensearch.common.settings.Settings; -import org.opensearch.rest.RestChannel; -import org.opensearch.rest.RestController; -import org.opensearch.rest.RestRequest; +import org.opensearch.core.rest.RestStatus; import org.opensearch.rest.RestRequest.Method; -import org.opensearch.security.DefaultObjectMapper; -import org.opensearch.security.auditlog.AuditLog; -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.EndpointValidator; 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.dlic.rest.validation.ValidationResult; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; -import org.opensearch.security.ssl.transport.PrincipalExtractor; +import org.opensearch.security.securityconf.impl.v7.ActionGroupsV7; 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.api.RequestHandler.methodNotImplementedHandler; +import static org.opensearch.security.dlic.rest.api.Responses.badRequestMessage; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; -public class ActionGroupsApiAction extends PatchableResourceApiAction { +public class ActionGroupsApiAction extends AbstractApiAction { private static final List routes = addRoutesPrefix( ImmutableList.of( @@ -65,26 +60,14 @@ public class ActionGroupsApiAction extends PatchableResourceApiAction { ) ); - @Override - protected Endpoint getEndpoint() { - return Endpoint.ACTIONGROUPS; - } - @Inject public ActionGroupsApiAction( - final Settings settings, - final Path configPath, - final RestController controller, - final Client client, - final AdminDNs adminDNs, - final ConfigurationRepository cl, - final ClusterService cs, - final PrincipalExtractor principalExtractor, - final PrivilegesEvaluator evaluator, - ThreadPool threadPool, - AuditLog auditLog + final ClusterService clusterService, + final ThreadPool threadPool, + final SecurityApiDependencies securityApiDependencies ) { - super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); + super(Endpoint.ACTIONGROUPS, clusterService, threadPool, securityApiDependencies); + this.requestHandlersBuilder.configureRequestHandlers(this::actionGroupsApiRequestHandlers); } @Override @@ -93,102 +76,119 @@ public List routes() { } @Override - protected RequestContentValidator createValidator(final Object... params) { - return RequestContentValidator.of(new RequestContentValidator.ValidationContext() { + protected CType getConfigType() { + return CType.ACTIONGROUPS; + } + + private void actionGroupsApiRequestHandlers(RequestHandler.RequestHandlersBuilder requestHandlersBuilder) { + requestHandlersBuilder.onChangeRequest(Method.PATCH, this::processPatchRequest).override(Method.POST, methodNotImplementedHandler); + } + + @Override + protected EndpointValidator createEndpointValidator() { + return new EndpointValidator() { @Override - public Object[] params() { - return params; + public Endpoint endpoint() { + return endpoint; } @Override - public Settings settings() { - return settings; + public RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator() { + return securityApiDependencies.restApiAdminPrivilegesEvaluator(); } @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(); + public ValidationResult onConfigChange(SecurityConfiguration securityConfiguration) throws IOException { + return EndpointValidator.super.onConfigChange(securityConfiguration).map(this::actionGroupNameIsNotSameAsRoleName) + .map(this::hasSelfReference); } @Override - public Set mandatoryKeys() { - return ImmutableSet.of("allowed_actions"); + public ValidationResult isAllowedToChangeImmutableEntity(SecurityConfiguration securityConfiguration) + throws IOException { + return EndpointValidator.super.isAllowedToChangeImmutableEntity(securityConfiguration).map( + this::isAllowedToChangeEntityWithRestAdminPermissions + ); } - }); - } - @Override - protected CType getConfigName() { - return CType.ACTIONGROUPS; - } + private ValidationResult actionGroupNameIsNotSameAsRoleName( + final SecurityConfiguration securityConfiguration + ) throws IOException { + // Prevent the case where action group and role share a same name. + return loadConfiguration(CType.ROLES, false, false).map( + rolesConfiguration -> actionGroupNameIsNotSameAsRoleName(securityConfiguration, rolesConfiguration) + ); + } - @Override - protected String getResourceName() { - return "actiongroup"; - } + private ValidationResult actionGroupNameIsNotSameAsRoleName( + final SecurityConfiguration securityConfiguration, + final SecurityDynamicConfiguration rolesConfiguration + ) { + if (rolesConfiguration.getCEntries().containsKey(securityConfiguration.entityName())) { + return ValidationResult.error( + RestStatus.BAD_REQUEST, + badRequestMessage( + securityConfiguration.entityName() + + " is an existing role. A action group cannot be named with an existing role name." + ) + ); + } + return ValidationResult.success(securityConfiguration); + } - @Override - protected void consumeParameters(final RestRequest request) { - request.param("name"); - } + private ValidationResult hasSelfReference(final SecurityConfiguration securityConfiguration) + throws IOException { + // Prevent the case where action group references to itself in the allowed_actions. + final var actionGroups = (ActionGroupsV7) Utils.toConfigObject( + securityConfiguration.requestContent(), + securityConfiguration.configuration().getImplementingClass() + ); + if (hasSelfReference(securityConfiguration.entityName(), actionGroups)) { + return ValidationResult.error( + RestStatus.BAD_REQUEST, + badRequestMessage(securityConfiguration.entityName() + " cannot be an allowed_action of itself") + ); + } + return ValidationResult.success(securityConfiguration); + } - @Override - protected void handlePut(RestChannel channel, RestRequest request, Client client, JsonNode content) throws IOException { - final String name = request.param("name"); - - if (name == null || name.length() == 0) { - badRequestResponse(channel, "No " + getResourceName() + " specified."); - return; - } - - // Prevent the case where action group and role share a same name. - SecurityDynamicConfiguration existingRolesConfig = load(CType.ROLES, false); - Set existingRoles = existingRolesConfig.getCEntries().keySet(); - if (existingRoles.contains(name)) { - badRequestResponse(channel, name + " is an existing role. A action group cannot be named with an existing role name."); - return; - } - - // Prevent the case where action group references to itself in the allowed_actions. - final SecurityDynamicConfiguration existingActionGroupsConfig = load(getConfigName(), false); - final Object actionGroup = DefaultObjectMapper.readTree(content, existingActionGroupsConfig.getImplementingClass()); - existingActionGroupsConfig.putCObject(name, actionGroup); - if (hasActionGroupSelfReference(existingActionGroupsConfig, name)) { - badRequestResponse(channel, name + " cannot be an allowed_action of itself"); - return; - } - // prevent creation of groups for REST admin api - if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(actionGroup)) { - forbidden(channel, "Not allowed"); - return; - } - super.handlePut(channel, request, client, content); - } + private boolean hasSelfReference(final String name, final ActionGroupsV7 actionGroups) { + List allowedActions = actionGroups.getAllowed_actions(); + return allowedActions.contains(name); + } - @Override - protected boolean hasPermissionsToCreate( - final SecurityDynamicConfiguration dynamicConfiguration, - final Object content, - final String resourceName - ) throws IOException { - if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(content)) { - return false; - } - return true; + @Override + public RequestContentValidator createRequestContentValidator(Object... params) { + return RequestContentValidator.of(new RequestContentValidator.ValidationContext() { + @Override + public Object[] params() { + return params; + } + + @Override + public Settings settings() { + return securityApiDependencies.settings(); + } + + @Override + public Map allowedKeys() { + final ImmutableMap.Builder allowedKeys = ImmutableMap.builder(); + if (isCurrentUserAdmin()) { + 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 - protected boolean isReadOnly(SecurityDynamicConfiguration existingConfiguration, String name) { - if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(existingConfiguration.getCEntry(name))) { - return true; - } - return super.isReadOnly(existingConfiguration, name); - } } 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 a6b93f07dd..349247ee01 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,36 +11,25 @@ package org.opensearch.security.dlic.rest.api; -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.common.inject.Inject; import org.opensearch.common.settings.Settings; -import org.opensearch.rest.RestChannel; -import org.opensearch.rest.RestController; import org.opensearch.rest.RestRequest; -import org.opensearch.security.DefaultObjectMapper; -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.EndpointValidator; 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.security.support.ConfigConstants; 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; +import static org.opensearch.security.dlic.rest.api.RequestHandler.methodNotImplementedHandler; + /** * This class implements GET and PUT operations to manage dynamic AllowlistingSettings. *

@@ -81,97 +70,33 @@ * be used to populate the index. *

*/ -public class AllowlistApiAction extends PatchableResourceApiAction { +public class AllowlistApiAction extends AbstractApiAction { + private static final List routes = ImmutableList.of( new Route(RestRequest.Method.GET, "/_plugins/_security/api/allowlist"), new Route(RestRequest.Method.PUT, "/_plugins/_security/api/allowlist"), new Route(RestRequest.Method.PATCH, "/_plugins/_security/api/allowlist") ); - private static final String name = "config"; - @Inject public AllowlistApiAction( - final Settings settings, - final Path configPath, - final RestController controller, - final Client client, - final AdminDNs adminDNs, - final ConfigurationRepository cl, - final ClusterService cs, - final PrincipalExtractor principalExtractor, - final PrivilegesEvaluator evaluator, - ThreadPool threadPool, - AuditLog auditLog - ) { - super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); - } - - @Override - protected boolean hasPermissionsToCreate( - final SecurityDynamicConfiguration dynamicConfigFactory, - final Object content, - final String resourceName + final Endpoint endpoint, + final ClusterService clusterService, + final ThreadPool threadPool, + final SecurityApiDependencies securityApiDependencies ) { - return true; - } - - @Override - protected void handleApiRequest(final RestChannel channel, final RestRequest request, final Client client) throws IOException { - if (!isSuperAdmin()) { - forbidden(channel, "API allowed only for super admin."); - return; - } - super.handleApiRequest(channel, request, client); - } - - @Override - protected void handleGet(final RestChannel channel, RestRequest request, Client client, final JsonNode content) throws IOException { - - final SecurityDynamicConfiguration configuration = load(getConfigName(), true); - filter(configuration); - successResponse(channel, configuration); - } - - @Override - protected void handleDelete(final RestChannel channel, final RestRequest request, final Client client, final JsonNode content) - throws IOException { - notImplemented(channel, RestRequest.Method.DELETE); + super(endpoint, clusterService, threadPool, securityApiDependencies); + this.requestHandlersBuilder.configureRequestHandlers(this::allowListApiRequestHandlers); } - @Override - protected void handlePut(final RestChannel channel, final RestRequest request, final Client client, final JsonNode content) - throws IOException { - final SecurityDynamicConfiguration existingConfiguration = load(getConfigName(), false); - - if (existingConfiguration.getSeqNo() < 0) { - forbidden( - channel, - "Security index need to be updated to support '" + getConfigName().toLCString() + "'. Use SecurityAdmin to populate." - ); - return; - } - - boolean existed = existingConfiguration.exists(name); - existingConfiguration.putCObject(name, DefaultObjectMapper.readTree(content, existingConfiguration.getImplementingClass())); - - saveAndUpdateConfigs( - this.securityIndexName, - client, - getConfigName(), - existingConfiguration, - new OnSucessActionListener(channel) { - - @Override - public void onResponse(IndexResponse response) { - if (existed) { - successResponse(channel, "'" + name + "' updated."); - } else { - createdResponse(channel, "'" + name + "' created."); - } - } - } - ); + private void allowListApiRequestHandlers(RequestHandler.RequestHandlersBuilder requestHandlersBuilder) { + requestHandlersBuilder.verifyAccessForAllMethods() + .onChangeRequest(RestRequest.Method.PATCH, this::processPatchRequest) + .onChangeRequest( + RestRequest.Method.PUT, + request -> loadConfigurationWithRequestContent("config", request).map(this::addEntityToConfig) + ) + .override(RestRequest.Method.DELETE, methodNotImplementedHandler); } @Override @@ -180,38 +105,44 @@ public List routes() { } @Override - protected Endpoint getEndpoint() { - return Endpoint.ALLOWLIST; + protected CType getConfigType() { + return CType.ALLOWLIST; } @Override - protected RequestContentValidator createValidator(Object... params) { - return RequestContentValidator.of(new RequestContentValidator.ValidationContext() { + protected EndpointValidator createEndpointValidator() { + return new EndpointValidator() { + @Override - public Object[] params() { - return params; + public Endpoint endpoint() { + return endpoint; } @Override - public Settings settings() { - return settings; + public RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator() { + return securityApiDependencies.restApiAdminPrivilegesEvaluator(); } @Override - public Map allowedKeys() { - return ImmutableMap.of("enabled", DataType.BOOLEAN, "requests", DataType.OBJECT); - } - }); - } + public RequestContentValidator createRequestContentValidator(Object... params) { + return RequestContentValidator.of(new RequestContentValidator.ValidationContext() { + @Override + public Object[] params() { + return params; + } - @Override - protected String getResourceName() { - return name; - } + @Override + public Settings settings() { + return securityApiDependencies.settings(); + } - @Override - protected CType getConfigName() { - return CType.ALLOWLIST; + @Override + public Map allowedKeys() { + return ImmutableMap.of("enabled", DataType.BOOLEAN, "requests", DataType.OBJECT); + } + }); + } + }; } } 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 61982eb296..00808675ac 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 @@ -17,36 +17,31 @@ 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.common.settings.Settings; -import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.rest.RestChannel; -import org.opensearch.rest.RestController; +import org.opensearch.core.rest.RestStatus; import org.opensearch.rest.RestRequest; 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.EndpointValidator; 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.api.RequestHandler.methodNotImplementedHandler; +import static org.opensearch.security.dlic.rest.api.Responses.badRequestMessage; +import static org.opensearch.security.dlic.rest.api.Responses.conflictMessage; +import static org.opensearch.security.dlic.rest.api.Responses.methodNotImplementedMessage; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; /** @@ -126,7 +121,7 @@ * [{"op": "replace", "path": "/config/audit/enable_rest", "value": "true"}] * [{"op": "replace", "path": "/config/compliance/internal_config", "value": "true"}] */ -public class AuditApiAction extends PatchableResourceApiAction { +public class AuditApiAction extends AbstractApiAction { private static final List routes = addRoutesPrefix( ImmutableList.of( new Route(RestRequest.Method.GET, "/audit/"), @@ -135,14 +130,11 @@ public class AuditApiAction extends PatchableResourceApiAction { ) ); - private static final String RESOURCE_NAME = "config"; @VisibleForTesting public static final String READONLY_FIELD = "_readonly"; @VisibleForTesting public static final String STATIC_RESOURCE = "/static_config/static_audit.yml"; private final List readonlyFields; - private final PrivilegesEvaluator privilegesEvaluator; - private final ThreadContext threadContext; public static class AuditRequestContentValidator extends RequestContentValidator { private static final Set DISABLED_REST_CATEGORIES = ImmutableSet.of( @@ -170,16 +162,16 @@ protected AuditRequestContentValidator(ValidationContext validationContext) { } @Override - public ValidationResult validate(RestRequest request) throws IOException { + public ValidationResult validate(RestRequest request) throws IOException { return super.validate(request).map(this::validateAuditPayload); } @Override - public ValidationResult validate(RestRequest request, JsonNode jsonContent) throws IOException { + public ValidationResult validate(RestRequest request, JsonNode jsonContent) throws IOException { return super.validate(request, jsonContent).map(this::validateAuditPayload); } - private ValidationResult validateAuditPayload(final JsonNode jsonContent) { + private ValidationResult validateAuditPayload(final JsonNode jsonContent) { try { // try parsing to target type final AuditConfig auditConfig = DefaultObjectMapper.readTree(jsonContent, AuditConfig.class); @@ -195,48 +187,45 @@ private ValidationResult validateAuditPayload(final JsonNode jsonContent) { // 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); + return ValidationResult.error(RestStatus.BAD_REQUEST, this); } } } public AuditApiAction( - final Settings settings, - final Path configPath, - final RestController controller, - final Client client, - final AdminDNs adminDNs, - final ConfigurationRepository cl, - final ClusterService cs, - final PrincipalExtractor principalExtractor, - final PrivilegesEvaluator privilegesEvaluator, + final ClusterService clusterService, final ThreadPool threadPool, - final AuditLog auditLog + final SecurityApiDependencies securityApiDependencies ) { - super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, privilegesEvaluator, threadPool, auditLog); - this.privilegesEvaluator = privilegesEvaluator; - this.threadContext = threadPool.getThreadContext(); + this(clusterService, threadPool, securityApiDependencies, readReadonlyFieldsFromFile()); + } + + private static List readReadonlyFieldsFromFile() { try { - this.readonlyFields = DefaultObjectMapper.YAML_MAPPER.readValue( - this.getClass().getResourceAsStream(STATIC_RESOURCE), + final var readonlyFields = DefaultObjectMapper.YAML_MAPPER.readValue( + AuditApiAction.class.getResourceAsStream(STATIC_RESOURCE), new TypeReference>>() { } ).get(READONLY_FIELD); - if (!AuditConfig.FIELD_PATHS.containsAll(this.readonlyFields)) { + if (!AuditConfig.FIELD_PATHS.containsAll(readonlyFields)) { throw new StaticResourceException("Invalid read-only field paths provided in static resource file " + STATIC_RESOURCE); } + return readonlyFields; + } catch (IOException e) { throw new StaticResourceException("Unable to load audit static resource file", e); } } - @Override - protected boolean hasPermissionsToCreate( - final SecurityDynamicConfiguration dynamicConfigFactory, - final Object content, - final String resourceName + protected AuditApiAction( + final ClusterService clusterService, + final ThreadPool threadPool, + final SecurityApiDependencies securityApiDependencies, + final List readonlyFields ) { - return true; + super(Endpoint.AUDIT, clusterService, threadPool, securityApiDependencies); + this.readonlyFields = readonlyFields; + this.requestHandlersBuilder.configureRequestHandlers(this::auditApiRequestHandlers); } @Override @@ -245,96 +234,94 @@ public List routes() { } @Override - protected void handleApiRequest(final RestChannel channel, final RestRequest request, final Client client) throws IOException { - // if audit config doc is not available in security index, - // disable audit APIs - if (!cl.isAuditHotReloadingEnabled()) { - notImplemented(channel, request.method()); - return; - } - super.handleApiRequest(channel, request, client); + protected CType getConfigType() { + return CType.AUDIT; } - @Override - protected void handlePut(final RestChannel channel, final RestRequest request, final Client client, final JsonNode content) - throws IOException { - if (!RESOURCE_NAME.equals(request.param("name"))) { - badRequestResponse(channel, "name must be config"); - return; - } - super.handlePut(channel, request, client, content); + private void auditApiRequestHandlers(RequestHandler.RequestHandlersBuilder requestHandlersBuilder) { + requestHandlersBuilder.onGetRequest( + request -> withEnabledAuditApi(request).map(this::processGetRequest).map(securityConfiguration -> { + final var configuration = securityConfiguration.configuration(); + configuration.putCObject(READONLY_FIELD, readonlyFields); + return ValidationResult.success(securityConfiguration); + }) + ) + .onChangeRequest(RestRequest.Method.PATCH, request -> withEnabledAuditApi(request).map(this::processPatchRequest)) + .onChangeRequest( + RestRequest.Method.PUT, + request -> withEnabledAuditApi(request).map(this::withConfigEntityNameOnly).map(ignore -> processPutRequest(request)) + ) + .override(RestRequest.Method.POST, methodNotImplementedHandler) + .override(RestRequest.Method.DELETE, methodNotImplementedHandler); } - @Override - protected void handleGet(final RestChannel channel, RestRequest request, Client client, final JsonNode content) { - final SecurityDynamicConfiguration configuration = load(getConfigName(), true); - filter(configuration); - - final String resourcename = getResourceName(); - if (!configuration.exists(resourcename)) { - notFound(channel, "Resource '" + resourcename + "' not found."); - return; + ValidationResult withEnabledAuditApi(final RestRequest request) { + if (!securityApiDependencies.configurationRepository().isAuditHotReloadingEnabled()) { + return ValidationResult.error(RestStatus.NOT_IMPLEMENTED, methodNotImplementedMessage(request.method())); } - - configuration.putCObject(READONLY_FIELD, readonlyFields); - successResponse(channel, configuration); + return ValidationResult.success(request); } - @Override - protected void handlePost(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) { - notImplemented(channel, RestRequest.Method.POST); + ValidationResult withConfigEntityNameOnly(final RestRequest request) { + final var name = nameParam(request); + if (!"config".equals(name)) { + return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage("name must be config")); + } + return ValidationResult.success(name); } @Override - protected void handleDelete(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) { - notImplemented(channel, RestRequest.Method.DELETE); - } + protected EndpointValidator createEndpointValidator() { + return new EndpointValidator() { - @Override - protected RequestContentValidator createValidator(final Object... params) { - return new AuditRequestContentValidator(new RequestContentValidator.ValidationContext() { @Override - public Object[] params() { - return params; + public Endpoint endpoint() { + return endpoint; } @Override - public Settings settings() { - return settings; + public RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator() { + return securityApiDependencies.restApiAdminPrivilegesEvaluator(); } @Override - public Map allowedKeys() { - return ImmutableMap.of("enabled", DataType.BOOLEAN, "audit", DataType.OBJECT, "compliance", DataType.OBJECT); + public ValidationResult onConfigChange(SecurityConfiguration securityConfiguration) throws IOException { + return EndpointValidator.super.onConfigChange(securityConfiguration).map(this::verifyNotReadonlyFieldUpdated); } - }); - } - @Override - protected String getResourceName() { - return RESOURCE_NAME; - } + private ValidationResult verifyNotReadonlyFieldUpdated( + final SecurityConfiguration securityConfiguration + ) { + if (!isCurrentUserAdmin()) { + final var existingResource = Utils.convertJsonToJackson(securityConfiguration.configuration(), false).get("config"); + final var targetResource = securityConfiguration.requestContent(); + if (readonlyFields.stream().anyMatch(path -> !existingResource.at(path).equals(targetResource.at(path)))) { + return ValidationResult.error(RestStatus.CONFLICT, conflictMessage("Attempted to update read-only property.")); + } + } + return ValidationResult.success(securityConfiguration); + } - @Override - protected Endpoint getEndpoint() { - return Endpoint.AUDIT; - } + @Override + public RequestContentValidator createRequestContentValidator(Object... params) { + return new AuditRequestContentValidator(new RequestContentValidator.ValidationContext() { + @Override + public Object[] params() { + return params; + } - @Override - protected CType getConfigName() { - return CType.AUDIT; - } + @Override + public Settings settings() { + return securityApiDependencies.settings(); + } - @Override - protected boolean isReadonlyFieldUpdated(final JsonNode existingResource, final JsonNode targetResource) { - if (!isSuperAdmin()) { - return readonlyFields.stream().anyMatch(path -> !existingResource.at(path).equals(targetResource.at(path))); - } - return false; + @Override + public Map allowedKeys() { + return ImmutableMap.of("enabled", DataType.BOOLEAN, "audit", DataType.OBJECT, "compliance", DataType.OBJECT); + } + }); + } + }; } - @Override - protected boolean isReadonlyFieldUpdated(final SecurityDynamicConfiguration configuration, final JsonNode targetResource) { - return isReadonlyFieldUpdated(Utils.convertJsonToJackson(configuration, false).get(getResourceName()), targetResource); - } } 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 dd9591f22e..e124abb5f7 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 @@ -11,31 +11,16 @@ package org.opensearch.security.dlic.rest.api; -import java.io.IOException; -import java.nio.file.Path; -import java.util.Collections; -import java.util.List; - -import com.fasterxml.jackson.databind.JsonNode; - -import org.opensearch.client.Client; import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.inject.Inject; -import org.opensearch.common.settings.Settings; -import org.opensearch.rest.RestChannel; -import org.opensearch.rest.RestController; -import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestRequest.Method; -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.RequestContentValidator; -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.util.Collections; +import java.util.List; + +import static org.opensearch.security.dlic.rest.api.Responses.ok; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; public class AuthTokenProcessorAction extends AbstractApiAction { @@ -43,28 +28,14 @@ public class AuthTokenProcessorAction extends AbstractApiAction { @Inject public AuthTokenProcessorAction( - final Settings settings, - final Path configPath, - final RestController controller, - final Client client, - final AdminDNs adminDNs, - final ConfigurationRepository cl, - final ClusterService cs, - final PrincipalExtractor principalExtractor, - final PrivilegesEvaluator evaluator, - ThreadPool threadPool, - AuditLog auditLog - ) { - super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); - } - - @Override - protected boolean hasPermissionsToCreate( - final SecurityDynamicConfiguration dynamicConfigFactory, - final Object content, - final String resourceName + final ClusterService clusterService, + final ThreadPool threadPool, + final SecurityApiDependencies securityApiDependencies ) { - return true; + super(Endpoint.AUTHTOKEN, clusterService, threadPool, securityApiDependencies); + this.requestHandlersBuilder.configureRequestHandlers( + builder -> builder.allMethodsNotImplemented().override(Method.POST, (channel, request, client) -> ok(channel, "")) + ); } @Override @@ -73,34 +44,10 @@ public List routes() { } @Override - protected void handlePost(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) - throws IOException { - - // Just do nothing here. Eligible authenticators will intercept calls and - // provide own responses. - successResponse(channel, ""); - } - - @Override - protected RequestContentValidator createValidator(Object... params) { - return RequestContentValidator.NOOP_VALIDATOR; - } - - @Override - protected String getResourceName() { - return "authtoken"; - } - - @Override - protected CType getConfigName() { + protected CType getConfigType() { return null; } - @Override - protected Endpoint getEndpoint() { - return Endpoint.AUTHTOKEN; - } - public static class Response { private String authorization; 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 9c36b971e7..640e52df6e 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 @@ -11,37 +11,24 @@ 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 org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.client.Client; import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.inject.Inject; -import org.opensearch.common.settings.Settings; import org.opensearch.core.action.ActionListener; -import org.opensearch.rest.RestChannel; -import org.opensearch.rest.RestController; import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestRequest.Method; import org.opensearch.security.action.configupdate.ConfigUpdateAction; 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.RequestContentValidator; -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.util.List; + +import static org.opensearch.security.dlic.rest.api.Responses.internalSeverError; +import static org.opensearch.security.dlic.rest.api.Responses.ok; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; public class FlushCacheApiAction extends AbstractApiAction { @@ -59,28 +46,12 @@ public class FlushCacheApiAction extends AbstractApiAction { @Inject public FlushCacheApiAction( - final Settings settings, - final Path configPath, - final RestController controller, - final Client client, - final AdminDNs adminDNs, - final ConfigurationRepository cl, - final ClusterService cs, - final PrincipalExtractor principalExtractor, - final PrivilegesEvaluator evaluator, - ThreadPool threadPool, - AuditLog auditLog - ) { - super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); - } - - @Override - protected boolean hasPermissionsToCreate( - final SecurityDynamicConfiguration dynamicConfigFactory, - final Object content, - final String resourceName + final ClusterService clusterService, + final ThreadPool threadPool, + final SecurityApiDependencies securityApiDependencies ) { - return true; + super(Endpoint.CACHE, clusterService, threadPool, securityApiDependencies); + this.requestHandlersBuilder.configureRequestHandlers(this::flushCacheApiRequestHandlers); } @Override @@ -88,73 +59,42 @@ public List routes() { return routes; } - @Override - protected Endpoint getEndpoint() { - return Endpoint.CACHE; - } - - @Override - protected void handleDelete(RestChannel channel, RestRequest request, Client client, final JsonNode content) throws IOException { + private void flushCacheApiRequestHandlers(RequestHandler.RequestHandlersBuilder requestHandlersBuilder) { + requestHandlersBuilder.allMethodsNotImplemented() + .override( + Method.DELETE, + (channel, request, client) -> client.execute( + ConfigUpdateAction.INSTANCE, + new ConfigUpdateRequest(CType.lcStringValues().toArray(new String[0])), + new ActionListener<>() { + + @Override + public void onResponse(ConfigUpdateResponse configUpdateResponse) { + if (configUpdateResponse.hasFailures()) { + LOGGER.error("Cannot flush cache due to", configUpdateResponse.failures().get(0)); + internalSeverError( + channel, + "Cannot flush cache due to " + configUpdateResponse.failures().get(0).getMessage() + "." + ); + return; + } + LOGGER.debug("cache flushed successfully"); + ok(channel, "Cache flushed successfully."); + } + + @Override + public void onFailure(final Exception e) { + LOGGER.error("Cannot flush cache due to", e); + internalSeverError(channel, "Cannot flush cache due to " + e.getMessage() + "."); + } - client.execute( - ConfigUpdateAction.INSTANCE, - new ConfigUpdateRequest(CType.lcStringValues().toArray(new String[0])), - new ActionListener() { - - @Override - public void onResponse(ConfigUpdateResponse ur) { - if (ur.hasFailures()) { - 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 (LOGGER.isDebugEnabled()) { - LOGGER.debug("cache flushed successfully"); } - } - - @Override - public void onFailure(Exception e) { - LOGGER.error("Cannot flush cache due to", e); - internalErrorResponse(channel, "Cannot flush cache due to " + e.getMessage() + "."); - } - - } - ); - } - - @Override - protected void handlePost(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) - throws IOException { - notImplemented(channel, Method.POST); - } - - @Override - protected void handleGet(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) - throws IOException { - notImplemented(channel, Method.GET); - } - - @Override - protected void handlePut(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) - throws IOException { - notImplemented(channel, Method.PUT); - } - - @Override - protected RequestContentValidator createValidator(final Object... params) { - return RequestContentValidator.NOOP_VALIDATOR; - } - - @Override - protected String getResourceName() { - // not needed - return null; + ) + ); } @Override - protected CType getConfigName() { + protected CType getConfigType() { return null; } 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 201d599117..14036918b3 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 @@ -11,47 +11,43 @@ 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.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.common.inject.Inject; import org.opensearch.common.settings.Settings; +import org.opensearch.core.common.Strings; +import org.opensearch.core.rest.RestStatus; import org.opensearch.rest.RestChannel; -import org.opensearch.rest.RestController; import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestRequest.Method; -import org.opensearch.security.DefaultObjectMapper; -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.EndpointValidator; 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; -import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; -import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.support.SecurityJsonNode; import org.opensearch.security.user.UserService; import org.opensearch.security.user.UserServiceException; import org.opensearch.threadpool.ThreadPool; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static org.opensearch.security.dlic.rest.api.Responses.badRequest; +import static org.opensearch.security.dlic.rest.api.Responses.badRequestMessage; +import static org.opensearch.security.dlic.rest.api.Responses.methodNotImplementedMessage; +import static org.opensearch.security.dlic.rest.api.Responses.ok; +import static org.opensearch.security.dlic.rest.api.Responses.payload; +import static org.opensearch.security.dlic.rest.api.Responses.response; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; import static org.opensearch.security.dlic.rest.support.Utils.hash; -public class InternalUsersApiAction extends PatchableResourceApiAction { +public class InternalUsersApiAction extends AbstractApiAction { + static final List RESTRICTED_FROM_USERNAME = ImmutableList.of( ":" // Not allowed in basic auth, see https://stackoverflow.com/a/33391003/533057 ); @@ -79,30 +75,14 @@ public class InternalUsersApiAction extends PatchableResourceApiAction { @Inject public InternalUsersApiAction( - final Settings settings, - final Path configPath, - final RestController controller, - final Client client, - final AdminDNs adminDNs, - final ConfigurationRepository cl, - final ClusterService cs, - final PrincipalExtractor principalExtractor, - final PrivilegesEvaluator evaluator, - ThreadPool threadPool, - UserService userService, - AuditLog auditLog + final ClusterService clusterService, + final ThreadPool threadPool, + final UserService userService, + final SecurityApiDependencies securityApiDependencies ) { - super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); + super(Endpoint.INTERNALUSERS, clusterService, threadPool, securityApiDependencies); this.userService = userService; - } - - @Override - protected boolean hasPermissionsToCreate( - final SecurityDynamicConfiguration dynamicConfigFactory, - final Object content, - final String resourceName - ) { - return true; + this.requestHandlersBuilder.configureRequestHandlers(this::internalUsersApiRequestHandlers); } @Override @@ -111,221 +91,177 @@ public List routes() { } @Override - protected Endpoint getEndpoint() { - return Endpoint.INTERNALUSERS; + protected CType getConfigType() { + return CType.INTERNALUSERS; } - @Override - protected void handlePut(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) - throws IOException { - - final String username = request.param("name"); - - SecurityDynamicConfiguration internalUsersConfiguration = load(getConfigName(), false); - - if (!isWriteable(channel, internalUsersConfiguration, username)) { - return; - } + private void internalUsersApiRequestHandlers(RequestHandler.RequestHandlersBuilder requestHandlersBuilder) { + requestHandlersBuilder + // Overrides the GET request functionality to allow for the special case of requesting an auth token. + .override( + Method.POST, + (channel, request, client) -> withAuthTokenPath(request).map( + username -> loadConfiguration(getConfigType(), true, false).map( + configuration -> ValidationResult.success(SecurityConfiguration.of(username, configuration)) + ) + ) + .map(endpointValidator::entityExists) + .map(endpointValidator::isAllowedToLoadOrChangeHiddenEntity) + .valid(securityConfiguration -> generateAuthToken(channel, securityConfiguration)) + .error((status, toXContent) -> response(channel, status, toXContent)) + ) + .onChangeRequest(Method.PATCH, this::processPatchRequest) + .onChangeRequest( + Method.PUT, + request -> endpointValidator.withRequiredEntityName(nameParam(request)) + .map(username -> loadConfigurationWithRequestContent(username, request)) + .map(endpointValidator::isAllowedToChangeImmutableEntity) + .map(this::validateSecurityRoles) + .map(securityConfiguration -> createOrUpdateAccount(request, securityConfiguration)) + .map(this::validateAndUpdatePassword) + .map(this::addEntityToConfig) + ); + } - final ObjectNode contentAsNode = (ObjectNode) content; - final SecurityJsonNode securityJsonNode = new SecurityJsonNode(contentAsNode); + ValidationResult withAuthTokenPath(final RestRequest request) throws IOException { + return endpointValidator.withRequiredEntityName(nameParam(request)).map(username -> { + // Handle auth token fetching + if (!(request.uri().contains("/internalusers/" + username + "/authtoken") && request.uri().endsWith("/authtoken"))) { + return ValidationResult.error(RestStatus.NOT_IMPLEMENTED, methodNotImplementedMessage(request.method())); + } + return ValidationResult.success(username); + }); + } - // Don't allow user to add non-existent role or a role for which role-mapping is hidden or reserved - final List securityRoles = securityJsonNode.get("opendistro_security_roles").asList(); - if (securityRoles != null) { - for (final String role : securityRoles) { - if (!isValidRolesMapping(channel, role)) { - return; - } + void generateAuthToken(final RestChannel channel, final SecurityConfiguration securityConfiguration) throws IOException { + try { + final var username = securityConfiguration.entityName(); + final var authToken = userService.generateAuthToken(username); + if (!Strings.isNullOrEmpty(authToken)) { + ok(channel, "'" + username + "' authtoken generated " + authToken); + } else { + badRequest(channel, "'" + username + "' authtoken failed to be created."); } + } catch (final UserServiceException e) { + badRequest(channel, e.getMessage()); } + } - final boolean userExisted = internalUsersConfiguration.exists(username); - - // when updating an existing user password hash can be blank, which means no - // changes + ValidationResult validateSecurityRoles(final SecurityConfiguration securityConfiguration) throws IOException { + return loadConfiguration(CType.ROLES, false, false).map(rolesConfiguration -> { + final var contentAsNode = (ObjectNode) securityConfiguration.requestContent(); + final var securityJsonNode = new SecurityJsonNode(contentAsNode); + final var securityRoles = securityJsonNode.get("opendistro_security_roles").asList(); + return endpointValidator.validateRoles(securityRoles, rolesConfiguration) + .map(ignore -> ValidationResult.success(securityConfiguration)); + }); + } + ValidationResult createOrUpdateAccount( + final RestRequest request, + final SecurityConfiguration securityConfiguration + ) throws IOException { try { + final var username = securityConfiguration.entityName(); + final var content = (ObjectNode) securityConfiguration.requestContent(); if (request.hasParam("service")) { - ((ObjectNode) content).put("service", request.param("service")); + content.put("service", request.param("service")); } if (request.hasParam("enabled")) { - ((ObjectNode) content).put("enabled", request.param("enabled")); + content.put("enabled", request.param("enabled")); } - ((ObjectNode) content).put("name", username); - internalUsersConfiguration = userService.createOrUpdateAccount((ObjectNode) content); + content.put("name", username); + // FIXME add better solution for account and internal users + final var updateConfiguration = userService.createOrUpdateAccount(content); + // remove extra user in case we deal with the new one. not nice better to redesign account users. + if (!securityConfiguration.entityExists()) updateConfiguration.remove(securityConfiguration.entityName()); + return ValidationResult.success(SecurityConfiguration.of(content, username, updateConfiguration)); } catch (UserServiceException ex) { - badRequestResponse(channel, ex.getMessage()); - return; - } catch (IOException ex) { - throw new IOException(ex); + return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage(ex.getMessage())); } + } + ValidationResult validateAndUpdatePassword(final SecurityConfiguration securityConfiguration) { + // when updating an existing user password hash can be blank, which means no changes // for existing users, hash is optional - if (userExisted && securityJsonNode.get("hash").asString() == null) { - // sanity check, this should usually not happen - final String hash = ((Hashed) internalUsersConfiguration.getCEntry(username)).getHash(); - if (hash == null || hash.length() == 0) { - internalErrorResponse( - channel, - "Existing user " + username + " has no password, and no new password or hash was specified." + final var username = securityConfiguration.entityName(); + final var contentAsNode = (ObjectNode) securityConfiguration.requestContent(); + final var securityJsonNode = new SecurityJsonNode(contentAsNode); + if (securityConfiguration.entityExists() && securityJsonNode.get("hash").asString() == null) { + final String hash = ((Hashed) securityConfiguration.configuration().getCEntry(username)).getHash(); + if (Strings.isNullOrEmpty(hash)) { + return ValidationResult.error( + RestStatus.INTERNAL_SERVER_ERROR, + payload( + RestStatus.INTERNAL_SERVER_ERROR, + "Existing user " + username + " has no password, and no new password or hash was specified." + ) ); - return; } contentAsNode.put("hash", hash); } - - internalUsersConfiguration.remove(username); - - // checks complete, create or update the user - Object userData = DefaultObjectMapper.readTree(contentAsNode, internalUsersConfiguration.getImplementingClass()); - internalUsersConfiguration.putCObject(username, userData); - - saveAndUpdateConfigs( - this.securityIndexName, - client, - CType.INTERNALUSERS, - internalUsersConfiguration, - new OnSucessActionListener(channel) { - - @Override - public void onResponse(IndexResponse response) { - if (userExisted) { - successResponse(channel, "'" + username + "' updated."); - } else { - createdResponse(channel, "'" + username + "' created."); - } - - } - } - ); + return ValidationResult.success(securityConfiguration); } - /** - * Overrides the GET request functionality to allow for the special case of requesting an auth token. - * - * @param channel The channel the request is coming through - * @param request The request itself - * @param client The client executing the request - * @param content The content of the request parsed into a node - * @throws IOException when parsing of configuration files fails (should not happen) - */ @Override - protected void handlePost(final RestChannel channel, RestRequest request, Client client, final JsonNode content) throws IOException { - - final String username = request.param("name"); - - final SecurityDynamicConfiguration internalUsersConfiguration = load(getConfigName(), true); - filter(internalUsersConfiguration); // Hides hashes - - // no specific resource requested - if (username == null || username.length() == 0) { - - notImplemented(channel, Method.POST); - return; - } - - final boolean userExisted = internalUsersConfiguration.exists(username); - - if (!userExisted) { - notFound(channel, "Resource '" + username + "' not found."); - return; - } - - String authToken = ""; - try { - if (request.uri().contains("/internalusers/" + username + "/authtoken") && request.uri().endsWith("/authtoken")) { // Handle - // auth - // token - // fetching - - authToken = userService.generateAuthToken(username); - } else { // Not an auth token request - - notImplemented(channel, Method.POST); - return; + protected EndpointValidator createEndpointValidator() { + return new EndpointValidator() { + @Override + public Endpoint endpoint() { + return endpoint; } - } catch (UserServiceException ex) { - badRequestResponse(channel, ex.getMessage()); - return; - } catch (IOException ex) { - throw new IOException(ex); - } - - if (!authToken.isEmpty()) { - createdResponse(channel, "'" + username + "' authtoken generated " + authToken); - } else { - badRequestResponse(channel, "'" + username + "' authtoken failed to be created."); - } - } - - @Override - protected void filter(SecurityDynamicConfiguration builder) { - super.filter(builder); - // replace password hashes in addition. We must not remove them from the - // Builder since this would remove users completely if they - // do not have any addition properties like roles or attributes - builder.clearHashes(); - } - - @Override - protected ValidationResult postProcessApplyPatchResult( - RestChannel channel, - RestRequest request, - JsonNode existingResourceAsJsonNode, - JsonNode updatedResourceAsJsonNode, - String resourceName - ) throws IOException { - RequestContentValidator retVal = null; - JsonNode passwordNode = updatedResourceAsJsonNode.get("password"); - if (passwordNode != null) { - String plainTextPassword = passwordNode.asText(); - 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 validationResult; - } - return null; - } - - @Override - protected String getResourceName() { - return "user"; - } - @Override - protected CType getConfigName() { - return CType.INTERNALUSERS; - } - - @Override - protected RequestContentValidator createValidator(final Object... params) { - return RequestContentValidator.of(new RequestContentValidator.ValidationContext() { @Override - public Object[] params() { - return params; + public RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator() { + return securityApiDependencies.restApiAdminPrivilegesEvaluator(); } @Override - public Settings settings() { - return settings; + public ValidationResult onConfigChange(SecurityConfiguration securityConfiguration) throws IOException { + // this method will be called only for PATCH + return EndpointValidator.super.onConfigChange(securityConfiguration).map(this::generateHashForPassword); } - @Override - public Map allowedKeys() { - final ImmutableMap.Builder allowedKeys = ImmutableMap.builder(); - if (isSuperAdmin()) { - allowedKeys.put("reserved", DataType.BOOLEAN); + private ValidationResult generateHashForPassword(final SecurityConfiguration securityConfiguration) { + final var content = (ObjectNode) securityConfiguration.requestContent(); + if (content.has("password")) { + final var plainTextPassword = content.get("password").asText(); + content.remove("password"); + content.put("hash", hash(plainTextPassword.toCharArray())); } - 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(); + return ValidationResult.success(securityConfiguration); } - }); + + @Override + public RequestContentValidator createRequestContentValidator(Object... params) { + return RequestContentValidator.of(new RequestContentValidator.ValidationContext() { + @Override + public Object[] params() { + return params; + } + + @Override + public Settings settings() { + return securityApiDependencies.settings(); + } + + @Override + public Map allowedKeys() { + final ImmutableMap.Builder allowedKeys = ImmutableMap.builder(); + if (isCurrentUserAdmin()) { + 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 9cb926497f..1f3871c4ca 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 @@ -12,14 +12,8 @@ package org.opensearch.security.dlic.rest.api; // CS-SUPPRESS-SINGLE: RegexpSingleline https://github.com/opensearch-project/OpenSearch/issues/3663 -import java.io.IOException; -import java.nio.file.Path; -import java.util.Collections; -import java.util.List; -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.admin.indices.create.CreateIndexResponse; @@ -31,24 +25,18 @@ import org.opensearch.client.Client; import org.opensearch.cluster.metadata.IndexMetadata; import org.opensearch.cluster.service.ClusterService; -import org.opensearch.core.action.ActionListener; -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; import org.opensearch.common.settings.Settings.Builder; import org.opensearch.common.xcontent.XContentHelper; import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.rest.RestChannel; -import org.opensearch.rest.RestController; import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestRequest.Method; -import org.opensearch.security.auditlog.AuditLog; 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.RequestContentValidator; -import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.securityconf.Migration; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.NodesDn; @@ -65,9 +53,15 @@ import org.opensearch.security.securityconf.impl.v7.RoleMappingsV7; import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.securityconf.impl.v7.TenantV7; -import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.threadpool.ThreadPool; +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +import static org.opensearch.security.dlic.rest.api.Responses.badRequest; +import static org.opensearch.security.dlic.rest.api.Responses.internalSeverError; +import static org.opensearch.security.dlic.rest.api.Responses.ok; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; // CS-ENFORCE-SINGLE @@ -78,19 +72,12 @@ public class MigrateApiAction extends AbstractApiAction { @Inject public MigrateApiAction( - final Settings settings, - final Path configPath, - final RestController controller, - final Client client, - final AdminDNs adminDNs, - final ConfigurationRepository cl, - final ClusterService cs, - final PrincipalExtractor principalExtractor, - final PrivilegesEvaluator evaluator, - ThreadPool threadPool, - AuditLog auditLog + final ClusterService clusterService, + final ThreadPool threadPool, + final SecurityApiDependencies securityApiDependencies ) { - super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); + super(Endpoint.MIGRATE, clusterService, threadPool, securityApiDependencies); + this.requestHandlersBuilder.configureRequestHandlers(this::migrateApiRequestHandlers); } @Override @@ -99,27 +86,26 @@ public List routes() { } @Override - protected Endpoint getEndpoint() { - return Endpoint.MIGRATE; + protected CType getConfigType() { + return null; } @Override - protected boolean hasPermissionsToCreate( - final SecurityDynamicConfiguration dynamicConfigFactory, - final Object content, - final String resourceName - ) { - return true; + protected void consumeParameters(final RestRequest request) { + // not needed + } + + private void migrateApiRequestHandlers(RequestHandler.RequestHandlersBuilder requestHandlersBuilder) { + requestHandlersBuilder.allMethodsNotImplemented().override(Method.POST, (channel, request, client) -> migrate(channel, client)); } @SuppressWarnings("unchecked") - @Override - protected void handlePost(RestChannel channel, RestRequest request, Client client, final JsonNode content) throws IOException { + protected void migrate(final RestChannel channel, final Client client) throws IOException { final SecurityDynamicConfiguration loadedConfig = load(CType.CONFIG, true); if (loadedConfig.getVersion() != 1) { - badRequestResponse(channel, "Can not migrate configuration because it was already migrated."); + badRequest(channel, "Can not migrate configuration because it was already migrated."); return; } @@ -167,10 +153,10 @@ protected void handlePost(RestChannel channel, RestRequest request, Client clien final SecurityDynamicConfiguration auditConfigV7 = Migration.migrateAudit(auditConfigV6); builder.add(auditConfigV7); - final int replicas = cs.state().metadata().index(securityIndexName).getNumberOfReplicas(); - final String autoExpandReplicas = cs.state() + final int replicas = clusterService.state().metadata().index(securityApiDependencies.securityIndexName()).getNumberOfReplicas(); + final String autoExpandReplicas = clusterService.state() .metadata() - .index(securityIndexName) + .index(securityApiDependencies.securityIndexName()) .getSettings() .get(IndexMetadata.SETTING_AUTO_EXPAND_REPLICAS); @@ -184,138 +170,102 @@ protected void handlePost(RestChannel channel, RestRequest request, Client clien securityIndexSettings.put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1); - client.admin().indices().prepareDelete(this.securityIndexName).execute(new ActionListener() { - - @Override - public void onResponse(AcknowledgedResponse response) { - - if (response.isAcknowledged()) { - LOGGER.debug("opendistro_security index deleted successfully"); - - client.admin() - .indices() - .prepareCreate(securityIndexName) - .setSettings(securityIndexSettings) - .execute(new ActionListener() { - - @Override - public void onResponse(CreateIndexResponse response) { - final List> dynamicConfigurations = builder.build(); - final ImmutableList.Builder cTypes = ImmutableList.builderWithExpectedSize( - dynamicConfigurations.size() - ); - final BulkRequestBuilder br = client.prepareBulk(securityIndexName); - br.setRefreshPolicy(RefreshPolicy.IMMEDIATE); - try { - for (SecurityDynamicConfiguration dynamicConfiguration : dynamicConfigurations) { - final String id = dynamicConfiguration.getCType().toLCString(); - final BytesReference xContent = XContentHelper.toXContent( - dynamicConfiguration, - XContentType.JSON, - false - ); - br.add(new IndexRequest().id(id).source(id, xContent)); - cTypes.add(id); + client.admin() + .indices() + .prepareDelete(securityApiDependencies.securityIndexName()) + .execute(new ActionListener() { + + @Override + public void onResponse(AcknowledgedResponse response) { + + if (response.isAcknowledged()) { + LOGGER.debug("opendistro_security index deleted successfully"); + + client.admin() + .indices() + .prepareCreate(securityApiDependencies.securityIndexName()) + .setSettings(securityIndexSettings) + .execute(new ActionListener() { + + @Override + public void onResponse(CreateIndexResponse response) { + final List> dynamicConfigurations = builder.build(); + final ImmutableList.Builder cTypes = ImmutableList.builderWithExpectedSize( + dynamicConfigurations.size() + ); + final BulkRequestBuilder br = client.prepareBulk(securityApiDependencies.securityIndexName()); + br.setRefreshPolicy(RefreshPolicy.IMMEDIATE); + try { + for (SecurityDynamicConfiguration dynamicConfiguration : dynamicConfigurations) { + final String id = dynamicConfiguration.getCType().toLCString(); + final BytesReference xContent = XContentHelper.toXContent( + dynamicConfiguration, + XContentType.JSON, + false + ); + br.add(new IndexRequest().id(id).source(id, xContent)); + cTypes.add(id); + } + } catch (final IOException e1) { + LOGGER.error("Unable to create bulk request " + e1, e1); + internalSeverError(channel, "Unable to create bulk request."); + return; } - } catch (final IOException e1) { - LOGGER.error("Unable to create bulk request " + e1, e1); - internalErrorResponse(channel, "Unable to create bulk request."); - return; - } - br.execute( - new ConfigUpdatingActionListener( - cTypes.build().toArray(new String[0]), - client, - new ActionListener() { - - @Override - public void onResponse(BulkResponse response) { - if (response.hasFailures()) { - LOGGER.error( - "Unable to upload migrated configuration because of " - + response.buildFailureMessage() - ); - internalErrorResponse( - channel, - "Unable to upload migrated configuration (bulk index failed)." - ); - } else { - LOGGER.debug("Migration completed"); - successResponse(channel, "Migration completed."); + br.execute( + new ConfigUpdatingActionListener( + cTypes.build().toArray(new String[0]), + client, + new ActionListener() { + + @Override + public void onResponse(BulkResponse response) { + if (response.hasFailures()) { + LOGGER.error( + "Unable to upload migrated configuration because of " + + response.buildFailureMessage() + ); + internalSeverError( + channel, + "Unable to upload migrated configuration (bulk index failed)." + ); + } else { + LOGGER.debug("Migration completed"); + ok(channel, "Migration completed."); + } + } + @Override + public void onFailure(Exception e) { + LOGGER.error("Unable to upload migrated configuration because of " + e, e); + internalSeverError(channel, "Unable to upload migrated configuration."); + } } + ) + ); - @Override - public void onFailure(Exception e) { - LOGGER.error("Unable to upload migrated configuration because of " + e, e); - internalErrorResponse(channel, "Unable to upload migrated configuration."); - } - } - ) - ); - - } + } - @Override - public void onFailure(Exception e) { - LOGGER.error("Unable to create opendistro_security index because of " + e, e); - internalErrorResponse(channel, "Unable to create opendistro_security index."); - } - }); + @Override + public void onFailure(Exception e) { + LOGGER.error("Unable to create opendistro_security index because of " + e, e); + internalSeverError(channel, "Unable to create opendistro_security index."); + } + }); - } else { - LOGGER.error("Unable to create opendistro_security index."); + } else { + LOGGER.error("Unable to create opendistro_security index."); + } } - } - @Override - public void onFailure(Exception e) { - LOGGER.error("Unable to delete opendistro_security index because of " + e, e); - internalErrorResponse(channel, "Unable to delete opendistro_security index."); - } - }); - - } - - @Override - protected void handleDelete(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) - throws IOException { - notImplemented(channel, Method.POST); - } - - @Override - protected void handleGet(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) - throws IOException { - notImplemented(channel, Method.GET); - } - - @Override - protected void handlePut(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) - throws IOException { - notImplemented(channel, Method.PUT); - } - - @Override - protected RequestContentValidator createValidator(final Object... params) { - return RequestContentValidator.NOOP_VALIDATOR; - } - - @Override - protected String getResourceName() { - // not needed - return null; - } - - @Override - protected CType getConfigName() { - return null; - } + @Override + public void onFailure(Exception e) { + LOGGER.error("Unable to delete opendistro_security index because of " + e, e); + internalSeverError(channel, "Unable to delete opendistro_security index."); + } + }); - @Override - protected void consumeParameters(final RestRequest request) { - // not needed } } 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 4e36101692..9928c7897a 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 @@ -11,8 +11,13 @@ package org.opensearch.security.dlic.rest.api; +import static org.opensearch.rest.RestRequest.Method.GET; +import static org.opensearch.rest.RestRequest.Method.PUT; +import static org.opensearch.security.dlic.rest.api.Responses.ok; +import static org.opensearch.security.dlic.rest.api.Responses.response; +import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; + import java.io.IOException; -import java.nio.file.Path; import java.util.List; import java.util.Map; import java.util.Objects; @@ -20,44 +25,27 @@ import java.util.Set; import java.util.stream.Collectors; -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.common.settings.Settings; -import org.opensearch.core.xcontent.XContentBuilder; -import org.opensearch.rest.BytesRestResponse; +import org.opensearch.core.xcontent.ToXContent; import org.opensearch.rest.RestChannel; -import org.opensearch.rest.RestController; -import org.opensearch.rest.RestRequest; -import org.opensearch.core.rest.RestStatus; -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.EndpointValidator; 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.securityconf.impl.v7.ConfigV7; -import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.support.ConfigConstants; import org.opensearch.threadpool.ThreadPool; -import static org.opensearch.rest.RestRequest.Method.GET; -import static org.opensearch.rest.RestRequest.Method.PUT; -import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; 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"; @@ -66,7 +54,7 @@ public class MultiTenancyConfigApiAction extends AbstractApiAction { ImmutableList.of(new Route(GET, "/tenancy/config"), new Route(PUT, "/tenancy/config")) ); - private final static Set ACCEPTABLE_DEFAULT_TENANTS = ImmutableSet.of( + private final static Set ACCEPTABLE_DEFAULT_TENANTS = Set.of( ConfigConstants.TENANCY_GLOBAL_TENANT_DEFAULT_NAME, ConfigConstants.TENANCY_GLOBAL_TENANT_NAME, ConfigConstants.TENANCY_PRIVATE_TENANT_NAME @@ -83,109 +71,98 @@ public List routes() { } public MultiTenancyConfigApiAction( - final Settings settings, - final Path configPath, - final RestController controller, - final Client client, - final AdminDNs adminDNs, - final ConfigurationRepository cl, - final ClusterService cs, - final PrincipalExtractor principalExtractor, - final PrivilegesEvaluator evaluator, + final ClusterService clusterService, final ThreadPool threadPool, - final AuditLog auditLog + final SecurityApiDependencies securityApiDependencies ) { - super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); + super(Endpoint.TENANTS, clusterService, threadPool, securityApiDependencies); + this.requestHandlersBuilder.configureRequestHandlers(this::multiTenancyConfigApiRequestHandlers); + } + + @Override + protected CType getConfigType() { + return CType.CONFIG; } @Override - protected RequestContentValidator createValidator(final Object... params) { - return RequestContentValidator.of(new RequestContentValidator.ValidationContext() { + protected EndpointValidator createEndpointValidator() { + return new EndpointValidator() { + @Override - public Object[] params() { - return params; + public Endpoint endpoint() { + return endpoint; } @Override - public Settings settings() { - return settings; + public RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator() { + return securityApiDependencies.restApiAdminPrivilegesEvaluator(); } @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 - ); + public RequestContentValidator createRequestContentValidator(Object... params) { + return RequestContentValidator.of(new RequestContentValidator.ValidationContext() { + @Override + public Object[] params() { + return params; + } + + @Override + public Settings settings() { + return securityApiDependencies.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 - protected Endpoint getEndpoint() { - return Endpoint.TENANTS; + }; } - @Override - protected String getResourceName() { - return null; + private ToXContent multitenancyContent(final ConfigV7 config) { + return (builder, params) -> builder.startObject() + .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(); } - @Override - protected CType getConfigName() { - return CType.CONFIG; + private void multiTenancyConfigApiRequestHandlers(RequestHandler.RequestHandlersBuilder requestHandlersBuilder) { + requestHandlersBuilder.allMethodsNotImplemented() + .override(GET, (channel, request, client) -> loadConfiguration(getConfigType(), false, false).valid(configuration -> { + final var config = (ConfigV7) configuration.getCEntry(CType.CONFIG.toLCString()); + ok(channel, multitenancyContent(config)); + }).error((status, toXContent) -> response(channel, status, toXContent))) + .override(PUT, (channel, request, client) -> { + loadConfigurationWithRequestContent("config", request).valid( + securityConfiguration -> updateMultitenancy(channel, client, securityConfiguration) + ).error((status, toXContent) -> response(channel, status, toXContent)); + }); } - @Override - protected void handleDelete(final RestChannel channel, final RestRequest request, final Client client, final JsonNode content) - throws IOException { - notImplemented(channel, RestRequest.Method.DELETE); - } - - private void multitenancyResponse(final ConfigV7 config, final RestChannel channel) { - try (final XContentBuilder contentBuilder = channel.newBuilder()) { - channel.sendResponse( - new BytesRestResponse( - RestStatus.OK, - contentBuilder.startObject() - .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()); - LOGGER.error("Error handle request ", e); - } - } - - @Override - protected void handleGet(final RestChannel channel, final RestRequest request, final Client client, final JsonNode content) - throws IOException { - final SecurityDynamicConfiguration dynamicConfiguration = load(CType.CONFIG, false); - final ConfigV7 config = (ConfigV7) dynamicConfiguration.getCEntry(CType.CONFIG.toLCString()); - multitenancyResponse(config, channel); - } + protected void updateMultitenancy( + final RestChannel channel, + final Client client, + final SecurityConfiguration securityConfiguration - @Override - protected void handlePut(final RestChannel channel, final RestRequest request, final Client client, final JsonNode content) - throws IOException { - final SecurityDynamicConfiguration dynamicConfiguration = (SecurityDynamicConfiguration) load( - CType.CONFIG, - false - ); - final ConfigV7 config = dynamicConfiguration.getCEntry(CType.CONFIG.toLCString()); - updateAndValidatesValues(config, content); + ) throws IOException { + @SuppressWarnings("unchecked") + final var dynamicConfiguration = (SecurityDynamicConfiguration) securityConfiguration.configuration(); + final var config = dynamicConfiguration.getCEntry(CType.CONFIG.toLCString()); + updateAndValidatesValues(config, securityConfiguration.requestContent()); dynamicConfiguration.putCEntry(CType.CONFIG.toLCString(), config); - saveAndUpdateConfigs(this.securityIndexName, client, getConfigName(), dynamicConfiguration, new OnSucessActionListener<>(channel) { + saveOrUpdateConfiguration(client, dynamicConfiguration, new OnSucessActionListener<>(channel) { @Override - public void onResponse(IndexResponse response) { - multitenancyResponse(config, channel); + public void onResponse(IndexResponse indexResponse) { + ok(channel, multitenancyContent(config)); } }); } @@ -210,7 +187,8 @@ private void updateAndValidatesValues(final ConfigV7 config, final JsonNode json return; } - final Set availableTenants = cl.getConfiguration(CType.TENANTS) + final Set availableTenants = securityApiDependencies.configurationRepository() + .getConfiguration(CType.TENANTS) .getCEntries() .keySet() .stream() 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 8d09705799..a10139b594 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 @@ -12,38 +12,32 @@ package org.opensearch.security.dlic.rest.api; import java.io.IOException; -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.common.inject.Inject; import org.opensearch.common.settings.Settings; -import org.opensearch.rest.RestChannel; -import org.opensearch.rest.RestController; +import org.opensearch.core.rest.RestStatus; import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestRequest.Method; -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.EndpointValidator; 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.dlic.rest.validation.ValidationResult; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.NodesDn; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; -import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.support.ConfigConstants; import org.opensearch.threadpool.ThreadPool; +import static org.opensearch.security.dlic.rest.api.Responses.forbiddenMessage; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; /** @@ -59,7 +53,8 @@ * * See {@link NodesDnApiTest} for usage examples. */ -public class NodesDnApiAction extends PatchableResourceApiAction { +public class NodesDnApiAction extends AbstractApiAction { + public static final String STATIC_OPENSEARCH_YML_NODES_DN = "STATIC_OPENSEARCH_YML_NODES_DN"; private final List staticNodesDnFromEsYml; @@ -76,88 +71,46 @@ public class NodesDnApiAction extends PatchableResourceApiAction { @Inject public NodesDnApiAction( - final Settings settings, - final Path configPath, - final RestController controller, - final Client client, - final AdminDNs adminDNs, - final ConfigurationRepository cl, - final ClusterService cs, - final PrincipalExtractor principalExtractor, - final PrivilegesEvaluator evaluator, - ThreadPool threadPool, - AuditLog auditLog - ) { - super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); - this.staticNodesDnFromEsYml = settings.getAsList(ConfigConstants.SECURITY_NODES_DN, Collections.emptyList()); - } - - @Override - protected boolean hasPermissionsToCreate( - final SecurityDynamicConfiguration dynamicConfigFactory, - final Object content, - final String resourceName + final ClusterService clusterService, + final ThreadPool threadPool, + final SecurityApiDependencies securityApiDependencies ) { - return true; + super(Endpoint.NODESDN, clusterService, threadPool, securityApiDependencies); + this.staticNodesDnFromEsYml = securityApiDependencies.settings().getAsList(ConfigConstants.SECURITY_NODES_DN, List.of()); + this.requestHandlersBuilder.configureRequestHandlers(this::nodesDnApiRequestHandlers); } @Override public List routes() { - if (settings.getAsBoolean(ConfigConstants.SECURITY_NODES_DN_DYNAMIC_CONFIG_ENABLED, false)) { + if (securityApiDependencies.settings().getAsBoolean(ConfigConstants.SECURITY_NODES_DN_DYNAMIC_CONFIG_ENABLED, false)) { return routes; } return Collections.emptyList(); } @Override - protected void handleApiRequest(RestChannel channel, RestRequest request, Client client) throws IOException { - if (!isSuperAdmin()) { - forbidden(channel, "API allowed only for admin."); - return; - } - super.handleApiRequest(channel, request, client); + protected CType getConfigType() { + return CType.NODESDN; } + @Override protected void consumeParameters(final RestRequest request) { request.param("name"); request.param("show_all"); } - @Override - protected boolean isReadOnly(SecurityDynamicConfiguration existingConfiguration, String name) { - if (STATIC_OPENSEARCH_YML_NODES_DN.equals(name)) { - return true; - } - return super.isReadOnly(existingConfiguration, name); - } - - @Override - 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) { - final Boolean showAll = request.paramAsBoolean("show_all", Boolean.FALSE); - if (showAll) { - putStaticEntry(configuration); + private void nodesDnApiRequestHandlers(RequestHandler.RequestHandlersBuilder requestHandlersBuilder) { + requestHandlersBuilder.verifyAccessForAllMethods().onGetRequest(request -> processGetRequest(request).map(securityConfiguration -> { + if (request.paramAsBoolean("show_all", false)) { + final var configuration = securityConfiguration.configuration(); + addStaticNodesDn(configuration); } - successResponse(channel, configuration); - return; - } - - if (!configuration.exists(resourcename)) { - notFound(channel, "Resource '" + resourcename + "' not found."); - return; - } - - configuration.removeOthers(resourcename); - successResponse(channel, configuration); + return ValidationResult.success(securityConfiguration); + })).onChangeRequest(Method.PATCH, this::processPatchRequest); } - private void putStaticEntry(SecurityDynamicConfiguration configuration) { + @SuppressWarnings("unchecked") + private void addStaticNodesDn(SecurityDynamicConfiguration configuration) { if (NodesDn.class.equals(configuration.getImplementingClass())) { NodesDn nodesDn = new NodesDn(); nodesDn.setNodesDn(staticNodesDnFromEsYml); @@ -168,42 +121,56 @@ private void putStaticEntry(SecurityDynamicConfiguration configuration) { } @Override - protected Endpoint getEndpoint() { - return Endpoint.NODESDN; - } - - @Override - protected String getResourceName() { - return "nodesdn"; - } + protected EndpointValidator createEndpointValidator() { + return new EndpointValidator() { - @Override - protected CType getConfigName() { - return CType.NODESDN; - } - - @Override - protected RequestContentValidator createValidator(final Object... params) { - return RequestContentValidator.of(new RequestContentValidator.ValidationContext() { @Override - public Object[] params() { - return params; + public Endpoint endpoint() { + return endpoint; } @Override - public Settings settings() { - return settings; + public RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator() { + return securityApiDependencies.restApiAdminPrivilegesEvaluator(); } @Override - public Set mandatoryKeys() { - return ImmutableSet.of("nodes_dn"); + public ValidationResult isAllowedToChangeImmutableEntity(SecurityConfiguration securityConfiguration) + throws IOException { + if (STATIC_OPENSEARCH_YML_NODES_DN.equals(securityConfiguration.entityName())) { + return ValidationResult.error( + RestStatus.FORBIDDEN, + forbiddenMessage("Resource '" + STATIC_OPENSEARCH_YML_NODES_DN + "' is read-only.") + ); + } + return EndpointValidator.super.isAllowedToChangeImmutableEntity(securityConfiguration); } @Override - public Map allowedKeys() { - return ImmutableMap.of("nodes_dn", DataType.ARRAY); + public RequestContentValidator createRequestContentValidator(Object... params) { + return RequestContentValidator.of(new RequestContentValidator.ValidationContext() { + @Override + public Object[] params() { + return params; + } + + @Override + public Settings settings() { + return securityApiDependencies.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 deleted file mode 100644 index 401110c9b1..0000000000 --- a/src/main/java/org/opensearch/security/dlic/rest/api/PatchableResourceApiAction.java +++ /dev/null @@ -1,325 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.dlic.rest.api; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.Iterator; -import java.util.List; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.flipkart.zjsonpatch.JsonPatch; -import com.flipkart.zjsonpatch.JsonPatchApplicationException; -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.common.settings.Settings; -import org.opensearch.common.xcontent.XContentType; -import org.opensearch.core.common.Strings; -import org.opensearch.rest.RestChannel; -import org.opensearch.rest.RestController; -import org.opensearch.rest.RestRequest; -import org.opensearch.rest.RestRequest.Method; -import org.opensearch.security.DefaultObjectMapper; -import org.opensearch.security.auditlog.AuditLog; -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.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; -import org.opensearch.security.securityconf.impl.v7.ActionGroupsV7; -import org.opensearch.security.ssl.transport.PrincipalExtractor; -import org.opensearch.threadpool.ThreadPool; - -public abstract class PatchableResourceApiAction extends AbstractApiAction { - - protected final Logger log = LogManager.getLogger(this.getClass()); - - public PatchableResourceApiAction( - Settings settings, - Path configPath, - RestController controller, - Client client, - AdminDNs adminDNs, - ConfigurationRepository cl, - ClusterService cs, - PrincipalExtractor principalExtractor, - PrivilegesEvaluator evaluator, - ThreadPool threadPool, - AuditLog auditLog - ) { - super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); - } - - private void handlePatch(RestChannel channel, final RestRequest request, final Client client) throws IOException { - if (request.getMediaType() != XContentType.JSON) { - badRequestResponse(channel, "PATCH accepts only application/json"); - return; - } - - String name = request.param("name"); - SecurityDynamicConfiguration existingConfiguration = load(getConfigName(), false); - - if (existingConfiguration.getSeqNo() < 0) { - forbidden(channel, "Config '" + getConfigName().toLCString() + "' isn't configured. Use SecurityAdmin to populate."); - return; - } - - JsonNode jsonPatch; - - try { - jsonPatch = DefaultObjectMapper.readTree(request.content().utf8ToString()); - } catch (IOException e) { - log.debug("Error while parsing JSON patch", e); - badRequestResponse(channel, "Error in JSON patch: " + e.getMessage()); - return; - } - - JsonNode existingAsJsonNode = Utils.convertJsonToJackson(existingConfiguration, true); - - if (!(existingAsJsonNode instanceof ObjectNode)) { - internalErrorResponse(channel, "Config " + getConfigName() + " is malformed"); - return; - } - - ObjectNode existingAsObjectNode = (ObjectNode) existingAsJsonNode; - - if (Strings.isNullOrEmpty(name)) { - handleBulkPatch(channel, request, client, existingConfiguration, existingAsObjectNode, jsonPatch); - } else { - handleSinglePatch(channel, request, client, name, existingConfiguration, existingAsObjectNode, jsonPatch); - } - } - - private void handleSinglePatch( - RestChannel channel, - RestRequest request, - Client client, - String name, - SecurityDynamicConfiguration existingConfiguration, - ObjectNode existingAsObjectNode, - JsonNode jsonPatch - ) throws IOException { - if (!isWriteable(channel, existingConfiguration, name)) { - return; - } - - if (!existingConfiguration.exists(name)) { - notFound(channel, getResourceName() + " " + name + " not found."); - return; - } - - JsonNode existingResourceAsJsonNode = existingAsObjectNode.get(name); - - JsonNode patchedResourceAsJsonNode; - - try { - patchedResourceAsJsonNode = applyPatch(jsonPatch, existingResourceAsJsonNode); - } catch (JsonPatchApplicationException e) { - log.debug("Error while applying JSON patch", e); - badRequestResponse(channel, e.getMessage()); - return; - } - - ValidationResult originalValidationResult = postProcessApplyPatchResult( - channel, - request, - existingResourceAsJsonNode, - patchedResourceAsJsonNode, - name - ); - - if (originalValidationResult != null) { - if (!originalValidationResult.isValid()) { - request.params().clear(); - badRequestResponse(channel, originalValidationResult.errorMessage()); - return; - } - } - - if (isReadonlyFieldUpdated(existingResourceAsJsonNode, patchedResourceAsJsonNode)) { - request.params().clear(); - conflict(channel, "Attempted to update read-only property."); - return; - } - - RequestContentValidator validator = createValidator(); - final ValidationResult validationResult = validator.validate(request, patchedResourceAsJsonNode); - if (!validationResult.isValid()) { - request.params().clear(); - badRequestResponse(channel, validator); - return; - } - - JsonNode updatedAsJsonNode = existingAsObjectNode.deepCopy().set(name, patchedResourceAsJsonNode); - - SecurityDynamicConfiguration mdc = SecurityDynamicConfiguration.fromNode( - updatedAsJsonNode, - existingConfiguration.getCType(), - existingConfiguration.getVersion(), - existingConfiguration.getSeqNo(), - existingConfiguration.getPrimaryTerm() - ); - - if (existingConfiguration.getCType().equals(CType.ACTIONGROUPS)) { - if (hasActionGroupSelfReference(mdc, name)) { - badRequestResponse(channel, name + " cannot be an allowed_action of itself"); - return; - } - } - - saveAndUpdateConfigs(this.securityIndexName, client, getConfigName(), mdc, new OnSucessActionListener(channel) { - - @Override - public void onResponse(IndexResponse response) { - successResponse(channel, "'" + name + "' updated."); - - } - }); - } - - private void handleBulkPatch( - RestChannel channel, - RestRequest request, - Client client, - SecurityDynamicConfiguration existingConfiguration, - ObjectNode existingAsObjectNode, - JsonNode jsonPatch - ) throws IOException { - - JsonNode patchedAsJsonNode; - - try { - patchedAsJsonNode = applyPatch(jsonPatch, existingAsObjectNode); - } catch (JsonPatchApplicationException e) { - log.debug("Error while applying JSON patch", e); - badRequestResponse(channel, e.getMessage()); - return; - } - - for (String resourceName : existingConfiguration.getCEntries().keySet()) { - JsonNode oldResource = existingAsObjectNode.get(resourceName); - JsonNode patchedResource = patchedAsJsonNode.get(resourceName); - if (oldResource != null && !oldResource.equals(patchedResource) && !isWriteable(channel, existingConfiguration, resourceName)) { - return; - } - } - - for (Iterator fieldNamesIter = patchedAsJsonNode.fieldNames(); fieldNamesIter.hasNext();) { - String resourceName = fieldNamesIter.next(); - - JsonNode oldResource = existingAsObjectNode.get(resourceName); - JsonNode patchedResource = patchedAsJsonNode.get(resourceName); - - ValidationResult originalValidationResult = postProcessApplyPatchResult( - channel, - request, - oldResource, - patchedResource, - resourceName - ); - - if (originalValidationResult != null) { - if (!originalValidationResult.isValid()) { - request.params().clear(); - badRequestResponse(channel, originalValidationResult.errorMessage()); - return; - } - } - - if (isReadonlyFieldUpdated(oldResource, patchedResource)) { - request.params().clear(); - conflict(channel, "Attempted to update read-only property."); - return; - } - - if (oldResource == null || !oldResource.equals(patchedResource)) { - RequestContentValidator validator = createValidator(); - final ValidationResult validationResult = validator.validate(request, patchedResource); - if (!validationResult.isValid()) { - request.params().clear(); - badRequestResponse(channel, validator); - return; - } - final Object newContent = DefaultObjectMapper.readTree(patchedResource, existingConfiguration.getImplementingClass()); - if (!hasPermissionsToCreate(existingConfiguration, newContent, resourceName)) { - request.params().clear(); - forbidden(channel, "No permissions"); - return; - } - } - } - SecurityDynamicConfiguration mdc = SecurityDynamicConfiguration.fromNode( - patchedAsJsonNode, - existingConfiguration.getCType(), - existingConfiguration.getVersion(), - existingConfiguration.getSeqNo(), - existingConfiguration.getPrimaryTerm() - ); - - if (existingConfiguration.getCType().equals(CType.ACTIONGROUPS)) { - for (String actiongroup : mdc.getCEntries().keySet()) { - if (hasActionGroupSelfReference(mdc, actiongroup)) { - badRequestResponse(channel, actiongroup + " cannot be an allowed_action of itself"); - return; - } - } - } - - saveAndUpdateConfigs(this.securityIndexName, client, getConfigName(), mdc, new OnSucessActionListener(channel) { - - @Override - public void onResponse(IndexResponse response) { - successResponse(channel, "Resource updated."); - } - }); - - } - - private JsonNode applyPatch(JsonNode jsonPatch, JsonNode existingResourceAsJsonNode) { - return JsonPatch.apply(jsonPatch, existingResourceAsJsonNode); - } - - protected ValidationResult postProcessApplyPatchResult( - RestChannel channel, - RestRequest request, - JsonNode existingResourceAsJsonNode, - JsonNode updatedResourceAsJsonNode, - String resourceName - ) throws IOException { - // do nothing by default - return null; - } - - @Override - protected void handleApiRequest(RestChannel channel, final RestRequest request, final Client client) throws IOException { - - if (request.method() == Method.PATCH) { - handlePatch(channel, request, client); - } else { - super.handleApiRequest(channel, request, client); - } - } - - // 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(); - return allowedActions.contains(name); - } -} diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/RequestHandler.java b/src/main/java/org/opensearch/security/dlic/rest/api/RequestHandler.java new file mode 100644 index 0000000000..d9b26c262b --- /dev/null +++ b/src/main/java/org/opensearch/security/dlic/rest/api/RequestHandler.java @@ -0,0 +1,231 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.dlic.rest.api; + +import org.opensearch.action.index.IndexResponse; +import org.opensearch.client.Client; +import org.opensearch.common.CheckedFunction; +import org.opensearch.common.TriConsumer; +import org.opensearch.rest.RestChannel; +import org.opensearch.rest.RestRequest; +import org.opensearch.security.dlic.rest.validation.ValidationResult; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import static org.opensearch.security.dlic.rest.api.Responses.created; +import static org.opensearch.security.dlic.rest.api.Responses.forbidden; +import static org.opensearch.security.dlic.rest.api.Responses.methodNotImplemented; +import static org.opensearch.security.dlic.rest.api.Responses.ok; +import static org.opensearch.security.dlic.rest.api.Responses.response; + +@FunctionalInterface +public interface RequestHandler { + + RequestHandler methodNotImplementedHandler = (channel, request, client) -> methodNotImplemented(channel, request.method()); + + RequestHandler accessDeniedHandler = (channel, request, client) -> forbidden(channel, "Access denied"); + + void handle(final RestChannel channel, final RestRequest request, final Client client) throws IOException; + + final class RequestHandlersBuilder { + + static final Set SUPPORTED_METHODS = Set.of( + RestRequest.Method.DELETE, + RestRequest.Method.GET, + RestRequest.Method.PATCH, + RestRequest.Method.POST, + RestRequest.Method.PUT + ); + + static final Set ON_CHANGE_REQUEST = Set.of( + RestRequest.Method.DELETE, + RestRequest.Method.PATCH, + RestRequest.Method.PUT + ); + + private TriConsumer< + Client, + SecurityDynamicConfiguration, + AbstractApiAction.OnSucessActionListener> saveOrUpdateConfigurationHandler; + + private Predicate accessHandler; + + private boolean checkPermissions = false; + + final Map requestHandlers = new HashMap<>(); + + RequestHandlersBuilder() { + SUPPORTED_METHODS.forEach(method -> requestHandlers.put(method, methodNotImplementedHandler)); + } + + public RequestHandlersBuilder verifyAccessForAllMethods() { + this.checkPermissions = true; + final var handlers = build(); + handlers.forEach(this::add); + return this; + } + + public RequestHandlersBuilder allMethodsNotImplemented() { + final var handlers = build(); + handlers.forEach((method, requestHandler) -> add(method, methodNotImplementedHandler)); + return this; + } + + public RequestHandlersBuilder add(final RestRequest.Method method, RequestHandler requestHandler) { + if (!SUPPORTED_METHODS.contains(method)) { + throw new IllegalArgumentException("Unsupported HTTP method " + method + ". Supported are: " + SUPPORTED_METHODS); + } + if (checkPermissions && requestHandler != methodNotImplementedHandler) { + requestHandlers.put(method, (channel, request, client) -> { + if (accessHandler.test(request)) { + requestHandler.handle(channel, request, client); + } else { + accessDeniedHandler.handle(channel, request, client); + } + }); + } else { + requestHandlers.put(method, requestHandler); + } + return this; + } + + public RequestHandlersBuilder override(final RestRequest.Method method, RequestHandler requestOperationHandler) { + add(method, requestOperationHandler); + return this; + } + + public RequestHandlersBuilder withAccessHandler(final Predicate accessHandler) { + this.accessHandler = Objects.requireNonNull(accessHandler, "accessHandler can't be null"); + return this; + } + + RequestHandlersBuilder withSaveOrUpdateConfigurationHandler( + final TriConsumer< + Client, + SecurityDynamicConfiguration, + AbstractApiAction.OnSucessActionListener> saveOrUpdateConfigurationHandler + ) { + this.saveOrUpdateConfigurationHandler = Objects.requireNonNull( + saveOrUpdateConfigurationHandler, + "saveOrUpdateConfigurationHandler can't be null" + ); + return this; + } + + public RequestHandlersBuilder onGetRequest( + final CheckedFunction, IOException> mapper + ) { + Objects.requireNonNull(mapper, "onGetRequest request handler can't be null"); + add( + RestRequest.Method.GET, + (channel, request, client) -> mapper.apply(request) + .valid(securityConfiguration -> ok(channel, securityConfiguration.configuration())) + .error((status, toXContent) -> response(channel, status, toXContent)) + ); + return this; + } + + public RequestHandlersBuilder onChangeRequest( + final RestRequest.Method method, + final CheckedFunction, IOException> mapper + ) { + Objects.requireNonNull(method, "method can't be null"); + Objects.requireNonNull(mapper, "onChangeRequest handler can't be null"); + if (!ON_CHANGE_REQUEST.contains(method)) { + throw new IllegalArgumentException("Unsupported HTTP method " + method + ". Supported are: " + ON_CHANGE_REQUEST); + } + switch (method) { + case PATCH: + add( + method, + (channel, request, client) -> mapper.apply(request) + .valid( + securityConfiguration -> saveOrUpdateConfigurationHandler.apply( + client, + securityConfiguration.configuration(), + new AbstractApiAction.OnSucessActionListener<>(channel) { + @Override + public void onResponse(IndexResponse indexResponse) { + if (securityConfiguration.maybeEntityName().isPresent()) { + ok(channel, "'" + securityConfiguration.entityName() + "' updated."); + } else { + ok(channel, "Resource updated."); + } + } + } + ) + ) + .error((status, toXContent) -> response(channel, status, toXContent)) + ); + break; + case PUT: + add(method, (channel, request, client) -> mapper.apply(request).valid(securityConfiguration -> { + saveOrUpdateConfigurationHandler.apply( + client, + securityConfiguration.configuration(), + new AbstractApiAction.OnSucessActionListener<>(channel) { + @Override + public void onResponse(IndexResponse response) { + if (securityConfiguration.entityExists()) { + ok(channel, "'" + securityConfiguration.entityName() + "' updated."); + } else { + created(channel, "'" + securityConfiguration.entityName() + "' created."); + } + } + } + ); + }).error((status, toXContent) -> response(channel, status, toXContent))); + break; + case DELETE: + Objects.requireNonNull(mapper, "onDeleteRequest request handler can't be null"); + add( + RestRequest.Method.DELETE, + (channel, request, client) -> mapper.apply(request) + .valid( + securityConfiguration -> saveOrUpdateConfigurationHandler.apply( + client, + securityConfiguration.configuration(), + new AbstractApiAction.OnSucessActionListener<>(channel) { + @Override + public void onResponse(IndexResponse response) { + ok(channel, "'" + securityConfiguration.entityName() + "' deleted."); + } + } + ) + ) + .error((status, toXContent) -> response(channel, status, toXContent)) + ); + break; + } + return this; + } + + public void configureRequestHandlers(final Consumer requestHandlersBuilderHandler) { + requestHandlersBuilderHandler.accept(this); + } + + public Map build() { + Objects.requireNonNull(accessHandler, "accessHandler hasn't been set"); + Objects.requireNonNull(saveOrUpdateConfigurationHandler, "saveOrUpdateConfigurationHandler hasn't been set"); + return Map.copyOf(requestHandlers); + } + + } + +} diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/Responses.java b/src/main/java/org/opensearch/security/dlic/rest/api/Responses.java new file mode 100644 index 0000000000..6af8f0e936 --- /dev/null +++ b/src/main/java/org/opensearch/security/dlic/rest/api/Responses.java @@ -0,0 +1,106 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.dlic.rest.api; + +import org.opensearch.ExceptionsHelper; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.rest.BytesRestResponse; +import org.opensearch.rest.RestChannel; +import org.opensearch.rest.RestRequest; + +import java.io.IOException; + +public class Responses { + + public static void ok(final RestChannel channel, final String message) { + response(channel, RestStatus.OK, message); + } + + public static void ok(final RestChannel channel, final ToXContent toXContent) { + response(channel, RestStatus.OK, toXContent); + } + + public static void created(final RestChannel channel, final String message) { + response(channel, RestStatus.CREATED, message); + } + + public static void methodNotImplemented(final RestChannel channel, final RestRequest.Method method) { + notImplemented(channel, "Method " + method.name() + " not supported for this action."); + } + + public static void notImplemented(final RestChannel channel, final String message) { + response(channel, RestStatus.NOT_IMPLEMENTED, message); + } + + public static void notFound(final RestChannel channel, final String message) { + response(channel, RestStatus.NOT_FOUND, message); + } + + public static void conflict(final RestChannel channel, final String message) { + response(channel, RestStatus.CONFLICT, message); + } + + public static void internalSeverError(final RestChannel channel, final String message) { + response(channel, RestStatus.INTERNAL_SERVER_ERROR, message); + } + + public static void forbidden(final RestChannel channel, final String message) { + response(channel, RestStatus.FORBIDDEN, message); + } + + public static void badRequest(final RestChannel channel, final String message) { + response(channel, RestStatus.BAD_REQUEST, message); + } + + public static void unauthorized(final RestChannel channel) { + response(channel, RestStatus.UNAUTHORIZED, "Unauthorized"); + } + + public static void response(RestChannel channel, RestStatus status, String message) { + response(channel, status, payload(status, message)); + } + + public static void response(final RestChannel channel, final RestStatus status, final ToXContent toXContent) { + try (final var builder = channel.newBuilder()) { + toXContent.toXContent(builder, ToXContent.EMPTY_PARAMS); + channel.sendResponse(new BytesRestResponse(status, builder)); + } catch (final IOException ioe) { + throw ExceptionsHelper.convertToOpenSearchException(ioe); + } + } + + public static ToXContent forbiddenMessage(final String message) { + return payload(RestStatus.FORBIDDEN, message); + } + + public static ToXContent badRequestMessage(final String message) { + return payload(RestStatus.BAD_REQUEST, message); + } + + public static ToXContent methodNotImplementedMessage(final RestRequest.Method method) { + return payload(RestStatus.NOT_FOUND, "Method " + method.name() + " not supported for this action."); + } + + public static ToXContent notFoundMessage(final String message) { + return payload(RestStatus.NOT_FOUND, message); + } + + public static ToXContent conflictMessage(final String message) { + return payload(RestStatus.CONFLICT, message); + } + + public static ToXContent payload(final RestStatus status, final String message) { + return (builder, params) -> builder.startObject().field("status", status.name()).field("message", message).endObject(); + } + +} diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/RestApiAdminPrivilegesEvaluator.java b/src/main/java/org/opensearch/security/dlic/rest/api/RestApiAdminPrivilegesEvaluator.java index c72e69876d..d05803be50 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/RestApiAdminPrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/RestApiAdminPrivilegesEvaluator.java @@ -98,7 +98,7 @@ public RestApiAdminPrivilegesEvaluator( this.restapiAdminEnabled = restapiAdminEnabled; } - public boolean isCurrentUserRestApiAdminFor(final Endpoint endpoint, final String action) { + public boolean isCurrentUserAdminFor(final Endpoint endpoint, final String action) { final Pair userAndRemoteAddress = Utils.userAndRemoteAddressFrom(threadContext); if (userAndRemoteAddress.getLeft() == null) { return false; @@ -154,8 +154,8 @@ public boolean containsRestApiAdminPermissions(final Object configObject) { } } - public boolean isCurrentUserRestApiAdminFor(final Endpoint endpoint) { - return isCurrentUserRestApiAdminFor(endpoint, null); + public boolean isCurrentUserAdminFor(final Endpoint endpoint) { + return isCurrentUserAdminFor(endpoint, null); } private static String buildEndpointActionPermission(final Endpoint endpoint, final String action) { 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 5523bc08a7..a7f9708e6e 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 @@ -16,35 +16,30 @@ 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.common.inject.Inject; import org.opensearch.common.settings.Settings; -import org.opensearch.rest.RestController; +import org.opensearch.core.rest.RestStatus; import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestRequest.Method; -import org.opensearch.security.auditlog.AuditLog; -import org.opensearch.security.configuration.AdminDNs; -import org.opensearch.security.configuration.ConfigurationRepository; import org.opensearch.security.configuration.MaskedField; import org.opensearch.security.configuration.Salt; +import org.opensearch.security.dlic.rest.validation.EndpointValidator; 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.api.RequestHandler.methodNotImplementedHandler; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; -public class RolesApiAction extends PatchableResourceApiAction { +public class RolesApiAction extends AbstractApiAction { + private static final List routes = addRoutesPrefix( ImmutableList.of( new Route(Method.GET, "/roles/"), @@ -65,23 +60,23 @@ protected RoleValidator(ValidationContext validationContext) { } @Override - public ValidationResult validate(RestRequest request) throws IOException { + public ValidationResult validate(RestRequest request) throws IOException { return super.validate(request).map(this::validateMaskedFields); } @Override - public ValidationResult validate(RestRequest request, JsonNode jsonContent) throws IOException { + public ValidationResult validate(RestRequest request, JsonNode jsonContent) throws IOException { return super.validate(request, jsonContent).map(this::validateMaskedFields); } - private ValidationResult validateMaskedFields(final JsonNode content) { + 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.error(RestStatus.BAD_REQUEST, this); } } } @@ -102,19 +97,12 @@ private boolean validateMaskedFieldSyntax(String mf) { @Inject public RolesApiAction( - Settings settings, - final Path configPath, - RestController controller, - Client client, - AdminDNs adminDNs, - ConfigurationRepository cl, - ClusterService cs, - final PrincipalExtractor principalExtractor, - final PrivilegesEvaluator evaluator, - ThreadPool threadPool, - AuditLog auditLog + final ClusterService clusterService, + final ThreadPool threadPool, + final SecurityApiDependencies securityApiDependencies ) { - super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); + super(Endpoint.ROLES, clusterService, threadPool, securityApiDependencies); + this.requestHandlersBuilder.configureRequestHandlers(this::rolesApiRequestHandlers); } @Override @@ -123,66 +111,65 @@ public List routes() { } @Override - protected Endpoint getEndpoint() { - return Endpoint.ROLES; + protected CType getConfigType() { + return CType.ROLES; + } + + private void rolesApiRequestHandlers(RequestHandler.RequestHandlersBuilder requestHandlersBuilder) { + requestHandlersBuilder.onChangeRequest(Method.PATCH, this::processPatchRequest).override(Method.POST, methodNotImplementedHandler); } @Override - protected RequestContentValidator createValidator(final Object... params) { - return new RoleValidator(new RequestContentValidator.ValidationContext() { + protected EndpointValidator createEndpointValidator() { + return new EndpointValidator() { + @Override - public Object[] params() { - return params; + public Endpoint endpoint() { + return endpoint; } @Override - public Settings settings() { - return settings; + public RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator() { + return securityApiDependencies.restApiAdminPrivilegesEvaluator(); } @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(); + public ValidationResult isAllowedToChangeImmutableEntity(SecurityConfiguration securityConfiguration) + throws IOException { + return EndpointValidator.super.isAllowedToChangeImmutableEntity(securityConfiguration).map(ignore -> { + if (isCurrentUserAdmin()) { + return ValidationResult.success(securityConfiguration); + } + return isAllowedToChangeEntityWithRestAdminPermissions(securityConfiguration); + }); } - }); - } - - @Override - protected String getResourceName() { - return "role"; - } - @Override - protected CType getConfigName() { - return CType.ROLES; - } + @Override + public RequestContentValidator createRequestContentValidator(Object... params) { + return new RoleValidator(new RequestContentValidator.ValidationContext() { + @Override + public Object[] params() { + return params; + } - @Override - protected boolean hasPermissionsToCreate( - final SecurityDynamicConfiguration dynamicConfiguration, - final Object content, - final String resourceName - ) throws IOException { - if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(content)) { - return isSuperAdmin(); - } else { - return true; - } - } + @Override + public Settings settings() { + return securityApiDependencies.settings(); + } - @Override - protected boolean isReadOnly(SecurityDynamicConfiguration existingConfiguration, String name) { - if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(existingConfiguration.getCEntry(name))) { - return !isSuperAdmin(); - } else { - return super.isReadOnly(existingConfiguration, name); - } + @Override + public Map allowedKeys() { + final ImmutableMap.Builder allowedKeys = ImmutableMap.builder(); + if (isCurrentUserAdmin()) 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(); + } + }); + } + }; } } 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 7fba7e897c..0c71a4731f 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 @@ -12,40 +12,30 @@ 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 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.common.inject.Inject; import org.opensearch.common.settings.Settings; -import org.opensearch.rest.RestChannel; -import org.opensearch.rest.RestController; -import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestRequest.Method; -import org.opensearch.security.DefaultObjectMapper; -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.EndpointValidator; 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.dlic.rest.validation.ValidationResult; 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 static org.opensearch.security.dlic.rest.api.RequestHandler.methodNotImplementedHandler; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; -public class RolesMappingApiAction extends PatchableResourceApiAction { +public class RolesMappingApiAction extends AbstractApiAction { + private static final List routes = addRoutesPrefix( ImmutableList.of( new Route(Method.GET, "/rolesmapping/"), @@ -59,139 +49,104 @@ public class RolesMappingApiAction extends PatchableResourceApiAction { @Inject public RolesMappingApiAction( - final Settings settings, - final Path configPath, - final RestController controller, - final Client client, - final AdminDNs adminDNs, - final ConfigurationRepository cl, - final ClusterService cs, - final PrincipalExtractor principalExtractor, - final PrivilegesEvaluator evaluator, - ThreadPool threadPool, - AuditLog auditLog + final ClusterService clusterService, + final ThreadPool threadPool, + final SecurityApiDependencies securityApiDependencies ) { - super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); - } - - @Override - protected void handlePut(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 rolesConfiguration = load(CType.ROLES, false); - final SecurityDynamicConfiguration rolesMappingConfiguration = load(getConfigName(), false); - final boolean rolesMappingExists = rolesMappingConfiguration.exists(name); - - if (!isValidRolesMapping(channel, name)) return; - - if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(rolesConfiguration.getCEntry(name))) { - if (!isSuperAdmin()) { - forbidden(channel, "No permissions"); - return; - } - } - rolesMappingConfiguration.putCObject(name, DefaultObjectMapper.readTree(content, rolesMappingConfiguration.getImplementingClass())); - - saveAndUpdateConfigs( - this.securityIndexName, - client, - getConfigName(), - rolesMappingConfiguration, - new OnSucessActionListener(channel) { - - @Override - public void onResponse(IndexResponse response) { - if (rolesMappingExists) { - successResponse(channel, "'" + name + "' updated."); - } else { - createdResponse(channel, "'" + name + "' created."); - } - - } - } + super(Endpoint.ROLESMAPPING, clusterService, threadPool, securityApiDependencies); + this.requestHandlersBuilder.configureRequestHandlers( + builder -> builder.onChangeRequest(Method.PATCH, this::processPatchRequest).override(Method.POST, methodNotImplementedHandler) ); } - @Override - protected boolean hasPermissionsToCreate( - final SecurityDynamicConfiguration dynamicConfigFactory, - final Object content, - final String resourceName - ) throws IOException { - final SecurityDynamicConfiguration rolesConfiguration = load(CType.ROLES, false); - if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(rolesConfiguration.getCEntry(resourceName))) { - return isSuperAdmin(); - } else { - return true; - } - } - - @Override - protected boolean isReadOnly(SecurityDynamicConfiguration existingConfiguration, String name) { - final SecurityDynamicConfiguration rolesConfiguration = load(CType.ROLES, false); - if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(rolesConfiguration.getCEntry(name))) { - return !isSuperAdmin(); - } else { - return super.isReadOnly(existingConfiguration, name); - } - } - @Override public List routes() { return routes; } @Override - protected Endpoint getEndpoint() { - return Endpoint.ROLESMAPPING; + protected CType getConfigType() { + return CType.ROLESMAPPING; } @Override - protected RequestContentValidator createValidator(final Object... params) { - return RequestContentValidator.of(new RequestContentValidator.ValidationContext() { + protected EndpointValidator createEndpointValidator() { + return new EndpointValidator() { @Override - public Object[] params() { - return params; + public Endpoint endpoint() { + return endpoint; } @Override - public Settings settings() { - return settings; + public RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator() { + return securityApiDependencies.restApiAdminPrivilegesEvaluator(); } @Override - public Set mandatoryOrKeys() { - return ImmutableSet.of("backend_roles", "and_backend_roles", "hosts", "users"); + public ValidationResult onConfigChange(SecurityConfiguration securityConfiguration) throws IOException { + return EndpointValidator.super.onConfigChange(securityConfiguration).map(this::validateRoleForMapping); + } + + private ValidationResult validateRoleForMapping(final SecurityConfiguration securityConfiguration) + throws IOException { + return loadConfiguration(CType.ROLES, false, false).map( + rolesConfiguration -> validateRoles(List.of(securityConfiguration.entityName()), rolesConfiguration) + ).map(ignore -> ValidationResult.success(securityConfiguration)); } @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(); + public ValidationResult isAllowedToChangeImmutableEntity(SecurityConfiguration securityConfiguration) + throws IOException { + return EndpointValidator.super.isAllowedToChangeImmutableEntity(securityConfiguration).map( + this::isAllowedToChangeRoleMappingWithRestAdminPermissions + ); } - }); - } - @Override - protected String getResourceName() { - return "rolesmapping"; - } + public ValidationResult isAllowedToChangeRoleMappingWithRestAdminPermissions( + SecurityConfiguration securityConfiguration + ) throws IOException { + return loadConfiguration(CType.ROLES, false, false).map(rolesConfiguration -> { + if (isCurrentUserAdmin()) { + return ValidationResult.success(securityConfiguration); + } + return isAllowedToChangeEntityWithRestAdminPermissions( + SecurityConfiguration.of(securityConfiguration.entityName(), rolesConfiguration) + ); + }).map(ignore -> ValidationResult.success(securityConfiguration)); + } - @Override - protected CType getConfigName() { - return CType.ROLESMAPPING; + @Override + public RequestContentValidator createRequestContentValidator(Object... params) { + return RequestContentValidator.of(new RequestContentValidator.ValidationContext() { + @Override + public Object[] params() { + return params; + } + + @Override + public Settings settings() { + return securityApiDependencies.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 (isCurrentUserAdmin()) 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(); + } + }); + } + }; } } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityApiDependencies.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityApiDependencies.java new file mode 100644 index 0000000000..498230423f --- /dev/null +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityApiDependencies.java @@ -0,0 +1,80 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.dlic.rest.api; + +import org.opensearch.common.settings.Settings; +import org.opensearch.security.auditlog.AuditLog; +import org.opensearch.security.configuration.AdminDNs; +import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.support.ConfigConstants; + +public class SecurityApiDependencies { + private AdminDNs adminDNs; + private final ConfigurationRepository configurationRepository; + private final RestApiPrivilegesEvaluator restApiPrivilegesEvaluator; + private final RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator; + private final AuditLog auditLog; + private final Settings settings; + + private final PrivilegesEvaluator privilegesEvaluator; + + public SecurityApiDependencies( + final AdminDNs adminDNs, + final ConfigurationRepository configurationRepository, + final PrivilegesEvaluator privilegesEvaluator, + final RestApiPrivilegesEvaluator restApiPrivilegesEvaluator, + final RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator, + final AuditLog auditLog, + final Settings settings + ) { + this.adminDNs = adminDNs; + this.configurationRepository = configurationRepository; + this.privilegesEvaluator = privilegesEvaluator; + this.restApiPrivilegesEvaluator = restApiPrivilegesEvaluator; + this.restApiAdminPrivilegesEvaluator = restApiAdminPrivilegesEvaluator; + this.auditLog = auditLog; + this.settings = settings; + } + + public AdminDNs adminDNs() { + return adminDNs; + } + + public PrivilegesEvaluator privilegesEvaluator() { + return privilegesEvaluator; + } + + public ConfigurationRepository configurationRepository() { + return configurationRepository; + } + + public RestApiPrivilegesEvaluator restApiPrivilegesEvaluator() { + return restApiPrivilegesEvaluator; + } + + public RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator() { + return restApiAdminPrivilegesEvaluator; + } + + public AuditLog auditLog() { + return auditLog; + } + + public Settings settings() { + return settings; + } + + public String securityIndexName() { + return settings().get(ConfigConstants.SECURITY_CONFIG_INDEX_NAME, ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX); + } +} 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 deleted file mode 100644 index 963d64a4df..0000000000 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfigAction.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.dlic.rest.api; - -import java.io.IOException; -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.common.inject.Inject; -import org.opensearch.common.settings.Settings; -import org.opensearch.rest.RestChannel; -import org.opensearch.rest.RestController; -import org.opensearch.rest.RestRequest; -import org.opensearch.rest.RestRequest.Method; -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.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.security.support.ConfigConstants; -import org.opensearch.threadpool.ThreadPool; - -import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; - -public class SecurityConfigAction extends PatchableResourceApiAction { - - private static final List getRoutes = addRoutesPrefix(Collections.singletonList(new Route(Method.GET, "/securityconfig/"))); - - private static final List allRoutes = new ImmutableList.Builder().addAll(getRoutes) - .addAll( - addRoutesPrefix(ImmutableList.of(new Route(Method.PUT, "/securityconfig/{name}"), new Route(Method.PATCH, "/securityconfig/"))) - ) - .build(); - - private final boolean allowPutOrPatch; - - @Inject - public SecurityConfigAction( - final Settings settings, - final Path configPath, - final RestController controller, - final Client client, - final AdminDNs adminDNs, - final ConfigurationRepository cl, - final ClusterService cs, - final PrincipalExtractor principalExtractor, - final PrivilegesEvaluator evaluator, - ThreadPool threadPool, - AuditLog auditLog - ) { - - super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); - allowPutOrPatch = settings.getAsBoolean(ConfigConstants.SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION, false); - } - - @Override - public List routes() { - return allowPutOrPatch ? allRoutes : getRoutes; - } - - @Override - protected boolean hasPermissionsToCreate( - final SecurityDynamicConfiguration dynamicConfigFactory, - final Object content, - final String resourceName - ) { - return true; - } - - @Override - protected void handleGet(RestChannel channel, RestRequest request, Client client, final JsonNode content) throws IOException { - final SecurityDynamicConfiguration configuration = load(getConfigName(), true); - - filter(configuration); - - successResponse(channel, configuration); - } - - @Override - protected void handleApiRequest(RestChannel channel, RestRequest request, Client client) throws IOException { - if (request.method() == Method.PATCH && !allowPutOrPatch) { - notImplemented(channel, Method.PATCH); - } else { - super.handleApiRequest(channel, request, client); - } - } - - @Override - protected void handlePut(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) - throws IOException { - if (allowPutOrPatch) { - - if (!"config".equals(request.param("name"))) { - badRequestResponse(channel, "name must be config"); - return; - } - - super.handlePut(channel, request, client, content); - } else { - notImplemented(channel, Method.PUT); - } - } - - @Override - protected void handlePost(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) - throws IOException { - notImplemented(channel, Method.POST); - } - - @Override - protected void handleDelete(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) - throws IOException { - notImplemented(channel, Method.DELETE); - } - - @Override - 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 - protected CType getConfigName() { - return CType.CONFIG; - } - - @Override - protected Endpoint getEndpoint() { - return Endpoint.CONFIG; - } - - @Override - protected String getResourceName() { - // not needed, no single resource - return null; - } - -} diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiAction.java new file mode 100644 index 0000000000..62865cf2e1 --- /dev/null +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiAction.java @@ -0,0 +1,136 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.dlic.rest.api; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import com.google.common.collect.ImmutableList; + +import com.google.common.collect.ImmutableMap; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.inject.Inject; +import org.opensearch.common.settings.Settings; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.RestRequest.Method; +import org.opensearch.security.dlic.rest.validation.EndpointValidator; +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.securityconf.impl.CType; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.threadpool.ThreadPool; + +import static org.opensearch.security.dlic.rest.api.RequestHandler.methodNotImplementedHandler; +import static org.opensearch.security.dlic.rest.api.Responses.badRequestMessage; +import static org.opensearch.security.dlic.rest.api.Responses.methodNotImplementedMessage; +import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; + +public class SecurityConfigApiAction extends AbstractApiAction { + + private static final List getRoutes = addRoutesPrefix(Collections.singletonList(new Route(Method.GET, "/securityconfig/"))); + + private static final List allRoutes = new ImmutableList.Builder().addAll(getRoutes) + .addAll( + addRoutesPrefix(ImmutableList.of(new Route(Method.PUT, "/securityconfig/{name}"), new Route(Method.PATCH, "/securityconfig/"))) + ) + .build(); + + private final boolean allowPutOrPatch; + + @Inject + public SecurityConfigApiAction( + final ClusterService clusterService, + final ThreadPool threadPool, + final SecurityApiDependencies securityApiDependencies + ) { + + super(Endpoint.CONFIG, clusterService, threadPool, securityApiDependencies); + allowPutOrPatch = securityApiDependencies.settings() + .getAsBoolean(ConfigConstants.SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION, false); + this.requestHandlersBuilder.configureRequestHandlers(this::securityConfigApiActionRequestHandlers); + } + + @Override + public List routes() { + return allowPutOrPatch ? allRoutes : getRoutes; + } + + @Override + protected CType getConfigType() { + return CType.CONFIG; + } + + private void securityConfigApiActionRequestHandlers(RequestHandler.RequestHandlersBuilder requestHandlersBuilder) { + requestHandlersBuilder.onChangeRequest( + Method.PUT, + request -> withAllowedEndpoint(request).map(this::withConfigEntityNameOnly).map(ignore -> processPutRequest(request)) + ) + .onChangeRequest(Method.PATCH, request -> withAllowedEndpoint(request).map(this::processPatchRequest)) + .override(Method.DELETE, methodNotImplementedHandler) + .override(Method.POST, methodNotImplementedHandler); + } + + ValidationResult withAllowedEndpoint(final RestRequest request) { + if (!allowPutOrPatch) { + return ValidationResult.error(RestStatus.NOT_IMPLEMENTED, methodNotImplementedMessage(request.method())); + } + return ValidationResult.success(request); + } + + ValidationResult withConfigEntityNameOnly(final RestRequest request) { + final var name = nameParam(request); + if (!"config".equals(name)) { + return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage("name must be config")); + } + return ValidationResult.success(name); + } + + @Override + protected EndpointValidator createEndpointValidator() { + return new EndpointValidator() { + + @Override + public Endpoint endpoint() { + return endpoint; + } + + @Override + public RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator() { + return securityApiDependencies.restApiAdminPrivilegesEvaluator(); + } + + @Override + public RequestContentValidator createRequestContentValidator(Object... params) { + return RequestContentValidator.of(new RequestContentValidator.ValidationContext() { + @Override + public Object[] params() { + return params; + } + + @Override + public Settings settings() { + return securityApiDependencies.settings(); + } + + @Override + public Map allowedKeys() { + return ImmutableMap.of("dynamic", DataType.OBJECT); + } + }); + } + }; + } + +} diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfiguration.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfiguration.java new file mode 100644 index 0000000000..68f17ac5f5 --- /dev/null +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfiguration.java @@ -0,0 +1,85 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.dlic.rest.api; + +import com.fasterxml.jackson.databind.JsonNode; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; + +import java.util.Objects; +import java.util.Optional; + +public class SecurityConfiguration { + + private final String entityName; + + private final boolean entityExists; + + private final JsonNode requestContent; + + private final SecurityDynamicConfiguration configuration; + + private SecurityConfiguration( + final String entityName, + final boolean entityExists, + final JsonNode requestContent, + final SecurityDynamicConfiguration configuration + ) { + this.entityName = entityName; + this.entityExists = entityExists; + this.requestContent = requestContent; + this.configuration = configuration; + } + + private SecurityConfiguration( + final String entityName, + final boolean entityExists, + final SecurityDynamicConfiguration configuration + ) { + this(entityName, entityExists, null, configuration); + } + + public SecurityDynamicConfiguration configuration() { + return configuration; + } + + public boolean entityExists() { + return entityExists; + } + + public JsonNode requestContent() { + return requestContent; + } + + public Optional maybeEntityName() { + return Optional.ofNullable(entityName); + } + + public String entityName() { + return maybeEntityName().orElse("empty"); + } + + public static SecurityConfiguration of(final String entityName, final SecurityDynamicConfiguration configuration) { + Objects.requireNonNull(configuration, "configuration hasn't been set"); + return new SecurityConfiguration(entityName, configuration.exists(entityName), configuration); + } + + public static SecurityConfiguration of( + final JsonNode requestContent, + final String entityName, + final SecurityDynamicConfiguration configuration + ) { + Objects.requireNonNull(configuration, "configuration hasn't been set"); + Objects.requireNonNull(requestContent, "requestContent hasn't been set"); + return new SecurityConfiguration(entityName, configuration.exists(entityName), requestContent, configuration); + } + +} diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java index e7d68a8677..2eda752a82 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java @@ -11,12 +11,6 @@ package org.opensearch.security.dlic.rest.api; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; - import org.opensearch.client.Client; import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.settings.Settings; @@ -31,6 +25,12 @@ import org.opensearch.security.user.UserService; import org.opensearch.threadpool.ThreadPool; +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; + +import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED; + public class SecurityRestApiActions { public static Collection getHandler( @@ -39,8 +39,8 @@ public static Collection getHandler( final RestController controller, final Client client, final AdminDNs adminDns, - final ConfigurationRepository cr, - final ClusterService cs, + final ConfigurationRepository configurationRepository, + final ClusterService clusterService, final PrincipalExtractor principalExtractor, final PrivilegesEvaluator evaluator, final ThreadPool threadPool, @@ -49,281 +49,54 @@ public static Collection getHandler( final UserService userService, final boolean certificatesReloadEnabled ) { - final List handlers = new ArrayList(16); - handlers.add( - new InternalUsersApiAction( - settings, - configPath, - controller, - client, - adminDns, - cr, - cs, - principalExtractor, - evaluator, - threadPool, - userService, - auditLog - ) - ); - handlers.add( - new RolesMappingApiAction( - settings, - configPath, - controller, - client, - adminDns, - cr, - cs, - principalExtractor, - evaluator, - threadPool, - auditLog - ) - ); - handlers.add( - new RolesApiAction( - settings, - configPath, - controller, - client, - adminDns, - cr, - cs, - principalExtractor, - evaluator, - threadPool, - auditLog - ) - ); - handlers.add( - new ActionGroupsApiAction( - settings, - configPath, - controller, - client, - adminDns, - cr, - cs, - principalExtractor, - evaluator, - threadPool, - auditLog - ) - ); - handlers.add( - new FlushCacheApiAction( - settings, - configPath, - controller, - client, - adminDns, - cr, - cs, - principalExtractor, - evaluator, - threadPool, - auditLog - ) - ); - handlers.add( - new SecurityConfigAction( - settings, - configPath, - controller, - client, - adminDns, - cr, - cs, - principalExtractor, - evaluator, - threadPool, - auditLog - ) - ); - handlers.add( + final var securityApiDependencies = new SecurityApiDependencies( + adminDns, + configurationRepository, + evaluator, + new RestApiPrivilegesEvaluator(settings, adminDns, evaluator, principalExtractor, configPath, threadPool), + new RestApiAdminPrivilegesEvaluator( + threadPool.getThreadContext(), + evaluator, + adminDns, + settings.getAsBoolean(SECURITY_RESTAPI_ADMIN_ENABLED, false) + ), + auditLog, + settings + ); + return List.of( + new InternalUsersApiAction(clusterService, threadPool, userService, securityApiDependencies), + new RolesMappingApiAction(clusterService, threadPool, securityApiDependencies), + new RolesApiAction(clusterService, threadPool, securityApiDependencies), + new ActionGroupsApiAction(clusterService, threadPool, securityApiDependencies), + new FlushCacheApiAction(clusterService, threadPool, securityApiDependencies), + new SecurityConfigApiAction(clusterService, threadPool, securityApiDependencies), + // FIXME Change inheritance for PermissionsInfoAction new PermissionsInfoAction( settings, configPath, controller, client, adminDns, - cr, - cs, - principalExtractor, - evaluator, - threadPool, - auditLog - ) - ); - handlers.add( - new AuthTokenProcessorAction( - settings, - configPath, - controller, - client, - adminDns, - cr, - cs, - principalExtractor, - evaluator, - threadPool, - auditLog - ) - ); - handlers.add( - new TenantsApiAction( - settings, - configPath, - controller, - client, - adminDns, - cr, - cs, + configurationRepository, + clusterService, principalExtractor, evaluator, threadPool, auditLog - ) - ); - handlers.add( - new MigrateApiAction( - settings, - configPath, - controller, - client, - adminDns, - cr, - cs, - principalExtractor, - evaluator, - threadPool, - auditLog - ) - ); - handlers.add( - new ValidateApiAction( - settings, - configPath, - controller, - client, - adminDns, - cr, - cs, - principalExtractor, - evaluator, - threadPool, - auditLog - ) - ); - handlers.add( - new AccountApiAction( - settings, - configPath, - controller, - client, - adminDns, - cr, - cs, - principalExtractor, - evaluator, - threadPool, - auditLog - ) - ); - handlers.add( - new NodesDnApiAction( - settings, - configPath, - controller, - client, - adminDns, - cr, - cs, - principalExtractor, - evaluator, - threadPool, - auditLog - ) - ); - handlers.add( - new WhitelistApiAction( - settings, - configPath, - controller, - client, - adminDns, - cr, - cs, - principalExtractor, - evaluator, - threadPool, - auditLog - ) - ); - handlers.add( - new AllowlistApiAction( - settings, - configPath, - controller, - client, - adminDns, - cr, - cs, - principalExtractor, - evaluator, - threadPool, - auditLog - ) - ); - handlers.add( - new AuditApiAction( - settings, - configPath, - controller, - client, - adminDns, - cr, - cs, - principalExtractor, - evaluator, - threadPool, - auditLog - ) - ); - handlers.add( - new MultiTenancyConfigApiAction( - settings, - configPath, - controller, - client, - adminDns, - cr, - cs, - principalExtractor, - evaluator, - threadPool, - auditLog - ) - ); - handlers.add( - new SecuritySSLCertsAction( - settings, - configPath, - controller, - client, - adminDns, - cr, - cs, - principalExtractor, - evaluator, - threadPool, - auditLog, - securityKeyStore, - certificatesReloadEnabled - ) + ), + new AuthTokenProcessorAction(clusterService, threadPool, securityApiDependencies), + new TenantsApiAction(clusterService, threadPool, securityApiDependencies), + new MigrateApiAction(clusterService, threadPool, securityApiDependencies), + new ValidateApiAction(clusterService, threadPool, securityApiDependencies), + new AccountApiAction(clusterService, threadPool, securityApiDependencies), + new NodesDnApiAction(clusterService, threadPool, securityApiDependencies), + new WhitelistApiAction(clusterService, threadPool, securityApiDependencies), + // FIXME change it as soon as WhitelistApiAction will be removed + new AllowlistApiAction(Endpoint.ALLOWLIST, clusterService, threadPool, securityApiDependencies), + new AuditApiAction(clusterService, threadPool, securityApiDependencies), + new MultiTenancyConfigApiAction(clusterService, threadPool, securityApiDependencies), + new SecuritySSLCertsAction(clusterService, threadPool, securityKeyStore, certificatesReloadEnabled, securityApiDependencies) ); - return Collections.unmodifiableCollection(handlers); } } 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 0e454eb8f6..639d93c6ab 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 @@ -11,43 +11,32 @@ package org.opensearch.security.dlic.rest.api; -import java.io.IOException; -import java.nio.file.Path; -import java.security.cert.X509Certificate; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import com.fasterxml.jackson.databind.JsonNode; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.client.Client; +import org.opensearch.OpenSearchSecurityException; import org.opensearch.cluster.service.ClusterService; -import org.opensearch.common.settings.Settings; -import org.opensearch.core.xcontent.XContentBuilder; -import org.opensearch.rest.BytesRestResponse; +import org.opensearch.core.rest.RestStatus; import org.opensearch.rest.RestChannel; -import org.opensearch.rest.RestController; import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestRequest.Method; -import org.opensearch.core.rest.RestStatus; -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.RequestContentValidator; -import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.dlic.rest.validation.ValidationResult; import org.opensearch.security.securityconf.impl.CType; -import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.ssl.SecurityKeyStore; -import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.ssl.util.SSLConfigConstants; import org.opensearch.security.support.ConfigConstants; import org.opensearch.threadpool.ThreadPool; +import java.io.IOException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.opensearch.security.dlic.rest.api.Responses.badRequest; +import static org.opensearch.security.dlic.rest.api.Responses.badRequestMessage; +import static org.opensearch.security.dlic.rest.api.Responses.ok; +import static org.opensearch.security.dlic.rest.api.Responses.response; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; /** @@ -58,11 +47,9 @@ */ public class SecuritySSLCertsAction extends AbstractApiAction { private static final List ROUTES = addRoutesPrefix( - ImmutableList.of(new Route(Method.GET, "/ssl/certs"), new Route(Method.PUT, "/ssl/{certType}/reloadcerts")) + ImmutableList.of(new Route(Method.GET, "/ssl/certs"), new Route(Method.PUT, "/ssl/{certType}/reloadcerts/")) ); - private final Logger log = LogManager.getLogger(this.getClass()); - private final SecurityKeyStore securityKeyStore; private final boolean certificatesReloadEnabled; @@ -70,57 +57,51 @@ public class SecuritySSLCertsAction extends AbstractApiAction { private final boolean httpsEnabled; public SecuritySSLCertsAction( - final Settings settings, - final Path configPath, - final RestController controller, - final Client client, - final AdminDNs adminDNs, - final ConfigurationRepository cl, - final ClusterService cs, - final PrincipalExtractor principalExtractor, - final PrivilegesEvaluator privilegesEvaluator, + final ClusterService clusterService, final ThreadPool threadPool, - final AuditLog auditLog, final SecurityKeyStore securityKeyStore, - final boolean certificatesReloadEnabled + final boolean certificatesReloadEnabled, + final SecurityApiDependencies securityApiDependencies ) { - super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, privilegesEvaluator, threadPool, auditLog); + super(Endpoint.SSL, clusterService, threadPool, securityApiDependencies); this.securityKeyStore = securityKeyStore; this.certificatesReloadEnabled = certificatesReloadEnabled; - this.httpsEnabled = settings.getAsBoolean(SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED, true); + this.httpsEnabled = securityApiDependencies.settings().getAsBoolean(SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED, true); + this.requestHandlersBuilder.configureRequestHandlers(this::securitySSLCertsRequestHandlers); } @Override - protected boolean hasPermissionsToCreate( - final SecurityDynamicConfiguration dynamicConfigFactory, - final Object content, - final String resourceName - ) { - return true; + public List routes() { + return ROUTES; } @Override - public List routes() { - return ROUTES; + public String getName() { + return "SSL Certificates Action"; } @Override - protected void handleApiRequest(final RestChannel channel, final RestRequest request, final Client client) throws IOException { - switch (request.method()) { - case GET: - if (!restApiAdminPrivilegesEvaluator.isCurrentUserRestApiAdminFor(getEndpoint(), "certs")) { - forbidden(channel, ""); - return; - } - handleGet(channel, request, client, null); - break; - case PUT: - if (!restApiAdminPrivilegesEvaluator.isCurrentUserRestApiAdminFor(getEndpoint(), "reloadcerts")) { - forbidden(channel, ""); - return; - } + protected void consumeParameters(RestRequest request) { + request.param("certType"); + } + + @Override + protected CType getConfigType() { + return null; + } + + private void securitySSLCertsRequestHandlers(RequestHandler.RequestHandlersBuilder requestHandlersBuilder) { + requestHandlersBuilder.withAccessHandler(this::accessHandler) + .allMethodsNotImplemented() + .verifyAccessForAllMethods() + .override( + Method.GET, + (channel, request, client) -> withSecurityKeyStore().valid(keyStore -> loadCertificates(channel, keyStore)) + .error((status, toXContent) -> response(channel, status, toXContent)) + ) + .override(Method.PUT, (channel, request, client) -> withSecurityKeyStore().valid(keyStore -> { if (!certificatesReloadEnabled) { - badRequestResponse( + badRequest( channel, String.format( "no handler found for uri [%s] and method [%s]. In order to use SSL reload functionality set %s to true", @@ -129,138 +110,38 @@ protected void handleApiRequest(final RestChannel channel, final RestRequest req ConfigConstants.SECURITY_SSL_CERT_RELOAD_ENABLED ) ); - return; + } else { + reloadCertificates(channel, request, keyStore); } - handlePut(channel, request, client, null); - break; + }).error((status, toXContent) -> response(channel, status, toXContent))); + } + + private boolean accessHandler(final RestRequest request) { + switch (request.method()) { + case GET: + return securityApiDependencies.restApiAdminPrivilegesEvaluator().isCurrentUserAdminFor(endpoint, "certs"); + case PUT: + return securityApiDependencies.restApiAdminPrivilegesEvaluator().isCurrentUserAdminFor(endpoint, "reloadcerts"); default: - notImplemented(channel, request.method()); - break; + return false; } } - /** - * GET request to fetch transport certificate details - * - * Sample request: - * GET _plugins/_security/api/ssl/certs - * - * Sample response: - * { - * "http_certificates_list" : [ - * { - * "issuer_dn" : "CN=Example Com Inc. Signing CA, OU=Example Com Inc. Signing CA, O=Example Com Inc., DC=example, DC=com", - * "subject_dn" : "CN=transport-0.example.com, OU=SSL, O=Test, L=Test, C=DE", - * "san", "[[2, node-0.example.com], [2, localhost], [7, 127.0.0.1], [8, 1.2.3.4.5.5]]", - * "not_before" : "2018-05-05T14:37:09.000Z", - * "not_after" : "2028-05-02T14:37:09.000Z" - * } - * "transport_certificates_list" : [ - * { - * "issuer_dn" : "CN=Example Com Inc. Signing CA, OU=Example Com Inc. Signing CA, O=Example Com Inc., DC=example, DC=com", - * "subject_dn" : "CN=transport-0.example.com, OU=SSL, O=Test, L=Test, C=DE", - * "san", "[[2, node-0.example.com], [2, localhost], [7, 127.0.0.1], [8, 1.2.3.4.5.5]]", - * "not_before" : "2018-05-05T14:37:09.000Z", - * "not_after" : "2028-05-02T14:37:09.000Z" - * } - * ] - * } - * - * @param request request to be served - * @param client client - * @throws IOException - */ - @Override - protected void handleGet(final RestChannel channel, final RestRequest request, final Client client, final JsonNode content) - throws IOException { + private ValidationResult withSecurityKeyStore() { if (securityKeyStore == null) { - noKeyStoreResponse(channel); - return; - } - try (final XContentBuilder contentBuilder = channel.newBuilder()) { - channel.sendResponse( - new BytesRestResponse( - RestStatus.OK, - contentBuilder.startObject() - .field("http_certificates_list", httpsEnabled ? generateCertDetailList(securityKeyStore.getHttpCerts()) : null) - .field("transport_certificates_list", generateCertDetailList(securityKeyStore.getTransportCerts())) - .endObject() - ) - ); - } catch (final Exception e) { - internalErrorResponse(channel, e.getMessage()); - log.error("Error handle request ", e); + return ValidationResult.error(RestStatus.OK, badRequestMessage("keystore is not initialized")); } + return ValidationResult.success(securityKeyStore); } - /** - * PUT request to reload SSL Certificates. - * - * Sample request: - * PUT _opendistro/_security/api/ssl/transport/reloadcerts - * PUT _opendistro/_security/api/ssl/http/reloadcerts - * - * NOTE: No request body is required. We will assume new certificates are loaded in the paths specified in your opensearch.yml file - * (https://docs-beta.opensearch.org/docs/security/configuration/tls/) - * - * Sample response: - * { "message": "updated http certs" } - * - * @param request request to be served - * @param client client - * @throws IOException - */ - @Override - protected void handlePut(final RestChannel channel, final RestRequest request, final Client client, final JsonNode content) - throws IOException { - if (securityKeyStore == null) { - noKeyStoreResponse(channel); - return; - } - final String certType = request.param("certType").toLowerCase().trim(); - try (final XContentBuilder contentBuilder = channel.newBuilder()) { - switch (certType) { - case "http": - if (!httpsEnabled) { - badRequestResponse(channel, "SSL for HTTP is disabled"); - return; - } - securityKeyStore.initHttpSSLConfig(); - channel.sendResponse( - new BytesRestResponse( - RestStatus.OK, - contentBuilder.startObject().field("message", "updated http certs").endObject() - ) - ); - break; - case "transport": - securityKeyStore.initTransportSSLConfig(); - channel.sendResponse( - new BytesRestResponse( - RestStatus.OK, - contentBuilder.startObject().field("message", "updated transport certs").endObject() - ) - ); - break; - default: - forbidden( - channel, - "invalid uri path, please use /_plugins/_security/api/ssl/http/reload or " - + "/_plugins/_security/api/ssl/transport/reload" - ); - break; - } - } catch (final Exception e) { - log.error("Reload of certificates for {} failed", certType, e); - try (final XContentBuilder contentBuilder = channel.newBuilder()) { - channel.sendResponse( - new BytesRestResponse( - RestStatus.INTERNAL_SERVER_ERROR, - contentBuilder.startObject().field("error", e.toString()).endObject() - ) - ); - } - } + protected void loadCertificates(final RestChannel channel, final SecurityKeyStore keyStore) throws IOException { + ok( + channel, + (builder, params) -> builder.startObject() + .field("http_certificates_list", httpsEnabled ? generateCertDetailList(keyStore.getHttpCerts()) : null) + .field("transport_certificates_list", generateCertDetailList(keyStore.getTransportCerts())) + .endObject() + ); } private List> generateCertDetailList(final X509Certificate[] certs) { @@ -290,38 +171,35 @@ private List> generateCertDetailList(final X509Certificate[] }).collect(Collectors.toList()); } - private void noKeyStoreResponse(final RestChannel channel) throws IOException { - response(channel, RestStatus.OK, "keystore is not initialized"); - } - - @Override - protected Endpoint getEndpoint() { - return Endpoint.SSL; - } - - @Override - public String getName() { - return "SSL Certificates Action"; - } - - @Override - protected RequestContentValidator createValidator(final Object... params) { - return null; - } - - @Override - protected void consumeParameters(RestRequest request) { - request.param("certType"); - } - - @Override - protected String getResourceName() { - return null; - } - - @Override - protected CType getConfigName() { - return null; + protected void reloadCertificates(final RestChannel channel, final RestRequest request, final SecurityKeyStore keyStore) + throws IOException { + final String certType = request.param("certType").toLowerCase().trim(); + try { + switch (certType) { + case "http": + if (!httpsEnabled) { + badRequest(channel, "SSL for HTTP is disabled"); + return; + } + keyStore.initHttpSSLConfig(); + ok(channel, (builder, params) -> builder.startObject().field("message", "updated http certs").endObject()); + break; + case "transport": + keyStore.initTransportSSLConfig(); + ok(channel, (builder, params) -> builder.startObject().field("message", "updated transport certs").endObject()); + break; + default: + Responses.forbidden( + channel, + "invalid uri path, please use /_plugins/_security/api/ssl/http/reload or " + + "/_plugins/_security/api/ssl/transport/reload" + ); + break; + } + } catch (final OpenSearchSecurityException e) { + // LOGGER.error("Reload of certificates for {} failed", certType, e); + throw new IOException(e); + } } } 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 f0c9be3ee9..ee716f70b5 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 @@ -27,34 +27,26 @@ package org.opensearch.security.dlic.rest.api; -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.common.inject.Inject; import org.opensearch.common.settings.Settings; -import org.opensearch.rest.RestController; -import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestRequest.Method; -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.EndpointValidator; 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 static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; -public class TenantsApiAction extends PatchableResourceApiAction { +public class TenantsApiAction extends AbstractApiAction { + private static final List routes = addRoutesPrefix( ImmutableList.of( new Route(Method.GET, "/tenants/{name}"), @@ -68,28 +60,12 @@ public class TenantsApiAction extends PatchableResourceApiAction { @Inject public TenantsApiAction( - final Settings settings, - final Path configPath, - final RestController controller, - final Client client, - final AdminDNs adminDNs, - final ConfigurationRepository cl, - final ClusterService cs, - final PrincipalExtractor principalExtractor, - final PrivilegesEvaluator evaluator, - ThreadPool threadPool, - AuditLog auditLog - ) { - super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); - } - - @Override - protected boolean hasPermissionsToCreate( - final SecurityDynamicConfiguration dynamicConfigFactory, - final Object content, - final String resourceName + final ClusterService clusterService, + final ThreadPool threadPool, + final SecurityApiDependencies securityApiDependencies ) { - return true; + super(Endpoint.TENANTS, clusterService, threadPool, securityApiDependencies); + this.requestHandlersBuilder.configureRequestHandlers(builder -> builder.onChangeRequest(Method.PATCH, this::processPatchRequest)); } @Override @@ -98,47 +74,47 @@ public List routes() { } @Override - protected Endpoint getEndpoint() { - return Endpoint.TENANTS; + protected CType getConfigType() { + return CType.TENANTS; } @Override - protected RequestContentValidator createValidator(final Object... params) { - return RequestContentValidator.of(new RequestContentValidator.ValidationContext() { + protected EndpointValidator createEndpointValidator() { + return new EndpointValidator() { @Override - public Object[] params() { - return params; + public Endpoint endpoint() { + return endpoint; } @Override - public Settings settings() { - return settings; + public RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator() { + return securityApiDependencies.restApiAdminPrivilegesEvaluator(); } @Override - public Map allowedKeys() { - final ImmutableMap.Builder allowedKeys = ImmutableMap.builder(); - if (isSuperAdmin()) { - allowedKeys.put("reserved", DataType.BOOLEAN); - } - return allowedKeys.put("description", DataType.STRING).build(); + public RequestContentValidator createRequestContentValidator(final Object... params) { + return RequestContentValidator.of(new RequestContentValidator.ValidationContext() { + @Override + public Object[] params() { + return params; + } + + @Override + public Settings settings() { + return securityApiDependencies.settings(); + } + + @Override + public Map allowedKeys() { + final ImmutableMap.Builder allowedKeys = ImmutableMap.builder(); + if (isCurrentUserAdmin()) { + allowedKeys.put("reserved", DataType.BOOLEAN); + } + return allowedKeys.put("description", DataType.STRING).build(); + } + }); } - }); - } - - @Override - protected CType getConfigName() { - return CType.TENANTS; - } - - @Override - protected String getResourceName() { - return "tenant"; - } - - @Override - protected void consumeParameters(final RestRequest request) { - request.param("name"); + }; } } 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 f3f882cc19..8f764e94c3 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 @@ -11,28 +11,13 @@ package org.opensearch.security.dlic.rest.api; -import java.io.IOException; -import java.nio.file.Path; -import java.util.Collections; -import java.util.List; - -import com.fasterxml.jackson.databind.JsonNode; - -import org.opensearch.client.Client; import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.collect.Tuple; import org.opensearch.common.inject.Inject; -import org.opensearch.common.settings.Settings; import org.opensearch.rest.RestChannel; -import org.opensearch.rest.RestController; import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestRequest.Method; -import org.opensearch.security.auditlog.AuditLog; 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.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; @@ -48,9 +33,15 @@ import org.opensearch.security.securityconf.impl.v7.RoleMappingsV7; import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.securityconf.impl.v7.TenantV7; -import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.threadpool.ThreadPool; +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +import static org.opensearch.security.dlic.rest.api.Responses.badRequest; +import static org.opensearch.security.dlic.rest.api.Responses.internalSeverError; +import static org.opensearch.security.dlic.rest.api.Responses.ok; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; public class ValidateApiAction extends AbstractApiAction { @@ -58,50 +49,42 @@ public class ValidateApiAction extends AbstractApiAction { @Inject public ValidateApiAction( - final Settings settings, - final Path configPath, - final RestController controller, - final Client client, - final AdminDNs adminDNs, - final ConfigurationRepository cl, - final ClusterService cs, - final PrincipalExtractor principalExtractor, - final PrivilegesEvaluator evaluator, - ThreadPool threadPool, - AuditLog auditLog + final ClusterService clusterService, + final ThreadPool threadPool, + final SecurityApiDependencies securityApiDependencies ) { - super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); + super(Endpoint.VALIDATE, clusterService, threadPool, securityApiDependencies); + this.requestHandlersBuilder.configureRequestHandlers(this::validateApiRequestHandlers); } @Override - protected boolean hasPermissionsToCreate( - final SecurityDynamicConfiguration dynamicConfigFactory, - final Object content, - final String resourceName - ) { - return true; + public List routes() { + return routes; } @Override - public List routes() { - return routes; + protected CType getConfigType() { + return null; } @Override - protected Endpoint getEndpoint() { - return Endpoint.VALIDATE; + protected void consumeParameters(final RestRequest request) { + request.paramAsBoolean("accept_invalid", false); + } + + private void validateApiRequestHandlers(RequestHandler.RequestHandlersBuilder requestHandlersBuilder) { + requestHandlersBuilder.allMethodsNotImplemented().override(Method.GET, (channel, request, client) -> validate(channel, request)); } @SuppressWarnings("unchecked") - @Override - protected void handleGet(RestChannel channel, RestRequest request, Client client, final JsonNode content) throws IOException { + protected void validate(RestChannel channel, RestRequest request) throws IOException { final boolean acceptInvalid = request.paramAsBoolean("accept_invalid", false); final SecurityDynamicConfiguration loadedConfig = load(CType.CONFIG, true, acceptInvalid); if (loadedConfig.getVersion() != 1) { - badRequestResponse(channel, "Can not migrate configuration because it was already migrated."); + badRequest(channel, "Can not migrate configuration because it was already migrated."); return; } @@ -140,57 +123,17 @@ protected void handleGet(RestChannel channel, RestRequest request, Client client final SecurityDynamicConfiguration rolesmappingV7 = Migration.migrateRoleMappings(rolesmappingV6); final SecurityDynamicConfiguration auditConfigV7 = Migration.migrateAudit(auditConfigV6); - successResponse(channel, "OK."); + ok(channel, "OK."); } catch (Exception e) { - internalErrorResponse(channel, "Configuration is not valid."); + internalSeverError(channel, "Configuration is not valid."); } } - @Override - protected void handleDelete(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) - throws IOException { - notImplemented(channel, Method.POST); - } - - @Override - protected void handlePost(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) - throws IOException { - notImplemented(channel, Method.GET); - } - - @Override - protected void handlePut(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) - throws IOException { - notImplemented(channel, Method.PUT); - } - - @Override - protected RequestContentValidator createValidator(final Object... params) { - return RequestContentValidator.NOOP_VALIDATOR; - } - - @Override - protected String getResourceName() { - // not needed - return null; - } - - @Override - protected CType getConfigName() { - return null; - } - - @Override - 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(); + private SecurityDynamicConfiguration load(final CType config, boolean logComplianceEvent, boolean acceptInvalid) { + SecurityDynamicConfiguration loaded = securityApiDependencies.configurationRepository() + .getConfigurationsFromIndex(Collections.singleton(config), logComplianceEvent, acceptInvalid) + .get(config) + .deepClone(); return DynamicConfigFactory.addStatics(loaded); } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/WhitelistApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/WhitelistApiAction.java index d3bc92959c..2545bb2e23 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/WhitelistApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/WhitelistApiAction.java @@ -11,24 +11,15 @@ package org.opensearch.security.dlic.rest.api; -import java.nio.file.Path; import java.util.Collections; import java.util.List; import com.google.common.collect.ImmutableList; -import org.opensearch.client.Client; import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.inject.Inject; -import org.opensearch.common.settings.Settings; -import org.opensearch.rest.RestController; import org.opensearch.rest.RestRequest; -import org.opensearch.security.auditlog.AuditLog; -import org.opensearch.security.configuration.AdminDNs; -import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.securityconf.impl.CType; -import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.threadpool.ThreadPool; import static org.opensearch.security.dlic.rest.support.Utils.addDeprecatedRoutesPrefix; @@ -96,19 +87,11 @@ public class WhitelistApiAction extends AllowlistApiAction { @Inject public WhitelistApiAction( - final Settings settings, - final Path configPath, - final RestController controller, - final Client client, - final AdminDNs adminDNs, - final ConfigurationRepository cl, - final ClusterService cs, - final PrincipalExtractor principalExtractor, - final PrivilegesEvaluator evaluator, - ThreadPool threadPool, - AuditLog auditLog + final ClusterService clusterService, + final ThreadPool threadPool, + final SecurityApiDependencies securityApiDependencies ) { - super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); + super(Endpoint.WHITELIST, clusterService, threadPool, securityApiDependencies); } public List routes() { @@ -121,12 +104,7 @@ public List deprecatedRoutes() { } @Override - protected Endpoint getEndpoint() { - return Endpoint.WHITELIST; - } - - @Override - protected CType getConfigName() { + protected CType getConfigType() { return CType.WHITELIST; } diff --git a/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java b/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java index 5a3392e2d4..3853ee5f46 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java +++ b/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java @@ -12,6 +12,7 @@ package org.opensearch.security.dlic.rest.support; import java.io.IOException; +import java.io.UncheckedIOException; import java.security.AccessController; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; @@ -35,6 +36,7 @@ import org.opensearch.ExceptionsHelper; import org.opensearch.OpenSearchParseException; import org.opensearch.SpecialPermission; +import org.opensearch.common.CheckedSupplier; import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.core.common.transport.TransportAddress; import org.opensearch.common.util.concurrent.ThreadContext; @@ -106,6 +108,14 @@ public static JsonNode convertJsonToJackson(BytesReference jsonContent) { } + public static JsonNode toJsonNode(final String content) throws IOException { + return DefaultObjectMapper.readTree(content); + } + + public static Object toConfigObject(final JsonNode content, final Class clazz) throws IOException { + return DefaultObjectMapper.readTree(content, clazz); + } + public static JsonNode convertJsonToJackson(ToXContent jsonContent, boolean omitDefaults) { try { Map pm = new HashMap<>(1); @@ -125,30 +135,6 @@ public static JsonNode convertJsonToJackson(ToXContent jsonContent, boolean omit } - public static T serializeToXContentToPojo(ToXContent jsonContent, Class clazz) { - try { - - if (jsonContent instanceof BytesReference) { - return serializeToXContentToPojo(((BytesReference) jsonContent).utf8ToString(), clazz); - } - - final BytesReference bytes = XContentHelper.toXContent(jsonContent, XContentType.JSON, false); - return DefaultObjectMapper.readValue(bytes.utf8ToString(), clazz); - } catch (IOException e1) { - throw ExceptionsHelper.convertToOpenSearchException(e1); - } - - } - - public static T serializeToXContentToPojo(String jsonContent, Class clazz) { - try { - return DefaultObjectMapper.readValue(jsonContent, clazz); - } catch (IOException e1) { - throw ExceptionsHelper.convertToOpenSearchException(e1); - } - - } - @SuppressWarnings("removal") public static byte[] jsonMapToByteArray(Map jsonAsMap) throws IOException { @@ -159,12 +145,7 @@ public static byte[] jsonMapToByteArray(Map jsonAsMap) throws IO } try { - return AccessController.doPrivileged(new PrivilegedExceptionAction() { - @Override - public byte[] run() throws Exception { - return internalMapper.writeValueAsBytes(jsonAsMap); - } - }); + return AccessController.doPrivileged((PrivilegedExceptionAction) () -> internalMapper.writeValueAsBytes(jsonAsMap)); } catch (final PrivilegedActionException e) { if (e.getCause() instanceof JsonProcessingException) { throw (JsonProcessingException) e.getCause(); @@ -186,13 +167,13 @@ public static Map byteArrayToMutableJsonMap(byte[] jsonBytes) th } try { - return AccessController.doPrivileged(new PrivilegedExceptionAction>() { - @Override - public Map run() throws Exception { - return internalMapper.readValue(jsonBytes, new TypeReference>() { - }); - } - }); + return AccessController.doPrivileged( + (PrivilegedExceptionAction>) () -> internalMapper.readValue( + jsonBytes, + new TypeReference>() { + } + ) + ); } catch (final PrivilegedActionException e) { if (e.getCause() instanceof IOException) { throw (IOException) e.getCause(); @@ -289,4 +270,12 @@ public static Pair userAndRemoteAddressFrom(final Thread return Pair.of(user, remoteAddress); } + public static T withIOException(final CheckedSupplier action) { + try { + return action.get(); + } catch (final IOException ioe) { + throw new UncheckedIOException(ioe); + } + } + } diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/EndpointValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/EndpointValidator.java new file mode 100644 index 0000000000..044ccd996a --- /dev/null +++ b/src/main/java/org/opensearch/security/dlic/rest/validation/EndpointValidator.java @@ -0,0 +1,185 @@ +package org.opensearch.security.dlic.rest.validation; + +import org.opensearch.core.rest.RestStatus; +import org.opensearch.security.dlic.rest.api.Endpoint; +import org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator; +import org.opensearch.security.dlic.rest.api.SecurityConfiguration; +import org.opensearch.security.dlic.rest.support.Utils; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; + +import static java.util.function.Predicate.not; +import static org.opensearch.security.dlic.rest.api.Responses.badRequestMessage; +import static org.opensearch.security.dlic.rest.api.Responses.forbiddenMessage; +import static org.opensearch.security.dlic.rest.api.Responses.notFoundMessage; +import static org.opensearch.security.dlic.rest.support.Utils.withIOException; + +public interface EndpointValidator { + + Endpoint endpoint(); + + RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator(); + + private String resourceName() { + if (Objects.isNull(endpoint())) { + return ""; + } + switch (endpoint()) { + case ACCOUNT: + return "account"; + case ACTIONGROUPS: + return "actiongroup"; + case ALLOWLIST: + case AUDIT: + case CONFIG: + return "config"; + case INTERNALUSERS: + return "user"; + case NODESDN: + return "nodesdn"; + case ROLES: + return "role"; + case ROLESMAPPING: + return "rolesmapping"; + case TENANTS: + return "tenant"; + default: + return ""; + } + } + + default boolean isCurrentUserAdmin() { + return restApiAdminPrivilegesEvaluator().isCurrentUserAdminFor(endpoint()); + } + + default ValidationResult withRequiredEntityName(final String entityName) { + if (entityName == null) { + return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage("No " + resourceName() + " specified.")); + } + return ValidationResult.success(entityName); + } + + default ValidationResult entityExists(final SecurityConfiguration securityConfiguration) { + return entityExists(resourceName(), securityConfiguration); + } + + default ValidationResult entityExists( + final String resourceName, + final SecurityConfiguration securityConfiguration + ) { + return securityConfiguration.maybeEntityName().>map(entityName -> { + if (!securityConfiguration.entityExists()) { + return ValidationResult.error( + RestStatus.NOT_FOUND, + notFoundMessage(resourceName + " '" + securityConfiguration.entityName() + "' not found.") + ); + } + return ValidationResult.success(securityConfiguration); + }).orElseGet(() -> ValidationResult.success(securityConfiguration)); + } + + default ValidationResult isAllowedToChangeImmutableEntity(final SecurityConfiguration securityConfiguration) + throws IOException { + final var immutableCheck = entityImmutable(securityConfiguration); + if (!immutableCheck.isValid() && !isCurrentUserAdmin()) { + return immutableCheck; + } + return ValidationResult.success(securityConfiguration); + } + + default ValidationResult isAllowedToLoadOrChangeHiddenEntity(final SecurityConfiguration securityConfiguration) { + final var hiddenCheck = entityHidden(securityConfiguration); + if (!hiddenCheck.isValid() && !isCurrentUserAdmin()) { + return hiddenCheck; + } + return ValidationResult.success(securityConfiguration); + } + + default ValidationResult entityImmutable(final SecurityConfiguration securityConfiguration) throws IOException { + return entityHidden(securityConfiguration).map(this::entityStatic).map(this::entityReserved); + } + + default ValidationResult entityStatic(final SecurityConfiguration securityConfiguration) { + final var configuration = securityConfiguration.configuration(); + final var entityName = securityConfiguration.entityName(); + if (configuration.isStatic(entityName)) { + return ValidationResult.error(RestStatus.FORBIDDEN, forbiddenMessage("Resource '" + entityName + "' is static.")); + } + return ValidationResult.success(securityConfiguration); + } + + default ValidationResult entityReserved(final SecurityConfiguration securityConfiguration) { + final var configuration = securityConfiguration.configuration(); + final var entityName = securityConfiguration.entityName(); + if (configuration.isReserved(entityName)) { + return ValidationResult.error(RestStatus.FORBIDDEN, forbiddenMessage("Resource '" + entityName + "' is reserved.")); + } + return ValidationResult.success(securityConfiguration); + } + + default ValidationResult entityHidden(final SecurityConfiguration securityConfiguration) { + final var configuration = securityConfiguration.configuration(); + final var entityName = securityConfiguration.entityName(); + if (configuration.isHidden(entityName)) { + return ValidationResult.error(RestStatus.NOT_FOUND, notFoundMessage("Resource '" + entityName + "' is not available.")); + } + return ValidationResult.success(securityConfiguration); + } + + default ValidationResult> validateRoles( + final List roles, + final SecurityDynamicConfiguration rolesConfiguration + ) { + final var rolesToCheck = roles == null ? List.of() : roles; + return rolesToCheck.stream().map(role -> withIOException(() -> { + final var roleSecConfig = SecurityConfiguration.of(role, rolesConfiguration); + return entityExists("role", roleSecConfig).map(this::isAllowedToChangeImmutableEntity); + })) + .filter(not(ValidationResult::isValid)) + .findFirst() + .>>map( + result -> ValidationResult.error(result.status(), result.errorMessage()) + ) + .orElseGet(() -> ValidationResult.success(rolesConfiguration)); + } + + default ValidationResult isAllowedToChangeEntityWithRestAdminPermissions( + final SecurityConfiguration securityConfiguration + ) throws IOException { + if (securityConfiguration.entityExists()) { + final var configuration = securityConfiguration.configuration(); + final var existingEntity = configuration.getCEntry(securityConfiguration.entityName()); + if (restApiAdminPrivilegesEvaluator().containsRestApiAdminPermissions(existingEntity)) { + return ValidationResult.error(RestStatus.FORBIDDEN, forbiddenMessage("Access denied")); + } + } else { + final var configuration = securityConfiguration.configuration(); + final var configEntityContent = Utils.toConfigObject( + securityConfiguration.requestContent(), + configuration.getImplementingClass() + ); + if (restApiAdminPrivilegesEvaluator().containsRestApiAdminPermissions(configEntityContent)) { + return ValidationResult.error(RestStatus.FORBIDDEN, forbiddenMessage("Access denied")); + } + } + return ValidationResult.success(securityConfiguration); + } + + default ValidationResult onConfigDelete(final SecurityConfiguration securityConfiguration) throws IOException { + return isAllowedToChangeImmutableEntity(securityConfiguration).map(this::entityExists); + } + + default ValidationResult onConfigLoad(final SecurityConfiguration securityConfiguration) throws IOException { + return isAllowedToLoadOrChangeHiddenEntity(securityConfiguration).map(this::entityExists); + } + + default ValidationResult onConfigChange(final SecurityConfiguration securityConfiguration) throws IOException { + return isAllowedToChangeImmutableEntity(securityConfiguration); + } + + RequestContentValidator createRequestContentValidator(final Object... params); + +} 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 index 4f9309b788..d1aafa37e3 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/RequestContentValidator.java +++ b/src/main/java/org/opensearch/security/dlic/rest/validation/RequestContentValidator.java @@ -11,7 +11,6 @@ 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; @@ -19,6 +18,7 @@ import org.apache.logging.log4j.Logger; import org.opensearch.common.settings.Settings; import org.opensearch.core.common.Strings; +import org.opensearch.core.rest.RestStatus; import org.opensearch.core.xcontent.ToXContent; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.rest.RestRequest; @@ -53,12 +53,12 @@ public Map allowedKeys() { } }) { @Override - public ValidationResult validate(RestRequest request) { + public ValidationResult validate(RestRequest request) { return ValidationResult.success(DefaultObjectMapper.objectMapper.createObjectNode()); } @Override - public ValidationResult validate(RestRequest request, JsonNode jsonNode) { + public ValidationResult validate(RestRequest request, JsonNode jsonNode) { return ValidationResult.success(DefaultObjectMapper.objectMapper.createObjectNode()); } }; @@ -120,37 +120,36 @@ protected RequestContentValidator(final ValidationContext validationContext) { this.validationContext = validationContext; } - public ValidationResult validate(final RestRequest request) throws IOException { + 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 { + 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) { + 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); + return ValidationResult.error(RestStatus.BAD_REQUEST, this); } } - private ValidationResult validateContentSize(final JsonNode jsonContent) { - if (jsonContent.size() == 0) { + private ValidationResult validateContentSize(final JsonNode jsonContent) { + if (jsonContent.isEmpty()) { this.validationError = ValidationError.PAYLOAD_MANDATORY; - return ValidationResult.error(this); + return ValidationResult.error(RestStatus.BAD_REQUEST, this); } return ValidationResult.success(jsonContent); } - private ValidationResult validateJsonKeys(final JsonNode jsonContent) { + private ValidationResult validateJsonKeys(final JsonNode jsonContent) { final Set requestedKeys = new HashSet<>(); jsonContent.fieldNames().forEachRemaining(requestedKeys::add); // mandatory settings, one of ... @@ -166,13 +165,13 @@ private ValidationResult validateJsonKeys(final JsonNode jsonContent) { invalidKeys.addAll(requestedKeys); if (!missingMandatoryKeys.isEmpty() || !invalidKeys.isEmpty() || !missingMandatoryOrKeys.isEmpty()) { this.validationError = ValidationError.INVALID_CONFIGURATION; - return ValidationResult.error(this); + return ValidationResult.error(RestStatus.BAD_REQUEST, this); } return ValidationResult.success(jsonContent); } - private ValidationResult validateDataType(final JsonNode jsonContent) { - try (final JsonParser parser = new JsonFactory().createParser(jsonContent.toString())) { + private ValidationResult validateDataType(final JsonNode jsonContent) { + try (final JsonParser parser = DefaultObjectMapper.objectMapper.treeAsTokens(jsonContent)) { JsonToken token; while ((token = parser.nextToken()) != null) { if (token.equals(JsonToken.FIELD_NAME)) { @@ -203,22 +202,22 @@ private ValidationResult validateDataType(final JsonNode jsonContent) { } catch (final IOException ioe) { LOGGER.error("Couldn't create JSON for payload {}", jsonContent, ioe); this.validationError = ValidationError.BODY_NOT_PARSEABLE; - return ValidationResult.error(this); + return ValidationResult.error(RestStatus.BAD_REQUEST, this); } if (!wrongDataTypes.isEmpty()) { this.validationError = ValidationError.WRONG_DATATYPE; - return ValidationResult.error(this); + return ValidationResult.error(RestStatus.BAD_REQUEST, this); } return ValidationResult.success(jsonContent); } - private ValidationResult nullValuesInArrayValidator(final JsonNode 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.error(RestStatus.BAD_REQUEST, this); } } } @@ -240,13 +239,13 @@ private boolean hasNullArrayElement(final JsonNode node) { return false; } - private ValidationResult validatePassword(final RestRequest request, final JsonNode jsonContent) { + 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_TOO_SHORT; - return ValidationResult.error(this); + return ValidationResult.error(RestStatus.BAD_REQUEST, this); } final String username = Utils.coalesce( request.param("name"), @@ -257,11 +256,11 @@ private ValidationResult validatePassword(final RestRequest request, final JsonN LOGGER.debug("Unable to validate username because no user is given"); } this.validationError = ValidationError.NO_USERNAME; - return ValidationResult.error(this); + return ValidationResult.error(RestStatus.BAD_REQUEST, this); } this.validationError = passwordValidator.validate(username, password); if (this.validationError != ValidationError.NONE) { - return ValidationResult.error(this); + return ValidationResult.error(RestStatus.BAD_REQUEST, this); } } return ValidationResult.success(jsonContent); 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 index 0247742a17..7fb91d8913 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/ValidationResult.java +++ b/src/main/java/org/opensearch/security/dlic/rest/validation/ValidationResult.java @@ -11,54 +11,70 @@ package org.opensearch.security.dlic.rest.validation; -import com.fasterxml.jackson.databind.JsonNode; +import org.opensearch.common.CheckedBiConsumer; import org.opensearch.common.CheckedConsumer; import org.opensearch.common.CheckedFunction; import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.core.rest.RestStatus; import java.io.IOException; import java.util.Objects; -public class ValidationResult { +public class ValidationResult { - private final JsonNode jsonContent; + private final RestStatus status; + + private final C content; private final ToXContent errorMessage; - private ValidationResult(final JsonNode jsonContent, final ToXContent errorMessage) { - this.jsonContent = jsonContent; + private ValidationResult(final C jsonContent) { + this(RestStatus.OK, jsonContent, null); + } + + private ValidationResult(final RestStatus status, final ToXContent errorMessage) { + this(status, null, errorMessage); + } + + private ValidationResult(final RestStatus status, final C jsonContent, final ToXContent errorMessage) { + this.status = status; + this.content = jsonContent; this.errorMessage = errorMessage; } - public static ValidationResult success(final JsonNode jsonContent) { - return new ValidationResult(jsonContent, null); + public static ValidationResult success(final L content) { + return new ValidationResult<>(content); } - public static ValidationResult error(final ToXContent errorMessage) { - return new ValidationResult(null, errorMessage); + public static ValidationResult error(final RestStatus status, final ToXContent errorMessage) { + return new ValidationResult<>(status, errorMessage); } - public ValidationResult map(final CheckedFunction validation) throws IOException { - if (jsonContent != null) { - return Objects.requireNonNull(validation).apply(jsonContent); + public ValidationResult map(final CheckedFunction, IOException> mapper) throws IOException { + if (content != null) { + return Objects.requireNonNull(mapper).apply(content); } else { - return this; + return ValidationResult.error(status, errorMessage); } } - public void error(final CheckedConsumer invalid) throws IOException { + public void error(final CheckedBiConsumer mapper) throws IOException { if (errorMessage != null) { - Objects.requireNonNull(invalid).accept(errorMessage); + Objects.requireNonNull(mapper).accept(status, errorMessage); } } - public ValidationResult valid(final CheckedConsumer contentHandler) throws IOException { - if (jsonContent != null) { - Objects.requireNonNull(contentHandler).accept(jsonContent); + public ValidationResult valid(final CheckedConsumer mapper) throws IOException { + if (content != null) { + Objects.requireNonNull(mapper).accept(content); } return this; } + public RestStatus status() { + return status; + } + public boolean isValid() { return errorMessage == null; } diff --git a/src/main/java/org/opensearch/security/securityconf/impl/SecurityDynamicConfiguration.java b/src/main/java/org/opensearch/security/securityconf/impl/SecurityDynamicConfiguration.java index e86eed779c..4d10cb8ad6 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/SecurityDynamicConfiguration.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/SecurityDynamicConfiguration.java @@ -270,7 +270,7 @@ public int getVersion() { @JsonIgnore public Class getImplementingClass() { - return ctype == null ? null : ctype.getImplementationClass().get(getVersion()); + return getCType() == null ? null : getCType().getImplementationClass().get(getVersion()); } @JsonIgnore diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/AbstractApiActionValidationTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/AbstractApiActionValidationTest.java new file mode 100644 index 0000000000..5336b5e8de --- /dev/null +++ b/src/test/java/org/opensearch/security/dlic/rest/api/AbstractApiActionValidationTest.java @@ -0,0 +1,137 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.dlic.rest.api; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +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.cluster.service.ClusterService; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.security.DefaultObjectMapper; +import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.threadpool.ThreadPool; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public abstract class AbstractApiActionValidationTest { + + @Mock + ClusterService clusterService; + + @Mock + ThreadPool threadPool; + + @Mock + ConfigurationRepository configurationRepository; + + @Mock + RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator; + + SecurityApiDependencies securityApiDependencies; + + @Mock + SecurityDynamicConfiguration configuration; + + SecurityDynamicConfiguration rolesConfiguration; + + ObjectMapper objectMapper = DefaultObjectMapper.objectMapper; + + @Before + public void setup() { + securityApiDependencies = new SecurityApiDependencies( + null, + configurationRepository, + null, + null, + restApiAdminPrivilegesEvaluator, + null, + Settings.EMPTY + ); + } + + void setupRolesConfiguration() throws IOException { + final var objectMapper = DefaultObjectMapper.objectMapper; + final var config = objectMapper.createObjectNode(); + config.set("_meta", objectMapper.createObjectNode().put("type", CType.ROLES.toLCString()).put("config_version", 2)); + config.set("kibana_read_only", objectMapper.createObjectNode().put("reserved", true)); + config.set("security_rest_api_access", objectMapper.createObjectNode().put("reserved", true)); + + final var array = objectMapper.createArrayNode(); + restApiAdminPermissions().forEach(array::add); + config.set("rest_api_admin_role", objectMapper.createObjectNode().set("cluster_permissions", array)); + + config.set("regular_role", objectMapper.createObjectNode().set("cluster_permissions", objectMapper.createArrayNode().add("*"))); + + rolesConfiguration = SecurityDynamicConfiguration.fromJson(objectMapper.writeValueAsString(config), CType.ROLES, 2, 1, 1); + when(configurationRepository.getConfigurationsFromIndex(List.of(CType.ROLES), false)).thenReturn( + Map.of(CType.ROLES, rolesConfiguration) + ); + } + + @Test + public void allCrudActionsForDefaultValidatorAreForbidden() throws Exception { + + final var defaultPessimisticValidator = new AbstractApiAction(null, clusterService, threadPool, securityApiDependencies) { + @Override + protected CType getConfigType() { + return CType.CONFIG; + } + }.createEndpointValidator(); + + var result = defaultPessimisticValidator.onConfigChange(SecurityConfiguration.of(null, configuration)); + assertEquals(RestStatus.FORBIDDEN, result.status()); + + result = defaultPessimisticValidator.onConfigDelete(SecurityConfiguration.of(null, configuration)); + assertEquals(RestStatus.FORBIDDEN, result.status()); + + result = defaultPessimisticValidator.onConfigLoad(SecurityConfiguration.of(null, configuration)); + assertEquals(RestStatus.FORBIDDEN, result.status()); + + } + + protected JsonNode xContentToJsonNode(final ToXContent toXContent) throws IOException { + try (final var xContentBuilder = XContentFactory.jsonBuilder()) { + toXContent.toXContent(xContentBuilder, ToXContent.EMPTY_PARAMS); + return DefaultObjectMapper.readTree(xContentBuilder.toString()); + } + } + + protected List restApiAdminPermissions() { + return List.of( + "restapi:admin/actiongroups", + "restapi:admin/allowlist", + "restapi:admin/internalusers", + "restapi:admin/nodesdn", + "restapi:admin/roles", + "restapi:admin/rolesmapping", + "restapi:admin/ssl/certs/info", + "restapi:admin/ssl/certs/reload", + "restapi:admin/tenants" + ); + } + +} diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiActionConfigValidationsTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiActionConfigValidationsTest.java new file mode 100644 index 0000000000..bae761d6c3 --- /dev/null +++ b/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiActionConfigValidationsTest.java @@ -0,0 +1,68 @@ +package org.opensearch.security.dlic.rest.api; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.bouncycastle.crypto.generators.OpenBSDBCrypt; +import org.junit.Test; +import org.mockito.Mockito; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.security.dlic.rest.support.Utils; +import org.opensearch.security.securityconf.impl.v7.InternalUserV7; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class AccountApiActionConfigValidationsTest extends AbstractApiActionValidationTest { + + @Test + public void verifyValidCurrentPassword() { + final var accountApiAction = new AccountApiAction(clusterService, threadPool, securityApiDependencies); + + final var u = createExistingUser(); + + var result = accountApiAction.validCurrentPassword(SecurityConfiguration.of(requestContent(), "u", configuration)); + assertFalse(result.isValid()); + assertEquals(RestStatus.BAD_REQUEST, result.status()); + + u.setHash(Utils.hash("aaaa".toCharArray())); + result = accountApiAction.validCurrentPassword(SecurityConfiguration.of(requestContent(), "u", configuration)); + assertTrue(result.isValid()); + } + + @Test + public void updatePassword() { + final var accountApiAction = new AccountApiAction(clusterService, threadPool, securityApiDependencies); + + final var requestContent = requestContent(); + requestContent.remove("password"); + final var u = createExistingUser(); + u.setHash(null); + + var result = accountApiAction.updatePassword(SecurityConfiguration.of(requestContent, "u", configuration)); + assertFalse(result.isValid()); + assertEquals(RestStatus.BAD_REQUEST, result.status()); + + requestContent.put("password", "cccccc"); + result = accountApiAction.updatePassword(SecurityConfiguration.of(requestContent, "u", configuration)); + assertTrue(result.isValid()); + assertTrue(OpenBSDBCrypt.checkPassword(u.getHash(), "cccccc".toCharArray())); + + requestContent.remove("password"); + requestContent.put("hash", Utils.hash("dddddd".toCharArray())); + result = accountApiAction.updatePassword(SecurityConfiguration.of(requestContent, "u", configuration)); + assertTrue(result.isValid()); + assertTrue(OpenBSDBCrypt.checkPassword(u.getHash(), "dddddd".toCharArray())); + } + + private ObjectNode requestContent() { + return objectMapper.createObjectNode().put("current_password", "aaaa").put("password", "bbbb"); + } + + private InternalUserV7 createExistingUser() { + final var u = new InternalUserV7(); + u.setHash(Utils.hash("sssss".toCharArray())); + Mockito.when(configuration.getCEntry("u")).thenReturn(u); + return u; + } + +} diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiActionValidationTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiActionValidationTest.java new file mode 100644 index 0000000000..477b44301d --- /dev/null +++ b/src/test/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiActionValidationTest.java @@ -0,0 +1,89 @@ +package org.opensearch.security.dlic.rest.api; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.v7.ActionGroupsV7; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +public class ActionGroupsApiActionValidationTest extends AbstractApiActionValidationTest { + + @Before + public void setupRoles() throws Exception { + setupRolesConfiguration(); + } + + @Test + public void hasNoRightsToChangeImmutableEntityFoAdminUser() throws Exception { + final var actionGroups = new ActionGroupsV7("ag", restApiAdminPermissions()); + when(configuration.exists("ag")).thenReturn(true); + Mockito.when(configuration.getCEntry("ag")).thenReturn(actionGroups); + when(restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(any(Object.class))).thenCallRealMethod(); + + final var actionGroupsApiActionEndpointValidator = new ActionGroupsApiAction(clusterService, threadPool, securityApiDependencies) + .createEndpointValidator(); + + final var result = actionGroupsApiActionEndpointValidator.isAllowedToChangeImmutableEntity( + SecurityConfiguration.of("ag", configuration) + ); + assertFalse(result.isValid()); + assertEquals(RestStatus.FORBIDDEN, result.status()); + } + + @Test + public void hasNoRightsToChangeImmutableEntityForRegularUser() throws Exception { + final var actionGroups = new ActionGroupsV7("ag", restApiAdminPermissions()); + when(configuration.exists("ag")).thenReturn(true); + Mockito.when(configuration.getCEntry("ag")).thenReturn(actionGroups); + when(restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(any(Object.class))).thenCallRealMethod(); + + final var actionGroupsApiActionEndpointValidator = new ActionGroupsApiAction(clusterService, threadPool, securityApiDependencies) + .createEndpointValidator(); + + final var result = actionGroupsApiActionEndpointValidator.isAllowedToChangeImmutableEntity( + SecurityConfiguration.of("ag", configuration) + ); + assertFalse(result.isValid()); + assertEquals(RestStatus.FORBIDDEN, result.status()); + } + + @Test + public void onConfigChangeActionGroupHasSameNameAsRole() throws Exception { + when(configuration.getCType()).thenReturn(CType.ACTIONGROUPS); + when(configuration.getVersion()).thenReturn(2); + when(configuration.getImplementingClass()).thenCallRealMethod(); + final var ag = objectMapper.createObjectNode() + .set("allowed_actions", objectMapper.createArrayNode().add("indices:*")); + final var actionGroupsApiActionEndpointValidator = new ActionGroupsApiAction(clusterService, threadPool, securityApiDependencies) + .createEndpointValidator(); + + final var result = actionGroupsApiActionEndpointValidator.onConfigChange(SecurityConfiguration.of(ag,"kibana_read_only", configuration)); + assertFalse(result.isValid()); + assertEquals(RestStatus.BAD_REQUEST, result.status()); + assertEquals("kibana_read_only is an existing role. A action group cannot be named with an existing role name.", xContentToJsonNode(result.errorMessage()).get("message").asText()); + } + + @Test + public void onConfigChangeActionGroupHasSelfReference() throws Exception { + when(configuration.getCType()).thenReturn(CType.ACTIONGROUPS); + when(configuration.getVersion()).thenReturn(2); + when(configuration.getImplementingClass()).thenCallRealMethod(); + final var ag = objectMapper.createObjectNode() + .set("allowed_actions", objectMapper.createArrayNode().add("ag")); + final var actionGroupsApiActionEndpointValidator = new ActionGroupsApiAction(clusterService, threadPool, securityApiDependencies) + .createEndpointValidator(); + + final var result = actionGroupsApiActionEndpointValidator + .onConfigChange(SecurityConfiguration.of(ag,"ag", configuration)); + assertFalse(result.isValid()); + assertEquals(RestStatus.BAD_REQUEST, result.status()); + assertEquals("ag cannot be an allowed_action of itself", xContentToJsonNode(result.errorMessage()).get("message").asText()); + } + +} 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 ab0cf53237..27a1360a65 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 @@ -77,7 +77,7 @@ private void checkGetAndPutAllowlistPermissions(final int expectedStatus, final } // FORBIDDEN FOR NON SUPER ADMIN if (expectedStatus == HttpStatus.SC_FORBIDDEN) { - assertTrue(response.getBody().contains("API allowed only for super admin.")); + assertTrue(response.getBody().contains("Access denied")); } // CHECK PUT REQUEST response = rh.executePutRequest( diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/AuditApiActionTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/AuditApiActionTest.java index 6c0421466f..8d431882da 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/AuditApiActionTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/AuditApiActionTest.java @@ -93,7 +93,7 @@ public void testInvalidPath() throws Exception { // should have /config for patch request response = rh.executePatchRequest(ENDPOINT, "{\"xxx\": 1}"); - assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); + assertEquals(response.getBody(), HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); // no delete supported response = rh.executeDeleteRequest(ENDPOINT); diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/AuditApiActionValidationTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/AuditApiActionValidationTest.java new file mode 100644 index 0000000000..b42072dcbf --- /dev/null +++ b/src/test/java/org/opensearch/security/dlic/rest/api/AuditApiActionValidationTest.java @@ -0,0 +1,100 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.dlic.rest.api; + +import org.junit.Test; +import org.opensearch.common.settings.Settings; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.security.auditlog.config.AuditConfig; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.util.FakeRestRequest; + +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +public class AuditApiActionValidationTest extends AbstractApiActionValidationTest { + + @Test + public void disabledAuditApi() { + final var auditApiAction = new AuditApiAction(clusterService, threadPool, securityApiDependencies); + when(configurationRepository.isAuditHotReloadingEnabled()).thenReturn(false); + + for (final var m : RequestHandler.RequestHandlersBuilder.SUPPORTED_METHODS) { + final var result = auditApiAction.withEnabledAuditApi(FakeRestRequest.builder().withMethod(m).build()); + assertFalse(result.isValid()); + assertEquals(RestStatus.NOT_IMPLEMENTED, result.status()); + } + } + + @Test + public void enabledAuditApi() { + final var auditApiAction = new AuditApiAction(clusterService, threadPool, securityApiDependencies); + when(configurationRepository.isAuditHotReloadingEnabled()).thenReturn(true); + for (final var m : RequestHandler.RequestHandlersBuilder.SUPPORTED_METHODS) { + final var result = auditApiAction.withEnabledAuditApi(FakeRestRequest.builder().withMethod(m).build()); + assertTrue(result.isValid()); + } + } + + @Test + public void configEntityNameOnly() { + final var auditApiAction = new AuditApiAction(clusterService, threadPool, securityApiDependencies); + var result = auditApiAction.withConfigEntityNameOnly(FakeRestRequest.builder().withParams(Map.of("name", "aaaaa")).build()); + assertFalse(result.isValid()); + assertEquals(RestStatus.BAD_REQUEST, result.status()); + + result = auditApiAction.withConfigEntityNameOnly(FakeRestRequest.builder().withParams(Map.of("name", "config")).build()); + assertTrue(result.isValid()); + } + + @Test + public void onChangeVerifyReadonlyFields() throws Exception { + final var auditApiActionEndpointValidator = new AuditApiAction( + clusterService, + threadPool, + securityApiDependencies, + List.of("/audit/enable_rest", "/audit/disabled_rest_categories", "/audit/ignore_requests", "/compliance/read_watched_fields") + ).createEndpointValidator(); + + final var auditFullConfig = objectMapper.createObjectNode(); + auditFullConfig.set("_meta", objectMapper.createObjectNode().put("type", "audit").put("config_version", 2)); + final var auditConfig = objectMapper.createObjectNode(); + auditConfig.put("enable_rest", false).set("disabled_rest_categories", objectMapper.createArrayNode()); + auditConfig.put("enable_transport", false).set("disabled_transport_categories", objectMapper.createArrayNode()); + auditConfig.set("ignore_users", objectMapper.createArrayNode().add("kibanaserver")); + auditConfig.set("ignore_requests", objectMapper.createArrayNode()); + auditConfig.put("resolve_bulk_requests", false) + .put("log_request_body", false) + .put("resolve_indices", false) + .put("exclude_sensitive_headers", false); + + auditFullConfig.set("config", objectMapper.createObjectNode().put("enabled", true).set("audit", auditConfig)); + final var dynamicConfiguration = SecurityDynamicConfiguration.fromJson( + objectMapper.writeValueAsString(auditFullConfig), + CType.AUDIT, + 2, + 1, + 1 + ); + final var result = auditApiActionEndpointValidator.onConfigChange( + SecurityConfiguration.of(objectMapper.valueToTree(AuditConfig.from(Settings.EMPTY)), "config", dynamicConfiguration) + ); + assertFalse(result.isValid()); + assertEquals(RestStatus.CONFLICT, result.status()); + } +} diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/InternalUsersApiActionValidationTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/InternalUsersApiActionValidationTest.java new file mode 100644 index 0000000000..137b4b5a9e --- /dev/null +++ b/src/test/java/org/opensearch/security/dlic/rest/api/InternalUsersApiActionValidationTest.java @@ -0,0 +1,101 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.dlic.rest.api; + +import org.bouncycastle.crypto.generators.OpenBSDBCrypt; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.rest.RestRequest; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.securityconf.impl.v7.InternalUserV7; +import org.opensearch.security.user.UserService; +import org.opensearch.security.util.FakeRestRequest; + +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +public class InternalUsersApiActionValidationTest extends AbstractApiActionValidationTest { + + @Mock + UserService userService; + + @Mock + SecurityDynamicConfiguration configuration; + + @Test + public void replacePasswordWithHash() throws Exception { + final var internalUsersApiActionEndpointValidator = createInternalUsersApiAction().createEndpointValidator(); + final var securityConfiguration = SecurityConfiguration.of( + objectMapper.createObjectNode().put("password", "aaaaaa"), + "some_user", + configuration + ); + final var result = internalUsersApiActionEndpointValidator.onConfigChange(securityConfiguration); + assertEquals(RestStatus.OK, result.status()); + assertFalse(securityConfiguration.requestContent().has("password")); + assertTrue(securityConfiguration.requestContent().has("hash")); + assertTrue(OpenBSDBCrypt.checkPassword(securityConfiguration.requestContent().get("hash").asText(), "aaaaaa".toCharArray())); + } + + @Test + public void withAuthTokenPath() throws Exception { + final var internalUsersApiAction = createInternalUsersApiAction(); + var result = internalUsersApiAction.withAuthTokenPath( + FakeRestRequest.builder() + .withMethod(RestRequest.Method.POST) + .withPath("_plugins/_security/api/internalusers/aaaa") + .withParams(Map.of("name", "aaaa")) + .build() + ); + assertFalse(result.isValid()); + assertEquals(RestStatus.NOT_IMPLEMENTED, result.status()); + + result = internalUsersApiAction.withAuthTokenPath( + FakeRestRequest.builder() + .withMethod(RestRequest.Method.POST) + .withPath("_plugins/_security/api/internalusers/aaaa/authtoken") + .withParams(Map.of("name", "aaaa")) + .build() + ); + assertTrue(result.isValid()); + assertEquals(RestStatus.OK, result.status()); + } + + @Test + public void validateAndUpdatePassword() throws Exception { + final var internalUsersApiAction = createInternalUsersApiAction(); + + var result = internalUsersApiAction.validateAndUpdatePassword( + SecurityConfiguration.of(objectMapper.createObjectNode().set("hash", objectMapper.nullNode()), "aaaa", configuration) + ); + assertTrue(result.isValid()); + + when(configuration.exists("aaaa")).thenReturn(true); + Mockito.when(configuration.getCEntry("aaaa")).thenReturn(new InternalUserV7()); + result = internalUsersApiAction.validateAndUpdatePassword( + SecurityConfiguration.of(objectMapper.createObjectNode(), "aaaa", configuration) + ); + assertFalse(result.isValid()); + assertEquals(RestStatus.INTERNAL_SERVER_ERROR, result.status()); + } + + private InternalUsersApiAction createInternalUsersApiAction() { + return new InternalUsersApiAction(clusterService, threadPool, userService, securityApiDependencies); + } + +} diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/NodesDnApiActionValidationTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/NodesDnApiActionValidationTest.java new file mode 100644 index 0000000000..99d2e55188 --- /dev/null +++ b/src/test/java/org/opensearch/security/dlic/rest/api/NodesDnApiActionValidationTest.java @@ -0,0 +1,36 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.dlic.rest.api; + +import org.junit.Test; +import org.opensearch.core.rest.RestStatus; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +public class NodesDnApiActionValidationTest extends AbstractApiActionValidationTest { + + @Test + public void isNotAllowedToChangeImmutableEntity() throws Exception { + + final var nodesDnApiActionEndpointValidator = new NodesDnApiAction(clusterService, threadPool, securityApiDependencies) + .createEndpointValidator(); + + final var result = nodesDnApiActionEndpointValidator.isAllowedToChangeImmutableEntity( + SecurityConfiguration.of(NodesDnApiAction.STATIC_OPENSEARCH_YML_NODES_DN, configuration) + ); + + assertFalse(result.isValid()); + assertEquals(RestStatus.FORBIDDEN, result.status()); + } + +} diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/RequestHandlersBuilderTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/RequestHandlersBuilderTest.java new file mode 100644 index 0000000000..80a6c0115a --- /dev/null +++ b/src/test/java/org/opensearch/security/dlic/rest/api/RequestHandlersBuilderTest.java @@ -0,0 +1,181 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.dlic.rest.api; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.opensearch.client.Client; +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.rest.BytesRestResponse; +import org.opensearch.rest.RestChannel; +import org.opensearch.rest.RestRequest; +import org.opensearch.security.DefaultObjectMapper; +import org.opensearch.security.dlic.rest.validation.ValidationResult; + +import java.io.IOException; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class RequestHandlersBuilderTest { + + @Mock + RestChannel channel; + + @Mock + RestRequest request; + + @Mock + Client client; + + @Captor + ArgumentCaptor responseArgumentCaptor; + + @Test + public void checkPermissionsForAllMethodsOnDemand() throws IOException { + var requestHandlers = new RequestHandler.RequestHandlersBuilder().withAccessHandler(r -> false) + .withSaveOrUpdateConfigurationHandler((client, configuration, indexResponseOnSucessActionListener) -> {}) + .add(RestRequest.Method.PATCH, (channel, request, client) -> {}) + .add(RestRequest.Method.POST, RequestHandler.methodNotImplementedHandler) + .add(RestRequest.Method.PUT, (channel, request, client) -> {}) + .onGetRequest(request -> ValidationResult.success(null)) + .verifyAccessForAllMethods() + .onChangeRequest(RestRequest.Method.DELETE, request -> ValidationResult.success(null)) + .build(); + + for (final var method : RequestHandler.RequestHandlersBuilder.SUPPORTED_METHODS) { + when(request.method()).thenReturn(method); + when(channel.newBuilder()).thenReturn(XContentFactory.jsonBuilder()); + requestHandlers.get(method).handle(channel, request, client); + verify(channel).sendResponse(responseArgumentCaptor.capture()); + final var responseBytes = responseArgumentCaptor.getValue(); + final var json = DefaultObjectMapper.readTree(responseBytes.content().utf8ToString()); + if (method == RestRequest.Method.POST) { + assertEquals(RestStatus.NOT_IMPLEMENTED.name(), json.get("status").asText()); + } else { + assertEquals(RestStatus.FORBIDDEN.name(), json.get("status").asText()); + } + reset(channel); + } + } + + @Test + public void overrideDefaultHandlers() { + var requestHandlers = new RequestHandler.RequestHandlersBuilder().withAccessHandler(r -> true) + .withSaveOrUpdateConfigurationHandler((client, configuration, indexResponseOnSucessActionListener) -> {}) + .override(RestRequest.Method.PATCH, (channel, request, client) -> {}) + .build(); + + assertNotEquals(RequestHandler.methodNotImplementedHandler, requestHandlers.get(RestRequest.Method.PATCH)); + + requestHandlers = new RequestHandler.RequestHandlersBuilder().withAccessHandler(r -> true) + .withSaveOrUpdateConfigurationHandler((client, configuration, indexResponseOnSucessActionListener) -> {}) + .add(RestRequest.Method.POST, RequestHandler.methodNotImplementedHandler) + .add(RestRequest.Method.PATCH, (channel, request, client) -> {}) + .add(RestRequest.Method.DELETE, (channel, request, client) -> {}) + .add(RestRequest.Method.POST, (channel, request, client) -> {}) + .build(); + + assertNotEquals(RequestHandler.methodNotImplementedHandler, requestHandlers.get(RestRequest.Method.POST)); + + requestHandlers = new RequestHandler.RequestHandlersBuilder().withAccessHandler(r -> true) + .withSaveOrUpdateConfigurationHandler((client, configuration, indexResponseOnSucessActionListener) -> {}) + .add(RestRequest.Method.DELETE, RequestHandler.methodNotImplementedHandler) + .add(RestRequest.Method.POST, (channel, request, client) -> {}) + .add(RestRequest.Method.PATCH, (channel, request, client) -> {}) + .add(RestRequest.Method.DELETE, (channel, request, client) -> {}) + .build(); + assertNotEquals(RequestHandler.methodNotImplementedHandler, requestHandlers.get(RestRequest.Method.DELETE)); + } + + @Test + public void allSupportedMethodsNotImplementedByDefault() { + final var requestHandlers = new RequestHandler.RequestHandlersBuilder().withAccessHandler(r -> true) + .withSaveOrUpdateConfigurationHandler((client, configuration, indexResponseOnSucessActionListener) -> {}) + .build(); + + assertEquals( + RequestHandler.RequestHandlersBuilder.SUPPORTED_METHODS.stream().sorted().collect(Collectors.toList()), + requestHandlers.keySet().stream().sorted().collect(Collectors.toList()) + ); + requestHandlers.forEach( + ((method, requestOperationHandler) -> assertEquals(RequestHandler.methodNotImplementedHandler, requestOperationHandler)) + ); + } + + @Test + public void failsForNullRequestHandlers() { + final var requestHandlersBuilder = new RequestHandler.RequestHandlersBuilder(); + assertThrows( + NullPointerException.class, + () -> requestHandlersBuilder.onChangeRequest(null, request -> ValidationResult.success(null)) + ); + assertThrows(NullPointerException.class, () -> requestHandlersBuilder.onChangeRequest(RestRequest.Method.PATCH, null)); + assertThrows(NullPointerException.class, () -> requestHandlersBuilder.onChangeRequest(RestRequest.Method.PUT, null)); + assertThrows(NullPointerException.class, () -> requestHandlersBuilder.onChangeRequest(RestRequest.Method.DELETE, null)); + assertThrows(NullPointerException.class, () -> requestHandlersBuilder.onGetRequest(null)); + } + + @Test + public void failsForUnsupportedMethods() { + final var requestHandlersBuilder = new RequestHandler.RequestHandlersBuilder(); + assertThrows( + IllegalArgumentException.class, + () -> requestHandlersBuilder.add(RestRequest.Method.CONNECT, RequestHandler.methodNotImplementedHandler) + ); + assertThrows( + IllegalArgumentException.class, + () -> requestHandlersBuilder.override(RestRequest.Method.OPTIONS, RequestHandler.methodNotImplementedHandler) + ); + } + + @Test + public void failsForUnsupportedMethodsForCreateOrUpdateHandler() { + final var requestHandlersBuilder = new RequestHandler.RequestHandlersBuilder(); + assertThrows(IllegalArgumentException.class, () -> requestHandlersBuilder.onChangeRequest(RestRequest.Method.OPTIONS, r -> null)); + assertThrows(IllegalArgumentException.class, () -> requestHandlersBuilder.onChangeRequest(RestRequest.Method.GET, r -> null)); + } + + @Test + public void failsForNullHandlers() { + final var requestHandlersBuilder = new RequestHandler.RequestHandlersBuilder(); + assertThrows(NullPointerException.class, () -> requestHandlersBuilder.withAccessHandler(null)); + assertThrows(NullPointerException.class, () -> requestHandlersBuilder.withSaveOrUpdateConfigurationHandler(null)); + } + + @Test + public void buildFailsIfHandlersNotSet() { + final var requestHandlersBuilder = new RequestHandler.RequestHandlersBuilder(); + requestHandlersBuilder.override(RestRequest.Method.DELETE, (channel, request, client) -> {}); + + assertThrows(NullPointerException.class, requestHandlersBuilder::build); + + requestHandlersBuilder.withAccessHandler(r -> true); + assertThrows(NullPointerException.class, requestHandlersBuilder::build); + + requestHandlersBuilder.withAccessHandler(r -> true); + requestHandlersBuilder.withSaveOrUpdateConfigurationHandler((client, configuration, indexResponseOnSucessActionListener) -> {}); + requestHandlersBuilder.build(); + } + +} diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/RolesApiActionValidationTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/RolesApiActionValidationTest.java new file mode 100644 index 0000000000..bff2056fa4 --- /dev/null +++ b/src/test/java/org/opensearch/security/dlic/rest/api/RolesApiActionValidationTest.java @@ -0,0 +1,58 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.dlic.rest.api; + +import org.junit.Test; +import org.mockito.Mockito; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.security.securityconf.impl.v7.RoleV7; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +public class RolesApiActionValidationTest extends AbstractApiActionValidationTest { + + @Test + public void isAllowedToChangeImmutableEntity() throws Exception { + when(restApiAdminPrivilegesEvaluator.isCurrentUserAdminFor(Endpoint.ROLES)).thenReturn(true); + + final var role = new RoleV7(); + role.setCluster_permissions(restApiAdminPermissions()); + + final var rolesApiActionEndpointValidator = new RolesApiAction(clusterService, threadPool, securityApiDependencies).createEndpointValidator(); + final var result = rolesApiActionEndpointValidator.isAllowedToChangeImmutableEntity(SecurityConfiguration.of( "sss", configuration)); + + assertTrue(result.isValid()); + } + + @Test + public void isNotAllowedRightsToChangeImmutableEntity() throws Exception { + when(restApiAdminPrivilegesEvaluator.isCurrentUserAdminFor(Endpoint.ROLES)).thenReturn(false); + + final var role = new RoleV7(); + role.setCluster_permissions(restApiAdminPermissions()); + + when(configuration.exists("sss")).thenReturn(true); + Mockito.when(configuration.getCEntry("sss")).thenReturn(role); + + when(restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(any(Object.class))).thenCallRealMethod(); + final var rolesApiActionEndpointValidator = new RolesApiAction(clusterService, threadPool, securityApiDependencies).createEndpointValidator(); + final var result = rolesApiActionEndpointValidator.isAllowedToChangeImmutableEntity(SecurityConfiguration.of( "sss", configuration)); + + assertFalse(result.isValid()); + assertEquals(RestStatus.FORBIDDEN, result.status()); + } + +} diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/RolesMappingApiActionValidationTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/RolesMappingApiActionValidationTest.java new file mode 100644 index 0000000000..bbcc5021ff --- /dev/null +++ b/src/test/java/org/opensearch/security/dlic/rest/api/RolesMappingApiActionValidationTest.java @@ -0,0 +1,84 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.dlic.rest.api; + +import org.junit.Before; +import org.junit.Test; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.security.securityconf.impl.CType; + +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +public class RolesMappingApiActionValidationTest extends AbstractApiActionValidationTest { + + @Before + public void setupRoles() throws Exception { + setupRolesConfiguration(); + } + + @Test + public void isAllowedRightsToChangeRoleEntity() throws Exception { + when(restApiAdminPrivilegesEvaluator.isCurrentUserAdminFor(Endpoint.ROLESMAPPING)).thenReturn(true); + final var rolesMappingApiActionEndpointValidator = new RolesMappingApiAction(clusterService, threadPool, securityApiDependencies) + .createEndpointValidator(); + final var result = rolesMappingApiActionEndpointValidator.isAllowedToChangeImmutableEntity( + SecurityConfiguration.of("rest_api_admin_role", configuration) + ); + assertTrue(result.isValid()); + } + + @Test + public void isNotAllowedNoRightsToChangeRoleEntity() throws Exception { + when(restApiAdminPrivilegesEvaluator.isCurrentUserAdminFor(Endpoint.ROLESMAPPING)).thenReturn(false); + when(restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(any(Object.class))).thenCallRealMethod(); + + final var rolesApiActionEndpointValidator = + new RolesMappingApiAction(clusterService, threadPool, + securityApiDependencies).createEndpointValidator(); + final var result = rolesApiActionEndpointValidator.isAllowedToChangeImmutableEntity( + SecurityConfiguration.of("rest_api_admin_role", configuration)); + + assertFalse(result.isValid()); + assertEquals(RestStatus.FORBIDDEN, result.status()); + } + + @Test + public void onConfigChangeShouldCheckRoles() throws Exception { + when(restApiAdminPrivilegesEvaluator.isCurrentUserAdminFor(Endpoint.ROLESMAPPING)).thenReturn(false); + when(restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(any(Object.class))).thenCallRealMethod(); + when(configurationRepository.getConfigurationsFromIndex(List.of(CType.ROLES), false)) + .thenReturn(Map.of(CType.ROLES, rolesConfiguration)); + final var rolesApiActionEndpointValidator = + new RolesMappingApiAction(clusterService, threadPool, + securityApiDependencies).createEndpointValidator(); + + // no role + var result = rolesApiActionEndpointValidator.onConfigChange(SecurityConfiguration.of("aaa", configuration)); + assertFalse(result.isValid()); + assertEquals(RestStatus.NOT_FOUND, result.status()); + //reserved role is not ok + result = rolesApiActionEndpointValidator.onConfigChange(SecurityConfiguration.of("kibana_read_only", configuration)); + assertFalse(result.isValid()); + assertEquals(RestStatus.FORBIDDEN, result.status()); + //just regular_role + result = rolesApiActionEndpointValidator.onConfigChange(SecurityConfiguration.of("regular_role", configuration)); + assertTrue(result.isValid()); + } + +} 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 4cfe057388..8d9b76274c 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 @@ -362,6 +362,13 @@ void verifyPatchForSuperAdmin(final Header[] header) throws Exception { Assert.assertTrue(response.getBody().matches(".*\"invalid_keys\"\\s*:\\s*\\{\\s*\"keys\"\\s*:\\s*\"hidden\"\\s*\\}.*")); // PATCH + // create a role since PATCH works same as PUT. It is impossible to create role mapping without role + final var securityRoleVulcans = DefaultObjectMapper.objectMapper.createObjectNode() + .set("cluster_permissions", DefaultObjectMapper.objectMapper.createArrayNode().add("cluster:monitor*")) + .toString(); + response = rh.executePutRequest(ENDPOINT + "/roles/opendistro_security_role_vulcans", securityRoleVulcans, header); + Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); + response = rh.executePatchRequest( ENDPOINT + "/rolesmapping/opendistro_security_role_vulcans", "[{ \"op\": \"add\", \"path\": \"/backend_roles/-\", \"value\": \"spring\" }]", @@ -414,6 +421,12 @@ void verifyPatchForSuperAdmin(final Header[] header) throws Exception { Assert.assertTrue(response.getBody().matches(".*\"invalid_keys\"\\s*:\\s*\\{\\s*\"keys\"\\s*:\\s*\"hidden\"\\s*\\}.*")); // PATCH + // create a role since PATCH works same as PUT. It is impossible to create role mapping without role + final var securityRoleBulknew1 = DefaultObjectMapper.objectMapper.createObjectNode() + .set("cluster_permissions", DefaultObjectMapper.objectMapper.createArrayNode().add("cluster:monitor*")) + .toString(); + response = rh.executePutRequest(ENDPOINT + "/roles/bulknew1", securityRoleBulknew1, header); + Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); response = rh.executePatchRequest( ENDPOINT + "/rolesmapping", "[{ \"op\": \"add\", \"path\": \"/bulknew1\", \"value\": { \"backend_roles\":[\"vulcanadmin\"]} }]", @@ -558,7 +571,7 @@ public void testChangeRestApiAdminRoleMappingForbiddenForNonSuperAdmin() throws createRestAdminPermissionsPayload(), restApiAdminHeader ); - Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); + Assert.assertEquals(response.getBody(), HttpStatus.SC_CREATED, response.getStatusCode()); response = rh.executePutRequest( ENDPOINT + "/roles/new_rest_api_role_without_mapping", createRestAdminPermissionsPayload(), diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiActionValidationTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiActionValidationTest.java new file mode 100644 index 0000000000..e5993b3698 --- /dev/null +++ b/src/test/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiActionValidationTest.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.api; + +import org.junit.Test; +import org.opensearch.common.settings.Settings; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.util.FakeRestRequest; + +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class SecurityConfigApiActionValidationTest extends AbstractApiActionValidationTest { + + @Test + public void configEntityNameOnly() { + final var securityConfigApiAction = new SecurityConfigApiAction(clusterService, threadPool, securityApiDependencies); + var result = securityConfigApiAction.withConfigEntityNameOnly( + FakeRestRequest.builder().withParams(Map.of("name", "aaaaa")).build() + ); + assertFalse(result.isValid()); + assertEquals(RestStatus.BAD_REQUEST, result.status()); + + result = securityConfigApiAction.withConfigEntityNameOnly(FakeRestRequest.builder().withParams(Map.of("name", "config")).build()); + assertTrue(result.isValid()); + } + + @Test + public void withAllowedEndpoint() { + var securityConfigApiAction = new SecurityConfigApiAction( + clusterService, + threadPool, + new SecurityApiDependencies(null, configurationRepository, null, null, restApiAdminPrivilegesEvaluator, null, Settings.EMPTY) + ); + + var result = securityConfigApiAction.withAllowedEndpoint(FakeRestRequest.builder().build()); + assertFalse(result.isValid()); + assertEquals(RestStatus.NOT_IMPLEMENTED, result.status()); + + securityConfigApiAction = new SecurityConfigApiAction( + clusterService, + threadPool, + new SecurityApiDependencies( + null, + configurationRepository, + null, + null, + restApiAdminPrivilegesEvaluator, + null, + Settings.builder().put(ConfigConstants.SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION, true).build() + ) + ); + result = securityConfigApiAction.withAllowedEndpoint(FakeRestRequest.builder().build()); + assertTrue(result.isValid()); + } + +} diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/SecurityConfigurationTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/SecurityConfigurationTest.java new file mode 100644 index 0000000000..67858261a4 --- /dev/null +++ b/src/test/java/org/opensearch/security/dlic/rest/api/SecurityConfigurationTest.java @@ -0,0 +1,80 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.dlic.rest.api; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Before; +import org.junit.Test; +import org.opensearch.security.DefaultObjectMapper; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.securityconf.impl.v7.RoleV7; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class SecurityConfigurationTest { + + SecurityDynamicConfiguration configuration; + + private final ObjectMapper objectMapper = DefaultObjectMapper.objectMapper; + + @Before + public void setConfiguration() throws Exception { + final var config = objectMapper.createObjectNode(); + config.set("_meta", objectMapper.createObjectNode().put("type", CType.ROLES.toLCString()).put("config_version", 2)); + config.set("kibana_read_only", objectMapper.createObjectNode().put("reserved", true)); + config.set("security_rest_api_access", objectMapper.createObjectNode().put("reserved", true)); + configuration = SecurityDynamicConfiguration.fromJson(objectMapper.writeValueAsString(config), CType.ROLES, 2, 1, 1); + } + + @Test + public void failsIfConfigurationNull() { + assertThrows(NullPointerException.class, () -> SecurityConfiguration.of("some_entity", null)); + } + + @Test + public void failsIfConfigurationOrRequestContentNull() { + assertThrows(NullPointerException.class, () -> SecurityConfiguration.of(objectMapper.createObjectNode(), "some_entity", null)); + assertThrows(NullPointerException.class, () -> SecurityConfiguration.of(null, "some_entity", configuration)); + } + + @Test + public void testNewOrUpdatedEntity() { + var securityConfiguration = SecurityConfiguration.of("security_rest_api_access", configuration); + assertTrue(securityConfiguration.entityExists()); + assertEquals("security_rest_api_access", securityConfiguration.entityName()); + + securityConfiguration = SecurityConfiguration.of("security_rest_api_access_v2", configuration); + assertFalse(securityConfiguration.entityExists()); + assertEquals("security_rest_api_access_v2", securityConfiguration.entityName()); + + final var newRole = new RoleV7(); + newRole.setCluster_permissions(List.of("cluster:admin/opendistro/alerting/alerts/get")); + configuration.putCObject("security_rest_api_access_v2", newRole); + assertTrue(configuration.exists("security_rest_api_access_v2")); + assertFalse(securityConfiguration.entityExists()); + assertEquals("security_rest_api_access_v2", securityConfiguration.entityName()); + } + + @Test + public void testNoEntityNameConfiguration() { + final var securityConfiguration = SecurityConfiguration.of(null, configuration); + assertFalse(securityConfiguration.entityExists()); + assertEquals("empty", securityConfiguration.entityName()); + } + +} 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 ab70d066ce..754e555fdf 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 @@ -239,7 +239,7 @@ private void verifyPut(final Header... header) throws Exception { ); Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(settings.get("message"), "Role 'non_existent' is not available for role-mapping."); + Assert.assertEquals(settings.get("message"), "role 'non_existent' not found."); // Wrong config keys response = rh.executePutRequest(ENDPOINT + "/internalusers/nagilum", "{\"some\": \"thing\", \"other\": \"thing\"}", header); @@ -358,7 +358,7 @@ private void verifyPatch(final boolean sendAdminCert, Header... restAdminHeader) "[{ \"op\": \"add\", \"path\": \"/bulknew1\", \"value\": {\"password\": \"bla bla bla password 42\", \"backend_roles\": [\"vulcan\"] } }]", restAdminHeader ); - Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); + Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); response = rh.executeGetRequest(ENDPOINT + "/internalusers/bulknew1", restAdminHeader); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); @@ -966,7 +966,7 @@ public void testUserApiForNonSuperAdmin() throws Exception { ); Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(settings.get("message"), "Resource 'opendistro_security_reserved' is read-only."); + Assert.assertEquals(settings.get("message"), "Resource 'opendistro_security_reserved' is reserved."); // Patch single hidden user response = rh.executePatchRequest( 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 8a1d8ce83f..398489b788 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 @@ -84,7 +84,7 @@ private void checkGetAndPutWhitelistPermissions(final int expectedStatus, final } // FORBIDDEN FOR NON SUPER ADMIN if (expectedStatus == HttpStatus.SC_FORBIDDEN) { - assertTrue(response.getBody().contains("API allowed only for super admin.")); + assertTrue(response.getBody().contains("Access denied")); } // CHECK PUT REQUEST response = rh.executePutRequest( diff --git a/src/test/java/org/opensearch/security/dlic/rest/validation/EndpointValidatorTest.java b/src/test/java/org/opensearch/security/dlic/rest/validation/EndpointValidatorTest.java new file mode 100644 index 0000000000..20cfe33126 --- /dev/null +++ b/src/test/java/org/opensearch/security/dlic/rest/validation/EndpointValidatorTest.java @@ -0,0 +1,413 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.dlic.rest.validation; + +import org.apache.commons.lang3.tuple.Triple; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.security.DefaultObjectMapper; +import org.opensearch.security.dlic.rest.api.Endpoint; +import org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator; +import org.opensearch.security.dlic.rest.api.SecurityConfiguration; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.securityconf.impl.v7.ActionGroupsV7; +import org.opensearch.security.securityconf.impl.v7.RoleV7; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class EndpointValidatorTest { + + @Mock + SecurityDynamicConfiguration configuration; + + @Mock + RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator; + + private EndpointValidator endpointValidator; + + @Before + public void createConfigurationValidator() { + endpointValidator = new EndpointValidator() { + @Override + public Endpoint endpoint() { + return Endpoint.INTERNALUSERS; + } + + @Override + public RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator() { + return restApiAdminPrivilegesEvaluator; + } + + @Override + public RequestContentValidator createRequestContentValidator(Object... params) { + return RequestContentValidator.NOOP_VALIDATOR; + } + }; + } + + @Test + public void requiredEntityName() { + var validationResult = endpointValidator.withRequiredEntityName(null); + assertFalse(validationResult.isValid()); + assertEquals(RestStatus.BAD_REQUEST, validationResult.status()); + + validationResult = endpointValidator.withRequiredEntityName("a"); + assertTrue(validationResult.isValid()); + assertEquals(RestStatus.OK, validationResult.status()); + } + + @Test + public void entityDoesNotExist() { + when(configuration.exists("some_role")).thenReturn(false); + final var validationResult = endpointValidator.entityExists(SecurityConfiguration.of("some_role", configuration)); + assertFalse(validationResult.isValid()); + assertEquals(RestStatus.NOT_FOUND, validationResult.status()); + } + + @Test + public void entityExists() { + when(configuration.exists("some_role")).thenReturn(true); + final var validationResult = endpointValidator.entityExists(SecurityConfiguration.of("some_role", configuration)); + assertTrue(validationResult.isValid()); + assertEquals(RestStatus.OK, validationResult.status()); + } + + @Test + public void entityExistsSkipEmptyEntityName() { + when(configuration.exists(null)).thenReturn(false); + final var validationResult = endpointValidator.entityExists(SecurityConfiguration.of(null, configuration)); + assertTrue(validationResult.isValid()); + assertEquals(RestStatus.OK, validationResult.status()); + } + + @Test + public void entityHidden() { + when(configuration.isHidden("some_entity")).thenReturn(true); + final var validationResult = endpointValidator.entityHidden( SecurityConfiguration.of("some_entity", configuration)); + assertFalse(validationResult.isValid()); + assertEquals(RestStatus.NOT_FOUND, validationResult.status()); + } + + @Test + public void entityNotHidden() { + when(configuration.isHidden("some_entity")).thenReturn(false); + final var validationResult = endpointValidator.entityHidden( SecurityConfiguration.of("some_entity", configuration)); + assertTrue(validationResult.isValid()); + assertEquals(RestStatus.OK, validationResult.status()); + } + + @Test + public void entityReserved() { + when(configuration.isReserved("some_entity")).thenReturn(true); + final var validationResult = endpointValidator.entityReserved( SecurityConfiguration.of("some_entity", configuration)); + assertFalse(validationResult.isValid()); + assertEquals(RestStatus.FORBIDDEN, validationResult.status()); + } + + @Test + public void entityNotReserved() { + when(configuration.isReserved("some_entity")).thenReturn(false); + final var validationResult = endpointValidator.entityReserved( SecurityConfiguration.of("some_entity", configuration)); + assertTrue(validationResult.isValid()); + assertEquals(RestStatus.OK, validationResult.status()); + } + + @Test + public void entityStatic() { + when(configuration.isStatic("some_entity")).thenReturn(true); + final var validationResult = endpointValidator.entityStatic( SecurityConfiguration.of("some_entity", configuration)); + assertFalse(validationResult.isValid()); + assertEquals(RestStatus.FORBIDDEN, validationResult.status()); + } + + @Test + public void entityNotStatic() { + when(configuration.isStatic("some_entity")).thenReturn(false); + final var validationResult = endpointValidator.entityStatic( SecurityConfiguration.of("some_entity", configuration)); + assertTrue(validationResult.isValid()); + assertEquals(RestStatus.OK, validationResult.status()); + } + + @Test + public void hiddenEntityImmutable() throws Exception { + when(configuration.isHidden("some_entity")).thenReturn(true); + + var validationResult = endpointValidator.entityImmutable( SecurityConfiguration.of("some_entity", configuration)); + assertFalse(validationResult.isValid()); + assertEquals(RestStatus.NOT_FOUND, validationResult.status()); + } + + @Test + public void staticEntityImmutable() throws Exception { + when(configuration.isHidden("some_entity")).thenReturn(false); + when(configuration.isStatic("some_entity")).thenReturn(true); + final var validationResult = endpointValidator.entityImmutable( SecurityConfiguration.of("some_entity", configuration)); + assertFalse(validationResult.isValid()); + assertEquals(RestStatus.FORBIDDEN, validationResult.status()); + } + + @Test + public void reservedEntityImmutable() throws Exception { + when(configuration.isHidden("some_entity")).thenReturn(false); + when(configuration.isStatic("some_entity")).thenReturn(false); + when(configuration.isReserved("some_entity")).thenReturn(true); + final var validationResult = endpointValidator.entityImmutable( SecurityConfiguration.of("some_entity", configuration)); + assertFalse(validationResult.isValid()); + assertEquals(RestStatus.FORBIDDEN, validationResult.status()); + } + + @Test + public void hasRightsToChangeImmutableEntity() throws Exception { + configImmutableEntities(false); + var result = endpointValidator.isAllowedToChangeImmutableEntity(SecurityConfiguration.of("hidden_entity", configuration)); + assertFalse(result.isValid()); + assertEquals(RestStatus.NOT_FOUND, result.status()); + + result = endpointValidator.isAllowedToChangeImmutableEntity(SecurityConfiguration.of("static_entity", configuration)); + assertFalse(result.isValid()); + assertEquals(RestStatus.FORBIDDEN, result.status()); + + result = endpointValidator.isAllowedToChangeImmutableEntity(SecurityConfiguration.of("reserved_entity", configuration)); + assertFalse(result.isValid()); + assertEquals(RestStatus.FORBIDDEN, result.status()); + + result = endpointValidator.isAllowedToChangeImmutableEntity(SecurityConfiguration.of("just_entity", configuration)); + assertTrue(result.isValid()); + assertEquals(RestStatus.OK, result.status()); + } + + @Test + public void hasRightsToChangeImmutableEntityForAdmin() throws Exception { + configImmutableEntities(true); + + var result = endpointValidator.isAllowedToChangeImmutableEntity(SecurityConfiguration.of("hidden_entity", configuration)); + assertTrue(result.isValid()); + assertEquals(RestStatus.OK, result.status()); + + result = endpointValidator.isAllowedToChangeImmutableEntity(SecurityConfiguration.of("static_entity", configuration)); + assertTrue(result.isValid()); + assertEquals(RestStatus.OK, result.status()); + + result = endpointValidator.isAllowedToChangeImmutableEntity(SecurityConfiguration.of("reserved_entity", configuration)); + assertTrue(result.isValid()); + assertEquals(RestStatus.OK, result.status()); + + result = endpointValidator.isAllowedToChangeImmutableEntity(SecurityConfiguration.of("just_entity", configuration)); + assertTrue(result.isValid()); + assertEquals(RestStatus.OK, result.status()); + } + + @Test + public void hasRightsToLoadOrChangeHiddenEntityForRegularUser() throws Exception { + configImmutableEntities(false); + + var result = endpointValidator.isAllowedToLoadOrChangeHiddenEntity(SecurityConfiguration.of("hidden_entity", configuration)); + assertFalse(result.isValid()); + assertEquals(RestStatus.NOT_FOUND, result.status()); + + result = endpointValidator.isAllowedToLoadOrChangeHiddenEntity(SecurityConfiguration.of("just_entity", configuration)); + assertTrue(result.isValid()); + assertEquals(RestStatus.OK, result.status()); + } + + @Test + public void hasRightsToLoadOrChangeHiddenEntityForAdmin() throws Exception { + configImmutableEntities(true); + + var result = endpointValidator.isAllowedToLoadOrChangeHiddenEntity(SecurityConfiguration.of("hidden_entity", configuration)); + assertTrue(result.isValid()); + assertEquals(RestStatus.OK, result.status()); + + result = endpointValidator.isAllowedToLoadOrChangeHiddenEntity(SecurityConfiguration.of("just_entity", configuration)); + assertTrue(result.isValid()); + assertEquals(RestStatus.OK, result.status()); + } + + private void configImmutableEntities(final boolean isAdmin) { + when(restApiAdminPrivilegesEvaluator.isCurrentUserAdminFor(any(Endpoint.class))).thenReturn(isAdmin); + when(configuration.isHidden("just_entity")).thenReturn(false); + when(configuration.isStatic("just_entity")).thenReturn(false); + when(configuration.isReserved("just_entity")).thenReturn(false); + + when(configuration.isHidden("hidden_entity")).thenReturn(true); + when(configuration.isStatic("static_entity")).thenReturn(true); + when(configuration.isReserved("reserved_entity")).thenReturn(true); + } + + @Test + public void entityNotImmutable() throws Exception { + when(configuration.isHidden("some_entity")).thenReturn(false); + when(configuration.isStatic("some_entity")).thenReturn(false); + when(configuration.isReserved("some_entity")).thenReturn(false); + + var validationResult = endpointValidator.entityImmutable( SecurityConfiguration.of("some_entity", configuration)); + assertTrue(validationResult.isValid()); + assertEquals(RestStatus.OK, validationResult.status()); + } + + @Test + public void validateRolesForAdmin() { + configureRoles(true); + final var expectedResultForRoles = List.of( + Triple.of("valid_role", true, RestStatus.OK), + Triple.of("reserved_role", true, RestStatus.OK), + Triple.of("static_role", true, RestStatus.OK), + Triple.of("hidden_role", true, RestStatus.OK), + Triple.of("non_existing_role", false, RestStatus.NOT_FOUND) + ); + + for (final var roleWithExpectedResults : expectedResultForRoles) { + final var validationResult = endpointValidator.validateRoles(List.of(roleWithExpectedResults.getLeft()), configuration); + assertEquals(roleWithExpectedResults.getMiddle(), validationResult.isValid()); + assertEquals(roleWithExpectedResults.getRight(), validationResult.status()); + } + + } + + @Test + public void validateRolesForRegularUser() { + configureRoles(false); + final var expectedResultForRoles = List.of( + Triple.of("valid_role", true, RestStatus.OK), + Triple.of("reserved_role", false, RestStatus.FORBIDDEN), + Triple.of("static_role", false, RestStatus.FORBIDDEN), + Triple.of("hidden_role", false, RestStatus.NOT_FOUND), + Triple.of("non_existing_role", false, RestStatus.NOT_FOUND) + ); + + for (final var roleWithExpectedResults : expectedResultForRoles) { + final var validationResult = endpointValidator.validateRoles(List.of(roleWithExpectedResults.getLeft()), configuration); + assertEquals(roleWithExpectedResults.getMiddle(), validationResult.isValid()); + assertEquals(roleWithExpectedResults.getRight(), validationResult.status()); + } + + } + + private void configureRoles(final boolean isAdmin) { + when(restApiAdminPrivilegesEvaluator.isCurrentUserAdminFor(any(Endpoint.class))).thenReturn(isAdmin); + + when(configuration.exists("non_existing_role")).thenReturn(false); + + when(configuration.exists("hidden_role")).thenReturn(true); + when(configuration.isHidden("hidden_role")).thenReturn(true); + + when(configuration.exists("static_role")).thenReturn(true); + when(configuration.isHidden("static_role")).thenReturn(false); + when(configuration.isStatic("static_role")).thenReturn(true); + + when(configuration.exists("reserved_role")).thenReturn(true); + when(configuration.isHidden("reserved_role")).thenReturn(false); + when(configuration.isStatic("reserved_role")).thenReturn(false); + when(configuration.isReserved("reserved_role")).thenReturn(true); + + when(configuration.exists("valid_role")).thenReturn(true); + when(configuration.isHidden("valid_role")).thenReturn(false); + when(configuration.isStatic("valid_role")).thenReturn(false); + when(configuration.isReserved("valid_role")).thenReturn(false); + } + + @Test + public void regularUserCanNotChangeObjectWithRestAdminPermissionsForExistingRoles() throws Exception { + final var role = new RoleV7(); + role.setCluster_permissions(restAdminPermissions()); + + when(configuration.exists("some_role")).thenReturn(true); + when(restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(any(Object.class))).thenCallRealMethod(); + Mockito.when(configuration.getCEntry("some_role")).thenReturn(role); + final var roleCheckResult = endpointValidator.isAllowedToChangeEntityWithRestAdminPermissions( + SecurityConfiguration.of("some_role", configuration) + ); + assertFalse(roleCheckResult.isValid()); + assertEquals(RestStatus.FORBIDDEN, roleCheckResult.status()); + } + + @Test + public void regularUserCanNotChangeObjectWithRestAdminPermissionsForNewRoles() throws Exception { + final var role = new RoleV7(); + role.setCluster_permissions(restAdminPermissions()); + + final var objectMapper = DefaultObjectMapper.objectMapper; + final var array = objectMapper.createArrayNode(); + restAdminPermissions().forEach(array::add); + + when(configuration.getCType()).thenReturn(CType.ROLES); + when(configuration.getVersion()).thenReturn(2); + when(configuration.getImplementingClass()).thenCallRealMethod(); + when(configuration.exists("some_role")).thenReturn(false); + when(restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(any(Object.class))).thenCallRealMethod(); + final var roleCheckResult = endpointValidator.isAllowedToChangeEntityWithRestAdminPermissions( + SecurityConfiguration.of(objectMapper.createObjectNode().set("cluster_permissions", array), "some_role", configuration) + ); + assertFalse(roleCheckResult.isValid()); + assertEquals(RestStatus.FORBIDDEN, roleCheckResult.status()); + } + + @Test + public void regularUserCanNotChangeObjectWithRestAdminPermissionsForExitingActionGroups() throws Exception { + final var actionGroups = new ActionGroupsV7("some_ag", restAdminPermissions()); + when(configuration.exists("some_ag")).thenReturn(true); + when(restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(any(Object.class))).thenCallRealMethod(); + Mockito.when(configuration.getCEntry("some_ag")).thenReturn(actionGroups); + var agCheckResult = endpointValidator.isAllowedToChangeEntityWithRestAdminPermissions( + SecurityConfiguration.of("some_ag", configuration) + ); + assertFalse(agCheckResult.isValid()); + assertEquals(RestStatus.FORBIDDEN, agCheckResult.status()); + } + + @Test + public void regularUserCanNotChangeObjectWithRestAdminPermissionsForMewActionGroups() throws Exception { + when(configuration.getCType()).thenReturn(CType.ACTIONGROUPS); + when(configuration.getVersion()).thenReturn(2); + when(configuration.getImplementingClass()).thenCallRealMethod(); + when(configuration.exists("some_ag")).thenReturn(false); + when(restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(any(Object.class))).thenCallRealMethod(); + + final var objectMapper = DefaultObjectMapper.objectMapper; + final var array = objectMapper.createArrayNode(); + restAdminPermissions().forEach(array::add); + + var agCheckResult = endpointValidator.isAllowedToChangeEntityWithRestAdminPermissions( + SecurityConfiguration.of(objectMapper.createObjectNode().set("allowed_actions", array), "some_ag", configuration) + ); + assertFalse(agCheckResult.isValid()); + assertEquals(RestStatus.FORBIDDEN, agCheckResult.status()); + } + + private List restAdminPermissions() { + return List.of( + "restapi:admin/actiongroups", + "restapi:admin/allowlist", + "restapi:admin/internalusers", + "restapi:admin/nodesdn", + "restapi:admin/roles", + "restapi:admin/rolesmapping", + "restapi:admin/ssl/certs/info", + "restapi:admin/ssl/certs/reload", + "restapi:admin/tenants" + ); + } + +} 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 index a64de6ddeb..7f12dc2e72 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/validation/RequestContentValidatorTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/validation/RequestContentValidatorTest.java @@ -85,7 +85,7 @@ public Map allowedKeys() { } }); when(httpRequest.content()).thenReturn(new BytesArray("{`a`: `b`}")); - final ValidationResult validationResult = validator.validate(request); + final ValidationResult validationResult = validator.validate(request); assertFalse(validationResult.isValid()); assertErrorMessage(validationResult.errorMessage(), RequestContentValidator.ValidationError.BODY_NOT_PARSEABLE); } @@ -109,7 +109,7 @@ public Map allowedKeys() { } }); when(httpRequest.content()).thenReturn(new BytesArray("")); - ValidationResult validationResult = validator.validate(request); + ValidationResult validationResult = validator.validate(request); assertFalse(validationResult.isValid()); assertErrorMessage(validationResult.errorMessage(), RequestContentValidator.ValidationError.PAYLOAD_MANDATORY); @@ -147,7 +147,7 @@ public Map allowedKeys() { 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 ValidationResult validationResult = validator.validate(request); final JsonNode errorMessage = xContentToJsonNode(validationResult.errorMessage()); assertFalse(validationResult.isValid()); @@ -184,7 +184,7 @@ public Map allowedKeys() { 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 ValidationResult validationResult = validator.validate(request); final JsonNode errorMessage = xContentToJsonNode(validationResult.errorMessage()); assertErrorMessage(errorMessage, RequestContentValidator.ValidationError.INVALID_CONFIGURATION); @@ -218,7 +218,7 @@ public Map allowedKeys() { 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); + final ValidationResult validationResult = validator.validate(request); assertErrorMessage(validationResult.errorMessage(), RequestContentValidator.ValidationError.NULL_ARRAY_ELEMENT); } @@ -247,7 +247,7 @@ public Map allowedKeys() { }); ObjectNode payload = DefaultObjectMapper.objectMapper.createObjectNode().put("password", "a"); when(httpRequest.content()).thenReturn(new BytesArray(payload.toString())); - ValidationResult validationResult = validator.validate(request); + ValidationResult validationResult = validator.validate(request); assertErrorMessage(validationResult.errorMessage(), RequestContentValidator.ValidationError.NO_USERNAME); when(httpRequest.uri()).thenReturn("/aaaa?name=a"); @@ -292,7 +292,7 @@ public Map allowedKeys() { payload.putObject("c"); when(httpRequest.content()).thenReturn(new BytesArray(payload.toString())); - final ValidationResult validationResult = validator.validate(request); + final ValidationResult validationResult = validator.validate(request); assertTrue(validationResult.isValid()); assertNull(validationResult.errorMessage()); } diff --git a/src/test/java/org/opensearch/security/ssl/SecuritySSLReloadCertsActionTests.java b/src/test/java/org/opensearch/security/ssl/SecuritySSLReloadCertsActionTests.java index ee1cd585f7..ea22981251 100644 --- a/src/test/java/org/opensearch/security/ssl/SecuritySSLReloadCertsActionTests.java +++ b/src/test/java/org/opensearch/security/ssl/SecuritySSLReloadCertsActionTests.java @@ -11,11 +11,6 @@ package org.opensearch.security.ssl; -import java.io.IOException; -import java.util.List; -import java.util.Map; -import java.util.Objects; - import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import net.minidev.json.JSONArray; @@ -27,8 +22,8 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; - import org.opensearch.common.settings.Settings; +import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.ssl.util.SSLConfigConstants; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.test.DynamicSecurityConfig; @@ -37,6 +32,11 @@ import org.opensearch.security.test.helper.file.FileHelper; import org.opensearch.security.test.helper.rest.RestHelper; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Objects; + public class SecuritySSLReloadCertsActionTests extends SingleClusterTest { private final ClusterConfiguration clusterConfiguration = ClusterConfiguration.DEFAULT; @@ -148,13 +148,11 @@ public void testSSLReloadFail_InvalidDNAndDate() throws Exception { RestHelper.HttpResponse reloadCertsResponse = rh.executePutRequest(RELOAD_TRANSPORT_CERTS_ENDPOINT, null); Assert.assertEquals(500, reloadCertsResponse.getStatusCode()); - JSONObject expectedResponse = new JSONObject(); - expectedResponse.appendField( - "error", + Assert.assertEquals( "OpenSearchSecurityException[Error while initializing transport SSL layer from PEM: java.lang.Exception: " - + "New Certs do not have valid Issuer DN, Subject DN or SAN.]; nested: Exception[New Certs do not have valid Issuer DN, Subject DN or SAN.];" + + "New Certs do not have valid Issuer DN, Subject DN or SAN.]; nested: Exception[New Certs do not have valid Issuer DN, Subject DN or SAN.];", + DefaultObjectMapper.readTree(reloadCertsResponse.getBody()).get("error").get("root_cause").get(0).get("reason").asText() ); - Assert.assertEquals(expectedResponse.toString(), reloadCertsResponse.getBody()); } @Test diff --git a/src/test/java/org/opensearch/security/util/FakeRestRequest.java b/src/test/java/org/opensearch/security/util/FakeRestRequest.java index ceb86d6579..f8d8aca508 100644 --- a/src/test/java/org/opensearch/security/util/FakeRestRequest.java +++ b/src/test/java/org/opensearch/security/util/FakeRestRequest.java @@ -105,6 +105,10 @@ public FakeRestRequest build() { } + public static FakeRestRequest.Builder builder() { + return new FakeRestRequest.Builder(); + } + private static Map> convert(Map headers) { Map> ret = new HashMap>(); for (String h : headers.keySet()) {