Development Best Practices

RESTHeart Platform Core

Get the MongoClient

// get the MongoClient
MongoClient client = MongoDBClientSingleton.getInstance().getClient();

Interact with Request and Response

The plugins’s methods of restheart-platform-core accept two arguments that allow to interact (read or modify) the request and the response:


public void handleRequest(HttpServerExchange exchange, RequestContext context) {
  ...
}

Both HttpServerExchange exchange and RequestContext context can be used to read and modify the request and the response.

  • HttpServerExchange is the generic, low-level Undertow class to interact with the exchange;
  • RequestContext is the specialized RESTHeart helper class that simplifies the most common operations.

As a general rule, always prefer using RequestContext. Only use HttpServerExchange for low-level operations not directly supported by RequestContext.

As an example, the filter query parameter can be retrieved as follows:

// RequestContext helper method to access the filter query parameter
Deque<String>  filterQParam1 = context.getFilter();

// RequestContext helper method that returns the filter as a BsonDocument
// note that multiple values of the parameters are composed with the $and operator
// ?filter={'a':1}&filter={'b':1} ->  { "$and": [ {'a':1}, {'b':1} ] } 
BsonDocument filters = context.getFiltersDocument();
 filters = context.getFiltersDocument();

// Undertow low-level method to access query paramers
Deque<String> filterQParam2 = exchange.getQueryParameters().get(RequestContext.FILTER_QPARAM_KEY);

Get user id and roles from restheart-platform-security

When the request is authenticated by restheart-platform-security the user id and roles are passed to restheart-platform-core via the following request headers:

  • X-Forwarded-Account-Id
  • X-Forwarded-Account-Roles

Note that for unauthenticated request these headers are not set.

var headers =  exchange.getRequestHeaders();

var id = headers.getFirst(HttpString.tryFromString("X-Forwarded-Account-Id"));
var _roles = hse.getRequestHeaders().get(HttpString.tryFromString("X-Forwarded-Account-Roles"));

var roles = new ArrayList<String>();

if (_roles != null) {
    _roles.forEach(role -> roles.add(role));
}

// check if authenticated user has role 'admin'

if (roles.contains("admin")) {
  ...
} else {
  ...
}

Filter out properties from Request or Response

RESTHeart includes the Transformer filterProperties that allows to filter out properties from both the Request and the Response.

You might want to:

  • filter out properties from the request body in write requests (POST, PUT and PATCH verbs)
  • filter out properties from the response body in read requests (GET verb)

filterProperties can only filter out root properties. Avoid using it to filter nested properties.

It is difficult to filter out properties from a write request because it can use the dot notation and update operators, so that properties to filter out could be in a complex structure as {"$set": "sub.secret": true }}. The suggested way is checking the request using an Interceptor of restheart-platform-security to forbid request containing them. See Forbid write requests containing specific properties to all roles but admin for an example.

In the following example, we add the Transformer filterProperties to Response to filter out the nested property secret, and apply it on read requests on collection /coll. We will filter out the property to all users but for admin.

In order to enable the Transformer we are going to programmatically apply it defining a Global Transformer and enable it using an Initializer

@RegisterPlugin(
        name = "secretHider",
        priority = 100,
        description = "adds a GlobalTranformer to filter out the property 'secret' "
        + "from the response on 'GET /coll' "
        + "when the user does not have the role 'admin'")
public class SecretHider implements Initializer {
    @Override
    public void init(Map<String, Object> confArgs) {
        // a predicate that resolves GET /coll and !roles.contains("admin")
        var predicate = new RequestContextPredicate() {
            @Override
            public boolean resolve(HttpServerExchange hse, RequestContext context) {
                var _roles = hse.getRequestHeaders()
                        .get(HttpString.tryFromString("X-Forwarded-Account-Roles"));

                var roles = new ArrayList<String>();
                
                if (_roles != null) {
                    _roles.forEach(role -> roles.add(role));
                }

                return context.isGet()
                        && "coll".equals(context.getCollectionName())
                        && (roles == null || !roles.contains("admin"));
            }
        };

        // Let's use filterTransformer to filter out properties from GET response
        var filterTransformer = new FilterTransformer();

        var filterTransformerArgs = new BsonArray();
        filterTransformerArgs.add(new BsonString("secret"));

        var globalTransformer = new GlobalTransformer(
                filterTransformer,
                predicate,
                TransformerMetadata.PHASE.RESPONSE,
                TransformerMetadata.SCOPE.CHILDREN,
                filterTransformerArgs,
                null); // finally add it to global checker list

        TransformerHandler.getGlobalTransformers().add(globalTransformer);
    }
}

RESTHeart Platform Security

Interact with the HttpServerExchange object

The helper classes ByteArrayRequest, JsonRequest, ByteArrayResponse and JsonResponse are available to make easy interacting the HttpServerExchange object. As a general rule, always prefer using the helper classes if the functionality you need is available.

For instance the following code snipped retrieves the request JSON content from the HttpServerExchange

HttpServerExchange exchange = ...;

if (Request.isContentTypeJson(exchange)) {
  JsonElement content = JsonRequest.wrap(exchange).readContent();
}

If you want to manipulate query parameters with a Request Interceptor, always use the Map exchange.getQueryParameters() or the method exchange.addQueryParamter() and exchange.addQueryParamter(). Do not update the query string directly: after Request Interceptors execution, the query string is rebuilt from the query parameters map, see QueryStringRebuiler

How to send the response

You just set the status code and the response content using helper classes ByteArrayResponse or JsonResponse. You don’t need to send the response explicitly using low level HttpServerExchange methods, since the ResponseSenderHandler is in the processing chain and will do it for you.

@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {

  JsonResponse response = JsonResponse.wrap(exchange);

  JsonObject resp = new JsonObject();
  resp.appProperty("message", "OK")

  response.writeContent(resp);
  response.setStatusCode(HttpStatus.SC_OK);
}

Forbid write requests containing specific properties to all roles but admin

In the following example, we add a Request Interceptor that forbids write requests to /coll when executed by a user that does not have to role admin.

In order to enable the Interceptor we are going to programmatically apply it using an Initializer

@RegisterPlugin(
        name = "onlyAdminCanWriteSecrets",
        priority = 100,
        description = "adds an Interceptor that forbis write requests "
        + "on 'GET /coll' "
        + "containing the property 'secret' "
        + "to users without the role 'admin'")
public class OnlyAdminCanWriteSecrets implements Initializer {
  @Override
  public void init() {

      PluginsRegistry.getInstance()
              .getRequestInterceptors()
              .add(new RequestInterceptor() {
                  @Override
                  public boolean requiresContent() {
                      return true;
                  }

                  @Override
                  public RequestInterceptor.IPOINT interceptPoint() {
                      return RequestInterceptor.IPOINT.AFTER_AUTH;
                  }

                  @Override
                  public void handleRequest(HttpServerExchange hse) throws Exception {
                      var content = JsonRequest.wrap(hse).readContent();

                      if (keys(content).stream()
                              .anyMatch(k -> "secret".equals(k)
                              || k.endsWith(".secret"))) {
                          var response = ByteArrayResponse.wrap(hse);

                          response.endExchangeWithMessage(HttpStatus.SC_FORBIDDEN, "cannot write secret");
                      };
                  }

                  @Override
                  public boolean resolve(HttpServerExchange hse) {
                      var req = ByteArrayRequest.wrap(hse);

                      return req.isContentTypeJson(hse)
                              && !req.isAccountInRole("admin")
                              && hse.getRequestPath().startsWith("/coll")
                              && (req.isPost() || req.isPatch() || req.isPut());
                  }

                  /**
                   * @return the keys of the JSON
                   */
                  private ArrayList<String> keys(JsonElement val) {
                      var keys = new ArrayList<String>();

                      if (val == null) {
                          return keys;
                      } else if (val.isJsonObject()) {
                          val.getAsJsonObject().keySet().forEach(k -> {
                              keys.add(k);
                              keys.addAll(keys(val.getAsJsonObject().get(k)));
                          });
                      } else if (val.isJsonArray()) {
                          val.getAsJsonArray().forEach(v -> keys.addAll(keys(v)));
                      }

                      return keys;
                  }
              });
  }
}

This interceptor is executed (see method resolve()):

  • to write requests: (req.isPost() || req.isPatch() || req.isPut()
  • that are executed by authenticated users without the role admin: !req.isAccountInRole("admin")
  • on URI starting with /coll: hse.getRequestPath().startsWith("/coll")

The interceptor needs the request body (requiresContent() returns true) and must be executed after authorization and authentication phases (interceptPoint() returns AFTER_AUTH)

In order to check that the request body does not contain the property secret, the helper method keys() collects all the JSON keys (name of the properties) in the request, and finally handleRequest() checks that those keys don’t contain the value secret or a key that ends with .secret (with restheart keys can use the dot notation).