From a234938f0da5277443e56fc95e3066c88663aaf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=B8cker-Larsen?= Date: Thu, 19 Oct 2017 18:35:44 +0800 Subject: [PATCH] feat: Added strict mode --- index.js | 119 +++++++++++++++++++++++++++++++++++++++++------ test/Acl.test.js | 7 +++ 2 files changed, 112 insertions(+), 14 deletions(-) diff --git a/index.js b/index.js index e43dda6..5f4e948 100644 --- a/index.js +++ b/index.js @@ -1,18 +1,22 @@ -// TODO consider Guad instead class Acl { - constructor() { + + /** + * constructor + * + * @access public + * @param {Boolean} {strict=false}={} Errors out on unknown verbs when true + * @returns {Acl} + */ + constructor({strict = false} = {}) { + this.strict = strict this.rules = new Map() this.policies = new Map() - this.subjectMapper = subject => { - if (typeof subject === 'string') { return subject } - return typeof subject === 'function' - ? subject.name - : subject.constructor.name - } } /** - * mixin + * Mix in augments your user class with a `can` function. This + * is optional and you can always cann `can` directly on your + * Acl instance. * * @access public * @param {Object} userClass A user class or contructor function @@ -24,32 +28,114 @@ class Acl { } } + + /** + * Rules are grouped by subjects and this default mapper tries to + * map any non falsy input to a subject name. + * + * This is important when you want to try a verb against a rule + * passing in an instance of a class. + * + * - strings becomes subjects + * - function's names are used for subject + * - objects's constructor name is used for subject + * + * Override this function if your models do not match this approach. + * + * E.g. say that you are using plain data objects with a type property + * to indicate the "class" of the object. + * + * acl.subjectMapper = subject => subject.type + * + * `can` will now use this function when you pass in your objects. + * + * @access public + * @param {Function|Object|string} subject + * @returns {string} A subject + */ + subjectMapper(subject) { + if (typeof subject === 'string') { return subject } + return typeof subject === 'function' + ? subject.name + : subject.constructor.name + } + /** - * rule + * You add rules by providing a verb, a subject and an optional + * test (that otherwise defaults to true). + * + * If the test is a function it will be evaluated with the params: + * user, subject, and subjectName. The test value is ultimately evaluated + * for thruthiness. * * @access public * @param {Array|string} verbs * @param {Function|Object|string} subject * @param {Boolean} test=true + * @returns {Acl} */ rule(verbs, subject, test = true) { + const subjectName = this.subjectMapper(subject) const verbs_ = Array.isArray(verbs) ? verbs : [verbs] - const subject_ = this.subjectMapper(subject) verbs_.forEach(verb => { - const rules = this.rules.get(subject_) || {} + const rules = this.rules.get(subjectName) || {} rules[verb] = test - this.rules.set(subject_, rules) + this.rules.set(subjectName, rules) }) return this } + /** + * You can group related rules into policies for a subject. The policies + * properties are verbs and they can plain values or functions. + * + * If the policy is a function it will be new'ed up before use. + * + * class Post { + * constructor() { + * this.view = true // no need for a functon + * + * this.delete = false // not really necessary since an abscent + * // verb has the same result + * }, + * edit(user, subject) { + * return subject.id === user.id + * } + * } + * + * Policies are useful for grouping rules and adding more comples logic. + * + * @access public + * @param {Object} policy A policy with properties that are verbs + * @param {Function|Object|string} subject + * @returns {Acl} + */ policy(policy, subject) { + const policy_ = typeof policy === 'function' ? new policy() : policy const subject_ = this.subjectMapper(subject) this.policies.set(subject_, policy) + return this } /** - * can + * Performs a test if a user can perform action on subject. + * + * The action is a verb and the subject can be anything the + * subjectMapper can map to a subject name. + * + * E.g. if you can to test if a user can delete a post you would + * pass the actual post. Where as if you are testing us a user + * can create a post you would pass the class function or a + * string. + * + * acl->can(user, 'create', Post) + * acl->can(user, 'edit', post) + * + * Note that these are also available on the user if you've used + * the mixin: + * + * user->can('create', Post) + * user->can('edit', post) * * @access public * @param {Object} user @@ -63,6 +149,11 @@ class Acl { if (typeof rules[verb] === 'function') { return Boolean(rules[verb](user, subject, subject_)) } + + if (this.strict && typeof rules[verb] === 'undefined') { + throw new Error(`Unknown verb "${verb}"`) + } + return Boolean(rules[verb]) } } diff --git a/test/Acl.test.js b/test/Acl.test.js index ad2c86f..f91f68a 100644 --- a/test/Acl.test.js +++ b/test/Acl.test.js @@ -54,6 +54,13 @@ test('Can eat apples (Object)', () => { expect(user.can('eat', new Apple())).toBe(true) }) +test('Throws on strict', () => { + const acl = new Acl({strict: true}) + acl.mixin(User) + const user = new User() + expect(user.can.bind(user, 'eat', new Apple())).toThrow('Unknown verb "eat"') +}) + test('Can create jobs complex', () => { const acl = new Acl() acl.mixin(User)