UNPKG

@superawesome/permissions

Version:

Fine grained permissions / access control with ownerships & attribute picking, done right.

667 lines (626 loc) 37.3 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 (@superawesome/permissions)</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 (@superawesome/permissions)</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 detailed-usage-examples.md.spec.ts</code></strong></p> <h1 id="detailed-usage--examples">Detailed Usage &amp; Examples</h1> <p>Let&#39;s look at some examples, so we can better guide the discussion.</p> <h3 id="hypothetical-schema">Hypothetical Schema</h3> <p>All examples use a simple schema that entails:</p> <ul> <li><p>A <strong>Document</strong> is created by a <strong>User</strong>.</p> </li> <li><p>A <strong>User</strong> belongs to one <strong>Company</strong> (and a <strong>Company</strong> has many <strong>Users</strong>)</p> </li> <li><p>A <strong>User</strong> (as manager) manages zero or more <strong>Users</strong></p> </li> </ul> <p>Note: our mock data layer resides in file <code>data.fixtures.ts</code>.</p> <h3 id="roles-and-their-crud-rules">Roles and their CRUD Rules:</h3> <p>Now consider the following simple Permissions (i.e our business rules, expressed as plain English), based on the above schema:</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> <blockquote> <p>As a <strong>SUPER_ADMIN</strong>, I can do all actions on <strong>any resource</strong> (not just documents), created by ANY User, ANY Company and access all attributes.</p> </blockquote> <p>We see that most Roles (and hence Users with these Roles) can perform different sets of actions on <strong>Documents</strong> they somehow &quot;own&quot;.</p> <p>But the definition of <em>ownership</em> in our apps are arbitrary - it can be &quot;Documents created by users of my company&quot;, or &quot;Documents created by users I manage&quot; or it could be any particular business rule such as <a href="https://github.com/onury/accesscontrol/issues/46#issue-330937936">&quot;users that are friends&quot; etc</a>.</p> <p>We define these ownership definitions as &quot;ownership hooks&quot;, by defining <a href="/classes/PermissionDefinition_DOCS.html#isOwner"><code>isOwner</code></a> and either <a href="/classes/PermissionDefinition_DOCS.html#listOwned"><code>listOwned</code></a> or <a href="/classes/PermissionDefinition_DOCS.html#limitOwned"><code>limitOwned</code></a> functions for each PermissionDefinition that has &quot;own&quot; possession rules.</p> <h1 id="1-adding-permissiondefinitions--build">1. Adding PermissionDefinitions &amp; build()</h1> <p>With that in mind, lets convert the above &quot;human permissions / business rules&quot; into <a href="/classes/PermissionDefinition_DOCS.html"><strong>PermissionDefinitions</strong></a>:</p> <div><pre class="line-numbers"><code class="language-js">const permissions = new Permissions({ permissionDefinitionDefaults: { resource: &#39;document&#39; }, permissionDefinitions: [ { 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;, isOwner: async ({ user, resourceId }) =&gt; isUserCreatorOfDocument({ user, resourceId }), listOwned: async (user) =&gt; listUserCreatedDocuments(user), 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;], }, }, { 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;, isOwner: async ({ user, resourceId }) =&gt; listDocsOfMeAndMyManagedUsers(user).includes(resourceId), listOwned: async (user) =&gt; listDocsOfMeAndMyManagedUsers(user), 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;], }, }, { 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;, isOwner: async ({ user, resourceId }) =&gt; listDocsOfMeAndMyCompanyUsers(user).includes(resourceId), listOwned: async (user) =&gt; listDocsOfMeAndMyCompanyUsers(user), possession: &#39;own&#39;, grant: [&#39;read&#39;, &#39;update&#39;, &#39;review&#39;], }, { roles: [&#39;SUPER_ADMIN&#39;], resource: &#39;*&#39;, descr: &#39;&gt; As a **SUPER_ADMIN**, I can do all actions on **any resource** (not just documents), created by ANY User, ANY Company and access all attributes.&#39;, grant: [&#39;*&#39;], }, ], }).build(); </code></pre></div><p>Its a good practice to keep the human description close in the PD &amp; keep them in sync.</p> <h1 id="2-granting-permissions">2. Granting Permissions</h1> <p>We can now start <strong>Granting Permissions</strong>, i.e <code>grantPermit()</code>.</p> <h3 id="example-1">Example 1</h3> <p>Lets grant permit of a simple EMPLOYEE user to &quot;read&quot; a document.</p> <div><pre class="line-numbers"><code class="language-js">const permit = await permissions.grantPermit({ user: { id: 1, roles: [&#39;EMPLOYEE&#39;] }, action: &#39;read&#39;, resource: &#39;document&#39;, }); </code></pre></div><p>which gives us a <a href="/classes/Permit.html">Permit</a> object we can use in our app:</p> <div><pre class="line-numbers"><code class="language-js">permit.granted === true; </code></pre></div><div><pre class="line-numbers"><code class="language-js">permit.anyGranted === false; </code></pre></div><div><pre class="line-numbers"><code class="language-js">permit.ownGranted === true; </code></pre></div><h2 id="basic-permissions---ownership-only">Basic Permissions - Ownership only</h2> <p>We see that this user has ONLY <strong>own</strong> access granted for this action &quot;read&quot;, so they can&#39;t access any random resource item.</p> <p><strong>Important: In your app you MUST offer only the resource items allowed for each permit, so <a href="https://github.com/onury/accesscontrol/issues/14#issuecomment-328316670">when ONLY own access is granted you MUST check the actual possession</a> and start filtering.</strong></p> <p>We need to handle a) check one item&#39;s onwership and b) retrieve a filtered list of many own items.</p> <h2 id="permitisown">permit.isOwn()</h2> <p>Lets check if a particular documentId is owned by this user:</p> <div><pre class="line-numbers"><code class="language-js">(await permit.isOwn(100)) === true; </code></pre></div><div><pre class="line-numbers"><code class="language-js">(await permit.isOwn(200)) === false; </code></pre></div><h2 id="permitlistown">permit.listOwn()</h2> <p>Lets now handle the set of documents owned by the user: there are 2 ways of achieving this, and it depends on your service.</p> <p>The simplest (but not so scalable) is the one we used in our PDs above, the eager <a href="/classes/Permit.html#listOwn"><code>listOwned</code> &amp; <code>listOwn()</code></a> way.</p> <p>But also check the lazy <a href="/classes/Permit.html#limitOwn"><code>limitOwned</code> &amp; <code>limitOwn()</code></a> way, if you plan to scale. The 2 are not compatible and cant be mixed (in the same resource), so choose wisely!</p> <p>Using <a href="/classes/Permit.html#listOwn"><code>listOwn()</code></a> we get a FULL list of <code>documentIds</code> that are &quot;owned&quot; by this user:</p> <div><pre class="line-numbers"><code class="language-js">await permit.listOwn(); // equals [1, 10, 100]; </code></pre></div><h1 id="3-filtering--picking-the-right-objects">3. Filtering &amp; Picking the right objects</h1> <p><strong>Important: In your service you MUST always be picking your resource items, before you return them</strong>.</p> <p>PermissionDefinitions &amp; Permit decide what objects the calling app will receive, irrespective of <code>permit.anyGranted</code> being true/false (see reason in Example 2).</p> <p>Its a good practice to pick just before sending the Output DTO object to the calling app.</p> <h2 id="permitattributes">permit.attributes()</h2> <p>First lets see what attributes we can access from an &quot;own&quot; document.</p> <div><pre class="line-numbers"><code class="language-js">await permit.attributes(100); // equals [&#39;*&#39;, &#39;!confidential&#39;]; </code></pre></div><p>We get the allowed attributes for an own document for this user, i.e all attributes except <code>&#39;confidential&#39;</code>.</p> <p>Now lets see what we get from any random Document object.</p> <div><pre class="line-numbers"><code class="language-js">await permit.attributes(); // equals []; </code></pre></div><div><pre class="line-numbers"><code class="language-js">await permit.attributes(200); // equals []; </code></pre></div><p>No attributes allowed! Why did that happen?</p> <p>Because we DONT own these random documents, we shouldn&#39;t be accessing them at all. Even if we try to return a document not owned (which we should not anyway), the <code>permit.pick()</code> operation below will give you an empty object.</p> <h2 id="permitpick">permit.pick()</h2> <p>The <code>permit.attributes()</code> is not very useful, you basically want to &quot;pick&quot; only the allowed attributes &amp; values.</p> <p>This is what <code>permit.pick()</code> does, similarly to lodash <code>_.pick</code>, but with the allowed attributes baked in.</p> <h3 id="owned">Owned</h3> <p>Passing an own document, we get only all the allowed attributes (including <code>&#39;someRandomField&#39;</code> since we have the &quot;*&quot; in our definition, but without <code>&#39;confidential&#39;</code>) :</p> <div><pre class="line-numbers"><code class="language-js">await permit.pick({ id: 100, title: &#39;Document 100 title&#39;, date: &#39;2020-02-19&#39;, confidential: &#39;100 secrets lie here&#39;, someRandomField: &#39;Some random 100 value&#39;, }); // equals ({ id: 100, title: &#39;Document 100 title&#39;, date: &#39;2020-02-19&#39;, someRandomField: &#39;Some random 100 value&#39; }); </code></pre></div><h3 id="not-owned">Not owned</h3> <p>But passing an non-owned document, we will get an empty object:</p> <div><pre class="line-numbers"><code class="language-js">await permit.pick({ id: 999, title: &#39;Document 999 title&#39;, date: &#39;1920-02-19&#39;, confidential: &#39;999 secrets lie here&#39;, someRandomField: &#39;Some random 999 value&#39;, }); // equals ({}); </code></pre></div><h1 id="helpers-to-filter-pick--map">Helpers to filter, pick &amp; map</h1> <p><code>Permit</code> has some useful helpers, which handle internally the async nature of ownership hooks and thus can save you some frustration.</p> <h2 id="permitfilterpick">permit.filterPick()</h2> <p>For example what if we are handling an array of Documents and we want to a) filter out non-owned ones and b) pick attributes of the owned ones?</p> <div><pre class="line-numbers"><code class="language-js">await permit.filterPick([ { id: 999, title: &#39;Document 999 title&#39;, date: &#39;1920-02-19&#39;, confidential: &#39;999 secrets lie here&#39;, someRandomField: &#39;Some random 999 value&#39;, }, { id: 100, title: &#39;Document 100 title&#39;, date: &#39;2020-02-19&#39;, confidential: &#39;100 secrets lie here&#39;, someRandomField: &#39;Some random 100 value&#39;, }, ]); // equals [{ id: 100, title: &#39;Document 100 title&#39;, date: &#39;2020-02-19&#39;, someRandomField: &#39;Some random 100 value&#39; }]; </code></pre></div><p>Note: ideally you should be filtering your data layer before you reach here, and this is where <code>listOwn()</code> &amp; <code>limitOwn()</code> come in. </p> <h2 id="permitmappick">permit.mapPick()</h2> <p>Another helper is <a href="/classes/Permit.html#mapPick"><code>permit.mapPick()</code></a>, which is not filtering but only does a mapping and attributes picking:</p> <div><pre class="line-numbers"><code class="language-js">await permit.mapPick( [ { id: 999, title: &#39;Document 999 title&#39;, date: &#39;1920-02-19&#39;, confidential: &#39;999 secrets lie here&#39;, someRandomField: &#39;Some random 999 value&#39;, }, { id: 100, title: &#39;Document 100 title&#39;, date: &#39;2020-02-19&#39;, confidential: &#39;100 secrets lie here&#39;, someRandomField: &#39;Some random 100 value&#39;, }, ], (doc) =&gt; ({ ...doc, title: doc.title.toUpperCase(), someNewField: &#39;Some new value&#39;, }) ); // equals [ {}, { id: 100, title: &#39;DOCUMENT 100 TITLE&#39;, date: &#39;2020-02-19&#39;, someRandomField: &#39;Some random 100 value&#39;, someNewField: &#39;Some new value&#39;, }, ]; </code></pre></div><p>It returns an empty object for documents that aren&#39;t owned</p> <h1 id="4-let-superawesome-permissions--permissiondefinitions-shape-your-apps-data">4. Let SuperAwesome Permissions &amp; PermissionDefinitions shape your App&#39;s data</h1> <h3 id="example-2">Example 2</h3> <p>With the same EMPLOYEE user, lets grant permit for &quot;list&quot; action this time.</p> <p>We see that the PD has both &quot;list:own&quot; &amp; &quot;list:any&quot;, with different set of attributes (i.e for non-own documents, I can only read title &amp; date).</p> <div><pre class="line-numbers"><code class="language-js">const permit = await permissions.grantPermit({ user: { id: 1, roles: [&#39;EMPLOYEE&#39;] }, action: &#39;list&#39;, resource: &#39;document&#39;, }); </code></pre></div><p>We indeed have &quot;any&quot;:</p> <div><pre class="line-numbers"><code class="language-js">permit.anyGranted &amp;&amp; permit.ownGranted === true; </code></pre></div><h4 id="why-you-should-be-picky">Why you should be picky</h4> <p>Just having &quot;list:any&quot; access doesnt mean all Documents are created equally:</p> <div><pre class="line-numbers"><code class="language-js">await permit.mapPick([ { id: 999, title: &#39;Document 999 title&#39;, date: &#39;1920-02-19&#39;, confidential: &#39;999 secrets lie here&#39;, someRandomField: &#39;Some random 999 value&#39;, }, { id: 100, title: &#39;Document 100 title&#39;, date: &#39;2020-02-19&#39;, confidential: &#39;100 secrets lie here&#39;, someRandomField: &#39;Some random 100 value&#39;, }, ]); // equals [ { title: &#39;Document 999 title&#39;, date: &#39;1920-02-19&#39; }, { id: 100, title: &#39;Document 100 title&#39;, date: &#39;2020-02-19&#39;, someRandomField: &#39;Some random 100 value&#39; }, ]; </code></pre></div><h4 id="now-the-question-to-filterpick-or-not-to-filterpick">Now the Question: to filterPick or not to filterPick?</h4> <p>How should <code>permit.filterPick</code> behave? Think for a minute.</p> <p>Well, it should give the same result as <code>permit.mapPick()</code> (without a <code>projectTo</code>), cause <strong>filterPick should filter out non-own items, only when we DONT HAVE &quot;any&quot; access</strong>.</p> <p>But this time we do, so it should respect that:</p> <div><pre class="line-numbers"><code class="language-js">await permit.filterPick([ { id: 999, title: &#39;Document 999 title&#39;, date: &#39;1920-02-19&#39;, confidential: &#39;999 secrets lie here&#39;, someRandomField: &#39;Some random 999 value&#39;, }, { id: 100, title: &#39;Document 100 title&#39;, date: &#39;2020-02-19&#39;, confidential: &#39;100 secrets lie here&#39;, someRandomField: &#39;Some random 100 value&#39;, }, ]); // equals [ { title: &#39;Document 999 title&#39;, date: &#39;1920-02-19&#39; }, { id: 100, title: &#39;Document 100 title&#39;, date: &#39;2020-02-19&#39;, someRandomField: &#39;Some random 100 value&#39; }, ]; </code></pre></div><h4 id="pick-has-your-back">Pick has your back</h4> <p>It follows that <code>permit.pick</code> behaves similarly, picking different attributes for &quot;own&quot; and &quot;non-own&quot; items, when &quot;any&quot; is granted:</p> <h1 id="picking-own">Picking Own</h1> <div><pre class="line-numbers"><code class="language-js">await permit.pick({ id: 100, title: &#39;Document 100 title&#39;, date: &#39;2020-02-19&#39;, confidential: &#39;100 secrets lie here&#39;, someRandomField: &#39;Some random 100 value&#39;, }); // equals ({ id: 100, title: &#39;Document 100 title&#39;, date: &#39;2020-02-19&#39;, someRandomField: &#39;Some random 100 value&#39; }); </code></pre></div><h1 id="picking-non-own-using-any">Picking non-own, using any</h1> <div><pre class="line-numbers"><code class="language-js">await permit.pick({ id: 999, title: &#39;Document 999 title&#39;, date: &#39;1920-02-19&#39;, confidential: &#39;999 secrets lie here&#39;, someRandomField: &#39;Some random 999 value&#39;, }); // equals ({ title: &#39;Document 999 title&#39;, date: &#39;1920-02-19&#39; }); </code></pre></div><h1 id="5-a-user-with-many-roles">5. A User with many Roles</h1> <p>Users can have many roles. The mantra with multiple roles is:</p> <blockquote> <p>A User with multiple roles, can do whatever <strong>each role could do individually</strong>, but NO MORE or NO LESS than that.</p> </blockquote> <p>This principle should be followed by your roles &amp; PermissionDefinitions as well. SuperAwesome Permissions follows this mantra, but there are some <a href="/additional-documentation/faq,-gotchas-&amp;-caveats.html">caveats</a> in the current version (to be fixed soon).</p> <h2 id="example-3---action-merging">Example 3 - Action merging</h2> <p>If one Role grants an action, action is granted with the greatest possible <strong>possession</strong> in any of the grants (where any &gt; own).</p> <div><pre class="line-numbers"><code class="language-js">const permit = await permissions.grantPermit({ user: { id: 2, roles: [&#39;EMPLOYEE&#39;, &#39;EMPLOYEE_MANAGER&#39;] }, action: &#39;create&#39;, resource: &#39;document&#39;, }); permit.ownGranted === true; // from EMPLOYEE role permit.anyGranted === false; // would be true only if some role had it </code></pre></div><h2 id="ownership-evaluation-merging">Ownership evaluation merging</h2> <h3 id="shared-action">Shared action</h3> <p>When handling one or many items with <code>permit.isOwn</code>, <code>permit.listOwn</code> or <code>permit.limitOwn</code>, the Permit will consider as &quot;owned&quot; the union of all resourceIds owned by each role that has the specific action.</p> <p>Consider these different <code>grantPermit()</code> cases for the <code>action: &#39;read</code> , always for same <strong>User with id: 2</strong>, but with different roles in each attempted case, where all roles have the &quot;read&quot; action granted:</p> <h4 id="with-roles-employee-we-get">With role(s) EMPLOYEE we get:</h4> <div><pre class="line-numbers"><code class="language-js">permit = await permissions.grantPermit({ user: { id: 2, roles: [&#39;EMPLOYEE&#39;] }, action: &#39;read&#39;, resource: &#39;document&#39;, }); await permit.listOwn(); // equals [2, 20, 200]; </code></pre></div><h4 id="with-roles-employee_manager-we-get">With role(s) EMPLOYEE_MANAGER we get:</h4> <div><pre class="line-numbers"><code class="language-js">permit = await permissions.grantPermit({ user: { id: 2, roles: [&#39;EMPLOYEE_MANAGER&#39;] }, action: &#39;read&#39;, resource: &#39;document&#39;, }); await permit.listOwn(); // equals [2, 20, 200, 1, 10, 100, 4, 40, 400]; </code></pre></div><h4 id="with-roles-company_admin-we-get">With role(s) COMPANY_ADMIN we get:</h4> <div><pre class="line-numbers"><code class="language-js">permit = await permissions.grantPermit({ user: { id: 2, roles: [&#39;COMPANY_ADMIN&#39;] }, action: &#39;read&#39;, resource: &#39;document&#39;, }); await permit.listOwn(); // equals [1, 10, 100, 2, 20, 200, 3, 30, 300, 7, 70, 700]; </code></pre></div><h4 id="with-roles-employee_manager-company_admin-we-get">With role(s) EMPLOYEE_MANAGER, COMPANY_ADMIN we get:</h4> <div><pre class="line-numbers"><code class="language-js">permit = await permissions.grantPermit({ user: { id: 2, roles: [&#39;EMPLOYEE_MANAGER&#39;, &#39;COMPANY_ADMIN&#39;] }, action: &#39;read&#39;, resource: &#39;document&#39;, }); await permit.listOwn(); // equals [2, 20, 200, 1, 10, 100, 4, 40, 400, 3, 30, 300, 7, 70, 700]; // merged - union of all owned hooks on all roles </code></pre></div><h3 id="not-shared-action">Not shared action</h3> <p>If the action is not shared among the different roles (in different PDs), then <strong>only the ownerships in PDs that have this action</strong> come into play.</p> <p>Consider the following cases for the <code>action:&#39;delete&#39;</code> this time, again for same <strong>User with id: 2</strong>, where only the EMPLOYEE_MANAGER role has the &quot;delete&quot; action granted:</p> <h4 id="with-roles-employee_manager-we-get-1">With role(s) EMPLOYEE_MANAGER we get:</h4> <div><pre class="line-numbers"><code class="language-js">permit = await permissions.grantPermit({ user: { id: 2, roles: [&#39;EMPLOYEE_MANAGER&#39;] }, action: &#39;delete&#39;, resource: &#39;document&#39;, }); expect(permit.granted).toBe(true); expect(permit.anyGranted).toBe(false); expect(permit.ownGranted).toBe(true); await permit.listOwn(); // equals [2, 20, 200, 1, 10, 100, 4, 40, 400]; </code></pre></div><h4 id="with-roles-company_admin-we-get-1">With role(s) COMPANY_ADMIN we get:</h4> <div><pre class="line-numbers"><code class="language-js">permit = await permissions.grantPermit({ user: { id: 2, roles: [&#39;COMPANY_ADMIN&#39;] }, action: &#39;delete&#39;, resource: &#39;document&#39;, }); expect(permit.granted).toBe(false); expect(permit.anyGranted).toBe(false); expect(permit.ownGranted).toBe(false); await permit.listOwn(); // Throws exception since even `permit.granted` is false </code></pre></div><h4 id="with-roles-employee_manager-company_admin-we-get-1">With role(s) EMPLOYEE_MANAGER, COMPANY_ADMIN we get:</h4> <div><pre class="line-numbers"><code class="language-js">permit = await permissions.grantPermit({ user: { id: 2, roles: [&#39;EMPLOYEE_MANAGER&#39;, &#39;COMPANY_ADMIN&#39;] }, action: &#39;delete&#39;, resource: &#39;document&#39;, }); expect(permit.granted).toBe(true); expect(permit.anyGranted).toBe(false); expect(permit.ownGranted).toBe(true); await permit.listOwn(); // equals [2, 20, 200, 1, 10, 100, 4, 40, 400]; // only the EMPLOYEE_MANAGER ownership is active for delete action </code></pre></div><p>In the real world this translates to</p> <blockquote> <p>An EMPLOYEE_MANAGER managing a team of People can <strong>delete</strong> their documents. But a COMPANY_MANAGER can NOT <strong>delete</strong> company documents.</p> </blockquote> <p>Therefore company documents are secured from being deleted, unlike the team&#39;s:</p> <div><pre class="line-numbers"><code class="language-js">// an EMPLOYEE_MANAGER document (await permit.isOwn(100)) === true; </code></pre></div><div><pre class="line-numbers"><code class="language-js">// a COMPANY_ADMIN document, not considered as owned for **delete** action (await permit.isOwn(700)) === false; </code></pre></div><h2 id="example-4---over-optimistic-attributes-merging-for-own-resources-on-multiple-roles-see-caveat-2">Example 4 - Over-optimistic attributes merging for own resources on multiple roles (see Caveat #2)</h2> <p>Attributes from all roles are merged as a union optimistically. This means that if any one Role can access an attribute, then the user can access it. This sounds right, until we think of ownership: the rule applies <strong>irrespective of the role that contributed to owning a resource</strong> which is problematic :-(</p> <p>Consider this example:</p> <div><pre class="line-numbers"><code class="language-js">const permit = await permissions.grantPermit({ user: { id: 2, roles: [&#39;EMPLOYEE&#39;, &#39;EMPLOYEE_MANAGER&#39;] }, action: &#39;list&#39;, resource: &#39;document&#39;, }); await permit.attributes(); // for non-own, its the merged of &quot;any&quot; attributes &#39;EMPLOYEE&#39; of &#39;EMPLOYEE_MANAGER&#39;, which is expected: [&#39;date&#39;, &#39;status&#39;, &#39;title&#39;]; </code></pre></div><p>We see that for own resources, again we get the merged of &quot;own&quot; attributes of both roles, but really it should depend on the specific ownership:</p> <div><pre class="line-numbers"><code class="language-js">await permit.attributes(200); // equals CORRECTLY to [&#39;*&#39;, &#39;!confidential&#39;]; </code></pre></div><p>We see that since ownership for <code>resourceId = 200</code> is established by both EMPLOYEE &amp; EMPLOYEE_MANAGER roles, it correctly equals to the most optimistic merged attributes.</p> <p>Now this is the issue: in EMPLOYEE_MANAGER we have an extra restricted attribute <code>!personal</code>. The real world analogy is that an EMPLOYEE_MANAGER can &quot;read&quot; their employee documents, BUT NOT their &quot;personal&quot; attribute, as we want only the <strong>employee as the creator</strong> to access it. Think of it as some personal information the employee is adding to the doc, but their manager should not be able access it.</p> <p>Notice now that documentId 400 is only owned by the EMPLOYEE_MANAGER role (and NOT by EMPLOYEE as the creator), i.e it is only the EMPLOYEE_MANAGER role that allows this user to access someones else&#39;s created document, hence the &quot;personal&quot; attribute on this particular item should not be accessed!</p> <p>But lets see:</p> <div><pre class="line-numbers"><code class="language-js">await permit.attributes(400); // 400 is owned only by EMPLOYEE_MANAGER, but attributes incorrectly equal to: [&#39;*&#39;, &#39;!confidential&#39;]; // Attributes should really equal to [&#39;*&#39;,&#39;!confidential&#39;,&#39;!personal&#39;]; </code></pre></div><p>It seems that our user inherited an <strong>optimistically merged version of attributes</strong> for all own resources, irrespective of <strong>which role allowed the actual ownership</strong> of the resource.</p> <p>It means a user with EMPLOYEE_MANAGER + EMPLOYEE together can do more things than EMPLOYEE alone and EMPLOYEE_MANAGER alone. This is contrary to our mantra &quot;no more and no less&quot;.</p> <p>So be aware of this glitch &amp; as in all security tools, test well! The issue will be fixed in a future version of SuperAwesome Permissions.</p> <h1 id="6-scaling-using-permitlimitown">6. Scaling using <code>permit.limitOwn()</code></h1> <p>Make sure you&#39;ve read <a href="/classes/Permit.html#limitOwn">how <code>limitOwned</code> / <code>permit.limitOwn</code> works</a></p> <h3 id="example-5---limitown">Example 5 - limitOwn</h3> <p>A simple Array collection using <code>limitOwnReduce</code> &amp; lodash:</p> <div><pre class="line-numbers"><code class="language-js">// example 5 in action await(async () =&gt; { const { Permissions } = require(&#39;&#64;superawesome/permissions&#39;); const _ = require(&#39;lodash&#39;); const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; const isEven = (n) =&gt; n % 2 === 0; const isLarge = (n) =&gt; n &gt; 7; const isUserIdMatchesNumber = async ({ user, resourceId }) =&gt; user.id === resourceId; // Setting up PermissionDefinitions const permissions = new Permissions({ permissionDefinitions: [ { roles: &#39;EvenNumbersRole&#39;, isOwner: async ({ resourceId }) =&gt; isEven(resourceId), limitOwned: ({ user, context: predicates = [] }) =&gt; [isEven, ...predicates], grant: [&#39;list&#39;], }, { roles: &#39;LargeNumbersRole&#39;, isOwner: async ({ resourceId }) =&gt; isLarge(resourceId), limitOwned: ({ user, context: predicates = [] }) =&gt; [isLarge, ...predicates], grant: [&#39;list&#39;], }, { roles: &#39;UserIdMatchesNumberRole&#39;, isOwner: isUserIdMatchesNumber, limitOwned: ({ user, context: predicates = [] }) =&gt; [(number) =&gt; user.id === number, ...predicates], grant: [&#39;list&#39;], }, ], permissionDefinitionDefaults: { resource: &#39;numbers&#39;, possession: &#39;own&#39;, }, limitOwnReduce: ({ user, limitOwneds, context: predicates = [] }) =&gt; { for (const limitOwned of limitOwneds) { predicates = limitOwned({ user, context: predicates }); } return _.overSome(predicates); }, }).build(); // Granting permit for a given User at runtime, based on the above permissions. const permit = await permissions.grantPermit({ user: { id: 1, roles: [&#39;EvenNumbersRole&#39;, &#39;LargeNumbersRole&#39;, &#39;UserIdMatchesNumberRole&#39;], }, resource: &#39;numbers&#39;, action: &#39;list&#39;, }); return numbers.filter(permit.limitOwn()); }); // equals [1, 2, 4, 6, 8, 9, 10, 11, 12]; </code></pre></div><h3 id="example-6">Example 6</h3> <p>We could simplify Example 5 more, cause if we dont need the <code>context</code> value, we can just omit it.</p> <p>So by slightly adjusting our <code>limitOwnReduce</code> from example 5:</p> <div><pre class="line-numbers"><code class="language-js"> limitOwnReduce: ({ user, limitOwneds }) =&gt; _.overSome(limitOwneds.map(limitOwned =&gt; limitOwned({user}))),</code></pre></div><p>our <code>limitOwned</code> callbacks would also become much simpler:</p> <div><pre class="line-numbers"><code class="language-js"> { roles: &#39;EvenNumbersRole&#39;, limitOwned: () =&gt; isEven, ... }, { roles: &#39;LargeNumbersRole&#39;, limitOwned: () =&gt; isLarge, ... }, { roles: &#39;UserIdMatchesNumberRole&#39;, limitOwned: ({ user}) =&gt; (number) =&gt; user.id === number, ... }</code></pre></div><p>The final code is neater:</p> <div><pre class="line-numbers"><code class="language-js">// example 6 in action await(async () =&gt; { const { Permissions } = require(&#39;&#64;superawesome/permissions&#39;); const _ = require(&#39;lodash&#39;); const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; const isEven = (n) =&gt; n % 2 === 0; const isLarge = (n) =&gt; n &gt; 7; const isUserIdMatchesNumber = async ({ user, resourceId }) =&gt; user.id === resourceId; // Setting up PermissionDefinitions const permissions = new Permissions({ permissionDefinitions: [ { roles: &#39;EvenNumbersRole&#39;, isOwner: async ({ resourceId }) =&gt; isEven(resourceId), limitOwned: () =&gt; isEven, grant: [&#39;list&#39;], }, { roles: &#39;LargeNumbersRole&#39;, isOwner: async ({ resourceId }) =&gt; isLarge(resourceId), limitOwned: () =&gt; isLarge, grant: [&#39;list&#39;], }, { roles: &#39;UserIdMatchesNumberRole&#39;, isOwner: isUserIdMatchesNumber, limitOwned: ({ user }) =&gt; (number) =&gt; user.id === number, grant: [&#39;list&#39;], }, ], permissionDefinitionDefaults: { resource: &#39;numbers&#39;, possession: &#39;own&#39;, }, limitOwnReduce: ({ user, limitOwneds }) =&gt; _.overSome(limitOwneds.map((limitOwned) =&gt; limitOwned({ user }))), }).build(); // Granting permit for a given User at runtime, based on the above permissions. const permit = await permissions.grantPermit({ user: { id: 1, roles: [&#39;EvenNumbersRole&#39;, &#39;LargeNumbersRole&#39;, &#39;UserIdMatchesNumberRole&#39;], }, resource: &#39;numbers&#39;, action: &#39;list&#39;, }); return numbers.filter(permit.limitOwn()); }); // equals [1, 2, 4, 6, 8, 9, 10, 11, 12]; </code></pre></div> </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 = 'detailed-usage-&amp;-examples.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>