Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add MySQL, PostgreSQL, and SQLite support via bookshelf #2

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
9 changes: 9 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
node_modules
test-sacl.db
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Seriously, why do all these ACL modules have to be so darn complex? I just need
The `--save` flag adds `simple-acl` to your `package.json` file. Then:

var acl = require('simple-acl');

And you're ready to go.

---
Expand All @@ -35,6 +35,21 @@ Any of the following works:

Don't forget to `npm install redis`, too. Since I don't want to force everyone to install `redis` which also includes `hiredis` if they're not gonna use it.

# SQL Database Stores

Using SQL database is possible via [Bookshelf.js](http://bookshelfjs.org/).
Currently, this package has been tested with MySQL, PostgreSQL, and SQLite.

npm install bookshelf knex mysql // MySQL
npm install bookshelf knex pg // PostgreSQL
npm install bookshelf knex sqlite3 // SQLite

Configure your database connections in `knexfile.js`.

A simple schema is provided in the `migrations` directory. To create the tables:

npm run migrate:mysql | migrate:sqlite | migrate:pg

---
# API

Expand Down Expand Up @@ -69,7 +84,7 @@ Yes, `require('acl')` is an `EventEmitter` with two events:

acl.on('grant', function(grantee, resource) { });
acl.on('revoke', function(grantee, resource) { });

It's pretty basic right now just to allows you to log grants and revokes as they happens.
I will add more event-based functionality if there is demand.

Expand Down
194 changes: 194 additions & 0 deletions bookshelf-store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
var async = require('async');
var bookshelf = require('bookshelf');
var knex = require('knex');

var dbName = function(model, field) {
var arr = [];
if (typeof this[model] !== 'undefined') {
arr.push(this[model].prototype.tableName);
}
if (field) {
arr.push(field);
}
return arr.join('.');
}

var tableName = function(table, prefix) {
return prefix ? prefix + table : table;
}

// bookshelf-store.js - bookshelf.js adapter for simple-acl
module.exports = (function() {

var Store = function(config, prefix) {
var self = this;
var driver = knex(config)
var orm = bookshelf(driver);
var Aro, Aco, Permission;

this.dbName = dbName.bind(this);

this.Aro = Aro = orm.Model.extend({
tableName: tableName('aros', prefix),
hasTimestamps: true,
});

this.Permission = Permission = orm.Model.extend({
tableName: tableName('permissions', prefix),
hasTimestamps: true,
});

this.Aco = Aco = orm.Model.extend({
tableName: tableName('acos', prefix),
hasTimestamps: true,
});

this.Aro.getByAlias = function(alias) {
return new Aro({alias: alias}).fetch();
}

this.Aco.getByAlias = function(alias) {
return new Aco({alias: alias}).fetch();
}

this._getPermission = function(grantee, resource) {
return new Promise(function(resolve, reject) {
Aro.getByAlias(grantee).then(function(aro) {
Aco.getByAlias(resource).then(function(aco) {
resolve({aro: aro, aco: aco});
})
})
})
}

this._grant = function(aro, aco, callback) {
Permission.forge({aro_id: aro.id, aco_id: aco.id}).fetch()
.then(function(result) {
if (result) {
return callback ? callback(null, true) : null;
}
this.save().then(function(result) {
return callback ? callback(null, result !== null) : result !== null;
}).catch(function(err) {
return callback ? callback(err) : err;
});
}).catch(function(err) {
return callback ? callback(err) : err;
});
}

};

Store.prototype =
{ 'createAro':
function(grantee, callback) {
return new this.Aro({alias: grantee}).fetch().then(function(result) {
if (result) {
return callback ? callback(null, result) : result;
}
this.save().then(function(result) {
return callback ? callback(null, result) : result;
});
}).catch(function(err) {
return callback ? callback(err) : err;
});
}

, 'createAco':
function(resource, callback) {
return new this.Aco({alias: resource}).fetch().then(function(result) {
if (result) {
return callback(null, result)
}
this.save().then(function(result) {
return callback(null, result)
})
}).catch(function(err) {
return callback(err);
});
}

, 'grant':
function(grantee, resource, callback) {
var self = this;
this._getPermission(grantee, resource).then(function(result) {
var ctx = { aro: result.aro, aco: result.aco }
async.series({
createAro: function(next) {
if (ctx.aro) {
return next();
}
self.createAro(grantee, function(err, aro) {
ctx.aro = aro;
return next();
});
},

createAco: function(next) {
if (ctx.aco) {
return next();
}
self.createAco(resource, function(err, aco) {
ctx.aco = aco;
return next();
});
},

createGrant: function(next) {
self._grant(ctx.aro, ctx.aco, callback);
return next()
},
});
}).catch(function(err) {
callback(err);
})
}

, 'assert':
function(grantee, resource, callback) {
var self = this;
var promises = [];
var dbName = self.dbName;

var conditions = {};
conditions[dbName('Aro', 'alias')] = grantee;
conditions[dbName('Aco', 'alias')] = resource;

this.Permission.query(function(qb) {
qb
.innerJoin(dbName('Aro'), function() {
this.on(dbName('Permission', 'aro_id'), '=', dbName('Aro', 'id'));
})
.innerJoin(dbName('Aco'), function() {
this.on(dbName('Permission', 'aco_id'), '=', dbName('Aco', 'id'));
});
}).where(conditions).fetch().then(function(result) {
return callback(null, result !== null);
}).catch(function(err) {
return callback(err);
});

}

, 'revoke':
function(grantee, resource, callback) {
var self = this;
this._getPermission(grantee, resource).then(function(result) {
if (!result) {
return callback(false);
}
return self.Permission.where({
aro_id: result.aro.id,
aco_id: result.aco.id,
}).destroy().then(function(result) {
return callback(null, result);
}).catch(function(err) {
return callback(err, result);
});
});
}
};

return Store;

})();
6 changes: 6 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ module.exports = (function() {
acl.RedisStore = require('./redis-store');
acl.MockStore = require('./mock-store');

try {
require.resolve('bookshelf');
acl.BookshelfStore = require('./bookshelf-store');
} catch (e) {
}

// function shim
var shim = function(obj, action) {
var func = obj[action];
Expand Down
47 changes: 47 additions & 0 deletions knexfile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Update with your config settings.

module.exports = {

dev_mysql: {
client: 'mysql',
connection: {
database: 'test_sacl',
user: 'root',
password: 'password',
timezone: 'UTC'
},
migrations: {
directory: './migrations',
tableName: 'migrations',
},
//debug: true,
},

dev_sqlite3: {
client: 'sqlite3',
connection: {
filename: './test/test-sacl.db',
},
useNullAsDefault: true,
migrations: {
directory: './migrations',
tableName: 'migrations',
},
//debug: true,
},

dev_pg: {
client: 'pg',
connection: {
database: 'test_sacl',
user: 'root',
password: 'password',
},
migrations: {
directory: './migrations',
tableName: 'migrations',
},
//debug: true,
},

};
35 changes: 35 additions & 0 deletions migrations/201609090919_initial.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
exports.up = function(knex, Promise) {
return Promise.all([

knex.schema.createTable('aros', function(table) {
table.increments('id');
table.string('alias', 100).notNullable()
.unique('un_aros_alias');
table.timestamps();
}),

knex.schema.createTable('acos', function(table) {
table.increments('id');
table.string('alias', 100).notNullable()
.unique('un_acos_alias');
table.timestamps();
}),

knex.schema.createTable('permissions', function(table) {
table.increments('id');
table.integer('aro_id').notNullable();
table.integer('aco_id').notNullable();
table.unique(['aro_id', 'aco_id'], 'un_permissions');
table.timestamps();
}),

]);
};

exports.down = function(knex, Promise) {
return Promise.all([
knex.schema.dropTable('aros'),
knex.schema.dropTable('acos'),
knex.schema.dropTable('permissions'),
]);
};
19 changes: 18 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@
"description": "Simple ACL. 'nuff said.",
"main": "index.js",
"scripts": {
"migrate:mysql": "./node_modules/.bin/knex migrate:latest --env dev_mysql",
"migrate:sqlite": "./node_modules/.bin/knex migrate:latest --env dev_sqlite3",
"migrate:pg": "./node_modules/.bin/knex migrate:latest --env dev_pg",
"rollback:mysql": "./node_modules/.bin/knex migrate:rollback --env dev_mysql",
"rollback:sqlite": "./node_modules/.bin/knex migrate:rollback --env dev_sqlite3",
"rollback:pg": "./node_modules/.bin/knex migrate:rollback --env dev_pg",
"migrate:all": "npm run migrate:mysql; npm run migrate:sqlite; npm run migrate:pg",
"rollback:all": "npm run rollback:mysql; npm run rollback:sqlite; npm run rollback:pg",
"test": "mocha --reporter spec"
},
"repository": {
Expand All @@ -13,12 +21,21 @@
"optionalDependencies": {
"redis": "~0.8.2"
},
"peerDependencies": {
"bookshelf": "^0.10.*",
"knex": "^0.11.*",
"mysql": "^2.11.*",
"sqlite3": "^3.1.*"
},
"devDependencies": {
"mocha": "~1.6.0"
},
"keywords": [
"acl"
],
"author": "Chakrit Wichian (http://chakrit.net) <[email protected]>",
"license": "BSD"
"license": "BSD",
"dependencies": {
"async": "^2.0.1"
}
}
6 changes: 6 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
var stores = 'MemoryStore,RedisStore,MockStore'.split(',')
, funcs = 'grant,revoke,assert'.split(',');

try {
require.resolve('bookshelf');
stores.push('BookshelfStore');
} catch (e) {
}

// tests
describe('acl', function() {
before(function() { this.acl = require('..'); });
Expand Down
Loading