SoftInstigate / restheart

Rapid API Development with MongoDB
https://restheart.org
GNU Affero General Public License v3.0
805 stars 171 forks source link

Add a filter at runtime to a request before it is sent to the MongoDB server #157

Closed indb-fr closed 7 years ago

indb-fr commented 8 years ago

I have to dynamically add a filter to restrict users to see only their list of organization units. This list (OrgUnitId in [ '0A1', '0B3', '1G4']) is recovered by calling a REST service, and I wish - for security reasons - make this call just before sending the request to the Mongo DB server.

The username can be cyphered and sent to RestHeart in a HTTP request header.

Do you know a way to do that?

Thanks in advance

ujibang commented 8 years ago

the user name is sent yo restheart via basic authentication headers. with security enabled restheart knows who is the caller.

you can transform requests and response data via the representation transformers features.

you have to develop a java class that implements the following interface:

(all code here ferers to 3.0 version, for 2.0 you have the same with few differences due to old Mongo java driver)

void transform(
            final HttpServerExchange exchange,
            final RequestContext context,
            BsonValue contentToTransform,
            final BsonValue args);
}

you also have to declare the transformer in the conf file and set it to apply to your collection. all information in the documentation page mentioned before.

in the context object you find the userid and roles. the contentToTranform is what you have to modify filtering out the OrgUnitId you want.

note that if you need more information than what available in the requestContext object to take the decision about what filtering out, the transformer class can make http requests (for instance, using unirest library) and also querying the db. You can retrieve the MongoClient as follows:

private final MongoClient MONGO_CLIENT = MongoDBClientSingleton.getInstance().getClient();

final note, if you perform a time consuming operation in the transformer you obviously slow down all the request to the involved collection. you might consider for this to cache data. RESTHeart provides a simple to use cache feature, example:

LoadingCache<String, BsonDocument> collCache = CacheFactory.createLocalLoadingCache(2, Cache.EXPIRE_POLICY.AFTER_WRITE, 5 * 1000, (String key) -> {
            return MONGO_CLIENT.getDatabase(DB)
                    .getCollection("coll", BsonDocument.class)
                    .find(new BsonDocument("_id", new BsonString(key)))
                    .first();
        });
indb-fr commented 8 years ago

Thanks, this is very usefull.

I read the documentation carefully but I did not understand that I could transform the request sent to MongoDB server.

In my case, I would add the following filter to my request GET localhost:9999/geo/postalCodes ?keys={properties.city:1} &keys={'properties.geo_point_2d':1} &keys={'properties.postal_code':1} &filter={'orgUnitId':{'[ "1B9", 0A3" ]'}}

In the documentation, i read this :

A transformer can be applied either in the REQUEST or RESPONSE phases

Applying it in the REQUEST phase makes sense only for PUT, POST and PATCH requests (when data is sent from the client). In these cases, the incoming data is first transformed and then stored in MongoDB Applying it in the RESPONSE phase, makes sense only for GET requests (when data is sent back to clients). The data is first retrieved from MongoDB, than transformed and passed back to the client."

My context is a GET REQUEST , isn't it ?

"you also have to declare the transformer in the conf file and set it to apply to your collection. all information in the documentation page mentioned before."

How can i do that ?

Are you sure the following command syntax is correct ? java -server -classpath restheart.jar:custom-transformer.jar org.restheart.Bootstrapper restheart.yml

Thanks in advance

ujibang commented 8 years ago

the transformer can be applied on

develop your transformer

you implement the Transformer class and package it in a jar file.

say that your class is com.acme.MyTransformer

declare it in the configuration file

open the default conf file restheart.yml and find the "group: transformer" section. here give your transformer a name

- group: transformers
      interface: org.restheart.hal.metadata.singletons.Transformer
      singletons:
        - name: myCoolTransformer
          class: com.acme.MyTransformer  

apply to your collection

add the rts metadata to your collection

PATCH /db/collection 

{
  "rts": [
    {
      "name": "myCoolTransformer",
      "phase": <phase>,
      "scope": "CHILDREN",
      "args": null
    }
  ]
}

phase should be either "REQUEST" or "RESPONSE". The former is for write operations (transformation is applied to request body) the latter for read operations (transformation is applied to response body)

the json is passed to your Transformer as the BsonValue args argument of the transform() method.

The command is correct on linux and OSX. On windows the path separator is ;

#on linux/osx
java -server -classpath restheart.jar:custom-transformer.jar org.restheart.Bootstrapper restheart.yml

#on windows
java -server -classpath restheart.jar;custom-transformer.jar org.restheart.Bootstrapper restheart.yml

note that the configuration file path is relative to the restheart.jar directory.

indb-fr commented 8 years ago

Ok, I made it work. Thanks for your help.

My request is the following :

GET localhost:9999/geo/postalCodes?keys={properties.city:1}&keys={'properties.geo_point_2d':1}&keys={'properties.postal_code':1}

I patched my collection like this :

   {
     "rts": [
       {
         "name": "accessControlTransformer",
         "phase": "RESPONSE",
         "scope": "CHILDREN",
         "args": null
       }
     ]
   }

And I can transform the results passed back to the client. But this not my use case because the MongoDB server's results are already avalaible in the RequestContext context.responseContent of my Transformer. This is too late. As expected because i patched my collection with phase = RESPONSE. Phase = REQUEST is not possible for my use case because verbs are only PUT PATCH POST.

Did I miss something?

Do you see any other way?

indb-fr commented 8 years ago

Maybe i have to develop a custom AccessManager and inject new MongoDB filter parameters on the fly with HttpServerExchange.addQueryParam ?

indb-fr commented 7 years ago

Maybe this is mostly a hack but a custom AccessManager did the trick.

` public boolean isAllowed(HttpServerExchange exchange, RequestContext context) {

    // get and check filter parameter
    // rem : the filter syntax is not checked
    exchange.addQueryParam("filter","{'orgUnitId':{'[ "1B9", 0A3" ]'}}");
    Deque<String> filters = exchange.getQueryParameters().get(FILTER_QPARAM_KEY);
    context.setFilter(filters);

`

Do you see any problem with something like this or a more elegant way ?

ujibang commented 7 years ago

I've better understood your need and sorry if I have put on the wrong direction.

Using a custom AccessManager is definitely the only way to go to add the filter.

Indeed request transformers should also apply to GET requests (for instance, to modify qparams), will do it for 3.0 https://softinstigate.atlassian.net/browse/RH-211

My notes:

indb-fr commented 7 years ago

It does not matter, I learned about representation transformers ;-)

Thanks for all