access-mate
Version:
Attribute base access control using o-is for the conditions
258 lines (207 loc) • 7.98 kB
Markdown
# Access Mate
[Attribute-based access control][1] inspired by [xacml][2].
## Benefits
Using this model, one can implement virtually any kind of access control; role
based access control, multi-level access control, identity based access
control, etc.
## Glossary
- resource: The data that wants to be accessed.
- target: Name of the resource.
- action: What the request wants to do with the resource.
- subject: In most cases this is essentially the authenticated user.
- environment: Data which is not specific to the resource or subject, e.g.,
global settings/configurations.
- effect: If the rule passes, what effect it will have (either deny or allow).
- policy: A policy is the smallest unit of access control. It represents a
single rule. A policy reaches a decision based on the context it is given,
which is the action, resource, subject,
- policy set: A policy set is a list of policies.
- strategy: A strategy decides how to use policies. Mainly, these define
how actions behave, and pass the context over to the policies.
## Conditions
Conditions are generated using the `o-is` module. See [documentation][3] for
details.
## API
### Policy
The policy class is an immutable builder. Methods returning a policy will
return a new instance with the same properties as the previous policy with some
additions done by the method call. For example, if you have a policy with an
action of "update" and decide to call `target('user')`, this will return a new
policy with an action of "update" and a target of "user" instead of mutating
the current existing one.
#### `decision(context)`
Returns true if the decision was made to allow access, false if denied,
undefined if unable to reach decision (meaning the condition didn't apply).
#### `target(value)`
Returns a new policy with the specified targets. Accepts multiple string values
or an array of values.
#### `action(value)`
Returns a new policy with the specified action. Accepts multiple string values
or an array of values.
#### `effect(value)`
Returns a new policy with the specified effect. Valid effects are "allow" and
"deny".
#### `fields(...values)`
Returns a new policy which will only apply to certain fields. This means that
if the policy denies access, it will add the specified fields to the `omit`
array in the result instead of causing the entire record to fail authorization.
#### `condition(value)`
Optionally accepts a value which will be "set" as the policie's condition.
If no value is given this will instead return an `o-is` instance, allowing
construction of the condition immediately.
Example when specifying the condition value:
```javascript
const isAdmin = oIs().true('isAdmin')
// You can define your conditions first and use them on policies later.
const policy = AccessMate.policy()
.condition(isAdmin)
.target('comment')
.effect('allow')
.action('update')
```
Example when not specifying the condition (results in the same thing as the
previous example):
```javascript
const policy = AccessMate.policy()
.condition()
.true('isAdmin')
.end()
.target('comment')
.effect('allow')
.action('update')
```
#### `deny()`
Concatenates the current policy to the policy set and returns a new policy with
a deny effect.
#### `allow()`
Concatenates the current policy to the policy set and returns a new policy with
an allow effect.
#### `end()`
Concatenates the policy to the policy set and returns the policy set.
### PolicySet
Much like the `Policy`, the `PolicySet` class is immutable. All calls
return a new policy set instead of mutating itself.
#### `concat(value)`
Concatenates policy sets together.
#### `allow()`
Returns a new policy with an "allow" effect.
#### `deny()`
Returns a new policy with a "deny" effect.
#### `toJSON()`
Returns a serializable representation of the policy set.
#### `static fromJSON(json)`
Takes the serializable representation and returns a policy set.
#### `authorize(context)`
Returns an object containing two properties: `authorize` and `omit`. If
`authorize` is true, the user has access to the resource. `omit` is a list of
fields that the user does not have access to.
The context object must contain an `action` (string), `target` (string),
`environment` (object), `resource` (object), and `subject` (object) property.
### strategies
Strategies determine how actions behave. Policy sets only use actions to
determine if a policy should be applied or not, strategies define additional
behaviour on top of this. All strategies require a policy set and an options
object. The options object usually requires at least `resource`, `subject`, and
`environment`.
#### `simple(policySet, options)`
A very basic strategy which simply returns the result of the policy set. There
is no special behaviour for this strategy.
#### `crud(policySet, options)`
The CRUD strategy is identical to the simple strategy except it defines
special behaviour for the updates. For update actions, you will need to pass in
an additional property `previousResource` which is the resource that was last
updated. There are 3 different kind of update actions which your policies can
use:
- `update-into`: This action is only applied on the resource which is to be
updated.
- `update-from`: This action is only applied on the previous revision of the
resource.
- `update`: This is just a combination of `update-into` and `update-from`.
### `bread(policySet, options)`
`BREAD` stands for "browse", "read", "edit", "add", "delete". "Edit" has the
"edit-from" and "edit-into" similar to the "update" action in the CRUD
strategy.
Browse represents a request to list multiple records. For this action, the
strategy requires a `resources` property instead of `resource`. This
`resources` object is then used to return an array of authorization results
instead of the usual single result.
## Full Example
```javascript
const AccessMate = require('access-mate')
const oIs = require('o-is')
// conditions can be re-used.
const isOwner = oIs().propsEqual('subject.id', 'resource.owner')
const policySet = AcessMate.policySet()
.deny()
.target('todo_item')
.action('create')
.condition()
.false('subject.premium_account')
.gt('subject.total_items', 50)
.end()
.allow()
.target('todo_item')
.action('create', 'update', 'delete', 'read')
.condition(isOwner)
// Rules can be composed together.
const policy = AccessMate.policy()
.allow()
.target('todo_item')
.action('read')
.condition()
.propsEqual('subject.id', 'resource.collaborators')
.end()
const fullPolicySet = policySet.concat(rule)
const decision = AccessMate.strategies.simple(fullPolicySet, {
environment: {
someGlobalConfigurations: {}
},
resource: {
owner: 'foobar',
value: 'example todo item',
collaborators: []
},
action: 'create',
target: 'todo_item'
subject: {
id: 'foobar'
}
})
```
This access control module can be serialized/deserialized. The above example
would look like the following when converted to JSON:
```json
[
{
"effect": "deny",
"target": "todo_item",
"action": ["create"],
"condition": [
{ "type": "false", "key": "subject.premium_account" },
{ "type": "gt", "key": "subject.total_items", "value": 50 }
]
},
{
"effect": "allow",
"target": "todo_item",
"action": ["create", "update", "delete", "read"],
"condition": [
{ "type": "propsEqual", "keys": ["subject.id", "resource.collaborator"] }
]
},
{
"effect": "allow",
"target": "todo_item",
"action": ["read"],
"condition": [
{ "type": "propsEqual", "keys": ["subject.id", "resource.collaborator"] }
]
}
]
```
## Debugging
This module uses the `debug` package for managing debug logs. Simply set your
`DEBUG` environment variable to include this package's name to enable it.
[1]: https://en.wikipedia.org/wiki/Attribute-based_access_control
[2]: http://docs.oasis-open.org/xacml/3.0/errata01/os/xacml-3.0-core-spec-errata01-os-complete.html
[3]: https://github.com/AGhost-7/o-is/tree/master/packages/o-is