All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.restheart.mongodb.interceptors.JsonSchemaBeforeWriteChecker Maven / Gradle / Ivy

There is a newer version: 8.1.5
Show newest version
/*-
 * ========================LICENSE_START=================================
 * restheart-mongodb
 * %%
 * Copyright (C) 2014 - 2024 SoftInstigate
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see .
 * =========================LICENSE_END==================================
 */
package org.restheart.mongodb.interceptors;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.bson.BsonDocument;
import org.bson.BsonValue;
import org.everit.json.schema.Schema;
import org.everit.json.schema.ValidationException;
import org.json.JSONObject;
import static org.restheart.exchange.ExchangeKeys._SCHEMAS;
import org.restheart.exchange.MongoRequest;
import org.restheart.exchange.MongoResponse;
import org.restheart.exchange.UnsupportedDocumentIdException;
import org.restheart.mongodb.handlers.schema.JsonSchemaCacheSingleton;
import org.restheart.mongodb.handlers.schema.JsonSchemaNotFoundException;
import org.restheart.mongodb.utils.MongoURLUtils;
import org.restheart.plugins.InterceptPoint;
import org.restheart.plugins.MongoInterceptor;
import org.restheart.plugins.RegisterPlugin;
import org.restheart.utils.HttpStatus;
import org.restheart.utils.BsonUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *
 * Checks documents according to the specified JSON schema
 *
 * This intercetor is able to check PUT and POST requests that don't use update
 * operators. PATCH requests are checked by jsonSchemaAfterWrite
 * 

* Note that checking bulk PATCH, i.e. PATCH /coll/*, is not supported. In this * case the optional metadata property 'skipNotSuppored' controls the behaviour: * if true, the request is not checked and executed, if false the request fails. * * It checks the request content against the JSON schema specified by the * 'jsonSchema' collection metadata: *

* { "jsonSchema": { "schemaId": <schemaId> "schemaStoreDb": * <schemaStoreDb>, "skipNotSupported": <boolean> } } *

* schemaStoreDb is optional, default value is same db, skipNotSuppored is * optional, defaul value is false * * @author Andrea Di Cesare {@literal } */ @RegisterPlugin( name = "jsonSchemaBeforeWrite", description = "Checks the request content against the JSON schema specified by the 'jsonSchema' collection metadata", interceptPoint = InterceptPoint.REQUEST_AFTER_AUTH) public class JsonSchemaBeforeWriteChecker implements MongoInterceptor { /** * */ public static final String SCHEMA_STORE_DB_PROPERTY = "schemaStoreDb"; /** * */ public static final String SCHEMA_ID_PROPERTY = "schemaId"; /** * */ public static final String SKIP_NOT_SUPPORTED_PROPERTY = "skipNotSupported"; static final Logger LOGGER = LoggerFactory.getLogger(JsonSchemaBeforeWriteChecker.class); @Override public void handle(MongoRequest request, MongoResponse response) throws Exception { var args = request.getCollectionProps() .get("jsonSchema") .asDocument(); // this request is not supported by jsonSchema checkers // if (request.isPatch() && request.isBulkDocuments()) { BsonValue skipNotSupported = args.get(SKIP_NOT_SUPPORTED_PROPERTY); if (skipNotSupported != null && skipNotSupported.isBoolean() && skipNotSupported.asBoolean().getValue()) { LOGGER.debug("skipping jsonSchema checking since the request is a bulk PATCH and skipNotSupported=true"); return; } else { response.setInError(HttpStatus.SC_NOT_IMPLEMENTED, "'jsonSchema' checker does not support bulk PATCH requests. " + "Set 'skipNotSupported:true' to allow them."); return; } } BsonValue _schemaStoreDb = args.get(SCHEMA_STORE_DB_PROPERTY); String schemaStoreDb; BsonValue schemaId = args.get(SCHEMA_ID_PROPERTY); if (schemaId == null) { response.setInError(HttpStatus.SC_INTERNAL_SERVER_ERROR, "wrong 'jsonSchema': missing property " + SCHEMA_ID_PROPERTY); return; } if (_schemaStoreDb == null) { // if not specified assume the current db as the schema store db schemaStoreDb = request.getDBName(); } else if (_schemaStoreDb.isString()) { schemaStoreDb = _schemaStoreDb.asString().getValue(); } else { response.setInError(HttpStatus.SC_INTERNAL_SERVER_ERROR, "wrong 'jsonSchema': " + "property " + SCHEMA_STORE_DB_PROPERTY + " must be a string"); return; } try { MongoURLUtils.checkId(schemaId); } catch (UnsupportedDocumentIdException ex) { response.setInError(HttpStatus.SC_INTERNAL_SERVER_ERROR, "wrong 'jsonSchema': " + "schema 'id' is not valid", ex); return; } Schema theschema; try { theschema = JsonSchemaCacheSingleton .getInstance() .get(schemaStoreDb, schemaId); } catch (JsonSchemaNotFoundException ex) { response.setInError(HttpStatus.SC_INTERNAL_SERVER_ERROR, "wrong 'jsonSchema': schema " + schemaStoreDb + "/" + _SCHEMAS + "/" + BsonUtils.getIdAsString(schemaId, false) + " not found"); return; } if (Objects.isNull(theschema)) { response.setInError(HttpStatus.SC_INTERNAL_SERVER_ERROR, "wrong 'jsonSchema': schema " + schemaStoreDb + "/" + _SCHEMAS + "/" + BsonUtils.getIdAsString(schemaId, false) + " not found"); return; } documentsToCheck(request, response) .stream() .forEachOrdered(doc -> { try { theschema.validate(doc); } catch (ValidationException ve) { var errors = new ArrayList(); errors.add(ve.getMessage().replaceAll("#: ", "")); ve.getCausingExceptions().stream() .map(ValidationException::getMessage) .forEach(errors::add); var errMsgBuilder = new StringBuilder(); errors.stream() .map(e -> e.replaceAll("#: ", "")) .forEachOrdered(e -> errMsgBuilder.append(e).append(", ")); var errMsg = errMsgBuilder.toString(); if (errMsg.length() > 2 && ", ".equals(errMsg.substring(errMsg.length() - 2, errMsg.length()))) { errMsg = errMsg.substring(0, errMsg.length() - 2); } response.setInError(HttpStatus.SC_BAD_REQUEST, "Request content violates schema " + BsonUtils.getIdAsString(schemaId, true) + ": " + errMsg); } }); } List documentsToCheck(MongoRequest request, MongoResponse response) { var ret = new ArrayList(); var content = request.getContent() == null ? new BsonDocument() : request.getContent(); if (content.isDocument()) { ret.add(new JSONObject(BsonUtils.toJson(content, request.getJsonMode()))); } else if (content.isArray()) { content.asArray() .stream() .filter(doc -> doc.isDocument()) .map(doc -> BsonUtils.toJson(doc, request.getJsonMode())) .map(doc -> new JSONObject(doc)) .forEachOrdered(ret::add); } return ret; } @Override public boolean resolve(MongoRequest request, MongoResponse response) { return request.isHandledBy("mongo") && ((request.isWriteDocument() && !request.isPatch()) || (request.isPatch() && request.isBulkDocuments())) && request.getCollectionProps() != null && request.getCollectionProps() .containsKey("jsonSchema") && request.getCollectionProps() .get("jsonSchema") .isDocument(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy