Important note: This documentation is generated from integration tests, so the examples execute and are tested against.

DO NOT EDIT THIS .md FILE - Its generated from ts-node document-protected-simple.controller.md.e2e-spec.ts

NestJS with SuperAwesome Permissions - Simple Example

Simple Documents Protected Controller Example

This is a trivial NestJS example, based on the same schema & PermissionDefinitions & data of the SuperAwesome Permissions example (file @superawesome/permissions/dist/__tests__/data.fixtures which you shouldn't ever have to import).

Notes:

  • These docs are generated by e2e-tests, the examples are actual e2e tests!

  • All example code is in /example.

Business Rules

Our business rules are exactly the same as permissions examples (they are imported from it):

As an EMPLOYEE, I can create, read & list only my OWN Documents (created by me) , all attributes except confidential. Also, I can list all Documents on the system, but only access the title & date attributes.

As a EMPLOYEE_MANAGER, I can read, list, review & delete all Documents created by any User that I am managing, all document attributes except confidential. Also, I can list all Documents on the system, but only access the title, date & status attributes.

As a COMPANY_ADMIN, I can read, update and review all Documents created by any User in my Company, all attributes.

Now, how super awesome would it be if only...

...we could fully protect our NestJS apps with the full effect of the above rules, just using a couple of decorators and declarative lines of code?

With SuperAwesome Permissions for NestJs that's exactly what we can do!

PermissionsDefinitions

The Business Rules give rise to PermissionsDefinitions, lets have them here reference.

Note: They have an important PermissionsDefinitions difference from the SuperAwesome Permissions example: the ownership hooks are replaced with a string (cause its impossible to inject on a guard/decorator on nestjs.

This small "glitch" will be solved in a future release (if you can help resolve this, please do!).

But this "glitch" highlights the way SuperAwesome Permissions could work in different languages, using JSON as the Lingua Franca for PermissionDefinitions: the string names correspond to method names of a Service, which we'll see shortly.

const documentPermissionDefinitions = [
  {
    roles: ['EMPLOYEE'],
    resource: 'document',
    descr:
      '> As an **EMPLOYEE**, I can **create**, **read** & **list** only my **OWN Documents (created by me)** , all attributes except **confidential**. Also, I can **list** all **Documents** on the system, but only access the **title** & **date** attributes.',
    possession: 'own',
    grant: {
      create: ['*', '!confidential'],
      read: ['*', '!confidential'],
      list: ['*', '!confidential'],
      'list:any': ['title', 'date'],
    },
    isOwner: 'isOwner_isUserCreatorOfDocument',
    limitOwned: 'limitOwned_listUserCreatedDocuments',
  },
  {
    roles: ['EMPLOYEE_MANAGER'],
    resource: 'document',
    descr:
      '> As a **EMPLOYEE_MANAGER**, I can **read**, **list**, **review** & **delete** all **Documents** created by **any User that I am managing**, all document attributes except **confidential**. Also, I can **list** all **Documents** on the system, but only access the **title**, **date** & **status** attributes.',
    possession: 'own',
    grant: {
      read: ['*', '!confidential', '!personal'],
      review: ['*', '!confidential', '!personal'],
      delete: ['*', '!confidential', '!personal'],
      list: ['*', '!confidential', '!personal'],
      'list:any': ['title', 'date', 'status'],
    },
    isOwner: 'isOwner_isDocCreatedByMeAndMyManagedUsers',
    limitOwned: 'limitOwned_DocsOfMeAndMyManagedUsers',
  },
  {
    roles: ['COMPANY_ADMIN'],
    resource: 'document',
    descr:
      '> As a **COMPANY_ADMIN**, I can **read**, **update** and **review** all **Documents** created by **any User in my Company**, all attributes.',
    possession: 'own',
    grant: ['read', 'update', 'review'],
    isOwner: 'isOwner_isDocCreatedByMeAndMyCompanyUsers',
    limitOwned: 'limitOwned_DocsOfMeAndMyCompanyUsers',
  },
];

The Unprotected Example (tests/document-unprotected.controller.ts)

Let's consider the simplest "naked" example, of an unprotected Controller for documents, without any permissions:

// file: ../document-unprotected.controller.ts
import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common';
import {
  ALL_DOCUMENTS,
  IDocument,
} from '@superawesome/permissions/dist/__tests__/data.fixtures';

@Controller('/documents')
export class DocumentUnprotectedController {
  @Get('/:id')
  async read(
    @Param('id', new ParseIntPipe()) id: number
  ): Promise<IDocument> {
    return ALL_DOCUMENTS.find((doc) => doc.id === id);
  }

  @Get()
  async list(): Promise<IDocument[]> {
    return ALL_DOCUMENTS;
  }
}

How quickly can we transform this code to be "protected"? Let see it before we delve into tests.

Simplest Protected Example

Time to reveal the most interesting part: the code that implements all of the above, has minimal impact: 5 tiny lines of declarative code! It is deceivingly simple:

// file: ../simple/document-protected-simple.controller.ts
@UseGuards(createPermissionsGuard({ resource: 'document' }, documentPermissionDefinitions))
@Controller('/documents-protected-simple')
export class DocumentProtectedSimpleController {
  @Get('/:id')
  async read(
    @Param('id', new ParseIntPipe()) id: number,
    @GetPermit() permit: Permit,
  ): Promise<Partial<IDocument>> {
    return await permit.pick(ALL_DOCUMENTS.find(doc => doc.id === id));
  }

  @Get()
  async list(@GetPermit() permit: Permit): Promise<Partial<IDocument>[]> {
    return await permit.filterPick(ALL_DOCUMENTS);
  }
}

We see that with just 5 simple LoCs we touched (2, 8, 10, 14 & 15), we have in effect the full blown permissions of the above definitions. And best of all, all business rules updates (i.e expressed as PermissionDefinition) will need zero code changes.

But there is no magic! There's just a lot going behind the scenes, with such little code, so lets dive in.

  • L2 creates our Guard, declaring only:

    • for which resource we query about & protect (i.e document in this case) as default, it can change per endpoint.

    • any relevant PermissionDefinitions we want to provide here. Note that we could add them in other places like the module & other controllers - they all come in effect equally at runtime.

  • L6 the read method name becomes the name of the action (by default, can be overridden).

  • L8 we inject the Permit instance in our method (its created by the Guard internally via .grantPermit()). It holds all the information we'll need for the authorization & permissions part of our app, including user, allowed attributes, ownership checks and pick / filter utils.

  • before reaching L10 permissions-nestjs already knows from L5 the a special id param on our endpoint (id as default, it can change - jump to reference & detailed example to see how). The library executes isOwn(id) behind the scenes, and if doc isnt owned by user (and user doesnt have permit.anyGranted) it returns 403 Forbidden before even reaching the method.

  • L10 simply permit.pick only the allowed read attributes from a document, depending on the User and their ownership of the resource. We dont need to check if it isOwn cause of the guard doing it on id param for us!

  • L14 & L15 for the list method we inject Permit so we can call permit.filterPick() on this simple implementation. Each each user can now list all but only the resources & their attributes that they are entitled to. There are ways of adapting & scaling such "many own items code" arbitrarily (eg if we were dealing with a DB) with little code - see permit.limitOwn() and check document-protected-detailed.controller example below.

The Specs

Note: These are actual tests against the protected controllers!

Example calls

Lets now call our endpoints, with different users, and see what we'll get.

Action "read"

First lets try "read" specific documents (OWN and NON-OWN), with different users (hint: no user has "read:any")

A user with EMPLOYEE

It returns only allowed attributes on OWN document (i.e all except confidential)

user = { id: 1, roles: ['EMPLOYEE'] };
// => GET http:///documents-protected-simple/10
({ id: 10, title: 'Document Title 10', date: '2020-02-010', someRandomField: 'Some random value 10' });

But on a NON-OWN document it forbids

// => GET http:///documents-protected-simple/1000
'403 - FORBIDDEN';

A user with COMPANY_ADMIN

It returns all attributes on OWN document

user = { id: 2, roles: ['COMPANY_ADMIN'] };
// => GET http:///documents-protected-simple/20
({
  id: 20,
  title: 'Document Title 20',
  date: '2020-02-020',
  someRandomField: 'Some random value 20',
  confidential: 'Confidential 20',
});

But on a NON-OWN document it forbids

// => GET http:///documents-protected-simple/2000
'403 - FORBIDDEN';

Action "list"

Action "list" will give us ALL the documents a user is allowed to browse, with only the allowed attributes, depending on ownership of each item.

user = { id: 1, roles: ['EMPLOYEE'] };
// => GET http:///documents-protected-simple
// showing only the first 5 docs for brevity
[
  { id: 1, title: 'Document Title 1', date: '2020-02-01', someRandomField: 'Some random value 1' },
  { id: 10, title: 'Document Title 10', date: '2020-02-010', someRandomField: 'Some random value 10' },
  { id: 100, title: 'Document Title 100', date: '2020-02-0100', someRandomField: 'Some random value 100' },
  { title: 'Document Title 2', date: '2020-02-02' },
  { title: 'Document Title 20', date: '2020-02-020' },
];

Forbids if user doesnt have "list" action granted.

user = { id: 4, roles: ['COMPANY_ADMIN'] };
// => GET http:///documents-protected-simple
'403 - FORBIDDEN';

Setting up the module

A no-comments module setting up follows.

// file: ../simple/example-simple.module.ts
// omitted imports
@Module({
  imports: [
    PermissionsModule.forRoot({
      extractUserFromRequest: async (req) => getUser(),

      limitOwnReduce: ({ user, limitOwneds }) =>
        _.overSome(limitOwneds.map((limitOwned) => limitOwned({ user }))),
      projectResourceId: (resourceIdStr: string | undefined) =>
        Number(resourceIdStr),
    }),
  ],
  controllers: [
    DocumentUnprotectedController,
    DocumentProtectedSimpleController,
  ],
  providers: [
    {
      provide: PERMISSIONS_OWNERSHIP_SERVICE_TOKEN,
      useClass: PermissionsOwnershipService,
    },
  ],
})
export class ExampleSimpleModule {}

Next steps

That's it, you've been initiated!

Now continue to the detailed example which also serves as a reference.

result-matching ""

    No results matching ""