Custom Policies
The AccessPolicy decorator supports more complex policies custom made for your specific use cases.
A policy is any function with the following signature:
type PolicyMethod = (ctx: ExecutionContext, authData: any, moduleRef: ModuleRef) => void;A short summary of what the parameters mean:
ctx: ExecutionContext: useful when therequestobject is needed for your business logic. See NestJS execution context for more.authData: any: the user's metadata generated by your authentication middleware. Commonly used for accessing user/session information. Retrieved using authDataKey.moduleRef: ModuleRef: commonly used when accessing injected dependencies such asPrismaServiceinside your policy functions. See NestJS module reference.
Recipes#
tip
Examples below only seek to illustrate the custom policies capabilities.
The features themselves would likely be implemented differently in practice!!
JohnnyIsBanned Policy#
This is an example of a simple static policy that throws 403 errors for the user named Johnny.
Implementation#
import { ExecutionContext, ForbiddenException } from '@nestjs/common';import { ModuleRef } from '@nestjs/core';
export const JohnnyIsBanned = (ctx: ExecutionContext, authData: any, moduleRef: ModuleRef) => { if (authData.user.name === 'Johnny') { throw new ForbiddenException('Bye Johnny!'); }};Usage#
@Controller('party')export class PartyController { // ... @Get() @AccessPolicy('everyone', JohnnyIsBanned) async getParties(@Query('crudQuery') crudQuery: string) { const match = await this.partyService.findMany(crudQuery); return match; }}BannedPeople Policy#
This takes the same idea of the previous example, but now we have a function that receives a list of names to ban, and returns a PolicyMethod.
Implementation#
import { ExecutionContext, ForbiddenException } from '@nestjs/common';import { ModuleRef } from '@nestjs/core';
export const BannedPeople = (bannedNames: string[]) => ( ctx: ExecutionContext, authData: any, moduleRef: ModuleRef,) => { if (bannedNames.includes(authData.user.name)) { throw new ForbiddenException('You are not alone Johnny!'); }};Usage#
@Controller('party')export class PartyController { // ... @Get() @AccessPolicy('everyone', BannedPeople(['Johnny', 'Danny'])) async getParties(@Query('crudQuery') crudQuery: string) { const match = await this.partyService.findMany(crudQuery); return match; }}DbBannedPeople Policy#
This example takes the same idea of the previous, but queries the database instead of using a hard coded list.
Implementation#
import { ExecutionContext, ForbiddenException } from '@nestjs/common';import { ModuleRef } from '@nestjs/core';
export const DbBannedPeople = (ctx: ExecutionContext, authData: any, moduleRef: ModuleRef) => { const prismaService = moduleRef.get(PrismaService, { strict: false }); const bannedPeople = prismaService.bannedPeople.findMany();
if (bannedPeople.includes(authData.user.name)) { throw new ForbiddenException('You have been banned!'); }};Usage#
@Controller('party')export class PartyController { // ... @Get() @AccessPolicy('everyone', DbBannedPeople) async getParties(@Query('crudQuery') crudQuery: string) { const match = await this.partyService.findMany(crudQuery); return match; }}HideDrafts Policy (extending crudQuery)#
In this section we are going to write a policy that modifies the final prisma query, by extending the client's provided crudQuery and adding additional constraints to it.
This technique is extremely useful when a user must be granted access to only a subset of records from a given table. It ensures that we scope the client's access while not breaking the requested sorting/filtering/pagination.
Below is a policy that only retrieves non-draft parties from the database. The highlighted additionalConstraints can be any prisma filter object .
Implementation#
import { ExecutionContext } from '@nestjs/common';import { ModuleRef } from '@nestjs/core';import { CrudQuery } from 'nestjs-prisma-crud';
export const HideDrafts = (ctx: ExecutionContext, _authData: any, _moduleRef: ModuleRef) => { // get crudQuery from the request's query params const request = ctx.switchToHttp().getRequest(); const query = request.query; const crudQuery: string = query.crudQuery;
// get client provided `.where`, if any const parsedCrudQuery: CrudQuery = crudQuery ? JSON.parse(crudQuery) : {}; const clientWhere = parsedCrudQuery.where || {};
// additionalConstraints is any valid prisma filter object const additionalConstraints = { state: { not: 'DRAFT' } };
// override client `.where`, adding the additional `AND` clause parsedCrudQuery.where = { AND: [additionalConstraints, clientWhere], };
request.query.crudQuery = JSON.stringify(parsedCrudQuery);};Usage#
@Controller('party')export class PartyController { // ... @Get() @AccessPolicy('everyone', HideDrafts) async getParties(@Query('crudQuery') crudQuery: string) { const match = await this.partyService.findMany(crudQuery); return match; }}