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.
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 |
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 |
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