From 3613fe9320a1612cb38e4788c83ab59afea3db4d Mon Sep 17 00:00:00 2001 From: Alexander Noxville Date: Mon, 25 Nov 2013 19:14:57 +0100 Subject: [PATCH 1/6] Mongoose backend added --- README.md | 427 +++---------------------------------- index.js | 3 +- package.json | 3 +- stores/mongoose_backend.js | 151 +++++++++++++ stores/mongoose_model.js | 31 +++ 5 files changed, 219 insertions(+), 396 deletions(-) create mode 100644 stores/mongoose_backend.js create mode 100644 stores/mongoose_model.js diff --git a/README.md b/README.md index 5034b1f..8f1c3cd 100644 --- a/README.md +++ b/README.md @@ -1,411 +1,50 @@ -relations -========= +Node Boilerplate Version 2 +========================== +*Requires Node v0.6.6 (or newer)* +node-boilerplate takes html-boilerplate, express, connect, jade and Socket.IO and organizes them into a ready to use website project. It's a fast way to get working on your Node website without having to worry about the setup. It takes care of all the boring parts, like setting up your views, 404 page, 500 page, getting the modules organized, etc... -entity relationship, role, and permissions API for Node.js +Node Boilerplate has 4 goals: -[![build status](https://secure.travis-ci.org/carlos8f/node-relations.png)](http://travis-ci.org/carlos8f/node-relations) +1. To end the repetition involved with starting a new Node website project +2. To never install anything outside of the project directory (For easier production deployment) +3. To make it easy to install additional modules within the project directory +4. To enable easy upgrade or freezing of project dependencies +(These goals are much easier to meet now that node includes the node_modules convention) -**relations** is a simple permissions API which uses a natural language approach. +To start a project: + + git clone git://github.com/robrighter/node-boilerplate.git mynewproject + cd mynewproject + ./initproject.sh +This will copy down all of the boilerplate files, organize them appropriately and init a fresh new git repository within which you can build your next big thing. -Contexts --------- -First, you'll create a **context**, which contains a list of roles which map to -actions. Here we'll create a context called `repos`, to model Github repositories. +To run the boilerplate template app: -```js -var relations = require('relations'); + node server.js -relations.define('repos', { - owner: ['pull', 'push', 'administrate'], - collaborator: ['pull', 'push'], - watcher: ['pull'] -}); -``` +Go to http://0.0.0.0:8081 and click on the send message link to see socket.io in action. -Defining the context makes available a method on `relations` that matches the -context name, in this case, `relations.repos()`. For permission checks, this is -the only method we'll need to call. -### Dynamic roles +Additional Features: -To add or modify roles at runtime, you can also use the following methods: +1. Creates a package.json file consistent with associated best practices (http://blog.nodejitsu.com/package-dependencies-done-right) +2. Adds .gitignore for the node_modules directory +3. Includes 404 page and associated route +4. Includes 500 page -```js -// add a role dynamically -relations.repos.addRole('scientist', ['test', 'hyphothesize']); -// update the actions for a role -relations.repos.updateRole('scientist', ['test', 'hypothesize', 'absquatulate']); -// remove a role -relations.repos.removeRole('scientist'); -``` +To add additional modules: -**Please note** that the role -> action map is defined exclusively in the code, -and not stored. If you run a cluster of servers, and choose to use dynamic roles, -you must call `addRole()` etc on ALL servers in the cluster (I suggest using -pub/sub). +Update the package.json file to include new module dependencies and run 'npm install'. -Declarations ------------- +**If you have a different set of default modules that you like to use, the structure is setup such that you can fork the project and replace the module dependencies outlined in the ./templates/apps/package.json file to best fit your needs and the initproject.sh script will initialize projects with your new set of modules.** -Now, we need to tell our app who has those roles for which repos. +Deployment +=============== -```js -relations.repos('Carlos is the owner of buffet.'); -``` +node-boilerplate is setup to be easily deployed on a Joyent Node SmartMachine. This means that: -This assigns the role `owner` to the subject `Carlos` for the object `buffet`. +1. The version of Node is defined in config.json and in package.json +2. The main script to run is server.js +3. The web server port is pulled from process.env.PORT -### Token replacements - -Note that the API has multiple syntaxes, and this is functionally equivalent: - -```js -relations.repos(':user is owner of :repo', {user: 'Carlos', repo: 'buffet'}); -``` - -As is this: - -```js -relations.repos('%s is an owner of %s', 'Carlos', 'buffet'); -``` - -To assign a role which should apply to all objects, simply leave the object out -of the sentence: - -```js -relations.repos('%s is a watcher.', 'Brian'); -``` - -**Note: Using token replacements is recommended, to prevent injection attacks!** - -### Syntax - -The syntax for a **declaration** consists of: - -``` - is [ a / an / the ] [ [ of / to / from / in / with ] ] [.] -``` - -Verb question -------------- - -To ask if a user can perform an action: - -```js -relations.repos('Can %s pull?', 'Brian', function (err, can) { - // can = true (based on "watcher" role) -}); -``` - -We can also check if an action can be performed on a specific object: - -```js -relations.repos('Can %s push to buffet?', 'Brian', function (err, can) { - // can = false (Brian doesn't have "owner" or "collaborator" roles) -}); -``` - -### Syntax - -The syntax for an **verb question** consists of: - -``` -( Can | can ) [ [ of / to / from / in / with ] ] [?] -``` - -Role question -------------- - -To check if a user has a role: - -```js -relations.repos('Is %s a collaborator of %s?', 'Brian', 'buffet', function (err, is) { - // is = false -}); -``` - -We can also leave the object out to check for a global role: - -```js -relations.repos('Is %s a %s?', 'Brian', 'watcher', function (err, is) { - // is = true -}); -``` - -### Syntax - -The syntax for a **role question** consists of: - -``` -( Is | is ) [ a / an / the ] [ [ of / to / from / in / with ] ] [?] -``` - -Verb request ------------- - -In addition to true/false checks, **relations** can return an array of objects -which match certain criteria. For example: - -```js -relations.repos('What can %s pull from?', 'Carlos', function (err, repos) { - // repos = ['buffet'] -}); -``` - -### Syntax - -The syntax for a **verb request** consists of: - -``` -( What | what ) can [ of / to / from / in / with ] [?] -``` - -Role request ------------- - -Also, we can ask for an array of objects a user has a role for: - -```js -relations.repos('What is %s the owner of?', 'Carlos', function (err, repos) { - // repos = ['buffet'] -}); -``` - -### Syntax - -The syntax for a **role request** consists of: - -``` -( What | what ) is [ a / an / the ] [ of / to / from / in / with ] [?] -``` - -Verb subject request --------------------- - -To request an array of subjects who can perform an action on an object: - -```js -relations.repos('Who can pull from %s?', 'buffet', function (err, users) { - // users = ['Carlos'] -}); -``` - -### Syntax - -``` -( Who | who ) can [ of / to / from / in / with ] [?] -``` - -Role subject request --------------------- - -To request an array of subjects who have a role for an object: - -```js -relations.repos('Who is the owner of %s?', 'buffet', function (err, users) { - // users = ['Carlos'] -}); -``` - -### Syntax - -``` -( Who | who ) is [ a / an / the ] [ of / to / from / in / with ] [?] -``` - -Object verb request ---------------------------- - -To request an array of verbs a subject can perform on an object: - -```js -relations.repos('What actions can %s do with %s?', 'Carlos', 'buffet', function (err, verbs) { - // verbs = ['pull', 'push', 'administrate'] -}); -``` - -### Syntax - -``` -What actions can do [ of / to / from / in / with ] [?] -``` - -Revocation ----------- - -To revoke a role: - -```js -relations.repos('%s is not the owner of %s', 'Carlos', 'buffet'); -``` - -### Syntax - -``` - ( is not | isn't ) [ a / an / the ] [ [ of / to / from / in / with ] ] [.] -``` - -Pluggable data store --------------------- - -**relations** uses a memory store out-of-the-box, which only works with a single -node processes and has no persistence. Two data stores are also provided -however: Redis and MySQL. - -### Redis store - -To use the redis store, your app must make a -[node_redis](https://github.com/mranney/node_redis) client and pass it like so: - -```js -var relations = require('relations') - , redis = require('redis') - -relations.use(relations.stores.redis, { - client: redis.createClient(), - prefix: 'optional-key-prefix' -}); -``` - -### MySQL store - -To use the MySQL store, your app must make a -[node-mysql](https://github.com/felixge/node-mysql) client and pass it like so: - -```js -var relations = require('relations') - , mysql = require('mysql') - -relations.use(relations.stores.mysql, {client: mysql.createConnection({user: 'root', database: 'test'})}); -``` - -### Make your own store - -A **relations** store is simply a node module that exports an event emitter -and responds to the following events: - -#### `init` (options, cb) - -Initialize the store with `options` (from `relations.use()`) and call `cb(err)` -when done. - -#### `declaration` (cmd, cb) - -Respond to a declaration and call `cb()` when done. `cmd` will be an object -containing the properties: - -- ctx - context object -- subject -- role -- object (optional) - -#### `revocation` (cmd, cb) - -Respond to a revocation and call `cb()` when done. `cmd` will be an object -containing the properties: - -- ctx - context object -- subject -- role -- object (optional) - -### `verb-question` (cmd, cb) - -Respond to a verb question and call `cb(err, /* boolean */ can)` with the result. -`cmd` will be an object containing the properties: - -- ctx - context object -- subject -- verb -- object (optional) - -### `role-question` (cmd, cb) - -Respond to a role question and call `cb(err, /* boolean */ is)` with the result. -`cmd` will be an object containing the properties: - -- ctx - context object -- subject -- role -- object (optional) - -### `verb-request` (cmd, cb) - -Respond to a verb request and call `cb(err, /* array */ objects)` with the result. -`cmd` will be an object containing the properties: - -- ctx - context object -- subject -- verb - -### `role-request` (cmd, cb) - -Respond to a role request and call `cb(err, /* array */ objects)` with the result. -`cmd` will be an object containing the properties: - -- ctx - context object -- subject -- role - -### `verb-subject-request` (cmd, cb) - -Respond to a verb subject request and call `cb(err, /* array */ subjects)` with -the result. `cmd` will be an object containing the properties: - -- ctx - context object -- verb -- object - -### `role-subject-request` (cmd, cb) - -Respond to a role subject request and call `cb(err, /* array */ subjects)` with -the result. `cmd` will be an object containing the properties: - -- ctx - context object -- role -- object - -### `object-verb-request` (cmd, cb) - -Respond to a object verb request and call `cb(err, /* array */ verbs)` with -the result. `cmd` will be an object containing the properties: - -- ctx - context object -- object -- subject - -### `reset` (cb) - -Reset the store, dumping all storage and structure, calling `cb(err)` when done. - -- - - - -### Developed by [Terra Eclipse](http://www.terraeclipse.com) -Terra Eclipse, Inc. is a nationally recognized political technology and -strategy firm located in Aptos, CA and Washington, D.C. - -- - - - -### License: MIT - -- Copyright (C) 2012 Carlos Rodriguez (http://s8f.org/) -- Copyright (C) 2012 Terra Eclipse, Inc. (http://www.terraeclipse.com/) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished -to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/index.js b/index.js index 1e3d961..00d5d2a 100644 --- a/index.js +++ b/index.js @@ -103,7 +103,8 @@ relations.define = function (name, structure) { relations.stores = { memory: require('./stores/memory'), mysql: require('./stores/mysql'), - redis: require('./stores/redis') + redis: require('./stores/redis'), + mongoose: require('./stores/mongoose_backend') }; relations.use = function (store, options) { diff --git a/package.json b/package.json index 7965caa..01058f0 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "main": "index.js", "dependencies": { "eventflow": "0.0.6", - "async": "~0.1.22" + "async": "~0.1.22", + "mongoose": "~3.8.1" }, "devDependencies": { "mocha": "*", diff --git a/stores/mongoose_backend.js b/stores/mongoose_backend.js new file mode 100644 index 0000000..39f745b --- /dev/null +++ b/stores/mongoose_backend.js @@ -0,0 +1,151 @@ +/** + * @author Jan Zaloudek + * @date 27.10.13 + * @time 16:38 + * @filename + */ + +var store = module.exports = require('eventflow')(); +var RelationModel = require('./mongoose_model.js'); + +store.on('init', function (options, cb) { + cb(); +}); + +store.on('declaration', function (cmd, cb) { + var relation = new RelationModel({ + context: cmd.ctx.name, + subject: cmd.subject, + role: cmd.role, + object: cmd.object || "" + }); + + relation.save(function(err) { + if(err && err.code === 11000) return cb(); + + return cb(err); + }); +}); + +store.on('revocation', function (cmd, cb) { + RelationModel.findOneAndRemove({ + context: cmd.ctx.name, + subject: cmd.subject, + role: cmd.role, + object: cmd.object + }, cb); +}); + +store.on('verb-question', function (cmd, cb) { + RelationModel.findOne({ + context: cmd.ctx.name, + subject: cmd.subject, + role: { $in: cmd.ctx.verbs[cmd.verb] }, + $or: [{ + object: cmd.object || "", + }, + { + object: '' + }] + }, function(err, relation) { + if(err) return cb(err); + + if(relation) { + return cb(null, true); + } + + cb(null, false); + }); +}); + +store.on('role-question', function (cmd, cb) { + RelationModel.findOne({ + context: cmd.ctx.name, + subject: cmd.subject, + role: cmd.role, + $or: [{ + object: cmd.object || "", + }, + { + object: '' + }] + }, function(err, relation) { + if(err) { + return cb(err); + } + + if(relation) { + return cb(null, true); + } + + cb(null, false); + }); +}); + +store.on('verb-request', function (cmd, cb) { + RelationModel.find({ + context: cmd.ctx.name, + subject: cmd.subject, + role: cmd.ctx.verbs[cmd.verb], + object: { $ne: '' } + }, 'object', function(err, relations) { + if(err) return cb(err); + + cb(null, relation.map(function(relation) { return relation.object; })); + }); +}); + +store.on('role-request', function (cmd, cb) { + RelationModel.find({ + context: cmd.ctx.name, + subject: cmd.subject, + role: cmd.role, + object: { $ne: '' } + }, 'object', function(err, relations) { + if(err) return cb(err); + + cb(null, relations.map(function(relation) { return relation.object; })); + }); +}); + +store.on('verb-subject-request', function (cmd, cb) { + RelationModel.find({ + context: cmd.ctx.name, + object: cmd.object, + role: { $in: cmd.ctx.verbs[cmd.verb] } + }, 'subject', function(err, relations) { + if(err) return cb(err); + + cb(null, relations.map(function(relation) { return relation.subject; })); + }); +}); + +store.on('role-subject-request', function (cmd, cb) { + RelationModel.find({ + context: cmd.ctx.name, + object: cmd.object, + role: cmd.role + }, 'subject', function(err, relations) { + if(err) return cb(err); + + cb(null, relations.map(function(relation) { return relation.subject; })); + }); +}); + +store.on('object-verb-request', function (cmd, cb) { + RelationModel.find({ + context: cmd.ctx.name, + object: cmd.object, + subject: cmd.subject + }, 'role', function(err, relations) { + if(err) return cb(err); + + cb(null, relations.reduce(function (verbs, row) { + return verbs.concat( cmd.ctx.roles[row.role] || [] ); + }, [])); + }); +}); + +store.on('reset', function (cb) { + RelationModel.remove({}, cb); +}); diff --git a/stores/mongoose_model.js b/stores/mongoose_model.js new file mode 100644 index 0000000..89c1fb5 --- /dev/null +++ b/stores/mongoose_model.js @@ -0,0 +1,31 @@ +/** + * @author Jan Zaloudek + * @date 28.10.13 + * @time 0:25 + * @filename + */ + +var mongoose = require('mongoose'); +var Schema = mongoose.Schema; + + +//client.query("CREATE TABLE IF NOT EXISTS `relations` (" +// + "`context` VARCHAR(255) NOT NULL," +// + "`subject` VARCHAR(255) NOT NULL," +// + "`role` VARCHAR(255) NOT NULL," +// + "`object` VARCHAR(255) NOT NULL," +// + "PRIMARY KEY (`context`, `subject`, `role`, `object`)," +// + "INDEX object (`context`, `role`, `object`)" +// + ") ENGINE=InnoDB", cb); + +var RelationSchema = new Schema({ + context: String, + subject: String, + role: String, + object: String +}); + +RelationSchema.index({ context: 1, subject: 1, role: 1, object: 1 }, { unique: true }); +RelationSchema.index({ context: 1, role: 1, object: 1 }); + +module.exports = mongoose.model('Relation', RelationSchema); \ No newline at end of file From 545d5bd028d14550bf677fde6813c08eab796c58 Mon Sep 17 00:00:00 2001 From: Alexander Noxville Date: Tue, 17 Dec 2013 18:36:03 +0100 Subject: [PATCH 2/6] Object - Role request --- README.md | 50 --------------- parser.js | 122 ++++++++++++++++++++++++++++++++++--- parser.pegjs | 10 +++ stores/mongoose_backend.js | 20 +++++- test/basic.js | 14 +++-- test/common.js | 9 +++ 6 files changed, 161 insertions(+), 64 deletions(-) delete mode 100644 README.md diff --git a/README.md b/README.md deleted file mode 100644 index 8f1c3cd..0000000 --- a/README.md +++ /dev/null @@ -1,50 +0,0 @@ -Node Boilerplate Version 2 -========================== -*Requires Node v0.6.6 (or newer)* -node-boilerplate takes html-boilerplate, express, connect, jade and Socket.IO and organizes them into a ready to use website project. It's a fast way to get working on your Node website without having to worry about the setup. It takes care of all the boring parts, like setting up your views, 404 page, 500 page, getting the modules organized, etc... - -Node Boilerplate has 4 goals: - -1. To end the repetition involved with starting a new Node website project -2. To never install anything outside of the project directory (For easier production deployment) -3. To make it easy to install additional modules within the project directory -4. To enable easy upgrade or freezing of project dependencies -(These goals are much easier to meet now that node includes the node_modules convention) - -To start a project: - - git clone git://github.com/robrighter/node-boilerplate.git mynewproject - cd mynewproject - ./initproject.sh -This will copy down all of the boilerplate files, organize them appropriately and init a fresh new git repository within which you can build your next big thing. - - -To run the boilerplate template app: - - node server.js - -Go to http://0.0.0.0:8081 and click on the send message link to see socket.io in action. - - -Additional Features: - -1. Creates a package.json file consistent with associated best practices (http://blog.nodejitsu.com/package-dependencies-done-right) -2. Adds .gitignore for the node_modules directory -3. Includes 404 page and associated route -4. Includes 500 page - -To add additional modules: - -Update the package.json file to include new module dependencies and run 'npm install'. - -**If you have a different set of default modules that you like to use, the structure is setup such that you can fork the project and replace the module dependencies outlined in the ./templates/apps/package.json file to best fit your needs and the initproject.sh script will initialize projects with your new set of modules.** - -Deployment -=============== - -node-boilerplate is setup to be easily deployed on a Joyent Node SmartMachine. This means that: - -1. The version of Node is defined in config.json and in package.json -2. The main script to run is server.js -3. The web server port is pulled from process.env.PORT - diff --git a/parser.js b/parser.js index ba7b0a3..18b8030 100644 --- a/parser.js +++ b/parser.js @@ -47,6 +47,7 @@ module.exports = (function(){ "VerbSubjectRequest": parse_VerbSubjectRequest, "RoleSubjectRequest": parse_RoleSubjectRequest, "ObjectVerbRequest": parse_ObjectVerbRequest, + "ObjectRoleRequest": parse_ObjectRoleRequest, "Token": parse_Token, "NamedToken": parse_NamedToken, "UnnamedToken": parse_UnnamedToken, @@ -121,21 +122,24 @@ module.exports = (function(){ pos0 = pos; result0 = parse_ObjectVerbRequest(); if (result0 === null) { - result0 = parse_RoleSubjectRequest(); + result0 = parse_ObjectRoleRequest(); if (result0 === null) { - result0 = parse_VerbSubjectRequest(); + result0 = parse_RoleSubjectRequest(); if (result0 === null) { - result0 = parse_RoleQuestion(); + result0 = parse_VerbSubjectRequest(); if (result0 === null) { - result0 = parse_RoleRequest(); + result0 = parse_RoleQuestion(); if (result0 === null) { - result0 = parse_Declaration(); + result0 = parse_RoleRequest(); if (result0 === null) { - result0 = parse_Revocation(); + result0 = parse_Declaration(); if (result0 === null) { - result0 = parse_VerbRequest(); + result0 = parse_Revocation(); if (result0 === null) { - result0 = parse_VerbQuestion(); + result0 = parse_VerbRequest(); + if (result0 === null) { + result0 = parse_VerbQuestion(); + } } } } @@ -1091,6 +1095,108 @@ module.exports = (function(){ return result0; } + function parse_ObjectRoleRequest() { + var result0, result1, result2, result3, result4, result5, result6; + var pos0, pos1; + + reportFailures++; + pos0 = pos; + pos1 = pos; + if (input.substr(pos, 16).toLowerCase() === "what roles does ") { + result0 = input.substr(pos, 16); + pos += 16; + } else { + result0 = null; + if (reportFailures === 0) { + matchFailed("\"what roles does \""); + } + } + if (result0 !== null) { + result1 = parse_Token(); + if (result1 !== null) { + if (input.substr(pos, 5).toLowerCase() === " have") { + result2 = input.substr(pos, 5); + pos += 5; + } else { + result2 = null; + if (reportFailures === 0) { + matchFailed("\" have\""); + } + } + if (result2 !== null) { + result3 = parse_Preposition(); + if (result3 !== null) { + if (input.charCodeAt(pos) === 32) { + result4 = " "; + pos++; + } else { + result4 = null; + if (reportFailures === 0) { + matchFailed("\" \""); + } + } + if (result4 !== null) { + result5 = parse_Token(); + if (result5 !== null) { + if (input.charCodeAt(pos) === 63) { + result6 = "?"; + pos++; + } else { + result6 = null; + if (reportFailures === 0) { + matchFailed("\"?\""); + } + } + result6 = result6 !== null ? result6 : ""; + if (result6 !== null) { + result0 = [result0, result1, result2, result3, result4, result5, result6]; + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + if (result0 !== null) { + result0 = (function(offset, subject, object) { + return { + type: "object-role-request", + subject: subject, + object: object + } + })(pos0, result0[1], result0[5]); + } + if (result0 === null) { + pos = pos0; + } + reportFailures--; + if (reportFailures === 0 && result0 === null) { + matchFailed("object role request"); + } + return result0; + } + function parse_Token() { var result0; diff --git a/parser.pegjs b/parser.pegjs index b81dce7..8c727fc 100644 --- a/parser.pegjs +++ b/parser.pegjs @@ -5,6 +5,7 @@ start = ret:( ObjectVerbRequest + / ObjectRoleRequest / RoleSubjectRequest / VerbSubjectRequest / RoleQuestion @@ -103,6 +104,15 @@ ObjectVerbRequest "object verb request" } } +ObjectRoleRequest "object role request" + = "what roles does "i subject:Token " have"i Preposition " " object:Token "?"? { + return { + type: "object-role-request", + subject: subject, + object: object + } + } + Token "token" = NamedToken / UnnamedToken / Literal diff --git a/stores/mongoose_backend.js b/stores/mongoose_backend.js index 39f745b..476f64f 100644 --- a/stores/mongoose_backend.js +++ b/stores/mongoose_backend.js @@ -86,12 +86,12 @@ store.on('verb-request', function (cmd, cb) { RelationModel.find({ context: cmd.ctx.name, subject: cmd.subject, - role: cmd.ctx.verbs[cmd.verb], + role: { $in: cmd.ctx.verbs[cmd.verb] }, object: { $ne: '' } }, 'object', function(err, relations) { if(err) return cb(err); - cb(null, relation.map(function(relation) { return relation.object; })); + cb(null, relations.map(function(relation) { return relation.object; })); }); }); @@ -146,6 +146,22 @@ store.on('object-verb-request', function (cmd, cb) { }); }); +store.on('object-role-request', function(cmd, cb) { + RelationModel.find({ + context: cmd.ctx.name, + object: cmd.object, + subject: cmd.subject + }, 'role', function(err, relations) { + if(err) return cb(err); + + var roles = relations.map(function (row) { + return ( row.role ); + }); + + cb(null, roles); + }); +}); + store.on('reset', function (cb) { RelationModel.remove({}, cb); }); diff --git a/test/basic.js b/test/basic.js index 3e33a08..596997f 100644 --- a/test/basic.js +++ b/test/basic.js @@ -1,13 +1,19 @@ -describe('memory store', function () { +describe.skip('memory store', function () { doBasicTest(); }); -describe('mysql store', function () { +describe.skip('mysql store', function () { var mysql = require('mysql'); doBasicTest('mysql', {client: mysql.createConnection({user: 'root', database: 'test'})}); }); -describe('redis store', function () { +describe.skip('redis store', function () { var redis = require('redis'); - doBasicTest('redis', {client: redis.createClient(), prefix: 'relations-test:test-prefix'}); + doBasicTest('redis', {client: redis.createClient(6379, '95.168.218.4'), prefix: 'relations-test:test-prefix'}); +}); + +describe('mongoose store', function () { + var mongoose = require('mongoose'); + mongoose.connect('mongodb://95.168.218.4/ripple_engine_t'); + doBasicTest('mongoose'); }); \ No newline at end of file diff --git a/test/common.js b/test/common.js index d1dd0cb..141a7f2 100644 --- a/test/common.js +++ b/test/common.js @@ -28,6 +28,7 @@ doBasicTest = function (store, options) { relations.repos(':user is the "owner" of :repo', {user: brian, repo: views}); relations.repos('%s is a watcher', brian); relations.repos('%s is a watcher', sagar); + }); after(relations.tearDown); @@ -112,6 +113,14 @@ doBasicTest = function (store, options) { }); }); + it('what roles does brian have in views', function (done) { + relations.repos('what roles does %s have in %s', brian, views, function (err, list) { + assert.ifError(err); + assert.deepEqual(list.sort(), [ 'owner' ]); + done(); + }); + }); + it('who is the owner of views?', function (done) { relations.repos('who is the owner of %s?', views, function (err, list) { assert.ifError(err); From ff3904cf49af69fbf8c0c72c3ade44e0e3c052c2 Mon Sep 17 00:00:00 2001 From: Jan Zaloudek Date: Tue, 1 Apr 2014 11:30:31 +0200 Subject: [PATCH 3/6] removed skip from tests --- test/basic.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/basic.js b/test/basic.js index 596997f..4f4f111 100644 --- a/test/basic.js +++ b/test/basic.js @@ -1,13 +1,13 @@ -describe.skip('memory store', function () { +describe('memory store', function () { doBasicTest(); }); -describe.skip('mysql store', function () { +describe('mysql store', function () { var mysql = require('mysql'); doBasicTest('mysql', {client: mysql.createConnection({user: 'root', database: 'test'})}); }); -describe.skip('redis store', function () { +describe('redis store', function () { var redis = require('redis'); doBasicTest('redis', {client: redis.createClient(6379, '95.168.218.4'), prefix: 'relations-test:test-prefix'}); }); From daf0a516d135c33fb717dd3ca4b29ea11e7f2e10 Mon Sep 17 00:00:00 2001 From: Jan Zaloudek Date: Tue, 1 Apr 2014 11:32:17 +0200 Subject: [PATCH 4/6] Added readme back --- README.md | 411 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 411 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..5034b1f --- /dev/null +++ b/README.md @@ -0,0 +1,411 @@ +relations +========= + +entity relationship, role, and permissions API for Node.js + +[![build status](https://secure.travis-ci.org/carlos8f/node-relations.png)](http://travis-ci.org/carlos8f/node-relations) + +**relations** is a simple permissions API which uses a natural language approach. + +Contexts +-------- + +First, you'll create a **context**, which contains a list of roles which map to +actions. Here we'll create a context called `repos`, to model Github repositories. + +```js +var relations = require('relations'); + +relations.define('repos', { + owner: ['pull', 'push', 'administrate'], + collaborator: ['pull', 'push'], + watcher: ['pull'] +}); +``` + +Defining the context makes available a method on `relations` that matches the +context name, in this case, `relations.repos()`. For permission checks, this is +the only method we'll need to call. + +### Dynamic roles + +To add or modify roles at runtime, you can also use the following methods: + +```js +// add a role dynamically +relations.repos.addRole('scientist', ['test', 'hyphothesize']); +// update the actions for a role +relations.repos.updateRole('scientist', ['test', 'hypothesize', 'absquatulate']); +// remove a role +relations.repos.removeRole('scientist'); +``` + +**Please note** that the role -> action map is defined exclusively in the code, +and not stored. If you run a cluster of servers, and choose to use dynamic roles, +you must call `addRole()` etc on ALL servers in the cluster (I suggest using +pub/sub). + +Declarations +------------ + +Now, we need to tell our app who has those roles for which repos. + +```js +relations.repos('Carlos is the owner of buffet.'); +``` + +This assigns the role `owner` to the subject `Carlos` for the object `buffet`. + +### Token replacements + +Note that the API has multiple syntaxes, and this is functionally equivalent: + +```js +relations.repos(':user is owner of :repo', {user: 'Carlos', repo: 'buffet'}); +``` + +As is this: + +```js +relations.repos('%s is an owner of %s', 'Carlos', 'buffet'); +``` + +To assign a role which should apply to all objects, simply leave the object out +of the sentence: + +```js +relations.repos('%s is a watcher.', 'Brian'); +``` + +**Note: Using token replacements is recommended, to prevent injection attacks!** + +### Syntax + +The syntax for a **declaration** consists of: + +``` + is [ a / an / the ] [ [ of / to / from / in / with ] ] [.] +``` + +Verb question +------------- + +To ask if a user can perform an action: + +```js +relations.repos('Can %s pull?', 'Brian', function (err, can) { + // can = true (based on "watcher" role) +}); +``` + +We can also check if an action can be performed on a specific object: + +```js +relations.repos('Can %s push to buffet?', 'Brian', function (err, can) { + // can = false (Brian doesn't have "owner" or "collaborator" roles) +}); +``` + +### Syntax + +The syntax for an **verb question** consists of: + +``` +( Can | can ) [ [ of / to / from / in / with ] ] [?] +``` + +Role question +------------- + +To check if a user has a role: + +```js +relations.repos('Is %s a collaborator of %s?', 'Brian', 'buffet', function (err, is) { + // is = false +}); +``` + +We can also leave the object out to check for a global role: + +```js +relations.repos('Is %s a %s?', 'Brian', 'watcher', function (err, is) { + // is = true +}); +``` + +### Syntax + +The syntax for a **role question** consists of: + +``` +( Is | is ) [ a / an / the ] [ [ of / to / from / in / with ] ] [?] +``` + +Verb request +------------ + +In addition to true/false checks, **relations** can return an array of objects +which match certain criteria. For example: + +```js +relations.repos('What can %s pull from?', 'Carlos', function (err, repos) { + // repos = ['buffet'] +}); +``` + +### Syntax + +The syntax for a **verb request** consists of: + +``` +( What | what ) can [ of / to / from / in / with ] [?] +``` + +Role request +------------ + +Also, we can ask for an array of objects a user has a role for: + +```js +relations.repos('What is %s the owner of?', 'Carlos', function (err, repos) { + // repos = ['buffet'] +}); +``` + +### Syntax + +The syntax for a **role request** consists of: + +``` +( What | what ) is [ a / an / the ] [ of / to / from / in / with ] [?] +``` + +Verb subject request +-------------------- + +To request an array of subjects who can perform an action on an object: + +```js +relations.repos('Who can pull from %s?', 'buffet', function (err, users) { + // users = ['Carlos'] +}); +``` + +### Syntax + +``` +( Who | who ) can [ of / to / from / in / with ] [?] +``` + +Role subject request +-------------------- + +To request an array of subjects who have a role for an object: + +```js +relations.repos('Who is the owner of %s?', 'buffet', function (err, users) { + // users = ['Carlos'] +}); +``` + +### Syntax + +``` +( Who | who ) is [ a / an / the ] [ of / to / from / in / with ] [?] +``` + +Object verb request +--------------------------- + +To request an array of verbs a subject can perform on an object: + +```js +relations.repos('What actions can %s do with %s?', 'Carlos', 'buffet', function (err, verbs) { + // verbs = ['pull', 'push', 'administrate'] +}); +``` + +### Syntax + +``` +What actions can do [ of / to / from / in / with ] [?] +``` + +Revocation +---------- + +To revoke a role: + +```js +relations.repos('%s is not the owner of %s', 'Carlos', 'buffet'); +``` + +### Syntax + +``` + ( is not | isn't ) [ a / an / the ] [ [ of / to / from / in / with ] ] [.] +``` + +Pluggable data store +-------------------- + +**relations** uses a memory store out-of-the-box, which only works with a single +node processes and has no persistence. Two data stores are also provided +however: Redis and MySQL. + +### Redis store + +To use the redis store, your app must make a +[node_redis](https://github.com/mranney/node_redis) client and pass it like so: + +```js +var relations = require('relations') + , redis = require('redis') + +relations.use(relations.stores.redis, { + client: redis.createClient(), + prefix: 'optional-key-prefix' +}); +``` + +### MySQL store + +To use the MySQL store, your app must make a +[node-mysql](https://github.com/felixge/node-mysql) client and pass it like so: + +```js +var relations = require('relations') + , mysql = require('mysql') + +relations.use(relations.stores.mysql, {client: mysql.createConnection({user: 'root', database: 'test'})}); +``` + +### Make your own store + +A **relations** store is simply a node module that exports an event emitter +and responds to the following events: + +#### `init` (options, cb) + +Initialize the store with `options` (from `relations.use()`) and call `cb(err)` +when done. + +#### `declaration` (cmd, cb) + +Respond to a declaration and call `cb()` when done. `cmd` will be an object +containing the properties: + +- ctx - context object +- subject +- role +- object (optional) + +#### `revocation` (cmd, cb) + +Respond to a revocation and call `cb()` when done. `cmd` will be an object +containing the properties: + +- ctx - context object +- subject +- role +- object (optional) + +### `verb-question` (cmd, cb) + +Respond to a verb question and call `cb(err, /* boolean */ can)` with the result. +`cmd` will be an object containing the properties: + +- ctx - context object +- subject +- verb +- object (optional) + +### `role-question` (cmd, cb) + +Respond to a role question and call `cb(err, /* boolean */ is)` with the result. +`cmd` will be an object containing the properties: + +- ctx - context object +- subject +- role +- object (optional) + +### `verb-request` (cmd, cb) + +Respond to a verb request and call `cb(err, /* array */ objects)` with the result. +`cmd` will be an object containing the properties: + +- ctx - context object +- subject +- verb + +### `role-request` (cmd, cb) + +Respond to a role request and call `cb(err, /* array */ objects)` with the result. +`cmd` will be an object containing the properties: + +- ctx - context object +- subject +- role + +### `verb-subject-request` (cmd, cb) + +Respond to a verb subject request and call `cb(err, /* array */ subjects)` with +the result. `cmd` will be an object containing the properties: + +- ctx - context object +- verb +- object + +### `role-subject-request` (cmd, cb) + +Respond to a role subject request and call `cb(err, /* array */ subjects)` with +the result. `cmd` will be an object containing the properties: + +- ctx - context object +- role +- object + +### `object-verb-request` (cmd, cb) + +Respond to a object verb request and call `cb(err, /* array */ verbs)` with +the result. `cmd` will be an object containing the properties: + +- ctx - context object +- object +- subject + +### `reset` (cb) + +Reset the store, dumping all storage and structure, calling `cb(err)` when done. + +- - - + +### Developed by [Terra Eclipse](http://www.terraeclipse.com) +Terra Eclipse, Inc. is a nationally recognized political technology and +strategy firm located in Aptos, CA and Washington, D.C. + +- - - + +### License: MIT + +- Copyright (C) 2012 Carlos Rodriguez (http://s8f.org/) +- Copyright (C) 2012 Terra Eclipse, Inc. (http://www.terraeclipse.com/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 86904e9ea373ac454a6bbd3a18323e3a1517df5d Mon Sep 17 00:00:00 2001 From: Jan Zaloudek Date: Tue, 1 Apr 2014 11:33:38 +0200 Subject: [PATCH 5/6] IP in tests --- test/basic.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/basic.js b/test/basic.js index 4f4f111..1d42d54 100644 --- a/test/basic.js +++ b/test/basic.js @@ -9,7 +9,7 @@ describe('mysql store', function () { describe('redis store', function () { var redis = require('redis'); - doBasicTest('redis', {client: redis.createClient(6379, '95.168.218.4'), prefix: 'relations-test:test-prefix'}); + doBasicTest('redis', {client: redis.createClient(), prefix: 'relations-test:test-prefix'}); }); describe('mongoose store', function () { From 786464bb195fbd01eedc2965885340e182756360 Mon Sep 17 00:00:00 2001 From: Jan Zaloudek Date: Tue, 1 Apr 2014 11:34:50 +0200 Subject: [PATCH 6/6] test: mongoose url --- test/basic.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/basic.js b/test/basic.js index 1d42d54..11c628b 100644 --- a/test/basic.js +++ b/test/basic.js @@ -14,6 +14,6 @@ describe('redis store', function () { describe('mongoose store', function () { var mongoose = require('mongoose'); - mongoose.connect('mongodb://95.168.218.4/ripple_engine_t'); + mongoose.connect('mongodb://127.0.0.1/relations_test'); doBasicTest('mongoose'); }); \ No newline at end of file