Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/char2sgu/nest-access-policy

Declarative and centralized access control.
https://github.com/char2sgu/nest-access-policy

access-control api crud declarative nestjs permissions rest restful typescript web

Last synced: about 1 month ago
JSON representation

Declarative and centralized access control.

Awesome Lists containing this project

README

        

# Nest Access Policy

Declarative and centralized access control.

> Inspired by [drf-access-policy](https://github.com/rsinger86/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.

```ts
// 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:

```ts
// 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 as `BaseAccessPolicy` 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:

```ts
// 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`:

```ts
// books.module.ts

@Module({
// ...
imports: [
// ...
AccessPolicyModule,
// ...
],
providers: [
// ...
BooksAccessPolicy,
// ...
],
// ...
})
export class BooksModule {}
```

Finally, of course, use the guard and apply the policy:

```ts
// books.controller.ts

@UseAccessPolicies(BooksAccessPolicy)
@UseGuards(AccessPolicyGuard)
@Controller()
export class BooksController {}
```

> To access `req.user`, you should ensure the `AuthGuard` is before the `AccessPolicyGuard`. See the [request lifecycle](https://docs.nestjs.com/faq/request-lifecycle) for more help.