Skip to content

Commit

Permalink
feat(extra): adds permittedAttributesOf
Browse files Browse the repository at this point in the history
This function allows to extract permitted fields from Ability rules

Relates to #18
  • Loading branch information
stalniy committed Mar 21, 2018
1 parent 239df75 commit 078314f
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 43 deletions.
6 changes: 3 additions & 3 deletions packages/casl-ability/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
"build.es": "rollup -c ../../tools/rollup.es.js -e sift",
"build.umd": "rollup -c ../../tools/rollup.umd.js -e sift -n casl",
"build.es5m": "rollup -c ../../tools/rollup.es5m.js -e sift",
"build.extra.es": "rollup -c ../../tools/rollup.es.js -i src/query.js -o dist/es6/extra.js",
"build.extra.es5m": "rollup -c ../../tools/rollup.es5m.js -i src/query.js -o extra.js",
"build.extra.umd": "rollup -c ../../tools/rollup.umd.js -i src/query.js -o dist/umd/extra.js -n casl.extra",
"build.extra.es": "rollup -c ../../tools/rollup.es.js -i src/extra.js -o dist/es6/extra.js",
"build.extra.es5m": "rollup -c ../../tools/rollup.es5m.js -i src/extra.js -o extra.js",
"build.extra.umd": "rollup -c ../../tools/rollup.umd.js -i src/extra.js -o dist/umd/extra.js -n casl.extra",
"build.extra": "npm run build.extra.es && npm run build.extra.es5m && npm run build.extra.umd",
"build": "npm run build.es && npm run build.umd && npm run build.es5m && npm run build.extra"
},
Expand Down
8 changes: 1 addition & 7 deletions packages/casl-ability/spec/ability.spec.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import { AbilityBuilder, ForbiddenError, Ability } from '../src'
import './spec_helper'

class Post {
constructor(attrs) {
Object.assign(this, attrs)
}
}
import { Post } from './spec_helper'

