Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,6 @@ build/Release
node_modules

.idea

/dist
/package-lock.json
1 change: 1 addition & 0 deletions browser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
global.atob = require('atob');
52 changes: 52 additions & 0 deletions browser/example.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>PBAC</title>
</head>
<body>
<p>Policies:</p>
<textarea id="policies" cols="80" rows="20">[
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["iam:CreateUser", "iam:UpdateUser", "iam:DeleteUser"],
"Resource": ["arn:aws:iam:::user/${req:UserName}"],
"Condition": {
"IpAddress": {
"req:IpAddress": "10.0.20.0/24"
}
}
}
]
}
]</textarea>

<p>Action:</p>
<input id="action" type="text" size="80" value="iam:CreateUser">

<p>Resource:</p>
<input id="resource" type="text" size="80" value="arn:aws:iam:::user/testuser">

<p>Context:</p>
<textarea id="context" cols="80" rows="10">{
"req": {
"IpAddress": "10.0.20.51",
"UserName": "testuser"
}
}</textarea>

<p><button type="button" onclick="checkPolicies()">Evaluate</button></p>

<p>Result: <span id="result">?</span></p>

<script src="https://cdn.jsdelivr.net/g/lodash@4(lodash.min.js+lodash.fp.min.js)"></script>
<script src="https://unpkg.com/ipaddr.js@1.6.0/lib/ipaddr.js"></script>
<script src="../dist/pbac.js"></script>
<script src="example.js"></script>
</body>
</html>
27 changes: 27 additions & 0 deletions browser/example.js
Original file line number Diff line number Diff line change
@@ -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;
}
}
68 changes: 48 additions & 20 deletions conditions.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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];
Expand All @@ -157,7 +186,6 @@ forEach(conditions, function (fn, condition) {
});
});
};

});
}, Object.keys(conditions));

module.exports = conditions;
19 changes: 16 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
51 changes: 24 additions & 27 deletions pbac.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 : {};
Expand All @@ -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({
Expand Down Expand Up @@ -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];
Expand All @@ -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;
},
});
Expand Down
14 changes: 11 additions & 3 deletions t/conditions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
32 changes: 32 additions & 0 deletions webpack.config.js
Original file line number Diff line number Diff line change
@@ -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',
},
};