nest-access-policy
TypeScript icon, indicating that this package has built-in type declarations

0.1.0 • Public • Published

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 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:

// 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 the AuthGuard is before the AccessPolicyGuard. See the request lifecycle for more help.

Package Sidebar

Install

npm i nest-access-policy

Weekly Downloads

0

Version

0.1.0

License

Apache-2.0

Unpacked Size

40.3 kB

Total Files

58

Last publish

Collaborators

  • char2s