Request Checkers

Introduction

Request Checkers feature allows to check the request so that, if it does not fulfill some conditions, it returns 400 BAD REQUEST response code thus enforcing a well defined structure to documents.

The checkers collection metadata

In RESTHeart, not only documents but also dbs and collections (and files buckets, schema stores, etc.) have properties. Some properties are metadata, i.e. have a special meaning for RESTheart that controls its behavior.

The collection metadata property checkers allows to declare checkers to be applied to write requests.

checkers is an array of checker objects. A checker object has the following format:

{ "name": <checker_name>,"args": <arguments>, "skipNotSupported": <boolean> }
Property
Description
Mandatory
name

name of the checker.

Yes
args arguments to be passed to the checker no
skipNotSupported

if true, skip the checking if this checker does not support the request (Checker.doesSupportRequests())

no

Global Checkers

Global Checkers are applied to all requests.

Global Checkers can be defined programmatically instantiating GlobalChecker objects:

    /**
     * 
     * @param checker
     * @param predicate checker is applied only to requests that resolve
     * the predicate
     * @param skipNotSupported
     * @param args
     * @param confArgs 
     */
public GlobalChecker(Checker checker,
            RequestContextPredicate predicate,
            boolean skipNotSupported,
            BsonValue args,
            BsonValue confArgs)

and adding them to the list CheckerHandler.getGlobalCheckers()

// a predicate that resolves POST /db/coll and PUT /db/coll/docid requests
RequestContextPredicate predicate = new RequestContextPredicate() {
        @Override
        public boolean resolve(HttpServerExchange hse, RequestContext context) {
            return (context.isPost() && context.isCollection())
            || (context.isPut() && context.isDocument());
        }
    };

// Let's use the predefined ContentSizeChecker to limit write requests size
Checker checker = new ContentSizeChecker(); 

// ContentSizeChecker requires argument max, use 1024 Kbyte
BsonDocument args = new BsonDocument("max", new BsonInt32(1024*1024));

// if the checker requires configuration arguments, define them here
BsonDocument confArgs = null;

GlobalChecker globalChecker = new GlobalChecker(checker, predicate, true, args, confArgs);

// finally add it to global checker list
CheckerHandler.getGlobalCheckers().add(globalChecker);

You can use an Initializer to add Global Checkers.

Checkers

RESTHeart comes with 3 ready-to-be-used checkers:

Custom checkers can also be implemented.

Schema validation with checkContent checker

checkContent is a checker shipped with RESTHeart that allows to enforce a schema to the documents of a collection.

The schema definition is passed via the checker metadata args property as an array of conditions. A condition has the following format

{ "path": <json_path>, "type": <property_type>, "regex": <regex>, "nullable": boolean, "optional": boolean }

If the type is ‘object’ the properties mandatoryFields and optionalFields apply as well:

{ "path": <json_path>, "type": "object", "mandatoryFields": [ <field_names> ], "optionalFields": [ <field_names>] }
Property
Description
Mandatory
Default value
path

The json path expression that selects the property to verify the condition on.

Yes
type

The type that the selected property must have. It can be the following BSON types:

  • null
  • object
  • array
  • string
  • number
  • boolean
  • objectid
  • date
  • timestamp
  • maxkey
  • minkey
  • symbol
  • code
Yes
regex If specified, this regular expression must match the property (or its string representation). No null
nullable If true, no check will be performed if the value of the selected property is null. No false
optional If true, no check will be performed if the property is missing. No false

mandatoryFields

If the property type is 'object', this is the array of the properties that the object must have.

If specified, the object cannot have any other field, as long as they are not listed in the optionalFields array.

No null
optionalFields

If the property type is 'object', this is the arrayof the properties that the object is allowed optionally to have.

If specified, the object cannot have any other field, as long as they are not listed in the mandatoryFields array.

No null

Json path expressions

A Json path expressions identifies a part of a Json document.

  • It uses the dot notation where the special symbol $ identifies the document itself. 
  • The special char * selects all the properties of an object.
  • The special string [*] selects all the elements of an array.

the [n] notation is not supported, i.e. you cannot use the following json path expression $.array.[3] to select the n-th element of an array.

For example, given the following document:

{
    "_id": {
        "$oid": "55f6ccf4c2e6be404fdef3dd"
    },
    "string": "hello",
    "object": {
        "pi": 3.14,
        "href": "https://en.wikipedia.org/wiki/Pi"
    },
    "array": [1, 2, 3]
}

The following table shows what document parts, different json path expressions select:

json pat expr selected part
$ the whole document, that is an object
$.string the property string with value "hello"
$.object

the object with value:

