Customize Ability
CASL was built with extensibility in mind and this allows you to extend conditions with custom operators, provide custom field matchers and even use your own implementation to match conditions (e.g., using functions or json-schema)! Let's see how
Extend conditions with custom operators
Thanks to ucast, it's possible to define conditions in CASL using MongoDB query language. Usually this is enough but sometimes you may want to restrict possible operators, add non-standard operator or one of those that is not included in CASL by default (e.g., $nor).
Let's see an example of how to add $nor operator. To do this, we will use buildMongoQueryMatcher helper function from @casl/ability package. It allows to add or override existing operators:
import {
createMongoAbility,
AbilityBuilder,
Abilities,
buildMongoQueryMatcher,
} from '@casl/ability';
import { $nor, nor } from '@ucast/mongo2js';
const conditionsMatcher = buildMongoQueryMatcher({ $nor }, { nor });
export default function defineAbilityFor(user: any) {
const { can, build } = new AbilityBuilder(createMongoAbility);
can('read', 'Article', {
$nor: [{ private: true }, { authorId: user.id }]
});
return build({ conditionsMatcher });
}
We use
user: anyfor the purpose of ease, you should avoid this in real apps
buildMongoQueryMatcher extends existing set of operators, so if you want to restrict available operators, you should not use it. For example, let's allow to use only $eq and $in operators:
import {
createMongoAbility,
MongoAbility,
AbilityBuilder,
Abilities,
MongoQueryFieldOperators,
ConditionsMatcher,
} from '@casl/ability';
import { $in, within, $eq, eq, createFactory, BuildMongoQuery } from '@ucast/mongo2js';
type RestrictedMongoQuery<T> = BuildMongoQuery<T, Pick<MongoQueryFieldOperators, '$eq' | '$in'>>;
const conditionsMatcher: ConditionsMatcher<RestrictedMongoQuery> = createFactory({ $in, $eq }, { in: within, eq });
type AppAbility = MongoAbility<Abilities, RestrictedMongoQuery>;
export default function defineAbilityFor(user: any) {
const { can, build } = new AbilityBuilder(createMongoAbility);
can('read', 'Article', { authorId: user.id } });
can('read', 'Article', { status: { $in: ['draft', 'published'] } });
return build({ conditionsMatcher });
}
Read @ucast/mongo2js docs page to learn how to create custom operators.
By restricting operators, you not only disallow other developers to use more complex conditions but also make your frontend bundle size smaller (thanks to bundlers with tree-shaking support).
Custom conditions matcher implementation
If you want to implement custom conditions matcher, you should use Ability class instead of createMongoAbility factory function. createMongoAbility is a factory function that creates an Ability instance with default values for conditionsMatcher and fieldMatcher options (i.e, mongo conditions matcher and field pattern matcher).
Conditions matcher is a factory function that accepts rule.conditions and returns a function that accepts an object and returns boolean. All these restrictions on conditions matcher is enforced by ConditionsMatcher generic type which you can import from @casl/ability.
Let's implement the matcher that allows to use a function as conditions matcher:
import {
Ability,
AbilityBuilder,
type AbilityTuple,
type MatchConditions,
} from '@casl/ability';
type AppAbility = Ability<AbilityTuple, MatchConditions>;
const lambdaMatcher = (matchConditions: MatchConditions) => matchConditions;
export default function defineAbilityFor(user: any): AppAbility {
const { can, build } = new AbilityBuilder<AppAbility>(Ability);
can('read', 'Article', ({ authorId }) => authorId === user.id);
can('read', 'Article', ({ status }) => ['draft', 'published'].includes(status));
return build({ conditionsMatcher: lambdaMatcher });
}
Conditions matchers can be only synchronous!
We don't recommend to use functions for matching logic if you need to serialize rules or convert them to a database query. The default matcher should cover majority of cases.
Custom field matcher
Field matcher is responsible for matching fields passed as 3rd argument to can method of Ability instance. It is a factory function that returns a function which accepts field and returns boolean. This logic is enforced by FieldMatcher type from @casl/ability.
We cannot imagine a reasonable case to override field matching logic, default implementation should be more than enough.
You can use this to reduce your bundle size or enforce simpler logic. For example, let's implement simple field matcher that doesn't support field patterns:
import { createMongoAbility, AbilityBuilder, FieldMatcher } from '@casl/ability';
export const fieldMatcher: FieldMatcher = fields => field => fields.includes(field);
export default function defineAbilityFor(user: any) {
const { can, build } = new AbilityBuilder(createMongoAbility);
can('read', 'Article', ['title', 'content']);
return build({ fieldMatcher });
}