diff --git a/.gitignore b/.gitignore index d532efc..8e4ff7e 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,6 @@ build/Release node_modules .idea + +/dist +/package-lock.json diff --git a/browser.js b/browser.js new file mode 100644 index 0000000..7103c68 --- /dev/null +++ b/browser.js @@ -0,0 +1 @@ +global.atob = require('atob'); diff --git a/browser/example.html b/browser/example.html new file mode 100644 index 0000000..2103c76 --- /dev/null +++ b/browser/example.html @@ -0,0 +1,52 @@ + + +
+ + + +Policies:
+ + +Action:
+ + +Resource:
+ + +Context:
+ + + + +Result: ?
+ + + + + + + diff --git a/browser/example.js b/browser/example.js new file mode 100644 index 0000000..b9519df --- /dev/null +++ b/browser/example.js @@ -0,0 +1,27 @@ +checkPolicies(); + +function checkPolicies() { + try { + const policies = JSON.parse(document.all.policies.value); + const action = document.all.action.value; + const resource = document.all.resource.value; + const context = JSON.parse(document.all.context.value); + + var pbac = new PBAC(policies, { + validateSchema: false, + validatePolicies: false, + }); + + var result = pbac.evaluate({ + action, + resource, + context, + }); + + document.all.result.textContent = JSON.stringify(result); + } catch (error) { + console.error(error); + + document.all.result.textContent = error.message; + } +} diff --git a/conditions.js b/conditions.js index 0f855ee..06c8867 100644 --- a/conditions.js +++ b/conditions.js @@ -1,15 +1,16 @@ 'use strict'; -const ipcheck = require('ipcheck'); - -const isString = require('lodash/isString'), - isBoolean = require('lodash/isBoolean'), - isNumber = require('lodash/isNumber'), - isArray = require('lodash/isArray'), - isUndefined = require('lodash/isUndefined'), - isEmpty = require('lodash/isEmpty'), - forEach = require('lodash/forEach'), - every = require('lodash/every'); +const ipaddr = require('ipaddr.js'); +const { + isString, + isBoolean, + isNumber, + isArray, + isUndefined, + isEmpty, + forEach, + every, +} = require('lodash/fp'); const conditions = { NumericEquals(a, b) { @@ -67,12 +68,24 @@ const conditions = { return !this.conditions.DateGreaterThan.apply(this, arguments); }, BinaryEquals(a, b) { - if (!isString(b) || !(a instanceof Buffer)) return false; - return a.equals(new Buffer(b, 'base64')); + if (process.env.BROWSER) { + if (!isString(b) || !(a instanceof Uint8Array)) return false; + const buf = new Uint8Array(atob(b).split('').map(function(s) { return s.charCodeAt(0); })); + return a.every(function(x, i) { return x === buf[i]; }); + } else { + if (!isString(b) || !(a instanceof Buffer)) return false; + return a.equals(new Buffer(b, 'base64')); + } }, BinaryNotEquals(a, b) { - if (!isString(b) || !(a instanceof Buffer)) return false; - return !a.equals(new Buffer(b, 'base64')); + if (process.env.BROWSER) { + if (!isString(b) || !(a instanceof Uint8Array)) return false; + const buf = new Uint8Array(atob(b).split('').map(function(s) { return s.charCodeAt(0); })); + return !a.every(function(x, i) { return x === buf[i]; }); + } else { + if (!isString(b) || !(a instanceof Buffer)) return false; + return !a.equals(new Buffer(b, 'base64')); + } }, ArnLike: function ArnLike(a, b) { if (!isString(b)) return false; @@ -97,7 +110,22 @@ const conditions = { return b ? isUndefined(a) : !isUndefined(a); }, IpAddress(a, b) { - return ipcheck.match(a, b); + try { + if (!a || !b) return false; + + const addr = ipaddr.parse(a); + + if (b.indexOf('/') !== -1) { + const range = ipaddr.parseCIDR(b); + return addr.match(range); + } + + const bddr = ipaddr.parse(b); + + return addr.toString() === bddr.toString(); + } catch(error) { + return false; + } }, NotIpAddress() { return !this.conditions.IpAddress.apply(this, arguments); @@ -136,18 +164,19 @@ const conditions = { }, }; -forEach(conditions, function (fn, condition) { +forEach(function (condition) { + const fn = conditions[condition]; conditions[condition + 'IfExists'] = function (a, b) { if (isUndefined(a)) return true; else return fn.apply(this, arguments); }; conditions['ForAllValues:' + condition] = function (a, b) { if (!isArray(a)) a = [a]; - return every(a, value => { + return every(value => { return b.find(key => { return fn.call(this, value, key); }); - }); + }, a); }; conditions['ForAnyValue:' + condition] = function (a, b) { if (!isArray(a)) a = [a]; @@ -157,7 +186,6 @@ forEach(conditions, function (fn, condition) { }); }); }; - -}); +}, Object.keys(conditions)); module.exports = conditions; diff --git a/package.json b/package.json index 5a8627f..6e64118 100644 --- a/package.json +++ b/package.json @@ -3,19 +3,32 @@ "version": "0.2.0", "description": "AWS IAM Policy inspired and (mostly) compatible evaluation engine", "dependencies": { - "ipcheck": "^0.1.0", + "ipaddr.js": "^1.6.0", "lodash": "^4.17.5", "z-schema": "^3.19.1" }, "scripts": { "test": "mocha --bail t/", - "docs": "cat output/intro.md LICENSE > README.md && doctoc --title Contents README.md" + "test:browser": "cross-env BROWSER=1 mocha --bail -r ./browser.js t/", + "docs": "cat output/intro.md LICENSE > README.md && doctoc --title Contents README.md", + "build": "webpack", + "prepublishOnly": "npm test && npm run test:browser && npm run build" }, "devDependencies": { + "atob": "^2.1.0", + "cross-env": "^5.1.4", "doctoc": "^1.3.1", "jsdox": "^0.4.9", - "mocha": "^2.2.5" + "mocha": "^2.2.5", + "webpack": "^4.4.1", + "webpack-cli": "^2.0.13" }, + "files": [ + "pbac.js", + "conditions.js", + "schema.json", + "dist/*" + ], "repository": { "type": "git", "url": "monken/node-pbac" diff --git a/pbac.js b/pbac.js index 2c962aa..863138d 100644 --- a/pbac.js +++ b/pbac.js @@ -2,21 +2,21 @@ const policySchema = require('./schema.json'); const conditions = require('./conditions'); const ZSchema = require('z-schema'); -const util = require('util'); -const isPlainObject = require('lodash/isPlainObject'); -const isBoolean = require('lodash/isBoolean'); -const isArray = require('lodash/isArray'); -const isUndefined = require('lodash/isUndefined'); -const isEmpty = require('lodash/isEmpty'); -const forEach = require('lodash/forEach'); -const every = require('lodash/every'); -const get = require('lodash/get'); - -const flow = require('lodash/fp/flow'); -const map = require('lodash/fp/map'); -const flatten = require('lodash/fp/flatten'); -const find = require('lodash/fp/find'); +const { + isPlainObject, + isBoolean, + isArray, + isUndefined, + isEmpty, + forEach, + every, + get, + flow, + map, + flatten, + find, +} = require('lodash/fp'); const PBAC = function constructor(policies, options) { options = isPlainObject(options) ? options : {}; @@ -40,31 +40,30 @@ Object.assign(PBAC.prototype, { this.policies.push.apply(this.policies, policies); }, addConditionsToSchema: function addConditionsToSchema() { - const definition = get(this.schema, 'definitions.Condition'); + const definition = get('definitions.Condition', this.schema); if (!definition) return; const props = definition.properties = {}; - forEach(this.conditions, function(condition, name) { + forEach(function(name) { props[name] = { type: 'object' }; - }, this); + }, Object.keys(this.conditions)); }, _validateSchema() { const validator = new ZSchema(); if (!validator.validateSchema(this.schema)) - this.throw('schema validation failed with', validator.getLastError()); + this.throw('schema validation failed with ' + validator.getLastError()); }, validate(policies) { policies = isArray(policies) ? policies : [policies]; const validator = new ZSchema({ noExtraKeywords: true, }); - return every(policies, policy => { + return every(policy => { const result = validator.validate(policy, this.schema); - if (!result) - this.throw('policy validation failed with', validator.getLastError()); + if (!result) this.throw('policy validation failed with ' + validator.getLastError()); return result; - }); + }, policies); }, evaluate(options) { options = Object.assign({ @@ -153,7 +152,7 @@ Object.assign(PBAC.prototype, { evaluateCondition(condition, context) { if (!isPlainObject(condition)) return true; const conditions = this.conditions; - return every(Object.keys(condition), key => { + return every(key => { const expression = condition[key]; const contextKey = Object.keys(expression)[0]; let values = expression[contextKey]; @@ -168,14 +167,12 @@ Object.assign(PBAC.prototype, { } else { return values.find(value => conditions[key].call(this, this.getContextValue(contextKey, context), value)); } - }); + }, Object.keys(condition)); }, throw(name, message) { - const args = [].slice.call(arguments, 2); - args.unshift(message); const e = new Error(); e.name = name; - e.message = util.format.apply(util, args); + e.message = message; throw e; }, }); diff --git a/t/conditions.js b/t/conditions.js index 16910b5..4cf4588 100644 --- a/t/conditions.js +++ b/t/conditions.js @@ -93,16 +93,24 @@ var tests = { ['2015-07-07T14 :00:00.123Z', '2015-07-07T15:00:00.123Z', false], ], BinaryEquals: [ - [new Buffer('SGVsbG8gV29ybGQ=', 'base64'), 'SGVsbG8gV29ybGQ=', true], + [fromBase64('SGVsbG8gV29ybGQ='), 'SGVsbG8gV29ybGQ=', true], ['SGVsbG8gV29ybGQ=', 'SGVsbG8gV29ybGQ=', false], ], BinaryNotEquals: [ - [new Buffer('SGVsbG8gV29ybGQ=', 'base64'), 'SGVsbG8gV29ybGQ=', false], - [new Buffer('SGVsbG8gV29ybGQ=', 'base64'), 'SGVsbG8gV29ybGq=', true], + [fromBase64('SGVsbG8gV29ybGQ='), 'SGVsbG8gV29ybGQ=', false], + [fromBase64('SGVsbG8gV29ybGQ='), 'SGVsbG8gV29ybGq=', true], ['SGVsbG8gV29ybGQ=', 'SGVsbG8gV29ybGQ=', false], ] }; +function fromBase64(str) { + if (process.env.BROWSER) { + return new Uint8Array(atob(str).split('').map(function(s) { return s.charCodeAt(0); })); + } else { + return new Buffer(str, 'base64'); + } +} + describe('conditions', function() { _.forEach(tests, function(list, fn) { it(fn, function() { diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..2543f31 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,32 @@ +const path = require('path'); +const webpack = require('webpack'); + +module.exports = { + entry: './pbac.js', + output: { + path: path.resolve(__dirname, 'dist'), + filename: 'pbac.js', + library: 'PBAC', + libraryTarget: 'umd', + }, + plugins: [ + new webpack.DefinePlugin({ + 'process.env.BROWSER': JSON.stringify(true), + }), + ], + externals: { + 'ipaddr.js': { + root: 'ipaddr', + commonjs: 'ipaddr.js', + commonjs2: 'ipaddr.js', + amd: 'ipaddr.js', + }, + 'lodash/fp': { + root: '_', + commonjs: 'lodash/fp', + commonjs2: 'lodash/fp', + amd: 'lodash/fp', + }, + 'z-schema': 'z-schema', + }, +};