SoftInstigate / restheart

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

Projections and Schemas based on Authorization ("Views")? #391

Closed StephenOTT closed 3 years ago

StephenOTT commented 3 years ago

Is there the current ability to create projection rules and schemas specific to a role/authorization against a collection?

It would seem to be a key feature: Where a document has administrative and user viewable properties: where some fields should not be shared with the authorized user, and they can edit specific fields.

Likely some sort list of field access restrictions per role, which impact default projections and user's ability to request fields

similar to something like: https://github.com/gethuman/fakeblock or something like wrapping filters in redaction rules?

Thanks!

ujibang commented 3 years ago

This can be done via Interceptors.

For instance, the Response Interceptor userPwdRemover removes the password field from GET /users

It is easy to make a similar one that applies different filtering logic depending on the user role that can be retrieved with request.getAuthenticatedAccount().getRoles().

The interceptor could also modify the projection qparam of the request request.getProjectionDocument() forcing the projection to apply on the MongoDB find()

Similarly, a REQUEST_AFTER_AUTH Interceptor can forbid write request containing some fields depending on the user role.

if (request.getContent().asDocument().containsKey("protected") && !request.getAuthenticatedAccount().getRoles().contains("ADMIN")) {
      response.setInError(true);
      response.setStatusCode(403);
      return;
}

A generic role-based fields filtering Interceptor and a role-based request content checker can be implemented simplifying your use-case. Maybe handling the configuration directly in the ACL document of the mongoAclAuthorizer. This would be super useful!

{
    "_id": { "$oid": "5d9485639eab3a852d48a1de" },
    "predicate": "path-prefix[/blog] and (method[GET] or method[POST])",
    "roles": ["editor"],
    "priority": 1,
    "readFilter": null,
    "writeFilter": null,
    "project": {"protected", 0} <------
    "forbid": [ "protected", "subdoc.protected"] <------
}
StephenOTT commented 3 years ago

it seems to be the missing feature in RestHeart to make the API more end-user friendly / not leaking data. Building public facing APIs directly with RH has the flaw of exposing all fields and opening all fields up to edit at the moment. So ya super useful to add this restriction!

How would you handle project/forbid rules in a ACL with multiple matching ACL rules?

ujibang commented 3 years ago

ok we will do it!

only one ACL permission is taken into account per each request. this is controlled by the field priority. if more that one rule have same priority, the first one is taken into account (first one means document with bigger _id)

with loglevel=DEBUG the matching permissions (role+matching predicate) are logged highlighting which one applied.

ujibang commented 3 years ago

Added this enhancement to the roadmap for 5.3.

See https://restheart.org/docs/roadmap/#restheart-53

ujibang commented 3 years ago

Hi @StephenOTT

I just pushed an improvement that makes easy filtering responses, authorizing requests by checking the json body and modifying the request content on the server-side.

you can try it either building from sources or with docker:

$ docker pull softinstigate/restheart:82bc66b

This will be available on the next stable release 6.0

The following is an example of permission on a POST request that uses

    # allow role 'user' to create documents under /{userid}
    # the request content must contain 'title' and 'content' <- bson-request-contains(title, content)
    # the request content cannot contain any property other than 'title' and 'content' <- bson-request-whitelist(title, content)
    # no qparams can be specified <- qparams-whitelist()
    # the property 'author' and 'status' are added to the request at server-side <- mergeRequest
    # the property 'log' with some request values is added to the request at server-side <- mergeRequest
    - roles: [ user ]
      predicate: >
        method(POST)
        and path-template('/{userid}')
        and equals(@user.userid, ${userid})
        and bson-request-whitelist(title, content)
        and bson-request-contains(title, content)
        and qparams-whitelist()
      priority: 100

      mongo:
        mergeRequest: >
          {"author": "@user.userid", "status": "draft", "log": "@request"}

The following is an example of permission that uses the mongo.projectResponse option to filter out the property log from the response (you can also use { "public": 1 } to just return the listed properties)

    # allow role 'user' GET document from /{userid}
    # a read filter apply, so only document with status=public or author=userid are returned <- readFilter
    # must use 'page' qparam <- qparams-contain(page)
    # cannot use 'filter' and 'sort' qparams <- qparams-blacklist(filter, sort)
    # the property 'log' is removed from the response <- projectResponse
    - roles: [ user ]
      predicate: >
        method(GET)
        and path-template('/{userid}')
        and equals(@user.userid, ${userid})
        and qparams-contain(page)
        and qparams-blacklist(filter, sort)

      priority: 100
      mongo:
        readFilter: >
          { "$or": [
            {"status": "public"},
            {"author": "@user.userid" }
            ]}
        projectResponse: >
          { "log": 0 }