Edit Page

Authorization

Introduction

See Understanding RESTHeart Security for an high level view of the RESTHeart security model.

RESTHeart is built around a pluggable architecture. It comes with a strong security implementation but you can easily extend it by implementing plugins. This section documents the authorizers available out-of-the-box.

RESTHeart offers two authorizers out-of-the-box:

  1. Mongo ACL Authorizer

  2. File ACL Authorizer

Both authorizers provide a simple, declarative and a rich, ACL-based model for authorization.

It’s even possible to develop custom authorizers. Please refer to Develop Security Plugins for more information.

Mongo ACL Authorizer

mongoAclAuthorizer authorizes requests according to the Access Control List defined in a MongoDB collection.

The configuration allows:

  • defining the collection to use to store ACL documents (acl-db and acl-collection).

  • enabling the root role (root-role): users with root role, can execute any requests, regardless of the permissions set in the ACL. Set it to null (root-role: null) to disable it.

  • controlling the ACL caching.

mongoAclAuthorizer:
    acl-db: restheart
    acl-collection: acl
    # clients with root-role can execute any request
    root-role: admin
    cache-enabled: true
    cache-size: 1000
    cache-ttl: 5000
    cache-expire-policy: AFTER_WRITE

Format of permissions

The properties of the permission document are:

property type description

predicate

string

If the undertow predicate resolves the request then the request is authorized. Many examples of predicates can be found in the file acl.yml

roles

JSON array of strings

The roles that are applied the ACL document. The special role $unauthenticated applies to requests that are not authenticated.

priority

number

A request might fulfill several predicates; an ACL document with higher priority has higher evaluation precedence.

mongo

null or JSON MongoPermissions object

