Namespace

acl

kiss.acl

A simple ACL manager

This small module provides an isomorphic record-based ACL.

The ACL ensures that a user has the PERMISSION to perform an ACTION. To check this, the ACL configuration consists of RULES and VALIDATOR functions (aka "validators"):

   acl: {
      permissions: {
          create: [
              { isOwner: true } // <= RULE
          ],
          read: [
              { authenticatedCanRead: true }, // RULE 1
              { isReader: true } // RULE 2 (evaluated only if RULE 1 is false)
          ],
          update: [
              { isOwner: true, isBanned: false }, // <= RULE with 2 conditions to fulfill
              { isDesigner: true }
          ],
          delete: [
              { isOwner: true }
          ],
          paintCar: [ // <= any arbitrary ACTION can be evaluated, not only CRUD operations
              { isOwner : true}
          ]
      },
  
      validators: {
          async isOwner({req}) { // <= VALIDATOR function
              if (kiss.isClient) return kiss.session.isOwner // <= ACL can be checked on the client or the server
              else return req.token.isOwner
          },
  
          async isDesigner({userACL, record}) {
              return kiss.tools.intersects(userACL, record.accessUpdate)
          },
  
          async authenticatedCanRead({record}) {
              return !!record.authenticatedCanRead
          },
  
          async isReader({userACL, record}) {
              if (record.accessRead && kiss.tools.intersects(userACL, record.accessRead)) return true
          },

          async isBanned({userACL, record}) {
              return (userACL.indexOf("banned") != -1)
          }
      }
   }

This way, it's very straightforward to read the permissions, even with complex business cases.

In the example above, the PERMISSION to UPDATE requires the user:

  • to be an owner AND not to be banned OR
  • to be a designer

The validator functions are called to evaluate those rules.

The rules are evaluated sequentially, and it stops if a rule is fulfilled. A single rule can have multiple validators, for example: { isOwner: false, isDesigner: true }

All the validators of a rule must pass to consider the rule fulfilled.

Inside a validator, we can use kiss.isClient and kiss.isServer to check where the code is executed. This allows to have specific code depending on the execution context.

Validator functions used for creation and mutations (create, patch, delete) receive an object with 4 properties:

  • req: the server request object
  • userACL: an array of string containing all the names that identify a user, including groups.
  • record: the database record we're trying to access
  • model: the record's model

A validator function used for the "read" action receives an object with 3 properties:

  • req: the server request object
  • userACL
  • record: the database record to evaluate. The validator returns true if the record matches the requirements.

For the "read" operation, the validators are evaluated against each record to filter data according to the user's permissions.

When executed on the CLIENT, the req property is not sent (the request doesn't exist here). Validators are asynchronous because they sometimes need to retrieve database objects.

View Source client/core/modules/acl.js, line 94

Methods

# async static check(config) → {boolean}

Check a permission to perform an action on a record

Parameters:
Name Type Description
config object
action string

Ex: "create", "update", "read", "paintCar", "setCharacterName"

record object

CLIENT ONLY - record which we want to check the access rights

req object

SERVER ONLY - Server request object

View Source client/core/modules/acl.js, line 116

true if permission is granted

boolean
Example
const record = await kiss.db.findOne("opportunity", {
 _id: "01890143-81ba-71bb-a1e9-155872656bdf"
})

const canUpdate = await kiss.acl.check({
 action: "update",
 record
})
console.log(canUpdate) // true or false

# async static filter(config) → {Array.<object>}

Filter records for "read"" operations (find, findAndSort)

The rules defined in the "read" configuration of the acl object are evaluated against each record. For this reason, this ACL system should be applied only to small sets of records (ex: workspaces, applications...) but not on big sets of records (like dynamic tables with thousands of records).

For big sets of records, the ACL should not be record-based (per record), but model-based (per table/collection)

Parameters:
Name Type Description
config object
records Array.<object>

records to filter

req object

SERVER ONLY - Server request

View Source client/core/modules/acl.js, line 198

filtered records

Array.<object>
Example
// Inside a NodeJS controller
const records = await kiss.db.find("opportunity", {}) // Retrieve all the opportunities

const authorizedRecords = await kiss.acl.filter({
 records,
 req
})
console.log(authorizedRecords) // Show only the records where the user has a read access