Skip to content

Commit 9f93d29

Browse files
committed
2.0.0
1 parent 35dc3fd commit 9f93d29

File tree

1 file changed

+116
-35
lines changed

1 file changed

+116
-35
lines changed

src/AccessControl.ts

Lines changed: 116 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Access, IAccessInfo, Query, IQueryInfo, Permission, AccessControlError } from './core';
22
import { Action, Possession, actions, possessions } from './enums';
3-
import utils from './utils';
3+
import { utils, ERR_LOCK } from './utils';
44

55
/**
66
* @classdesc
@@ -12,7 +12,7 @@ import utils from './utils';
1212
* array fetched from database) or simple omit `grants` parameter if you are
1313
* willing to build it programmatically.
1414
*
15-
* <p><pre><code> var grants = {
15+
* <p><pre><code> const grants = {
1616
* role1: {
1717
* resource1: {
1818
* "create:any": [ attrs ],
@@ -25,39 +25,39 @@ import utils from './utils';
2525
* },
2626
* role2: { ... }
2727
* };
28-
* var ac = new AccessControl(grants);</code></pre></p>
28+
* const ac = new AccessControl(grants);</code></pre></p>
2929
*
3030
* The `grants` object can also be an array, such as a flat list
3131
* fetched from a database.
3232
*
33-
* <p><pre><code> var flatList = [
34-
* { role: "role1", resource: "resource1", action: "create:any", attributes: [ attrs ] },
35-
* { role: "role1", resource: "resource1", action: "read:own", attributes: [ attrs ] },
36-
* { role: "role2", ... },
33+
* <p><pre><code> const flatList = [
34+
* { role: 'role1', resource: 'resource1', action: 'create:any', attributes: [ attrs ] },
35+
* { role: 'role1', resource: 'resource1', action: 'read:own', attributes: [ attrs ] },
36+
* { role: 'role2', ... },
3737
* ...
3838
* ];</code></pre></p>
3939
*
4040
* We turn this list into a hashtable for better performance. We aggregate
4141
* the list by roles first, resources second. If possession (in action
4242
* value or as a separate property) is omitted, it will default to `"any"`.
43-
* e.g. `"create"` —> `"create:any"`
43+
* e.g. `"create"` —» `"create:any"`
4444
*
4545
* Below are equivalent:
46-
* <p><pre><code> var grants = { role: "role1", resource: "resource1", action: "create:any", attributes: [ attrs ] }
47-
* var same = { role: "role1", resource: "resource1", action: "create", possession: "any", attributes: [ attrs ] }</code></pre></p>
46+
* <p><pre><code> const grants = { role: 'role1', resource: 'resource1', action: 'create:any', attributes: [ attrs ] }
47+
* const same = { role: 'role1', resource: 'resource1', action: 'create', possession: 'any', attributes: [ attrs ] }</code></pre></p>
4848
*
4949
* So we can also initialize with this flat list of grants:
50-
* <p><pre><code> var ac = new AccessControl(flatList);
50+
* <p><pre><code> const ac = new AccessControl(flatList);
5151
* console.log(ac.getGrants());</code></pre></p>
5252
*
53-
* @author Onur Yıldırım (onur@cutepilot.com)
53+
* @author Onur Yıldırım <onur@cutepilot.com>
5454
* @license MIT
5555
*
5656
* @class
5757
* @global
5858
*
5959
* @example
60-
* var ac = new AccessControl(grants);
60+
* const ac = new AccessControl(grants);
6161
*
6262
* ac.grant('admin').createAny('profile');
6363
*
@@ -83,7 +83,7 @@ import utils from './utils';
8383
* .deleteOwn('video');
8484
*
8585
* // now we can check for granted or denied permissions
86-
* var permission = ac.can('admin').readAny('profile');
86+
* const permission = ac.can('admin').readAny('profile');
8787
* permission.granted // true
8888
* permission.attributes // ["*", "!password"]
8989
* permission.filter(data) // { uid, email, address, account }
@@ -94,13 +94,17 @@ import utils from './utils';
9494
* // To add a grant but deny access via attributes
9595
* ac.grant('admin').createAny('profile', []); // no attributes allowed
9696
* ac.can('admin').createAny('profile').granted; // false
97+
*
98+
* // To prevent any more changes:
99+
* ac.lock();
97100
*/
98101
class AccessControl {
99102

100103
/**
101104
* @private
102105
*/
103106
private _grants:any;
107+
private _locked:boolean = false;
104108

105109
/**
106110
* Initializes a new instance of `AccessControl` with the given grants.
@@ -109,10 +113,26 @@ class AccessControl {
109113
* @param {Object|Array} grants - A list containing the access grant
110114
* definitions. See the structure of this object in the examples.
111115
*/
112-
constructor(grants:any = {}) {
116+
constructor(grants:any) {
117+
// explicit undefined is not allowed
118+
if (arguments.length === 0) grants = {};
113119
this.setGrants(grants);
114120
}
115121

122+
// -------------------------------
123+
// PUBLIC PROPERTIES
124+
// -------------------------------
125+
126+
/**
127+
* Specifies whether the underlying grants object is frozen and all
128+
* functionality for modifying it is disabled.
129+
* @name AccessControl#isLocked
130+
* @type {Boolean}
131+
*/
132+
get isLocked():boolean {
133+
return this._locked && Object.isFrozen(this._grants);
134+
}
135+
116136
// -------------------------------
117137
// PUBLIC METHODS
118138
// -------------------------------
@@ -166,23 +186,19 @@ class AccessControl {
166186
}
167187

168188
/**
169-
* Sets all access grants at once, from an object or array.
170-
* Note that this will reset the object and remove all previous grants.
189+
* Sets all access grants at once, from an object or array. Note that this
190+
* will reset the object and remove all previous grants.
171191
* @chainable
172192
*
173193
* @param {Object|Array} grantsObject - A list containing the access grant
174-
* definitions.
194+
* definitions. If `null` or `undefined`; grants will be reset
195+
* (emptied).
175196
*
176197
* @returns {AccessControl} - `AccessControl` instance for chaining.
177198
*/
178199
setGrants(grantsObject:any):AccessControl {
179-
this._grants = {};
180-
let type:string = utils.type(grantsObject);
181-
if (type === 'object') {
182-
this._grants = grantsObject;
183-
} else if (type === 'array') {
184-
grantsObject.forEach((item:any) => utils.commitToGrants(this._grants, item, true));
185-
}
200+
if (this.isLocked) throw new AccessControlError(ERR_LOCK);
201+
this._grants = utils.getInspectedGrants(grantsObject);
186202
return this;
187203
}
188204

@@ -193,10 +209,44 @@ class AccessControl {
193209
* @returns {AccessControl} - `AccessControl` instance for chaining.
194210
*/
195211
reset():AccessControl {
212+
if (this.isLocked) throw new AccessControlError(ERR_LOCK);
196213
this._grants = {};
197214
return this;
198215
}
199216

217+
/**
218+
* Freezes the underlying grants model and disables all functionality for
219+
* modifying it. This is useful when you want to restrict any changes. Any
220+
* attempts to modify (such as `#setGrants()`, `#reset()`, `#grant()`,
221+
* `#deny()`, etc) will throw after grants are locked. Note that <b>there
222+
* is no `unlock()` method</b>.
223+
*
224+
* Remember that this does not prevent the `AccessControl` instance from
225+
* being altered/replaced. Only the grants inner object is locked.
226+
*
227+
* <b>A note about performance</b>: This uses recursive `Object.freeze()`.
228+
* In NodeJS & V8, enumeration performance is not impacted because of this.
229+
* In fact, it increases the performance because of V8 optimization.
230+
* @chainable
231+
*
232+
* @returns {AccessControl} - `AccessControl` instance for chaining.
233+
*
234+
* @example
235+
* ac.grant('admin').create('product');
236+
* ac.lock(); // called on the AccessControl instance.
237+
* // or
238+
* ac.grant('admin').create('product').lock(); // called on the chained Access instance.
239+
*
240+
* // After this point, any attempt of modification will throw
241+
* ac.setGrants({}); // throws
242+
* ac.grant('user'); // throws..
243+
* // underlying grants model is not changed
244+
*/
245+
lock():AccessControl {
246+
utils.lockAC(this);
247+
return this;
248+
}
249+
200250
/**
201251
* Extends the given role(s) with privileges of one or more other roles.
202252
* @chainable
@@ -218,6 +268,7 @@ class AccessControl {
218268
* If a role is extended by itself or a non-existent role.
219269
*/
220270
extendRole(roles:string|string[], extenderRoles:string|string[]):AccessControl {
271+
if (this.isLocked) throw new AccessControlError(ERR_LOCK);
221272
utils.extendRole(this._grants, roles, extenderRoles);
222273
return this;
223274
}
@@ -232,6 +283,8 @@ class AccessControl {
232283
* @returns {AccessControl} - `AccessControl` instance for chaining.
233284
*/
234285
removeRoles(roles:string|string[]):AccessControl {
286+
if (this.isLocked) throw new AccessControlError(ERR_LOCK);
287+
235288
let rolesToRemove:string[] = utils.toStringArray(roles);
236289
rolesToRemove.forEach((role:string) => {
237290
delete this._grants[role];
@@ -260,6 +313,8 @@ class AccessControl {
260313
* @returns {AccessControl} - `AccessControl` instance for chaining.
261314
*/
262315
removeResources(resources:string|string[], roles?:string|string[]):AccessControl {
316+
if (this.isLocked) throw new AccessControlError(ERR_LOCK);
317+
263318
// _removePermission has a third argument `actionPossession`. if
264319
// omitted (like below), removes the parent resource object.
265320
this._removePermission(resources, roles);
@@ -279,6 +334,30 @@ class AccessControl {
279334
return Object.keys(this._grants);
280335
}
281336

337+
/**
338+
* Gets the list of inherited roles by the given role.
339+
* @name AccessControl#getInheritedRolesOf
340+
* @alias AccessControl#getExtendedRolesOf
341+
* @function
342+
*
343+
* @param {String} role - Target role name.
344+
*
345+
* @returns {Array<String>}
346+
*/
347+
getInheritedRolesOf(role:string):string[] {
348+
let roles:string[] = utils.getInheritedRolesOf(this._grants, role);
349+
roles.shift();
350+
return roles;
351+
}
352+
353+
/**
354+
* Alias of `getInheritedRolesOf`
355+
* @private
356+
*/
357+
getExtendedRolesOf(role:string):string[] {
358+
return this.getInheritedRolesOf(role);
359+
}
360+
282361
/**
283362
* Gets all the unique resources that are granted access for at
284363
* least one role.
@@ -326,7 +405,7 @@ class AccessControl {
326405
* This object provides chainable methods to define and query the access
327406
* permissions to be checked.
328407
* @name AccessControl#can
329-
* @alias AccessControl#access
408+
* @alias AccessControl#query
330409
* @function
331410
* @chainable
332411
*
@@ -339,7 +418,7 @@ class AccessControl {
339418
* See {@link ?api=ac#AccessControl~Query|`Query` inner class}.
340419
*
341420
* @example
342-
* var ac = new AccessControl(grants);
421+
* const ac = new AccessControl(grants);
343422
*
344423
* ac.can('admin').createAny('profile');
345424
* // equivalent to:
@@ -359,7 +438,7 @@ class AccessControl {
359438
* Alias of `can()`.
360439
* @private
361440
*/
362-
access(role:string|string[]|IQueryInfo):Query {
441+
query(role:string|string[]|IQueryInfo):Query {
363442
return this.can(role);
364443
}
365444

@@ -379,8 +458,8 @@ class AccessControl {
379458
* {@link ?api=ac#AccessControl~Permission|`Permission` inner class}.
380459
*
381460
* @example
382-
* var ac = new AccessControl(grants);
383-
* var permission = ac.permission({
461+
* const ac = new AccessControl(grants);
462+
* const permission = ac.permission({
384463
* role: "user",
385464
* action: "update:own",
386465
* resource: "profile"
@@ -412,8 +491,8 @@ class AccessControl {
412491
* See {@link ?api=ac#AccessControl~Access|`Access` inner class}.
413492
*
414493
* @example
415-
* var ac = new AccessControl(),
416-
* attributes = ['*'];
494+
* const ac = new AccessControl();
495+
* let attributes = ['*'];
417496
*
418497
* ac.grant('admin').createAny('profile', attributes);
419498
* // equivalent to:
@@ -448,7 +527,8 @@ class AccessControl {
448527
* // which means all attributes (of the resource) are allowed.
449528
*/
450529
grant(role:string|string[]|IAccessInfo):Access {
451-
return new Access(this._grants, role, false);
530+
if (this.isLocked) throw new AccessControlError(ERR_LOCK);
531+
return new Access(this, role, false);
452532
}
453533

454534
/**
@@ -480,7 +560,7 @@ class AccessControl {
480560
* See {@link ?api=ac#AccessControl~Access|`Access` inner class}.
481561
*
482562
* @example
483-
* var ac = new AccessControl();
563+
* const ac = new AccessControl();
484564
*
485565
* ac.deny('admin').createAny('profile');
486566
* // equivalent to:
@@ -510,7 +590,8 @@ class AccessControl {
510590
* ac.deny(['admin', 'user']).createOwn('profile');
511591
*/
512592
deny(role:string|string[]|IAccessInfo):Access {
513-
return new Access(this._grants, role, true);
593+
if (this.isLocked) throw new AccessControlError(ERR_LOCK);
594+
return new Access(this, role, true);
514595
}
515596

516597
/**

0 commit comments

Comments
 (0)