Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
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.
- Host: GitHub
- URL: https://github.com/char2sgu/nest-access-policy
- Owner: Char2sGu
- License: apache-2.0
- Created: 2021-07-11T04:26:45.000Z (over 3 years ago)
- Default Branch: develop
- Last Pushed: 2021-09-24T15:50:20.000Z (over 3 years ago)
- Last Synced: 2024-11-11T13:32:04.258Z (about 1 month ago)
- Topics: access-control, api, crud, declarative, nestjs, permissions, rest, restful, typescript, web
- Language: TypeScript
- Homepage:
- Size: 125 KB
- Stars: 1
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
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.