For requests handled by the MongoService (i.e. the service that implements the REST API for MongoDB) the permission can specify the [MongoPermissions](#mongopermissions) object

An example permission document follows (for more examples check acl.json):

{
  "_id": "userCanGetOwnCollection",
  "roles": ["user"],
  "predicate": "method(GET) and path-template('/{userid}') and equals(@user._id, ${userid}) and qparams-contain(page) and qparams-blacklist(filter, sort)",
  "priority": 100,
  "mongo": {
    "readFilter": { "_$or": [{ "status": "public" }, { "author": "@user._id" }] },
    "projectResponse": { "log": 0 }
  }
}

The permission allows the request if:

  • the authenticated user has user role

  • the request method is GET

  • the request path is /{userid}, where the {userid} is the id of the authenticated user

  • the request specifies the query parameter page, due to predicate qparams-contain(page)

  • the request does not specify the query parameters filter and sort, due to predicate qparams-blacklist(filter, sort)

If the request is authorized by this permission then:

  • the property log is removed from the response, due to projectResponse

  • a readFilter applies, so only document with status=public or author=<userid> are returned

Predicates

The ACL permissions use the undertow predicate language to define the condition a request must met to be authorized.

A simple example of predicate is (method(GET) or method(POST)) and path('/coll') that authorizes GET or POST requests when the URI is /coll

RESTHeart extends this language introducing the following new predicates that extend and simplify the ACL permission definition:

predicate

qparams-contain

true if the request query string contains the specified parameter

qparams-contain(page,pagesize)

qparams-blacklist

true if the request query string does not contain the blacklisted parameters

qparams-contain(filter,sort)

qparams-whitelist

true if the request query string contains only whitelisted parameters

qparams-whitelist(page,pagesize)

qparams-size

true if the request query string the specified number of parameters

qparams-size(3)

bson-request-contains

true if the request content is BSON and contains the specified properties

bson-request-contains(foo,bar.sub)

bson-request-whitelist

true if the request content is BSON and only contains whitelisted properties

bson-request-whitelist(foo,bar.sub)

bson-request-blacklist

true if the request content is BSON and doesn’t contain blacklisted properties

bson-request-blacklist(foo,bar.sub)

in

checks if value is contained in array

path-template('/{tenant}') and in(value=${tenant}, array=@user.tenants)

bson-request-prop-equals

true if the request content is bson and the value of the property key (can use the dot notation) is equal to value

if the request content is {"sub": { "foo": "bar" }} then bson-request-prop-equals(key=sub.foo, value='"bar"') and bson-request-prop-equals(key=sub, value='{"foo": "bar"}') are true; bson-request-prop-equals(key=sub.foo, value='"baz"') is false

bson-request-array-contains

true if the request content is bson and the property key (can use the dot notation) is an array that contains all values

if the request content is { "a": [ "foo", "bar" ] } then bson-request-array-contains(key=a, values='"foo"' ) and bson-request-array-contains(key=a, values={ '"foo"', '"bar"' } ) are true; bson-request-array-contains(key=a, values={ '"foo"', '"baz"' } ) is false

bson-request-array-is-subset

true if the request content is bson and the property key (can use the dot notation) is an array that is a subset of values

if the request content is { "a": [ "foo", "bar" ] } then bson-request-array-is-subset(key=a, values={ '"foo"', '"bar"', '"baz"' }) is true; bson-request-array-is-subset(key=a, values={ '"foo"', '"baz"' }) is false

Note: the double quotes in values since each element must be valid bson such as 1 (number), "1" (string), "bar" (string) or {"foo": "bar"} (object)

MongoPermissions

For requests handled by the MongoService (i.e. the service that implements the REST API for MongoDB) the permission can specify the MongoPermissions object.

{
  "mongo": {
    "allowManagementRequests": false,
    "allowBulkPatch": false,
    "allowBulkDelete": false,
    "allowWriteMode": false,
    "readFilter": {"$or": [ {"status": "public"}, {"author": "@user._id"} ] },
    "writeFilter": {"author": "@user._id"},
    "mergeRequest": {"author": "@user._id"},
    "projectResponse": {"secret": 0 }
  }
}
mongo permission description

allowManagementRequests

DB Management Requests are forbidden by default (create/delete/update dbs, collection, file buckets schema stores and schemas, list/create/delete indexes, read db and collection metadata). To allow these requests, allowManagementRequests must be set to true

allowBulkPatch

bulk PATCH requests are forbidden by default, to allow these requests, allowBulkPatch must be set to true

allowBulkDelete

bulk DELETE requests are forbidden by default, to allow these requests, allowBulkDelete must be set to true

allowWriteMode

requests cannot use the query parameter ?wm=insert|update|upsert by default. To allow it, allowWriteMode must be set to true

Note that, in order to allow those requests, not only the corresponding flag must be set to true but the permission predicate must resolve to true.

Consider the following examples.

The next one won’t allow the role user to execute a bulk PATCH even if the allowBulkPatch is true since the predicate requires the request verb to be GET

{
  "roles": [ "user" ],
  "predicate": "path-prefix('coll') and method(GET)"
  "mongo": {
    "allowBulkPatch": true
  }
}

The next request allows to PATCH the collection coll and all documents in it, but won’t allow to execute a bulk PATCH (i.e. the request PATCH /coll/*?filter={ "status": "draft" } since the allowBulkPatch is false

{
  "roles": [ "user" ],
  "predicate": "path-prefix('coll') and method(PATCH)",
  "mongo": {
    "allowBulkPatch": false
  }
}

readFilter and writeFilter

Tip
readFilter and writeFilter allows to partition data by roles.

These are optional filters that are added to read and write requests respectively when authorized by an ACL permission that defines them.

The readFilter applies to GET requests to limits the returned document to the ones that match the specified condition.

The writeFilter applies to write request to allow updating only the documents that match the specified condition.

Warning
writeFilter only limits updates and cannot avoid creating documents that don’t match the filter. The properties used in the filter should be set using mongo.mergeRequest.

mergeRequest

mergeRequest allows to merge the specified properties to the request content. In this way, server-side evaluated properties can be enforced.

In the following example:

{
  "roles": [ "user" ],
  "predicate": "path-prefix('coll') and method(PATCH)",
  "mongo": {
    "mergeRequest": {"author": "@user._id"}
  }
}

the property author is evaluated to be the userid of the authenticated client.

@user is a special variable that allows accessing the properties of the user object. The following variables are available:

variable description

@user

the user object (excluding the password), e.g. @user.userid (for users defined in acl.yml by FileRealmAuthenticator) or @user._id (for users defined in MongoDB by MongoRealmAuthenticator)

@request

the properties of the request, e.g. @request.remoteIp

@mongoPermissions

the MongoPermissions object, e.g. @mongoPermissions.writeFilter

@now

the current date time

@filter

the value of the filter query parameter

projectResponse

projectResponse allows to project the response content, i.e. to remove properties.

It can be used with positive or negative logic.

The following hides the properties secret and a.nested.secret (you can use the dot notation!). All other properties are returned.

{
  "roles": [ "user" ]
  "predicate": "path-prefix('coll') and method(PATCH)",
  "mongo": {
    "projectResponse": {"secret": 0, "a.nested.secret": 0 }
  }
}

The following only returns the property public (you can use the dot notation!). All other properties are hidden.

{
  "roles": [ "user" ]
  "predicate": "path-prefix('coll') and method(PATCH)",
  "mongo": {
    "projectResponse": {"public": 1 }
  }
}

File ACL Authorizer

fileAclAuthorizer allows defining roles permissions in the configuration or in a separate YAML configuration file.

fileAclAuthorizer:
  #conf-file: ./acl.yml
  permissions:
    - role: admin
      predicate: path-prefix('/')
      priority: 0

The conf-file path is either absolute, or relative to the restheart configuration file (if specified) or relative to the plugins directory (if using the default configuration).

The permission’s options are fully equivalent to the ones handled by the mongoAclAuthorizer, only the yml format is used in place of Json.

An example follows (for more examples check acl.yml):

  roles: [ "user" ]
  predicate: >
        method(GET) and path-template('/{userid}') and equals(@user._id, ${userid}) and qparams-contain(page) and qparams-blacklist(filter, sort)
  priority: 100
  mongo:
    readFilter: >
        { "_$or": [{ "status": "public" }, { "author": "@user._id" }] }
    projectResponse: >
        { "log": 0 }

Programmatic Configuration of ACL

Security policy rules can be defined programmatically by allowing both inclusive and exclusive security policies through veto and permission predicates.

The ACLRegistry can be injected with @Inject("acl-registry") and allows defining Access Control Lists (ACLs) programmatically:

public interface ACLRegistry {
    /**
     * Registers a veto predicate that determines if a request should be denied.
     * When the predicate evaluates to true, the request is immediately forbidden (vetoed).
     * Additionally, a request will also be denied if it is not explicitly authorized by any
     * allow predicates or any other active allowing authorizers.
     *
     * @param veto The veto predicate to register. This predicate should return true to veto (deny) the request,
     *             and false to let the decision be further evaluated by allow predicates or other authorizers.
     */
    public void registerVeto(Predicate<Request<?>> veto);

    /**
     * Registers an allow predicate that determines if a request should be authorized.
     * The request is authorized if this predicate evaluates to true, provided that no veto predicates
     * or other active vetoer authorizers subsequently deny the request. This method helps in setting up
     * conditions under which requests can proceed unless explicitly vetoed.
     *
     * @param allow The allow predicate to register. This predicate should return true to authorize the request,
     *              unless it is vetoed by any veto predicates or other vetoing conditions.
     */
    public void registerAllow(Predicate<Request<?>> allow);

    /**
     * Registers a predicate that determines whether requests handled by the ACLRegistryAllower
     * require authentication. This method is used to specify conditions under which authentication
     * is mandatory. Typically, authentication is required unless there are allow predicates
     * explicitly authorizing requests that are not authenticated.
     *
     * @param authenticationRequired The predicate to determine if authentication is necessary.
     *                               It should return true if the request must be authenticated,
     *                               otherwise false if unauthenticated requests might be allowed.
     */
    public void registerAuthenticationRequirement(Predicate<Request<?>> authenticationRequired);
}

This registry is utilized by the ACLRegistryVetoer and ACLRegistryAllower authorizers to manage request permissions. The ACLRegistryVetoer denies requests based on veto predicates, while the ACLRegistryAllower grants permission to proceed with requests based on allow predicates.

A request is permitted to proceed if it is not denied by any ACLRegistryVetoer and at least one ACLRegistryAllower approves it.

Example

The following code registers a veto that denies all request to /deny and a permission that allows any request to /allow:

@Inject("acl-registry")
ACLRegistry registry;

@OnInit
public void init() {
  registry.registerVeto(r -> r.getPath().equals("/deny"));
  registry.registerAllow(r -> r.getPath().equals("/allow"));
}