describe('Ability', () => {
let ability
Expand Down
73 changes: 73 additions & 0 deletions packages/casl-ability/spec/permitted_fields.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { AbilityBuilder } from '../src'
import { permittedFieldsOf } from '../src/extra'
import { Post } from './spec_helper'

describe('permittedFieldsOf', () => {
it('returns an empty array for `Ability` with empty rules', () => {
const ability = AbilityBuilder.define(() => {})
expect(permittedFieldsOf(ability, 'read', 'Post')).to.be.empty
})

it('returns an empty array if none of rules contain fields', () => {
const ability = AbilityBuilder.define(can => can('read', 'Post'))

expect(permittedFieldsOf(ability, 'read', 'Post')).to.be.empty
})

it('returns a unique array of fields if there are duplicated fields across fields', () => {
const ability = AbilityBuilder.define(can => {
can('read', 'Post', ['title'])
can('read', 'Post', ['title', 'description'], { id: 1 })
})
const fields = permittedFieldsOf(ability, 'read', 'Post')

expect(fields).to.have.length(2)
expect(fields).to.have.all.members(['title', 'description'])
})

it('returns unique fields for array which contains direct and inverted rules', () => {
const ability = AbilityBuilder.define((can, cannot) => {
can('read', 'Post', ['title', 'description'])
cannot('read', 'Post', ['description'])
})
const fields = permittedFieldsOf(ability, 'read', 'Post')

expect(fields).to.have.length(1)
expect(fields).to.have.all.members(['title'])
})

it('allows to provide an option `fieldsFrom` which extract fields from rule', () => {
const ability = AbilityBuilder.define(can => can('read', 'Post'))
const fields = permittedFieldsOf(ability, 'read', 'Post', {
fieldsFrom: rule => rule.fields || ['title']
})

expect(fields).to.deep.equal(['title'])
})

describe('when `subject` is an instance', () => {
let ability

beforeEach(() => {
ability = AbilityBuilder.define((can, cannot) => {
can('read', 'Post', ['title'])
can('read', 'Post', ['title', 'description'], { id: 1 })
cannot('read', 'Post', ['description'], { private: true })
})
})

it('allows to return fields for specific instance', () => {
const post = new Post({ title: 'does not match conditions' })
const fields = permittedFieldsOf(ability, 'read', post)

expect(fields).to.deep.equal(['title'])
})

it('allows to return fields for subject instance which matches specified rule conditions', () => {
const post = new Post({ id: 1, title: 'matches conditions' })
const fields = permittedFieldsOf(ability, 'read', post)

expect(fields).to.deep.equal(['title', 'description'])
})
})
})
3 changes: 1 addition & 2 deletions packages/casl-ability/spec/query.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { AbilityBuilder } from '../src'
import { rulesToQuery } from '../src/query'
import './spec_helper'
import { rulesToQuery } from '../src/extra'

function toQuery(rules) {
return rulesToQuery(rules, rule => {
Expand Down
54 changes: 27 additions & 27 deletions packages/casl-ability/spec/spec_helper.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
(function(factory) {
if (typeof require === 'function' && typeof module !== 'undefined') {
require('chai').use(require('chai-spies'));
factory(require('chai'), global);
} else if (typeof window === 'object') {
window.global = window;
factory(window.chai, window);
}
})(function(chai) {
chai.Assertion.addMethod('allow', function(action, subject, field) {
const subjectRepresantation = prettifyObject(subject)
this.assert(
this._obj.can(action, subject, field),
`expected ability to allow ${action} on ${subjectRepresantation}`,
`expected ability to not allow ${action} on ${subjectRepresantation}`
);
});
import chai from 'chai'
import spies from 'chai-spies'

function prettifyObject(object) {
if (!object || typeof object === 'string') {
return object;
}
chai.use(spies)

if (typeof object === 'function') {
return object.name;
}
chai.Assertion.addMethod('allow', function(action, subject, field) {
const subjectRepresantation = prettifyObject(subject)
this.assert(
this._obj.can(action, subject, field),
`expected ability to allow ${action} on ${subjectRepresantation}`,
`expected ability to not allow ${action} on ${subjectRepresantation}`
);
});

const attrs = JSON.stringify(object);
return `${object.constructor.name} { ${attrs[0] === '{' ? attrs.slice(1, -1) : attrs} }`
function prettifyObject(object) {
if (!object || typeof object === 'string') {
return object;
}
});

if (typeof object === 'function') {
return object.name;
}

const attrs = JSON.stringify(object);
return `${object.constructor.name} { ${attrs[0] === '{' ? attrs.slice(1, -1) : attrs} }`
}

export class Post {
constructor(attrs) {
Object.assign(this, attrs)
}
}
10 changes: 7 additions & 3 deletions packages/casl-ability/src/ability.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,18 @@ export class Ability {
return false;
}

rulesFor(action, subject, field) {
possibleRulesFor(action, subject) {
const subjectName = this[PRIVATE_FIELD].subjectName(subject);
const { rules } = this[PRIVATE_FIELD];
const specificRules = rules.hasOwnProperty(subjectName) ? rules[subjectName][action] : null;
const generalRules = rules.hasOwnProperty('all') ? rules.all[action] : null;
const relevantRules = (specificRules || []).concat(generalRules || []);

return relevantRules.filter(rule => rule.matchesField(subject, field));
return (specificRules || []).concat(generalRules || []);
}

rulesFor(action, subject, field) {
return this.possibleRulesFor(action, subject)
.filter(rule => rule.isRelevantFor(subject, field));
}

cannot(...args) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,21 @@ export function rulesToQuery(rules, convert) {

return rules.length > 0 ? query : null;
}

const getRuleFields = rule => rule.fields;

export function permittedFieldsOf(ability, action, subject, options = {}) {
const fieldsFrom = options.fieldsFrom || getRuleFields;
const uniqueFields = ability.possibleRulesFor(action, subject).reduce((fields, rule) => {
const names = fieldsFrom(rule);

if (names && !rule.inverted) {
names.forEach(fields.add, fields);
}

return fields;
}, new Set());

return Array.from(uniqueFields)
.filter(field => ability.can(action, subject, field));
}
2 changes: 1 addition & 1 deletion packages/casl-ability/src/rule.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class Rule {
return !this._matches || typeof object === 'string' || this._matches(object);
}

matchesField(object, field) {
isRelevantFor(object, field) {
if (!this.fields) {
return true;
}
Expand Down

0 comments on commit 078314f

Please sign in to comment.