Nest Access Policy
Declarative and centralized access control.
Inspired by drf-access-policy.
Tutorial
Creating the Access Policy
An AccessPolicy
is a special provider where we define our access statements.
A common usage is to make access policies and controllers have one-to-one relationships.
// books.access-policy.ts
@Injetable()
export class BooksAccessPolicy implements AccessPolicy {
statements: AccessPolicyStatement[] = [
{
actions: ["list", "create", "retrieve", "update", "destroy"],
effect: Effect.Allow,
},
];
}
Each method of the controller is considered as an action. The actions
defines the names of the actions controlled by the statement.
There are two kinds of statements: allow statements with effect
set to Effect.Allow
and forbid statements with effect
set to Effect.Forbid
. An action is allowed only when all allow statements are passed (at least one) and no forbid statements are passed.
Therefore, it is recommended to allow all the actions in the first statement as above and manage the permissions in next statements.
The
AccessPolicy
,AccessPolicyStatement
,AccessPolicyCondition
types takes two optional type args, the first is the actions' type while the second is the request's type.
Now let's define the really important parts:
// books.access-policy.ts
@Injetable()
export class BooksAccessPolicy implements AccessPolicy {
private isOwn: AccessPolicyCondition = async ({ req }) =>
(await this.getBook(req)).owner == req.user;
private notOwn: AccessPolicyCondition = async (ctx) =>
!(await this.isOwn(ctx));
private isPublic: AccessPolicyCondition = async ({ req }) =>
(await this.getBook(req)).isPublic;
private notImmutable: AccessPolicyCondition = async ({ req }) =>
!(await this.getBook(req)).isImmutable;
statements: AccessPolicyStatement[] = [
{
actions: ["list", "create", "retrieve", "update", "destroy"],
effect: Effect.Allow,
},
{
actions: ["retrieve"],
effect: Effect.Forbid,
conditions: [this.notOwn],
},
{
actions: ["update", "destroy"],
effect: Effect.Allow,
conditions: [this.notImmutable, [this.isOwn, this.isPublic]],
},
];
@Inject()
private booksService: BooksService;
private async getBook(req: Request) {
if (!req.entity) {
const id = Number(req.params.id);
const book = await this.booksService.retrieve(id);
if (!book) throw new NotFoundException();
req.entity = book;
}
return req.entity;
}
}
All the elements in conditions
are considered a condition group, which can be either a single condition or a list of conditions.
The logical relationship between the condition groups is and, and the one between the member conditions of each condition groups is or.
Since an
AccessPolicy
is a class, we can define the common conditions in a public policy such asBaseAccessPolicy
and inherit the common conditions from it.
If you prefer to define the conditions after the statements, you can make the statements a
getter
.
Although the statements above work well, they lack of error messages. Actually they are also organized improperly: The failure of a statement should be able to be described as a single reason.
Let's fix this:
// books.access-policy.ts
@Injetable()
export class BooksAccessPolicy implements AccessPolicy {
// ...
statements: AccessPolicyStatement[] = [
// ...
{
actions: ["retrieve", "update", "destroy"],
effect: Effect.Allow,
conditions: [[this.isOwn, this.isPublic]],
reason: "Only your own books or public books can be managed",
},
{
actions: ["update", "destroy"],
effect: Effect.Allow,
conditions: [this.notImmutable],
reason: "The immutable books cannot be managed",
},
];
// ...
}
When checking the statements, if a statement causes the request to be denied, such as a failed allow statement or a passed forbid statement, an ForbiddenException
will be thrown with the reason of the statement as its message.
Applying the Access Policy
Now we've done with the AccessPolicy
, it only takes a few steps to apply it.
Since the AccessPolicy
is injectable, it should be added to the providers
list of the module. Whatsmore, we will use a AccessPolicyGuard
to protect our routes, who injects AccessPolicyService
to check the statements, so we also need to import the AccessPolicyModule
:
// books.module.ts
@Module({
// ...
imports: [
// ...
AccessPolicyModule,
// ...
],
providers: [
// ...
BooksAccessPolicy,
// ...
],
// ...
})
export class BooksModule {}
Finally, of course, use the guard and apply the policy:
// books.controller.ts
@UseAccessPolicies(BooksAccessPolicy)
@UseGuards(AccessPolicyGuard)
@Controller()
export class BooksController {}
To access
req.user
, you should ensure theAuthGuard
is before theAccessPolicyGuard
. See the request lifecycle for more help.