@superawesome/permissions
Version:
Fine grained permissions / access control with ownerships & attribute picking, done right.
1,284 lines (1,083 loc) • 52.8 kB
HTML
<!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 class">
<div class="content-data">
<ol class="breadcrumb">
<li>Classes</li>
<li>Permissions</li>
</ol>
<ul class="nav nav-tabs" role="tablist">
<li class="active">
<a href="#info" role="tab" id="info-tab" data-toggle="tab" data-link="info">Info</a>
</li>
<li >
<a href="#source" role="tab" id="source-tab" data-toggle="tab" data-link="source">Source</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane fade active in" id="c-info">
<p class="comment">
<h3>File</h3>
</p>
<p class="comment">
<code>src/Permissions.ts</code>
</p>
<p class="comment">
<h3>Description</h3>
</p>
<p class="comment">
<p>The main class - see <a href="/additional-documentation/basic-usage.html">Basic Usage</a></p>
</p>
<section>
<h3 id="index">Index</h3>
<table class="table table-sm table-bordered index-table">
<tbody>
<tr>
<td class="col-md-4">
<h6><b>Methods</b></h6>
</td>
</tr>
<tr>
<td class="col-md-4">
<ul class="index-list">
<li>
<span class="modifier">Public</span>
<a href="#addDefinitions">addDefinitions</a>
</li>
<li>
<span class="modifier">Public</span>
<a href="#build">build</a>
</li>
<li>
<span class="modifier">Public</span>
<a href="#compare">compare</a>
</li>
<li>
<span class="modifier">Public</span>
<a href="#getActions">getActions</a>
</li>
<li>
<span class="modifier">Public</span>
<a href="#getDefinitions">getDefinitions</a>
</li>
<li>
<span class="modifier">Public</span>
<a href="#getGrants">getGrants</a>
</li>
<li>
<span class="modifier">Public</span>
<a href="#getResources">getResources</a>
</li>
<li>
<span class="modifier">Public</span>
<a href="#getRoles">getRoles</a>
</li>
<li>
<span class="modifier">Public</span>
<span class="modifier">Async</span>
<a href="#grantPermit">grantPermit</a>
</li>
</ul>
</td>
</tr>
<tr>
<td class="col-md-4">
<h6><b>Accessors</b></h6>
</td>
</tr>
<tr>
<td class="col-md-4">
<ul class="index-list">
<li>
<a href="#isBuilt">isBuilt</a>
</li>
</ul>
</td>
</tr>
</tbody>
</table>
</section>
<section>
<h3 id="constructor">Constructor</h3>
<table class="table table-sm table-bordered">
<tbody>
<tr>
<td class="col-md-4">
<code>constructor(undefined: <a href="../interfaces/IPermissionsOptions.html">IPermissionsOptions<TUserId | TResourceId></a>)</code>
</td>
</tr>
<tr>
<td class="col-md-4">
<div class="io-line">Defined in <a href="" data-line="67" class="link-to-prism">src/Permissions.ts:67</a></div>
</td>
</tr>
<tr>
<td class="col-md-4">
<div>
<b>Parameters :</b>
<table class="params">
<thead>
<tr>
<td>Name</td>
<td>Type</td>
<td>Optional</td>
</tr>
</thead>
<tbody>
<tr>
<td>
<code><a href="../interfaces/IPermissionsOptions.html" target="_self" >IPermissionsOptions<TUserId | TResourceId></a></code>
</td>
<td>
No
</td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
</tbody>
</table>
</section>
<section>
<h3 id="methods">
Methods
</h3>
<table class="table table-sm table-bordered">
<tbody>
<tr>
<td class="col-md-4">
<a name="addDefinitions"></a>
<span class="name">
<b>
<span class="modifier">Public</span>
addDefinitions
</b>
<a href="#addDefinitions"><span class="icon ion-ios-link"></span></a>
</span>
</td>
</tr>
<tr>
<td class="col-md-4">
<span class="modifier-icon icon ion-ios-reset"></span>
<code>addDefinitions(permissionDefinitions: <a href="../undefineds/PermissionDefinition.html">PermissionDefinition<TUserId | TResourceId> | PermissionDefinition<TUserId, TResourceId>[]</a>, permissionDefinitionDefaults: <a href="../classes/PermissionDefinitionDefaults.html">PermissionDefinitionDefaults</a>)</code>
</td>
</tr>
<tr>
<td class="col-md-4">
<div class="io-line">Defined in <a href="" data-line="78"
class="link-to-prism">src/Permissions.ts:78</a></div>
</td>
</tr>
<tr>
<td class="col-md-4">
<div class="io-description">
<b>Parameters :</b>
<table class="params">
<thead>
<tr>
<td>Name</td>
<td>Type</td>
<td>Optional</td>
<td>Default value</td>
</tr>
</thead>
<tbody>
<tr>
<td>permissionDefinitions</td>
<td>
<code><a href="../miscellaneous/typealiases.html#PermissionDefinition" target="_self" >PermissionDefinition<TUserId | TResourceId> | PermissionDefinition<TUserId, TResourceId>[]</a></code>
</td>
<td>
No
</td>
<td>
</td>
</tr>
<tr>
<td>permissionDefinitionDefaults</td>
<td>
<code><a href="../classes/PermissionDefinitionDefaults.html" target="_self" >PermissionDefinitionDefaults</a></code>
</td>
<td>
No
</td>
<td>
<code>{}</code>
</td>
</tr>
</tbody>
</table>
</div>
<div>
</div>
<div class="io-description">
<b>Returns : </b> <code><a href="https://www.typescriptlang.org/docs/handbook/basic-types.html" target="_blank" >void</a></code>
</div>
<div class="io-description">
</div>
</td>
</tr>
</tbody>
</table>
<table class="table table-sm table-bordered">
<tbody>
<tr>
<td class="col-md-4">
<a name="build"></a>
<span class="name">
<b>
<span class="modifier">Public</span>
build
</b>
<a href="#build"><span class="icon ion-ios-link"></span></a>
</span>
</td>
</tr>
<tr>
<td class="col-md-4">
<span class="modifier-icon icon ion-ios-reset"></span>
<code>build()</code>
</td>
</tr>
<tr>
<td class="col-md-4">
<div class="io-line">Defined in <a href="" data-line="194"
class="link-to-prism">src/Permissions.ts:194</a></div>
</td>
</tr>
<tr>
<td class="col-md-4">
<div class="io-description">
<b>Returns : </b> <code>this</code>
</div>
</td>
</tr>
</tbody>
</table>
<table class="table table-sm table-bordered">
<tbody>
<tr>
<td class="col-md-4">
<a name="compare"></a>
<span class="name">
<b>
<span class="modifier">Public</span>
compare
</b>
<a href="#compare"><span class="icon ion-ios-link"></span></a>
</span>
</td>
</tr>
<tr>
<td class="col-md-4">
<span class="modifier-icon icon ion-ios-reset"></span>
<code>compare(permissions1: <a href="../classes/Permissions.html">Permissions<any | any></a>, permissions2: <a href="../classes/Permissions.html">Permissions<any | any></a>)</code>
</td>
</tr>
<tr>
<td class="col-md-4">
<div class="io-line">Defined in <a href="" data-line="383"
class="link-to-prism">src/Permissions.ts:383</a></div>
</td>
</tr>
<tr>
<td class="col-md-4">
<div class="io-description">
<b>Parameters :</b>
<table class="params">
<thead>
<tr>
<td>Name</td>
<td>Type</td>
<td>Optional</td>
<td>Default value</td>
</tr>
</thead>
<tbody>
<tr>
<td>permissions1</td>
<td>
<code><a href="../classes/Permissions.html" target="_self" >Permissions<any | any></a></code>
</td>
<td>
No
</td>
<td>
</td>
</tr>
<tr>
<td>permissions2</td>
<td>
<code><a href="../classes/Permissions.html" target="_self" >Permissions<any | any></a></code>
</td>
<td>
No
</td>
<td>
<code>this</code>
</td>
</tr>
</tbody>
</table>
</div>
<div>
</div>
<div class="io-description">
<b>Returns : </b> <code><a href="https://www.typescriptlang.org/docs/handbook/basic-types.html" target="_blank" >any</a></code>
</div>
<div class="io-description">
</div>
</td>
</tr>
</tbody>
</table>
<table class="table table-sm table-bordered">
<tbody>
<tr>
<td class="col-md-4">
<a name="getActions"></a>
<span class="name">
<b>
<span class="modifier">Public</span>
getActions
</b>
<a href="#getActions"><span class="icon ion-ios-link"></span></a>
</span>
</td>
</tr>
<tr>
<td class="col-md-4">
<span class="modifier-icon icon ion-ios-reset"></span>
<code>getActions()</code>
</td>
</tr>
<tr>
<td class="col-md-4">
<div class="io-line">Defined in <a href="" data-line="366"
class="link-to-prism">src/Permissions.ts:366</a></div>
</td>
</tr>
<tr>
<td class="col-md-4">
<div class="io-description">
<b>Returns : </b> <code>string[]</code>
</div>
</td>
</tr>
</tbody>
</table>
<table class="table table-sm table-bordered">
<tbody>
<tr>
<td class="col-md-4">
<a name="getDefinitions"></a>
<span class="name">
<b>
<span class="modifier">Public</span>
getDefinitions
</b>
<a href="#getDefinitions"><span class="icon ion-ios-link"></span></a>
</span>
</td>
</tr>
<tr>
<td class="col-md-4">
<span class="modifier-icon icon ion-ios-reset"></span>
<code>getDefinitions(filter?: literal type, consolidateFlag: boolean | "force")</code>
</td>
</tr>
<tr>
<td class="col-md-4">
<div class="io-line">Defined in <a href="" data-line="403"
class="link-to-prism">src/Permissions.ts:403</a></div>
</td>
</tr>
<tr>
<td class="col-md-4">
<div class="io-description"><p>Returns a list of the <code>PermissionDefinition</code> objects stored in this instance, with optional filtering & consolidations removing duplicates and redundant grants (<strong>WARNING</strong>: this is experimental)</p>
</div>
<div class="io-description">
<b>Parameters :</b>
<table class="params">
<thead>
<tr>
<td>Name</td>
<td>Type</td>
<td>Optional</td>
<td>Default value</td>
<td>Description</td>
</tr>
</thead>
<tbody>
<tr>
<td>filter</td>
<td>
<code>literal type</code>
</td>
<td>
Yes
</td>
<td>
</td>
<td>
<p>allows you to filter PDs:
Use an object eg <code>{ resource: 'document' }</code> as the <code>_.matches</code> iteratee shorthand.
If this <code>_.matches</code> object is used, the props used for filtering are considered "default" and are omitted from each PD.
OR use a function returning boolean for each PD, eg (pd) => pd.resource === 'document'
See <a href="https://lodash.com/docs/4.17.11#filter">https://lodash.com/docs/4.17.11#filter</a></p>
</td>
</tr>
<tr>
<td>consolidateFlag</td>
<td>
<code>boolean | "force"</code>
</td>
<td>
No
</td>
<td>
<code>false</code>
</td>
<td>
<p>is <strong>experimental</strong>, it tries to consolidate PermissionDefinitions, remove duplicates and merge compatible ones</p>
</td>
</tr>
</tbody>
</table>
</div>
<div>
</div>
<div class="io-description">
<b>Returns : </b> <code>Partial[]</code>
</div>
<div class="io-description">
</div>
</td>
</tr>
</tbody>
</table>
<table class="table table-sm table-bordered">
<tbody>
<tr>
<td class="col-md-4">
<a name="getGrants"></a>
<span class="name">
<b>
<span class="modifier">Public</span>
getGrants
</b>
<a href="#getGrants"><span class="icon ion-ios-link"></span></a>
</span>
</td>
</tr>
<tr>
<td class="col-md-4">
<span class="modifier-icon icon ion-ios-reset"></span>
<code>getGrants()</code>
</td>
</tr>
<tr>
<td class="col-md-4">
<div class="io-line">Defined in <a href="" data-line="376"
class="link-to-prism">src/Permissions.ts:376</a></div>
</td>
</tr>
<tr>
<td class="col-md-4">
<div class="io-description"><p>Returns a deep clone of <a href="https://onury.io/accesscontrol/?api=ac#AccessControl#getGrants"><code>AccessControl#getGrants()</code></a> (which according to its docs <code>Gets the internal grants object that stores all current grants.</code>), but omitting empty arrays eg <code>'rollover:any': []</code>.</p>
</div>
<div class="io-description">
<b>Returns : </b> <code><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/object" target="_blank" >object</a></code>
</div>
</td>
</tr>
</tbody>
</table>
<table class="table table-sm table-bordered">
<tbody>
<tr>
<td class="col-md-4">
<a name="getResources"></a>
<span class="name">
<b>
<span class="modifier">Public</span>
getResources
</b>
<a href="#getResources"><span class="icon ion-ios-link"></span></a>
</span>
</td>
</tr>
<tr>
<td class="col-md-4">
<span class="modifier-icon icon ion-ios-reset"></span>
<code>getResources()</code>
</td>
</tr>
<tr>
<td class="col-md-4">
<div class="io-line">Defined in <a href="" data-line="361"
class="link-to-prism">src/Permissions.ts:361</a></div>
</td>
</tr>
<tr>
<td class="col-md-4">
<div class="io-description">
<b>Returns : </b> <code>string[]</code>
</div>
</td>
</tr>
</tbody>
</table>
<table class="table table-sm table-bordered">
<tbody>
<tr>
<td class="col-md-4">
<a name="getRoles"></a>
<span class="name">
<b>
<span class="modifier">Public</span>
getRoles
</b>
<a href="#getRoles"><span class="icon ion-ios-link"></span></a>
</span>
</td>
</tr>
<tr>
<td class="col-md-4">
<span class="modifier-icon icon ion-ios-reset"></span>
<code>getRoles()</code>
</td>
</tr>
<tr>
<td class="col-md-4">
<div class="io-line">Defined in <a href="" data-line="356"
class="link-to-prism">src/Permissions.ts:356</a></div>
</td>
</tr>
<tr>
<td class="col-md-4">
<div class="io-description">
<b>Returns : </b> <code>string[]</code>
</div>
</td>
</tr>
</tbody>
</table>
<table class="table table-sm table-bordered">
<tbody>
<tr>
<td class="col-md-4">
<a name="grantPermit"></a>
<span class="name">
<b>
<span class="modifier">Public</span>
<span class="modifier">Async</span>
grantPermit
</b>
<a href="#grantPermit"><span class="icon ion-ios-link"></span></a>
</span>
</td>
</tr>
<tr>
<td class="col-md-4">
<span class="modifier-icon icon ion-ios-reset"></span>
<code>grantPermit(undefined: GrantPermitQuery<TUserId | TResourceId>)</code>
</td>
</tr>
<tr>
<td class="col-md-4">
<div class="io-line">Defined in <a href="" data-line="213"
class="link-to-prism">src/Permissions.ts:213</a></div>
</td>
</tr>
<tr>
<td class="col-md-4">
<div class="io-description"><p>The <code>grantPermit()</code> is the way to <em>query</em> the Permissions instance for granting permissions to a User.</p>
<p>The method responds with an instance of <a href="/classes/Permit.html">Permit</a> that holds all known information about the queried <strong>user</strong>, <strong>resource</strong> and <strong>action</strong>.</p>
<p>In short, the question is "can some of <code>user.roles</code> perform <code>action</code> either a) on <strong>any</strong> <code>resource</code> or b) on an <strong>own</strong> <code>resource</code> (AND the specific <code>resourceId</code> if passed)?</p>
<p>We are checking all roles for both <strong>any</strong> & <strong>own</strong>, while collecting all <code>isOwner</code> & <code>listOwned</code> and feed all known information into a <strong>Permit</strong> object.</p>
</div>
<div class="io-description">
<b>Parameters :</b>
<table class="params">
<thead>
<tr>
<td>Name</td>
<td>Type</td>
<td>Optional</td>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>GrantPermitQuery<TUserId | TResourceId></code>
</td>
<td>
No
</td>
</tr>
</tbody>
</table>
</div>
<div>
</div>
<div class="io-description">
<b>Returns : </b> <code><a href="../classes/Permit.html" target="_self" >Promise<Permit<TUserId, TResourceId>></a></code>
</div>
<div class="io-description">
<p>Promise<Permit> a Promise of a <a href="/classes/Permit.html">Permit</a> instance.</p>
</div>
</td>
</tr>
</tbody>
</table>
</section>
<section>
<h3 id="accessors">
Accessors
</h3>
<table class="table table-sm table-bordered">
<tbody>
<tr>
<td class="col-md-4">
<a name="isBuilt"></a>
<span class="name"><b>isBuilt</b><a href="#isBuilt"><span class="icon ion-ios-link"></span></a></span>
</td>
</tr>
<tr>
<td class="col-md-4">
<span class="accessor"><b>get</b><code>isBuilt()</code></span>
</td>
</tr>
<tr>
<td class="col-md-4">
<div class="io-line">Defined in <a href="" data-line="190" class="link-to-prism">src/Permissions.ts:190</a></div>
</td>
</tr>
<tr>
<td class="col-md-4">
<div class="io-description"><p>Check is this Permissions instance has been built (so no more .addDefinitions() allowed)</p>
</div>
<div class="io-description">
<b>Returns : </b> <code><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/boolean" target="_blank" >boolean</a></code>
</div>
</td>
</tr>
</tbody>
</table>
</section>
</div>
<div class="tab-pane fade tab-source-code" id="c-source">
<pre class="line-numbers compodoc-sourcecode"><code class="language-typescript">import * as _ from 'lodash';
import * as _f from 'lodash/fp';
import { diff } from 'json-diff';
import { AccessControl, IQueryInfo, Permission } from 'accesscontrol';
// own
import { AccessControlRe } from 'accesscontrol-re';
import {
EPossession,
GrantPermitQuery,
isValidIUser,
Tid,
TisOwner,
TlimitOwned,
TlimitOwnReduce,
TlistOwned,
} from './types';
import {
buildAccessControl,
deleteEmptyArrayKeys,
hasSomeOwnGrant,
stringify,
projectPDWithDefaultsToInternal,
} from './utils';
import { consolidatePermissionDefinitions } from './consolidations';
import { Permit } from './Permit.class';
import {
PermissionDefinition,
PermissionDefinitionDefaults,
PermissionDefinitionInternal,
} from './PermissionDefinitions';
import { getLogger } from './logger';
/**
The options passed at the `Permissions` constructor
*/
export interface IPermissionsOptions<
TUserId extends Tid = number,
TResourceId extends Tid = number
> {
permissionDefinitions?:
| PermissionDefinition<TUserId, TResourceId>
| PermissionDefinition<TUserId, TResourceId>[];
permissionDefinitionDefaults?: PermissionDefinitionDefaults;
limitOwnReduce?: TlimitOwnReduce<TUserId, any>;
}
/**
The main class - see [Basic Usage](/additional-documentation/basic-usage.html)
*/
export class Permissions<TUserId extends Tid = number, TResourceId extends Tid = number> {
private _permissionDefinitionsInternal: PermissionDefinitionInternal[] = [];
private _accessControl: AccessControl;
private _acre: AccessControlRe;
private _rolesNotFound = {};
private roles: string[];
private _limitOwnReduce: TlimitOwnReduce<TUserId, any>;
private _isBuilt = false;
constructor({
permissionDefinitions,
permissionDefinitionDefaults,
limitOwnReduce,
}: IPermissionsOptions<TUserId, TResourceId> = {}) {
this._limitOwnReduce = limitOwnReduce;
this.addDefinitions(permissionDefinitions || [], permissionDefinitionDefaults);
}
public addDefinitions(
permissionDefinitions:
| PermissionDefinition<TUserId, TResourceId>
| PermissionDefinition<TUserId, TResourceId>[],
permissionDefinitionDefaults: PermissionDefinitionDefaults = {}
) {
this.ensureHasNotBuild();
if (!permissionDefinitions)
throw new Error(
`SA-Permissions: in addDefinitions(), invalid permissionDefinitions: ${stringify(
permissionDefinitions
)}`
);
if (!_.isArray(permissionDefinitions)) permissionDefinitions = [permissionDefinitions];
const ipdsToAdd = permissionDefinitions.map(
projectPDWithDefaultsToInternal(permissionDefinitionDefaults)
);
// sanity checks before adding ipds
_.each(ipdsToAdd, (ipdToAdd, ipdToAddIdx): any => {
// if we are trying to redefine a role+resource+action:possession
// with DIFFERENT attributes (i.e non-strict) throw as its very dangerous!
const nonStrictDuplicatePds = this.filterPDsWithDuplicateGrantActions(ipdToAdd);
if (!_.isEmpty(nonStrictDuplicatePds)) {
const firstConflictingAction = _.findKey(
ipdToAdd.grant,
(attributes, action) => !!nonStrictDuplicatePds[0].grant[action]
);
throw new Error(
`SA-Permissions: InvalidPermissionDefinitionError: addDefinitions() redefining action error.
Action: "${firstConflictingAction}"
Action Attributes: ${stringify(ipdToAdd.grant[firstConflictingAction])}
While adding PD: ${stringify(ipdToAdd)}
Conflicted with PD: ${stringify(nonStrictDuplicatePds[0])}`
);
}
// if we are trying to redefine a role+resource+action:possession
// even with SAME different attributes (i.e very strict) warn as obsolete!
const strictDuplicatePds = this.filterPDsWithDuplicateGrantActions(ipdToAdd, true);
if (!_.isEmpty(strictDuplicatePds)) {
const firstConflictingAction = _.findKey(
ipdToAdd.grant,
(attributes, action) => !!strictDuplicatePds[0].grant[action]
);
getLogger().warn(
`addDefinitions() redefining action in a PD with same attributes is obsolete:`,
{
action: firstConflictingAction,
attributes: ipdToAdd.grant[firstConflictingAction],
permissionDefinition: ipdToAdd,
}
);
}
if (hasSomeOwnGrant(ipdToAdd)) {
const isOwnerFound = !!ipdToAdd.isOwner;
let listOwnedFound = !!ipdToAdd.listOwned;
let limitOwnedFound = !!ipdToAdd.limitOwned;
// on this PD
if (listOwnedFound && limitOwnedFound)
throw new Error(
`SA-Permissions: in addDefinitions() found BOTH "listOwned" & "limitOwned" callbacks in the added PermissionDefinition. Use one or the other, but not both. PermissionDefinition = ${JSON.stringify(
permissionDefinitions[ipdToAddIdx],
null,
2
)}`
);
// It has some OWN Grant, but no owner hooks found, throw
if (!isOwnerFound || (!listOwnedFound && !limitOwnedFound)) {
throw new Error(
`SA-Permissions: in addDefinitions() PermissionDefinition has 'own' action but no ${
!isOwnerFound ? '"isOwner"' : '"listOwned" nor "limitOwned"'
} callbacks are there. PermissionDefinition = ${stringify(ipdToAdd)} `
);
}
// check all for same resource as ipdToAdd
let conflictedPD;
for (const opd of this._permissionDefinitionsInternal) {
if (ipdToAdd.resource === opd.resource) {
listOwnedFound = listOwnedFound || !!opd.listOwned;
limitOwnedFound = limitOwnedFound || !!opd.limitOwned;
}
if (listOwnedFound && limitOwnedFound) {
conflictedPD = opd;
break;
}
}
if (listOwnedFound && limitOwnedFound)
throw new Error(
`SA-Permissions: in addDefinitions() found BOTH "listOwned" & "limitOwned" callbacks in some PermissionDefinition for resource "${
ipdToAdd.resource
}". Use one or the other, but not both.
Adding PD: ${stringify(permissionDefinitions[ipdToAddIdx])}
Conflicted with PD: ${stringify(conflictedPD)}`
);
}
this._permissionDefinitionsInternal.push(ipdToAdd); // all ok, add it!
});
}
/**
* Check is this Permissions instance has been built (so no more .addDefinitions() allowed)
*/
public get isBuilt(): boolean {
return this._isBuilt;
}
public build() {
this._isBuilt = true;
if (this._acre) return this;
[this._accessControl, this._acre] = buildAccessControl(this._permissionDefinitionsInternal);
this.roles = this.getRoles();
return this;
}
/**
The `grantPermit()` is the way to *query* the Permissions instance for granting permissions to a User.
The method responds with an instance of [Permit](/classes/Permit.html) that holds all known information about the queried **user**, **resource** and **action**.
In short, the question is "can some of `user.roles` perform `action` either a) on **any** `resource` or b) on an **own** `resource` (AND the specific `resourceId` if passed)?
We are checking all roles for both **any** & **own**, while collecting all `isOwner` & `listOwned` and feed all known information into a **Permit** object.
@return Promise<Permit> a Promise of a [Permit](/classes/Permit.html) instance.
*/
public async grantPermit({
// <TUserId extends Tid = number, TResourceId extends Tid = number>
user,
action,
resource,
resourceId,
}: GrantPermitQuery<TUserId, TResourceId>): Promise<Permit<TUserId, TResourceId>> {
this.ensureHasBuild();
if (!isValidIUser(user))
throw new Error(
'SA-Permissions: at grantPermit(), user is not a valid `interface IUser {id: TId; roles: string[];}`'
);
if (!this.getResources().includes(resource))
throw new Error(`SA-Permissions: at grantPermit(), Invalid resource: "${resource}"`);
if (action.split(':').length > 1)
throw new Error(
`SA-Permissions: at grantPermit(), Invalid action structure: "${action}". The colon ":" in the action is not allowed on grantPermit() and you must NOT specify ":own" or ":any" after the action at it. SA-Permissions always returns a Permit that checks for both any & own.`
);
let acPermission: Permission; // = { granted: false } as any;
let anyAcPermission: Permission;
let ownAcPermission: Permission;
// The `Permit` values
const isOwners: TisOwner<TUserId, TResourceId>[] = [];
const listOwneds: TlistOwned<TUserId, TResourceId>[] = [];
const limitOwneds: TlimitOwned<any, TUserId>[] = [];
// 2 passes: check all EPossession against all roles.
// if any permissions.granted is true, granted is true
// but continue to gather all permissions.attributes, isOwner & listOwned
for (const queryPossession of [EPossession.any, EPossession.own]) {
getLogger().debug('grantPermit: possession', { possession: queryPossession });
const unknownRoles = _.difference(user.roles, this.roles);
const roles = _.without(user.roles, ...unknownRoles);
_.each(unknownRoles, (rl) => {
if (!this._rolesNotFound[rl]) {
this._rolesNotFound[rl] = true;
getLogger().warn(
`SA-Permissions(): at grantPermit(), role not found: ${rl} (will not warn again about this role)`
);
}
});
const queryInfo: IQueryInfo = {
role: roles,
action: `${action}:${queryPossession}`,
resource,
};
try {
acPermission = this._acre.permission(queryInfo);
} catch (error) {
// @todo: handle
throw error;
}
getLogger().debug('grantPermit: this._accessControl.permission(queryInfo)', {
queryInfo,
'permission.granted': acPermission.granted,
'permission.attributes': acPermission.attributes,
});
switch (queryPossession) {
case EPossession.any: {
anyAcPermission = acPermission;
break;
}
case EPossession.own: {
ownAcPermission = acPermission;
if (ownAcPermission.granted) {
const matchingCpds = _.filter(this._permissionDefinitionsInternal, (pd) => {
return (
_.some(pd.roles, (pdRole) => user.roles.includes(pdRole)) &&
(resource === pd.resource || pd.resource === '*') &&
(!!(pd?.grant || {})[`${action}:${EPossession.own}`] ||
!!(pd?.grant || {})[`*:${EPossession.own}`] ||
!!(pd?.grant || {})[`${action}:${EPossession.any}`] ||
!!(pd?.grant || {})[`*:${EPossession.any}`])
);
});
// prettier-ignore
if (!anyAcPermission.granted && _.isEmpty(matchingCpds))
throw new Error(
`SA-Permissions: own access granted but no matching PermissionDefinitions found: ` +
`${stringify({ user, action, resource })}`,
);
_.each(matchingCpds, (cpd) => {
const { isOwner, listOwned, limitOwned } = cpd;
if (isOwner) isOwners.push(isOwner as any);
if (listOwned) listOwneds.push(listOwned as any);
if (limitOwned) limitOwneds.push(limitOwned as any);
});
}
break;
}
default:
throw new Error(
`SA-Permissions::grantPermit: invalid EPossession in queryPossession "${queryPossession}"`
);
}
}
const permit = new (Permit as any)( // constructor is best kept private, only we should use it!
user,
action,
resource,
resourceId,
anyAcPermission,
ownAcPermission,
_.uniq(isOwners),
_.uniq(listOwneds),
_.uniq(limitOwneds),
this._limitOwnReduce
);
// prettier-ignore
if (!anyAcPermission.granted && ownAcPermission.granted) {
// The following checks SHOULD NOT be needed, they should be caught at the addDefinitions() call. Please report to authors if you encounter them.
const createError = (butDetail: string) =>
new Error(`SA-Permissions: grantPermit() "OWN" access granted but ${butDetail
}. The error should have been caught at addDefinitions() call, please report to authors. GrantPermitQuery = ${
stringify({ user, action, resource, resourceId })}`);
if (_.isEmpty(isOwners)) throw createError('no "isOwner" ownership hook found');
if (_.isEmpty(listOwneds) && _.isEmpty(limitOwneds)) throw createError('no "listOwned" nor "limitOwned" ownership hooks found');
if (!_.isEmpty(listOwneds) && !_.isEmpty(limitOwneds)) throw createError('found BOTH "listOwned" & "limitOwned" ownership hooks. Use one or the other, but not both');
if (resourceId) (permit as any).resourceIdOwnPermissionGranted = await permit.isOwn(resourceId);
}
return permit as Permit<TUserId, TResourceId>;
}
// some helpers
public getRoles(): string[] {
this.ensureHasBuild();
return this._acre.getRoles();
}
public getResources(): string[] {
this.ensureHasBuild();
return this._acre.getResources();
}
public getActions(): string[] {
this.ensureHasBuild();
return this._acre.getActions();
}
/**
* Returns a deep clone of [`AccessControl#getGrants()`](https://onury.io/accesscontrol/?api=ac#AccessControl#getGrants) (which according to its docs `Gets the internal grants object that stores all current grants.`), but omitting empty arrays eg `'rollover:any': []`.
*
* @see https://onury.io/accesscontrol/?api=ac#AccessControl#getGrants
*/
public getGrants(): object {
// @todo: typings
this.ensureHasBuild();
// delete empty arrays, eg `'rollover:any': []` cause they are useless & break our `compare()`
return deleteEmptyArrayKeys(_.cloneDeep(this._accessControl.getGrants()));
}
public compare(permissions1: Permissions<any, any>, permissions2: Permissions<any, any> = this) {
return diff(permissions1.getGrants(), permissions2.getGrants());
}
// Grab accessControl.getGrants(), BUT delete all empty / denied grants that
/**
Returns a list of the `PermissionDefinition` objects stored in this instance, with optional filtering & consolidations removing duplicates and redundant grants (**WARNING**: this is experimental)
@param filter allows you to filter PDs:
* Use an object eg `{ resource: 'document' }` as the `_.matches` iteratee shorthand.
If this `_.matches` object is used, the props used for filtering are considered "default" and are omitted from each PD.
* OR use a function returning boolean for each PD, eg (pd) => pd.resource === 'document'
See https://lodash.com/docs/4.17.11#filter
@param consolidateFlag is **experimental**, it tries to consolidate PermissionDefinitions, remove duplicates and merge compatible ones
*/
public getDefinitions(
filter?: {
[key: string]: any;
},
consolidateFlag: boolean | 'force' = false
): Partial<PermissionDefinitionInternal>[] {
const filteredPDs = _f.flow(
_f.filter(filter),
_f.reject((opd) => _.isEmpty(opd.grant))
)(this._permissionDefinitionsInternal);
const resultPDs = consolidateFlag
? consolidatePermissionDefinitions(
filter,
consolidateFlag
)(_.cloneDeep(this._permissionDefinitionsInternal))
: filteredPDs;
const resultedSaPermissions = new Permissions({
permissionDefinitions: resultPDs,
permissionDefinitionDefaults: filter,
}).build();
const filteredInstancePermissions = new Permissions({
permissionDefinitions: filteredPDs as any,
permissionDefinitionDefaults: filter,
}).build();
const difference = this.compare(resultedSaPermissions, filteredInstancePermissions);
if (difference !== undefined) {
throw new Error(
`SA-Permissions: getDefinitions diff:
${stringify(difference)}
Existing grants:
${stringify(this.getGrants())}
Generated grants:
${stringify(resultedSaPermissions.getGrants())}
`
);
}
return resultPDs;
}
private ensureHasBuild() {
if (!this._acre)
throw new Error(
`SA-Permissions InvalidInvocation: calling permissions methods before having build()`
);
}
private ensureHasNotBuild() {
if (this._acre)
throw new Error(
`SA-Permissions InvalidInvocation: calling addDefinitions() after having build()`
)