UNPKG

@superawesome/permissions-nestjs

Version:

NestJS Guard & Decorators for @superawesome/permissions, promoting orthogonal fine-grained API access control to resources.

422 lines (332 loc) 18.1 kB
<!doctype html> <html class="no-js" lang=""> <head> <meta charset="utf-8"> <meta http-equiv="x-ua-compatible" content="ie=edge"> <title>SuperAwesome Permissions for NestJs (@superawesome/permissions-nestjs)</title> <meta name="description" content=""> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" type="image/x-icon" href="../images/favicon.ico"> <link rel="stylesheet" href="../styles/style.css"> <link rel="stylesheet" href="../styles/postmark.css"> </head> <body> <div class="navbar navbar-default navbar-fixed-top visible-xs"> <a href="../" class="navbar-brand">SuperAwesome Permissions for NestJs (@superawesome/permissions-nestjs)</a> <button type="button" class="btn btn-default btn-menu ion-ios-menu" id="btn-menu"></button> </div> <div class="xs-menu menu" id="mobile-menu"> <div id="book-search-input" role="search"><input type="text" placeholder="Type to search"></div> <compodoc-menu></compodoc-menu> </div> <div class="container-fluid main"> <div class="row main"> <div class="hidden-xs menu"> <compodoc-menu mode="normal"></compodoc-menu> </div> <!-- START CONTENT --> <div class="content additional-page"> <div class="content-data"> <p><strong>Important note</strong>: This documentation is generated from integration tests, so the examples execute and are tested against.</p> <p><strong>DO NOT EDIT THIS .md FILE - Its generated from <code>ts-node document-protected-detailed.controller.md.e2e-spec.ts</code></strong></p> <h1 id="nestjs-with-superawesome-permissions---reference--detailed-example">NestJS with SuperAwesome Permissions - Reference &amp; Detailed example</h1> <h2 id="detailed-documents-protected-controller-example">Detailed Documents Protected Controller Example</h2> <p>Lets now see a slightly more complicated example, based on the <a href="/additional-documentation/how-to-use-simple-example.html">previous simple example</a>.</p> <p>This example is also acting as a reference, using inline comments. Hence, it has to list every single parameter and option, which most times you will not need to touch.</p> <div><pre class="line-numbers"><code class="language-js">// file: ../detailed/document-protected-detailed.controller.ts // omitted imports /** The `createPermissionsGuard()` is essential, if we want to protect our Controller with SuperAwesome Permissions. It creates an instance of a Permissions NestJS Guard, configured for ALL the endpoints/methods in this controller. We then pass this instance to nestjs &#64;UseGuards. */ &#64;UseGuards( createPermissionsGuard( { /** The default `resource` (eg &quot;document&quot;) this Guard will use for `permissions.grantPermit({resource, ...})` (Optional). __Notes__ : - Can be overridden per method with `&#64;PermitGrant()`. - You&#39;re advised to have a default `resource` here, at the controller level. */ resource: &#39;document&#39;, /** Project (i.e map) a `resourceId` on your QueryParams (by default the param named &quot;id&quot;), from string to what is needed by the ownership hooks (eg a `number`) for this controller (Optional). __Notes__ : - It should be able to tolerate if `resourceId` is missing or null etc and not throw. - We can also have `projectResourceId` as default at module level at `PermissionsModule.forRoot()`, i.e working for the whole module/app. Here we can override for this Guard only, if that&#39;s needed (rare). - We can also override all at `&#64;PermitGrant()` level only for that endpoint (even more rare). */ projectResourceId: (id) =&gt; parseInt(id, 10), }, /** Add any relevant PermissionDefinitions as 2nd argument (Optional). __Notes__: - If we &#39;ve already added the specific `documentPermissionDefinitions` from &#39;./document.permissions&#39; somewhere (either in our module or another controller) we dont need to add them again (we&#39;ll actually get a warning of &quot;redefining action in a PD with same attributes&quot; if we do). __Notes__: - The PDs are of type `IPermissionDefinitionStringOwnHooks`, see below. */ documentPermissionDefinitions, /** Add any relevant PermissionDefinitionDefaults as 3rd argument, used as defaults only for the PermissionDefinitions at the 2nd argument (Optional). */ { resource: &#39;document&#39; } ) ) &#64;Controller(&#39;/documents-protected-detailed&#39;) export class DocumentProtectedDetailedController { constructor( /** Optionally, we can inject the SAPermission instance that PermissionsModule has build for us, so we can manually permit / deny access to resources or do any other things with it - see `securityHole` method below. */ &#64;InjectPermissions() private permissions: Permissions ) {} /** The &#64;PermitGrant method decorator is optional, it allows us to configure an endpoint. All its arguments are also optional. If omitted, the default &#64;PermitGrant is in place, as all methods using the `&#64;UseGuards(createPermissionsGuard(...))` are protected by default. Pass `&#64;PermitGrant(false)` to disable this - see below. */ &#64;PermitGrant({ /** If the name of the method is different than the action name, we can override it here (Optional). Ideally we should not, its great if they are consistent. */ action: &#39;read&#39;, /** If the &#64;Get param for our `resourceId` is different than the default &quot;id&quot;, we can override it with `resourceIdKey` (Optional). Ideally we should always go with &quot;id&quot; . */ resourceIdKey: &#39;documentId&#39;, /** If want to override the &quot;resource&quot; this endpoint is dealing with (i.e the one configured above at the `createPermissionsGuard`), we can override it here (Optional). */ resource: &#39;document&#39;, /** If want to override the projection function this endpoint is using (i.e the one configured above at the `createPermissionsGuard`), we can override it here (Optional). */ projectResourceId: Number, }) &#64;Get(&#39;/:documentId&#39;) async single( &#64;Param(&#39;documentId&#39;, new ParseIntPipe()) id: number, /** The &#64;GetPermit parameter decorator injects this method&#39;s Permit object into our method (Optional). Useful for resource filtering, attribute picking etc. __Notes__: - It is already configured with the current user, action, resource &amp; optionally resourceId if it exists. - If `resourceId` exists, then the Guard checks the User&#39;s ownership of the specific resource item and throws a 403 (FORBIDDEN) before even hitting the method body. */ &#64;GetPermit() permit: Permit ): Promise&lt;Partial&lt;IDocument&gt;&gt; { return await permit.pick(ALL_DOCUMENTS.find((doc) =&gt; doc.id === id)); } /** An empty `&#64;PermitGrant()` can be omitted, if we don&#39;t override anything from its args (i.e `PermitGrantArgs`). In this case &quot;action&quot; equals the method name &quot;list&quot;, so its useless. __Notes__: - leaving without &#64;PermitGrant() at all means &quot;use default &#64;PermitGrant()&quot;, like in this example. - The default `&#64;PermitGrant()` has all the information it needs: - `user` from `extractUserFromRequest`, configured at the module. - `action` by default is the method&#39;s name. - `resource` by default from `createPermissionsGuard` 1st argument `GuardOptions.resource` - `resourceId` by default reading the request&#39;s prop named &#39;id&#39;. */ &#64;PermitGrant() &#64;Get() async list( &#64;GetPermit() permit: Permit, &#64;Query(&#39;any&#39;) any?: string ): Promise&lt;Partial&lt;IDocument&gt;[]&gt; { /** Inside our method, we decide based on our rules **if we can allow any resource** OR if we need to filter only user&#39;s own. __Notes:__ - In this example API call, if it has any=&#39;true&#39; in its query params AND the user has `permit.anyGranted` for this action, then we allow all resources to pass, otherwise we limit only to their own. We could have thrown Forbidden if `permit.anyGranted` is false, or have any other kind of behavior, using `Permit` as our permissions guide. - In a realistic app, this is how you can restrict your DB or API etc results. The `permit.limitOwn()` method gives you infinite scalability, since its architecture is agnostic &amp; hence compatible with any kind of data layer. - In this simplistic example our `permit.limitOwn()` produces just an Array.filter function. If we wanted to add restrictive clauses to a WHERE SQL query, it would be as easy as: `query.andWhere(new Brackets(qb =&gt; permit.limitOwn(qb)));` using TypeORM - read more in [`Permit.limitOwn()` docs](https://permissions.docs.superawesome.com/classes/Permit.html#limitOwn). - In a realistic app, most of this logic would live inside the Service layer, to which you just pass the `permit` object around for the duration of the request, for the actual advanced permissions checks and query building to be taking place. */ const allowedDocs = permit.anyGranted &amp;&amp; any === &#39;true&#39; ? ALL_DOCUMENTS : ALL_DOCUMENTS.filter(permit.limitOwn()); /** pick allowed attributes, depending on ownership of each resource item. */ return await permit.mapPick(allowedDocs); } /** If we want to completely disable the &#64;PermitGrant check for an endpoint/method and bypass the Guard, then we **must** pass `false` to `&#64;PermitGrant()`. */ &#64;PermitGrant(false) &#64;Post(&#39;/security-hole&#39;) securityHole() { /** Anyone can access this method bypassing the Guard, even requests without a user. But we have the injected permissions instance to the rescue! We can use permissions.grantPermit() the usual way, manually passing our user, resource, action etc, and get back a Permit object and then manually permit or deny actions. Or we can just introspect our permissions instance, for example: */ return [ &#39;These are all actions, roles, resources and PermissionDefinitions of this Permissions instance.&#39;, this.permissions.getActions(), this.permissions.getRoles(), this.permissions.getResources(), this.permissions.getDefinitions({ resource: &#39;document&#39; }), ]; } } </code></pre></div><h1 id="setting-up-the-nestjs-module">Setting up the NestJS module</h1> <p>The NestJS module is very simple.</p> <div><pre class="line-numbers"><code class="language-js">// file: ../detailed/example-detailed.module.ts import * as _ from &#39;lodash&#39;; import { Module } from &#39;&#64;nestjs/common&#39;; import { PermissionsOwnershipService } from &#39;../permissions/permissions-ownership.service&#39;; import { PERMISSIONS_OWNERSHIP_SERVICE_TOKEN, PermissionsModule, } from &#39;&#64;superawesome/permissions-nestjs&#39;; import { getUser } from &#39;../permissions/getUser&#39;; import { DocumentUnprotectedController } from &#39;../document-unprotected.controller&#39;; import { DocumentProtectedDetailedController } from &#39;./document-protected-detailed.controller&#39;; &#64;Module({ imports: [ /** Use PermissionsModule.forRoot() to configure Permissions for your module. */ PermissionsModule.forRoot({ /** If your req.user object doesnt already comply with [SuperAwesome Permissions IUser](https://permissions.docs.superawesome.com/interfaces/IUser.html), here you can extract and transform it (Optional). &#64;param req an expressjs request object */ extractUserFromRequest: async (req) =&gt; getUser(), /** * Here you can override the default reducer for the [limitOwn](https://permissions.docs.superawesome.com/classes/Permit.html#limitOwn) (Optional). * * &#64;param user: IUser the request user passed at runtime * * &#64;param limitOwneds: TlimitOwned[] an array all the `limitOwn` ownership hooks for the particular user */ limitOwnReduce: ({ user, limitOwneds }) =&gt; _.overSome(limitOwneds.map((limitOwned) =&gt; limitOwned({ user }))), /** Project (i.e map) a `resourceId` on your QueryParams from string (by default the param named &quot;id&quot;), to what is needed by the ownership hooks (eg a `number`) for the whole module (Optional). __Notes__ : - It should be able to tolerate if `resourceId` is missing or null etc and not throw. - We can override this `projectResourceId` at the Controller&#39;s Guard level at `createPermissionsGuard()` - We can also override all at `&#64;PermitGrant()` level only a specific endpoint (even more rare). */ projectResourceId: (resourceIdStr: string | void) =&gt; Number(resourceIdStr), }), ], controllers: [ DocumentUnprotectedController, DocumentProtectedDetailedController, ], providers: [ /** Using the special PERMISSIONS_OWNERSHIP_SERVICE_TOKEN, we must provide our `PermissionsOwnershipService` class where our ownership hook methods live. See `PermissionsOwnershipService` below on how this looks. */ { provide: PERMISSIONS_OWNERSHIP_SERVICE_TOKEN, useClass: PermissionsOwnershipService, }, ], }) export class ExampleDetailedModule {} </code></pre></div><h1 id="the-permissionsownershipservice">The PermissionsOwnershipService</h1> <p>You will need an @Injectable (a.k.a a Service) that holds all the ownership hooks as methods, provided via the special <code>PERMISSIONS_OWNERSHIP_SERVICE_TOKEN</code> in your module (see above).</p> <p>In this simple example we don&#39;t have any dependencies to inject, but in the real world in this Service you inject you DB repos and any other services you&#39;ll need to lookup the actual ownerships of your current user, against the resources they are trying to access.</p> <p><strong>Note:</strong> If a method name declared in the special PermissionsDefinitions variant <a href="/interfaces/IPermissionDefinitionStringOwnHooks.html">IPermissionDefinitionStringOwnHooks</a> is missing from this <code>PermissionsOwnershipService</code>, you&#39;ll get an exception at the module build time.</p> <div><pre class="line-numbers"><code class="language-js">// file: ../permissions/permissions-ownership.service.ts import { Injectable } from &#39;&#64;nestjs/common&#39;; import { isOwner_isDocCreatedByMeAndMyCompanyUsers, isOwner_isDocCreatedByMeAndMyManagedUsers, isOwner_isUserCreatorOfDocument, limitOwned_DocsOfMeAndMyCompanyUsers, limitOwned_DocsOfMeAndMyManagedUsers, limitOwned_listUserCreatedDocuments, } from &#39;&#64;superawesome/permissions/dist/__tests__/data.fixtures&#39;; &#64;Injectable() export class PermissionsOwnershipService { // EMPLOYEE isOwner_isUserCreatorOfDocument = isOwner_isUserCreatorOfDocument; limitOwned_listUserCreatedDocuments = limitOwned_listUserCreatedDocuments; // EMPLOYEE_MANAGER isOwner_isDocCreatedByMeAndMyManagedUsers = isOwner_isDocCreatedByMeAndMyManagedUsers; limitOwned_DocsOfMeAndMyManagedUsers = limitOwned_DocsOfMeAndMyManagedUsers; // COMPANY_ADMIN isOwner_isDocCreatedByMeAndMyCompanyUsers = isOwner_isDocCreatedByMeAndMyCompanyUsers; limitOwned_DocsOfMeAndMyCompanyUsers = limitOwned_DocsOfMeAndMyCompanyUsers; } </code></pre></div><p>That&#39;s it, you have all you need to start using permissions-nestjs!</p> <p>Happy permitting!</p> </div><div class="search-results"> <div class="has-results"> <h1 class="search-results-title"><span class='search-results-count'></span> result-matching "<span class='search-query'></span>"</h1> <ul class="search-results-list"></ul> </div> <div class="no-results"> <h1 class="search-results-title">No results matching "<span class='search-query'></span>"</h1> </div> </div> </div> <!-- END CONTENT --> </div> </div> <script> var COMPODOC_CURRENT_PAGE_DEPTH = 1; var COMPODOC_CURRENT_PAGE_CONTEXT = 'additional-page'; var COMPODOC_CURRENT_PAGE_URL = 'reference-&amp;-detailed-example.html'; </script> <script src="../js/libs/custom-elements.min.js"></script> <script src="../js/libs/lit-html.js"></script> <!-- Required to polyfill modern browsers as code is ES5 for IE... --> <script src="../js/libs/custom-elements-es5-adapter.js" charset="utf-8" defer></script> <script src="../js/menu-wc.js" defer></script> <script src="../js/libs/bootstrap-native.js"></script> <script src="../js/libs/es6-shim.min.js"></script> <script src="../js/libs/EventDispatcher.js"></script> <script src="../js/libs/promise.min.js"></script> <script src="../js/libs/zepto.min.js"></script> <script src="../js/compodoc.js"></script> <script src="../js/tabs.js"></script> <script src="../js/menu.js"></script> <script src="../js/libs/clipboard.min.js"></script> <script src="../js/libs/prism.js"></script> <script src="../js/sourceCode.js"></script> <script src="../js/search/search.js"></script> <script src="../js/search/lunr.min.js"></script> <script src="../js/search/search-lunr.js"></script> <script src="../js/search/search_index.js"></script> <script src="../js/lazy-load-graphs.js"></script> </body> </html>