@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
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 & PermissionDefinitions & data of the <a href="https://permissions.docs.superawesome.com/additional-documentation/detailed-usage-&-examples.html">SuperAwesome Permissions example</a> (file <code>@superawesome/permissions/dist/__tests__/data.fixtures</code> which you shouldn'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> & <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> & <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> & <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> & <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'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-&-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 "glitch" will be solved in a future release (if you can help resolve this, please do!).</p>
<p> But this "glitch" 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'll see shortly.</p>
<div><pre class="line-numbers"><code class="language-js">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',
},
];
</code></pre></div><h2 id="the-unprotected-example-testsdocument-unprotectedcontrollerts">The Unprotected Example (<strong>tests</strong>/document-unprotected.controller.ts)</h2>
<p>Let's consider the simplest "naked" 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 '@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;
}
}
</code></pre></div><p>How quickly can we transform this code to be "protected"? 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
@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);
}
}
</code></pre></div><p>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 <strong>zero code changes</strong>.</p>
<p>But there is no magic! There'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 & 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 & 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'll need for the authorization & 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-&-detailed-example.html">reference & 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> & <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 & their attributes</strong> 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 <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'll get.</p>
<h3 id="action-read">Action "read"</h3>
<p>First lets try "read" specific documents (OWN and NON-OWN), with different users (hint: no user has <code>"read:any"</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: ['EMPLOYEE'] };
// => GET http:///documents-protected-simple/10
({ id: 10, title: 'Document Title 10', date: '2020-02-010', someRandomField: 'Some random value 10' });
</code></pre></div><p>But on a NON-OWN document it forbids</p>
<div><pre class="line-numbers"><code class="language-js">// => GET http:///documents-protected-simple/1000
'403 - FORBIDDEN';
</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: ['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',
});
</code></pre></div><p>But on a NON-OWN document it forbids</p>
<div><pre class="line-numbers"><code class="language-js">// => GET http:///documents-protected-simple/2000
'403 - FORBIDDEN';
</code></pre></div><h3 id="action-list">Action "list"</h3>
<p>Action "list" 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: ['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' },
];
</code></pre></div><p>Forbids if user doesnt have "list" action granted.</p>
<div><pre class="line-numbers"><code class="language-js">user = { id: 4, roles: ['COMPANY_ADMIN'] };
// => GET http:///documents-protected-simple
'403 - FORBIDDEN';
</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
@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 {}
</code></pre></div><h1 id="next-steps">Next steps</h1>
<p>That's it, you've been initiated!</p>
<p>Now continue to the <a href="/additional-documentation/reference-&-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>