@superawesome/permissions
Version:
Fine grained permissions / access control with ownerships & attribute picking, done right.
667 lines (626 loc) • 37.3 kB
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 & Examples</h1>
<p>Let'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> & <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>
<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 "own".</p>
<p>But the definition of <em>ownership</em> in our apps are arbitrary - it can be "Documents created by users of my company", or "Documents created by users I manage" or it could be any particular business rule such as <a href="https://github.com/onury/accesscontrol/issues/46#issue-330937936">"users that are friends" etc</a>.</p>
<p>We define these ownership definitions as "ownership hooks", 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 "own" possession rules.</p>
<h1 id="1-adding-permissiondefinitions--build">1. Adding PermissionDefinitions & build()</h1>
<p>With that in mind, lets convert the above "human permissions / business rules" 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: 'document' },
permissionDefinitions: [
{
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.',
isOwner: async ({ user, resourceId }) => isUserCreatorOfDocument({ user, resourceId }),
listOwned: async (user) => listUserCreatedDocuments(user),
possession: 'own',
grant: {
create: ['*', '!confidential'],
read: ['*', '!confidential'],
list: ['*', '!confidential'],
'list:any': ['title', 'date'],
},
},
{
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.',
isOwner: async ({ user, resourceId }) => listDocsOfMeAndMyManagedUsers(user).includes(resourceId),
listOwned: async (user) => listDocsOfMeAndMyManagedUsers(user),
possession: 'own',
grant: {
read: ['*', '!confidential', '!personal'],
review: ['*', '!confidential', '!personal'],
delete: ['*', '!confidential', '!personal'],
list: ['*', '!confidential', '!personal'],
'list:any': ['title', 'date', 'status'],
},
},
{
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.',
isOwner: async ({ user, resourceId }) => listDocsOfMeAndMyCompanyUsers(user).includes(resourceId),
listOwned: async (user) => listDocsOfMeAndMyCompanyUsers(user),
possession: 'own',
grant: ['read', 'update', 'review'],
},
{
roles: ['SUPER_ADMIN'],
resource: '*',
descr:
'> 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.',
grant: ['*'],
},
],
}).build();
</code></pre></div><p>Its a good practice to keep the human description close in the PD & 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 "read" a document.</p>
<div><pre class="line-numbers"><code class="language-js">const permit = await permissions.grantPermit({
user: { id: 1, roles: ['EMPLOYEE'] },
action: 'read',
resource: 'document',
});
</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 "read", so they can'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'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> & <code>listOwn()</code></a> way.</p>
<p>But also check the lazy <a href="/classes/Permit.html#limitOwn"><code>limitOwned</code> & <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 "owned" 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 & 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 & 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 "own" document.</p>
<div><pre class="line-numbers"><code class="language-js">await permit.attributes(100);
// equals
['*', '!confidential'];
</code></pre></div><p>We get the allowed attributes for an own document for this user, i.e all attributes except <code>'confidential'</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'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 "pick" only the allowed attributes & 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>'someRandomField'</code> since we have the "*" in our definition, but without <code>'confidential'</code>) :</p>
<div><pre class="line-numbers"><code class="language-js">await permit.pick({
id: 100,
title: 'Document 100 title',
date: '2020-02-19',
confidential: '100 secrets lie here',
someRandomField: 'Some random 100 value',
});
// equals
({ id: 100, title: 'Document 100 title', date: '2020-02-19', someRandomField: 'Some random 100 value' });
</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: 'Document 999 title',
date: '1920-02-19',
confidential: '999 secrets lie here',
someRandomField: 'Some random 999 value',
});
// equals
({});
</code></pre></div><h1 id="helpers-to-filter-pick--map">Helpers to filter, pick & 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: 'Document 999 title',
date: '1920-02-19',
confidential: '999 secrets lie here',
someRandomField: 'Some random 999 value',
},
{
id: 100,
title: 'Document 100 title',
date: '2020-02-19',
confidential: '100 secrets lie here',
someRandomField: 'Some random 100 value',
},
]);
// equals
[{ id: 100, title: 'Document 100 title', date: '2020-02-19', someRandomField: 'Some random 100 value' }];
</code></pre></div><p>Note: ideally you should be filtering your data layer before you reach here, and this is where <code>listOwn()</code> & <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: 'Document 999 title',
date: '1920-02-19',
confidential: '999 secrets lie here',
someRandomField: 'Some random 999 value',
},
{
id: 100,
title: 'Document 100 title',
date: '2020-02-19',
confidential: '100 secrets lie here',
someRandomField: 'Some random 100 value',
},
],
(doc) => ({
...doc,
title: doc.title.toUpperCase(),
someNewField: 'Some new value',
})
);
// equals
[
{},
{
id: 100,
title: 'DOCUMENT 100 TITLE',
date: '2020-02-19',
someRandomField: 'Some random 100 value',
someNewField: 'Some new value',
},
];
</code></pre></div><p>It returns an empty object for documents that aren't owned</p>
<h1 id="4-let-superawesome-permissions--permissiondefinitions-shape-your-apps-data">4. Let SuperAwesome Permissions & PermissionDefinitions shape your App's data</h1>
<h3 id="example-2">Example 2</h3>
<p>With the same EMPLOYEE user, lets grant permit for "list" action this time.</p>
<p>We see that the PD has both "list:own" & "list:any", with different set of attributes (i.e for non-own documents, I can only read title & date).</p>
<div><pre class="line-numbers"><code class="language-js">const permit = await permissions.grantPermit({
user: { id: 1, roles: ['EMPLOYEE'] },
action: 'list',
resource: 'document',
});
</code></pre></div><p>We indeed have "any":</p>
<div><pre class="line-numbers"><code class="language-js">permit.anyGranted && permit.ownGranted === true;
</code></pre></div><h4 id="why-you-should-be-picky">Why you should be picky</h4>
<p>Just having "list:any" access doesnt mean all Documents are created equally:</p>
<div><pre class="line-numbers"><code class="language-js">await permit.mapPick([
{
id: 999,
title: 'Document 999 title',
date: '1920-02-19',
confidential: '999 secrets lie here',
someRandomField: 'Some random 999 value',
},
{
id: 100,
title: 'Document 100 title',
date: '2020-02-19',
confidential: '100 secrets lie here',
someRandomField: 'Some random 100 value',
},
]);
// equals
[
{ title: 'Document 999 title', date: '1920-02-19' },
{ id: 100, title: 'Document 100 title', date: '2020-02-19', someRandomField: 'Some random 100 value' },
];
</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 "any" 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: 'Document 999 title',
date: '1920-02-19',
confidential: '999 secrets lie here',
someRandomField: 'Some random 999 value',
},
{
id: 100,
title: 'Document 100 title',
date: '2020-02-19',
confidential: '100 secrets lie here',
someRandomField: 'Some random 100 value',
},
]);
// equals
[
{ title: 'Document 999 title', date: '1920-02-19' },
{ id: 100, title: 'Document 100 title', date: '2020-02-19', someRandomField: 'Some random 100 value' },
];
</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 "own" and "non-own" items, when "any" 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: 'Document 100 title',
date: '2020-02-19',
confidential: '100 secrets lie here',
someRandomField: 'Some random 100 value',
});
// equals
({ id: 100, title: 'Document 100 title', date: '2020-02-19', someRandomField: 'Some random 100 value' });
</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: 'Document 999 title',
date: '1920-02-19',
confidential: '999 secrets lie here',
someRandomField: 'Some random 999 value',
});
// equals
({ title: 'Document 999 title', date: '1920-02-19' });
</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 & PermissionDefinitions as well. SuperAwesome Permissions follows this mantra, but there are some <a href="/additional-documentation/faq,-gotchas-&-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 > own).</p>
<div><pre class="line-numbers"><code class="language-js">const permit = await permissions.grantPermit({
user: { id: 2, roles: ['EMPLOYEE', 'EMPLOYEE_MANAGER'] },
action: 'create',
resource: 'document',
});
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 "owned" 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: 'read</code> , always for same <strong>User with id: 2</strong>, but with different roles in each attempted case, where all roles have the "read" 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: ['EMPLOYEE'] },
action: 'read',
resource: 'document',
});
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: ['EMPLOYEE_MANAGER'] },
action: 'read',
resource: 'document',
});
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: ['COMPANY_ADMIN'] },
action: 'read',
resource: 'document',
});
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: ['EMPLOYEE_MANAGER', 'COMPANY_ADMIN'] },
action: 'read',
resource: 'document',
});
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:'delete'</code> this time, again for same <strong>User with id: 2</strong>, where only the EMPLOYEE_MANAGER role has the "delete" 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: ['EMPLOYEE_MANAGER'] },
action: 'delete',
resource: 'document',
});
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: ['COMPANY_ADMIN'] },
action: 'delete',
resource: 'document',
});
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: ['EMPLOYEE_MANAGER', 'COMPANY_ADMIN'] },
action: 'delete',
resource: 'document',
});
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'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: ['EMPLOYEE', 'EMPLOYEE_MANAGER'] },
action: 'list',
resource: 'document',
});
await permit.attributes();
// for non-own, its the merged of "any" attributes 'EMPLOYEE' of 'EMPLOYEE_MANAGER', which is expected:
['date', 'status', 'title'];
</code></pre></div><p>We see that for own resources, again we get the merged of "own" 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
['*', '!confidential'];
</code></pre></div><p>We see that since ownership for <code>resourceId = 200</code> is established by both EMPLOYEE & 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 "read" their employee documents, BUT NOT their "personal" 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's created document, hence the "personal" 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:
['*', '!confidential']; // Attributes should really equal to ['*','!confidential','!personal'];
</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 "no more and no less".</p>
<p>So be aware of this glitch & 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'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> & lodash:</p>
<div><pre class="line-numbers"><code class="language-js">// example 5 in action
await(async () => {
const { Permissions } = require('@superawesome/permissions');
const _ = require('lodash');
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
const isEven = (n) => n % 2 === 0;
const isLarge = (n) => n > 7;
const isUserIdMatchesNumber = async ({ user, resourceId }) => user.id === resourceId;
// Setting up PermissionDefinitions
const permissions = new Permissions({
permissionDefinitions: [
{
roles: 'EvenNumbersRole',
isOwner: async ({ resourceId }) => isEven(resourceId),
limitOwned: ({ user, context: predicates = [] }) => [isEven, ...predicates],
grant: ['list'],
},
{
roles: 'LargeNumbersRole',
isOwner: async ({ resourceId }) => isLarge(resourceId),
limitOwned: ({ user, context: predicates = [] }) => [isLarge, ...predicates],
grant: ['list'],
},
{
roles: 'UserIdMatchesNumberRole',
isOwner: isUserIdMatchesNumber,
limitOwned: ({ user, context: predicates = [] }) => [(number) => user.id === number, ...predicates],
grant: ['list'],
},
],
permissionDefinitionDefaults: {
resource: 'numbers',
possession: 'own',
},
limitOwnReduce: ({ user, limitOwneds, context: predicates = [] }) => {
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: ['EvenNumbersRole', 'LargeNumbersRole', 'UserIdMatchesNumberRole'],
},
resource: 'numbers',
action: 'list',
});
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 }) => _.overSome(limitOwneds.map(limitOwned => 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: 'EvenNumbersRole',
limitOwned: () => isEven,
...
},
{
roles: 'LargeNumbersRole',
limitOwned: () => isLarge,
...
},
{
roles: 'UserIdMatchesNumberRole',
limitOwned: ({ user}) => (number) => 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 () => {
const { Permissions } = require('@superawesome/permissions');
const _ = require('lodash');
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
const isEven = (n) => n % 2 === 0;
const isLarge = (n) => n > 7;
const isUserIdMatchesNumber = async ({ user, resourceId }) => user.id === resourceId;
// Setting up PermissionDefinitions
const permissions = new Permissions({
permissionDefinitions: [
{
roles: 'EvenNumbersRole',
isOwner: async ({ resourceId }) => isEven(resourceId),
limitOwned: () => isEven,
grant: ['list'],
},
{
roles: 'LargeNumbersRole',
isOwner: async ({ resourceId }) => isLarge(resourceId),
limitOwned: () => isLarge,
grant: ['list'],
},
{
roles: 'UserIdMatchesNumberRole',
isOwner: isUserIdMatchesNumber,
limitOwned: ({ user }) => (number) => user.id === number,
grant: ['list'],
},
],
permissionDefinitionDefaults: {
resource: 'numbers',
possession: 'own',
},
limitOwnReduce: ({ user, limitOwneds }) =>
_.overSome(limitOwneds.map((limitOwned) => 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: ['EvenNumbersRole', 'LargeNumbersRole', 'UserIdMatchesNumberRole'],
},
resource: 'numbers',
action: 'list',
});
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-&-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>