{ "pi": 3.14, "href": "https://en.wikipedia.org/wiki/Pi"}

$.object.* the 2 properties pi and href with values 3.14 (number) and "https://en.wikipedia.org/wiki/Pi" (string) respectively
$.object.pi the property pi with numeric value 3.14
$.array the array with value [ 1, 2, 3]
$.array.[*] the 3 elements of the array with numeric values 1, 2 and 3.

Example

The following example creates the collection user enforcing its document to have following fields:

name type mandatory notes
_id string yes the string must satisfy the given regex, that makes sure that the string is a valid email address.
name string yes
password string yes
roles array of strings yes
bio string no
$ http -a a:a PUT 127.0.0.1:8080/test/users \
checkers:='[{\
    "name":"checkContent","args":[\
        {"path":"$", "type":"object", "mandatoryFields": ["_id", "name", "password", "roles"], "optionalFields": ["bio"]},\
        {"path":"$._id", "type":"string", "regex": "^\\u0022[A-Z0-9._%+-]+@[A-Z0-9.-]+\\u005C.[A-Z]{2,6}\\u0022$"},\
        {"path":"$.password", "type":"string"},\
        {"path":"$.roles", "type":"array"},\
        {"path":"$.roles.[*]", "type":"string"},\
        {"path":"$.name", "type":"string"},\
        {"path":"$.bio", "type":"string", "nullable": true}
 ]\
}]'

Limiting file size with checkContentSize

checkContentSize is a checker shipped with RESTHeart that allows to check the size of a request. It is very useful with file resources to limit the maximum size of the uploaded file.

The following example, creates a file bucket resource, limiting the file size from 64 to 32768 bytes:

$ http -a a:a PUT 127.0.0.1:8080/test/icons.files descr="icons" \
checkers:='[{"name":"checkContentSize","args":{"min": 64, "max": 32768}}]

Custom Checkers

A checker is a java class that implements the interface org.restheart.metadata.checkers.Checker.

It only requires to implement the method check() with 3 arguments:

  1. HttpServerExchange exachange
  2. RequestContext context (that is the suggested way to retrieve the information of the request such as the payload) 
  3. BsonValue args (the arguments passed via the args property of the transformer metadata object)
  4. BsonValue confArgs (the arguments passed via the args property specified in the configuration file)
    boolean check(
            HttpServerExchange exchange,
            RequestContext context,
            BsonValue contentToCheck,
            BsonValue args);

    /**
     * Specify when the checker should be performed: with BEFORE_WRITE the
     * checkers gets the request data (that may use the dot notation and update
     * operators); with AFTER_WRITE the data is optimistically written to the db
     * and rolled back eventually. Note that AFTER_WRITE helps checking data
     * with dot notation and update operators since the data to check is
     * retrieved normalized from the db.
     *
     * @param context
     * @return BEFORE_WRITE or AFTER_WRITE
     */
    PHASE getPhase(RequestContext context);

    /**
     *
     * @param context
     * @return true if the checker supports the requests
     */
    boolean doesSupportRequests(RequestContext context);

Once a checker has been implemented, it can be given a name (to be used as the name property of the checker metadata object) in the configuration file.

The following is the default configuration file section declaring the two off-the-shelf checkers provided with RESTHeart (checkContent and checkContentSize) and a third, custom one.

metadata-named-singletons:
    - group: checkers
      interface: org.restheart.hal.metadata.singletons.Checker
      singletons:
        - name: checkContent
          class: org.restheart.hal.metadata.singletons.SimpleContentChecker
        - name: checkContentSize
          class: org.restheart.hal.metadata.singletons.ContentSizeChecker
        - name: myChecker
          class: com.whatever.MyChecker

The class of the custom checker must be added to the java classpath. See (How to package custom code)[/learn/custom-code-packaging-howto] for more information.

For example, RESTHeart could be started with the following command:

$ java -server -classpath restheart.jar:custom-checker.jar org.restheart.Bootstrapper restheart.yml

The following code, is an example checker that checks if the number property is an integer between 0 and 10.

package com.whatever;

public class MyChecker implements Checker {
    boolean check(HttpServerExchange exchange, RequestContext context, BsonValue args) {
        BsonValue content = context.getContent();
        BsonValue _value = content.get("number");
        if (context.getMethod() == RequestContext.METHOD.PATCH) {
            if (_value == null) {
                // if this is a PATCH and property value is not in the request, it won't be modified
                return true;
            }
        }
        if (_value != null && _value.isNumber()) {
            Integer value = _value..asInt32().getValue();
            
            return value < 10 && value > 0;
        } else {
            return false; // BAD REQUEST
        }
    }
}