Define Rules
There are 3 ways you can define abilities:
- using
defineAbilityfunction - using
AbilityBuilderclass - using
JSONobjects
In order to understand which way to use, let's learn more about each one.
defineAbility function
This function is a DSL that allows to create MongoAbility instance using can and cannot methods. It allows to define and use MongoAbility instance without writing too much code.
defineAbility example
import { defineAbility } from '@casl/ability';
export default defineAbility((can, cannot) => {
can('read', 'Post');
cannot('delete', 'Post', { published: true });
});
You can pass multiple actions, subjects and fields in the single can (or cannot) function. So, instead of
import { defineAbility } from '@casl/ability';
export default defineAbility((can, cannot) => {
can('read', 'Post');
can('update', 'Post');
can('read', 'Comment');
can('update', 'Comment');
});
you can do:
import { defineAbility } from '@casl/ability';
export default defineAbility((can, cannot) => {
can(['read', 'update'], ['Post', 'Comment']);
});
To learn more about
canandcannotfunctions' parameters, read AbilityBuilder API
When to use defineAbility
- unit tests
- examples and learning resources
- prototypes or really simple applications
So, to keep examples simple and clear, we will use defineAbility in majority of code snippets in this documentation.
Why defineAbility is not recommended
There is nothing extremely wrong with this function but there are several drawbacks when you use it in your app:
- In most cases, rules depends on user request, so using callback style to define permissions, adds additional nesting and increases cognitive complexity.
- It can create only
MongoAbilityinstance which works with MongoDB conditions , so you won't be able to use another language to match conditions.
See Customize Ability to know more about different ability classes and possibilities to customize it.
AbilityBuilder class
This class implements can and cannot functions, that makes possible to write rules using DSL syntax. This is the recommended way to define rules using DSL syntax
AbilityBuilder example
defineAbility example written using AbilityBuilder class looks a bit more wordy:
import { AbilityBuilder, createMongoAbility } from '@casl/ability'
const { can, cannot, build } = new AbilityBuilder(createMongoAbility);
can('read', 'Post');
cannot('delete', 'Post', { published: true });
export default build();
But it allows to define rules without additional nesting, this is especially important when you build rules based on conditional logic:
import { AbilityBuilder, createMongoAbility } from '@casl/ability'
export default function defineAbilityFor(user) {
const { can, cannot, build } = new AbilityBuilder(createMongoAbility);
if (user.isAdmin) {
can('manage', 'all'); // read-write access to everything
} else {
can('read', 'all') // read-only access to everything
}
cannot('delete', 'Post', { published: true });
return build();
}
To learn more about
canandcannotfunctions' parameters, read AbilityBuilder API
For more advanced cases, it's possible to use rules property of AbilityBuilder and create MongoAbility instance manually:
import { AbilityBuilder, createMongoAbility } from '@casl/ability'
export default function defineAbilityFor(user) {
const { can, cannot, rules } = new AbilityBuilder(createMongoAbility);
// defined permissions
return createMongoAbility(rules);
}
When to use AbilityBuilder
- in apps which have static permissions (i.e., permissions are not changed by admin user but defined inside system)
- anywhere where you use custom ability factory functions
See Customize Ability to learn more about
Abilityclass.
JSON objects
It's not required to use AbilityBuilder to define rules in the app, especially if your rules are dynamic (i.e., stored in database or managed by admin users). In such cases, the preferred way is to use JSON objects. You can directly pass array of JSON rules into Ability constructor. Such rules are called raw rules
you can read more about TypeScript types and their shapes in TypeScript Support
JSON objects example
The same example using JSON:
import { createMongoAbility } from '@casl/ability';
export default createMongoAbility([
{
action: 'read',
subject: 'Post'
},
{
inverted: true,
action: 'delete',
subject: 'Post',
conditions: { published: true }
}
])
Pay attention to the inverted field, it indicates that a rule is an inverted one (i.e., forbids something).
The shape of raw rule
The simplified version (without generics) of raw rule shape in TypeScript looks like this:
interface RawRule {
action: string | string[]
subject?: string | string[]
/** an array of fields to which user has (or not) access */
fields?: string[]
/** an object of conditions which restricts the rule scope */
conditions?: any
/** indicates whether rule allows or forbids something */
inverted?: boolean
/** message which explains why rule is forbidden */
reason?: string
}
Don't worry if you are not familiar with TypeScript, you still have time to learn it ;) Kidding, or not?
In the example above, ? after field name means optional field, so everything is optional except action. string[] means an array of strings and string | string[] means either regular string or an array of strings. Now you know almost everything about TypeScript, do not thank :)
When to use JSON objects
- in apps, that have dynamic permissions (i.e., permissions are changed by admin user)
- in apps, that receive permissions via network layer (e.g., single page applications or microservices)
- to make the app's bundle size smaller (if you don't use
AbilityBuilderordefineAbility, they can be shook out by bundlers such as rollup or webpack)
Now, as we know all possible ways to define rules, let's dive deeper into can and cannot methods.
Rules
You can define as much rules as you need, CASL builds an index under the hood to keep checking logic fast. So, don't worry about performance.
You can define the same pair of action and subject with different conditions multiple times. For example:
import { defineAbility } from '@casl/ability';
export default defineAbility((can) => {
can('read', 'Article', { published: true });
can('read', 'Article', { published: false, status: 'review' });
});
In such case, the pair of action/subject rules are combined by logical OR. More formally this can be translated as "users can read Article if it's published OR users can read Article if it's not published AND in review status".
But be careful, because OR logic returns true if one of its expressions are true, so the next code will always return true, even for articles that were not published:
import { defineAbility } from '@casl/ability';
export default defineAbility((can) => {
can('read', 'Article');
can('read', 'Article', { published: true }); // (2)
});
More formally this code can be read as "can read any article OR can read published articles". As you see, the first statement always valid, so the (2) can with conditions has no effect.
It's also possible to restrict scope of direct rule with the inverted one for the same pair of action and subject. In such case, inverted rules are combined with direct ones by logical AND.
Let's change the example above to disallow reading unpublished articles:
import { defineAbility } from '@casl/ability';
export default defineAbility((can, cannot) => {
can('read', 'Article'); // direct rule
cannot('read', 'Article', { published: false }); // inverted rule
});
This is read as "can read any article AND cannot read unpublished articles".
Inverted rules order
It is important that in the example above cannot read article unpublished line comes after the can read article line. If they were reversed, cannot read article unpublished would be overridden by can read article.
The rule of thumb is to define general rules first and more specific after general ones.
It was done so in order to be able to override inverted rules by regular ones.
Best practice
Direct logic is easier to reason about for human mind, so use direct rules as much as possible! Moreover, this allows to keep permissions clean, more readable and reduces the risk of giving wrong permissions to the wrong users.
You can remember this rule as: Give permissions, don't take them away
So, are there a valid usecases for inverted rules? Yes! They work very well when are not combined with regular rules and defined explicitly to express why particular action on particular subject was forbidden, for example:
import { AbilityBuilder, createMongoAbility } from '@casl/ability';
async function defineAbility(user) {
const hasPaidSubscription = await user.hasPaidSubscription();
const { can, cannot, build } = new AbilityBuilder(createMongoAbility);
if (hasPaidSubscription) {
can('create', 'BlogPost');
} else {
cannot('create', 'BlogPost').because('You have not paid for monthly subscription');
}
return build()
}