UNPKG

@superawesome/permissions-nestjs

Version:

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

318 lines (281 loc) 19.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-simple.controller.md.e2e-spec.ts</code></strong></p> <h1 id="nestjs-with-superawesome-permissions---simple-example">NestJS with SuperAwesome Permissions - Simple Example</h1> <h2 id="simple-documents-protected-controller-example">Simple Documents Protected Controller Example</h2> <p>This is a trivial NestJS example, based on the same schema &amp; PermissionDefinitions &amp; data of the <a href="https://permissions.docs.superawesome.com/additional-documentation/detailed-usage-&amp;-examples.html">SuperAwesome Permissions example</a> (file <code>@superawesome/permissions/dist/__tests__/data.fixtures</code> which you shouldn&#39;t ever have to import).</p> <p><strong>Notes:</strong></p> <ul> <li><p>These docs are generated by e2e-tests, the <strong>examples are actual e2e tests</strong>!</p> </li> <li><p>All example code is in <code>/example</code>.</p> </li> </ul> <h2 id="business-rules">Business Rules</h2> <p>Our business rules are exactly the same as permissions examples (they are imported from it):</p> <blockquote> <p>As an <strong>EMPLOYEE</strong>, I can <strong>create</strong>, <strong>read</strong> &amp; <strong>list</strong> only my <strong>OWN Documents (created by me)</strong> , all attributes except <strong>confidential</strong>. Also, I can <strong>list</strong> all <strong>Documents</strong> on the system, but only access the <strong>title</strong> &amp; <strong>date</strong> attributes.</p> </blockquote> <blockquote> <p>As a <strong>EMPLOYEE_MANAGER</strong>, I can <strong>read</strong>, <strong>list</strong>, <strong>review</strong> &amp; <strong>delete</strong> all <strong>Documents</strong> created by <strong>any User that I am managing</strong>, all document attributes except <strong>confidential</strong>. Also, I can <strong>list</strong> all <strong>Documents</strong> on the system, but only access the <strong>title</strong>, <strong>date</strong> &amp; <strong>status</strong> attributes.</p> </blockquote> <blockquote> <p>As a <strong>COMPANY_ADMIN</strong>, I can <strong>read</strong>, <strong>update</strong> and <strong>review</strong> all <strong>Documents</strong> created by <strong>any User in my Company</strong>, all attributes.</p> </blockquote> <p><strong>Now, how super awesome would it be if only...</strong></p> <blockquote> <p>...we could <strong>fully protect our NestJS apps</strong> with the full effect of the above rules, just using a couple of decorators and declarative lines of code?</p> </blockquote> <p>With <strong>SuperAwesome Permissions for NestJs</strong> that&#39;s exactly what we can do!</p> <h2 id="permissionsdefinitions">PermissionsDefinitions</h2> <p>The Business Rules give rise to PermissionsDefinitions, lets have them here reference.</p> <p><strong>Note</strong>: They have an important <strong>PermissionsDefinitions</strong> difference from the <a href="https://permissions.docs.superawesome.com/additional-documentation/detailed-usage-&amp;-examples.html">SuperAwesome Permissions example</a>: the <a href="/interfaces/IPermissionDefinitionStringOwnHooks.html">ownership hooks are replaced with a string</a> (cause <a href="https://stackoverflow.com/questions/55560858/in-nest-js-is-it-possible-to-get-service-instance-inside-a-param-decorator">its impossible</a> to <a href="https://github.com/nestjs/nest/issues/1038">inject on a guard/decorator</a> on nestjs.</p> <p> This small &quot;glitch&quot; will be solved in a future release (if you can help resolve this, please do!).</p> <p> But this &quot;glitch&quot; highlights the way SuperAwesome Permissions could work in different languages, using JSON as the Lingua Franca for PermissionDefinitions: the <strong>string names correspond to method names of a Service</strong>, which we&#39;ll see shortly.</p> <div><pre class="line-numbers"><code class="language-js">const documentPermissionDefinitions = [ { roles: [&#39;EMPLOYEE&#39;], resource: &#39;document&#39;, descr: &#39;&gt; As an **EMPLOYEE**, I can **create**, **read** &amp; **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** &amp; **date** attributes.&#39;, possession: &#39;own&#39;, grant: { create: [&#39;*&#39;, &#39;!confidential&#39;], read: [&#39;*&#39;, &#39;!confidential&#39;], list: [&#39;*&#39;, &#39;!confidential&#39;], &#39;list:any&#39;: [&#39;title&#39;, &#39;date&#39;], }, isOwner: &#39;isOwner_isUserCreatorOfDocument&#39;, limitOwned: &#39;limitOwned_listUserCreatedDocuments&#39;, }, { roles: [&#39;EMPLOYEE_MANAGER&#39;], resource: &#39;document&#39;, descr: &#39;&gt; As a **EMPLOYEE_MANAGER**, I can **read**, **list**, **review** &amp; **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** &amp; **status** attributes.&#39;, possession: &#39;own&#39;, grant: { read: [&#39;*&#39;, &#39;!confidential&#39;, &#39;!personal&#39;], review: [&#39;*&#39;, &#39;!confidential&#39;, &#39;!personal&#39;], delete: [&#39;*&#39;, &#39;!confidential&#39;, &#39;!personal&#39;], list: [&#39;*&#39;, &#39;!confidential&#39;, &#39;!personal&#39;], &#39;list:any&#39;: [&#39;title&#39;, &#39;date&#39;, &#39;status&#39;], }, isOwner: &#39;isOwner_isDocCreatedByMeAndMyManagedUsers&#39;, limitOwned: &#39;limitOwned_DocsOfMeAndMyManagedUsers&#39;, }, { roles: [&#39;COMPANY_ADMIN&#39;], resource: &#39;document&#39;, descr: &#39;&gt; As a **COMPANY_ADMIN**, I can **read**, **update** and **review** all **Documents** created by **any User in my Company**, all attributes.&#39;, possession: &#39;own&#39;, grant: [&#39;read&#39;, &#39;update&#39;, &#39;review&#39;], isOwner: &#39;isOwner_isDocCreatedByMeAndMyCompanyUsers&#39;, limitOwned: &#39;limitOwned_DocsOfMeAndMyCompanyUsers&#39;, }, ]; </code></pre></div><h2 id="the-unprotected-example-testsdocument-unprotectedcontrollerts">The Unprotected Example (<strong>tests</strong>/document-unprotected.controller.ts)</h2> <p>Let&#39;s consider the simplest &quot;naked&quot; example, of an unprotected Controller for documents, without any permissions:</p> <div><pre class="line-numbers"><code class="language-js">// file: ../document-unprotected.controller.ts import { Controller, Get, Param, ParseIntPipe } from &#39;&#64;nestjs/common&#39;; import { ALL_DOCUMENTS, IDocument, } from &#39;&#64;superawesome/permissions/dist/__tests__/data.fixtures&#39;; &#64;Controller(&#39;/documents&#39;) export class DocumentUnprotectedController { &#64;Get(&#39;/:id&#39;) async read( &#64;Param(&#39;id&#39;, new ParseIntPipe()) id: number ): Promise&lt;IDocument&gt; { return ALL_DOCUMENTS.find((doc) =&gt; doc.id === id); } &#64;Get() async list(): Promise&lt;IDocument[]&gt; { return ALL_DOCUMENTS; } } </code></pre></div><p>How quickly can we transform this code to be &quot;protected&quot;? Let see it before we delve into tests.</p> <h2 id="simplest-protected-example">Simplest Protected Example</h2> <p>Time to reveal the most interesting part: the code that implements all of the above, has minimal impact: <strong>5 tiny lines</strong> of declarative code! It is <strong>deceivingly simple</strong>:</p> <div><pre class="line-numbers"><code class="language-js">// file: ../simple/document-protected-simple.controller.ts &#64;UseGuards(createPermissionsGuard({ resource: &#39;document&#39; }, documentPermissionDefinitions)) &#64;Controller(&#39;/documents-protected-simple&#39;) export class DocumentProtectedSimpleController { &#64;Get(&#39;/:id&#39;) async read( &#64;Param(&#39;id&#39;, new ParseIntPipe()) id: number, &#64;GetPermit() permit: Permit, ): Promise&lt;Partial&lt;IDocument&gt;&gt; { return await permit.pick(ALL_DOCUMENTS.find(doc =&gt; doc.id === id)); } &#64;Get() async list(&#64;GetPermit() permit: Permit): Promise&lt;Partial&lt;IDocument&gt;[]&gt; { return await permit.filterPick(ALL_DOCUMENTS); } } </code></pre></div><p>We see that with just 5 simple LoCs we touched (2, 8, 10, 14 &amp; 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 <strong>zero code changes</strong>.</p> <p>But there is no magic! There&#39;s just a lot going behind the scenes, with such little code, so lets dive in.</p> <ul> <li><p><strong>L2</strong> creates our Guard, declaring only:</p> <ul> <li><p>for which <code>resource</code> we query about &amp; protect (i.e <strong>document</strong> in this case) as default, it can change per endpoint.</p> </li> <li><p>any relevant <code>PermissionDefinitions</code> we want to provide here. Note that we could add them in other places like the module &amp; other controllers - they <strong>all come in effect equally at runtime</strong>.</p> </li> </ul> </li> <li><p><strong>L6</strong> the <strong>read</strong> method name becomes the <strong>name of the action</strong> (by default, can be overridden).</p> </li> <li><p><strong>L8</strong> we inject the <code>Permit</code> instance in our method (its created by the Guard internally via <code>.grantPermit()</code>). It holds all the information we&#39;ll need for the authorization &amp; permissions part of our app, including user, allowed attributes, ownership checks and pick / filter utils.</p> </li> <li><p><strong>before reaching L10</strong> permissions-nestjs already knows from L5 the a special <code>id</code> param on our endpoint (<code>id</code> as default, it can change - jump to <a href="/additional-documentation/reference-&amp;-detailed-example.html">reference &amp; detailed example</a> to see how). The library executes <code>isOwn(id)</code> behind the scenes, and if doc isnt owned by user (and user doesnt have <code>permit.anyGranted</code>) it returns <code>403 Forbidden</code> before even reaching the method.</p> </li> <li><p><strong>L10</strong> simply <code>permit.pick</code> only the allowed <strong>read</strong> attributes from a document, depending on the User and their ownership of the resource. We dont need to check if it <code>isOwn</code> cause of the guard doing it on <code>id</code> param for us!</p> </li> <li><p><strong>L14</strong> &amp; <strong>L15</strong> for the <strong>list</strong> method we inject <code>Permit</code> so we can call <code>permit.filterPick()</code> on this simple implementation. Each each user can now list <strong>all but only the resources &amp; their attributes</strong> that they are entitled to. There are ways of adapting &amp; scaling such &quot;many own items code&quot; arbitrarily (eg if we were dealing with a DB) with little code - see <a href="https://permissions.docs.superawesome.com/classes/Permit.html#limitOwn"><code>permit.limitOwn()</code></a> and check <code>document-protected-detailed.controller</code> example below.</p> </li> </ul> <h2 id="the-specs">The Specs</h2> <p><strong>Note:</strong> These are actual tests against the protected controllers!</p> <h2 id="example-calls">Example calls</h2> <p>Lets now call our endpoints, with different users, and see what we&#39;ll get.</p> <h3 id="action-read">Action &quot;read&quot;</h3> <p>First lets try &quot;read&quot; specific documents (OWN and NON-OWN), with different users (hint: no user has <code>&quot;read:any&quot;</code>)</p> <h4 id="a-user-with-employee">A user with EMPLOYEE</h4> <p>It returns <strong>only allowed attributes</strong> on OWN document (i.e all except <code>confidential</code>)</p> <div><pre class="line-numbers"><code class="language-js">user = { id: 1, roles: [&#39;EMPLOYEE&#39;] }; // =&gt; GET http:///documents-protected-simple/10 ({ id: 10, title: &#39;Document Title 10&#39;, date: &#39;2020-02-010&#39;, someRandomField: &#39;Some random value 10&#39; }); </code></pre></div><p>But on a NON-OWN document it forbids</p> <div><pre class="line-numbers"><code class="language-js">// =&gt; GET http:///documents-protected-simple/1000 &#39;403 - FORBIDDEN&#39;; </code></pre></div><h4 id="a-user-with-company_admin">A user with COMPANY_ADMIN</h4> <p>It returns <strong>all attributes</strong> on OWN document </p> <div><pre class="line-numbers"><code class="language-js">user = { id: 2, roles: [&#39;COMPANY_ADMIN&#39;] }; // =&gt; GET http:///documents-protected-simple/20 ({ id: 20, title: &#39;Document Title 20&#39;, date: &#39;2020-02-020&#39;, someRandomField: &#39;Some random value 20&#39;, confidential: &#39;Confidential 20&#39;, }); </code></pre></div><p>But on a NON-OWN document it forbids</p> <div><pre class="line-numbers"><code class="language-js">// =&gt; GET http:///documents-protected-simple/2000 &#39;403 - FORBIDDEN&#39;; </code></pre></div><h3 id="action-list">Action &quot;list&quot;</h3> <p>Action &quot;list&quot; will give us ALL the documents a user is <strong>allowed</strong> to browse, with only the allowed attributes, <strong>depending on ownership of each item</strong>.</p> <div><pre class="line-numbers"><code class="language-js">user = { id: 1, roles: [&#39;EMPLOYEE&#39;] }; // =&gt; GET http:///documents-protected-simple // showing only the first 5 docs for brevity [ { id: 1, title: &#39;Document Title 1&#39;, date: &#39;2020-02-01&#39;, someRandomField: &#39;Some random value 1&#39; }, { id: 10, title: &#39;Document Title 10&#39;, date: &#39;2020-02-010&#39;, someRandomField: &#39;Some random value 10&#39; }, { id: 100, title: &#39;Document Title 100&#39;, date: &#39;2020-02-0100&#39;, someRandomField: &#39;Some random value 100&#39; }, { title: &#39;Document Title 2&#39;, date: &#39;2020-02-02&#39; }, { title: &#39;Document Title 20&#39;, date: &#39;2020-02-020&#39; }, ]; </code></pre></div><p>Forbids if user doesnt have &quot;list&quot; action granted.</p> <div><pre class="line-numbers"><code class="language-js">user = { id: 4, roles: [&#39;COMPANY_ADMIN&#39;] }; // =&gt; GET http:///documents-protected-simple &#39;403 - FORBIDDEN&#39;; </code></pre></div><h1 id="setting-up-the-module">Setting up the module</h1> <p>A no-comments module setting up follows.</p> <div><pre class="line-numbers"><code class="language-js">// file: ../simple/example-simple.module.ts // omitted imports &#64;Module({ imports: [ PermissionsModule.forRoot({ extractUserFromRequest: async (req) =&gt; getUser(), limitOwnReduce: ({ user, limitOwneds }) =&gt; _.overSome(limitOwneds.map((limitOwned) =&gt; limitOwned({ user }))), projectResourceId: (resourceIdStr: string | undefined) =&gt; Number(resourceIdStr), }), ], controllers: [ DocumentUnprotectedController, DocumentProtectedSimpleController, ], providers: [ { provide: PERMISSIONS_OWNERSHIP_SERVICE_TOKEN, useClass: PermissionsOwnershipService, }, ], }) export class ExampleSimpleModule {} </code></pre></div><h1 id="next-steps">Next steps</h1> <p>That&#39;s it, you&#39;ve been initiated!</p> <p>Now continue to the <a href="/additional-documentation/reference-&amp;-detailed-example.html">detailed example which also serves as a reference</a>.</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 = 'how-to-use-simple-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>