I'm building a fairly large cms-type application with Backbone and Knockout and Knockback (ko + bb bridge library), and I'm trying to figure out a good way to abstract permissions. Also sorry in advance for the novel.
First of all, this is a pretty non-standard architecture, and the second question you might ask - why don't you use something more comprehensive like Ember or Angular? Point taken. It is what it is at this point. :)
So here's my quandary. I want an elegant api at both the controller and viewmodel level for permissions.
I have an object available to me that looks like this:
{
'api/pages': {
create: true, read: true, update: true, destroy: true
},
'api/links': {
create: false, read: true, update: false, destroy: false
}
...
}
So in my router/controllers, I'm newing up my collections/models/viewmodels, and then calling a customized render method on an already existing view. The view takes care of things like releasing the viewmodels.
initialize: function() {
this.pages = new PagesCollection();
this.links = new LinksCollection();
},
list: function() {
var vm = new PageListViewmodel(this.pages, this.links);
// adminPage method is available through inheritance
this.adminPage('path/to/template', vm); // delegates to kb.renderTemplate under the hood.
}
So the problem with this, are these collections are totally unstructured, ie. the router doesn't know anything about them.
But what I need is for it to redirect to an unauthorized page if you're not allowed to view a particular resource.
So with the example above, I've thought about coding in before/after filters? But where would you specify what each router method is trying to access?
list: function() {
this.authorize([this.pages, this.links], ['read'], function(pages, links) {
// return view.
});
}
The previous code is really cludgy..
For the viewmodels, which are more straightforward I had the idea of doing something like this - ala Ruby's CanCan:
this.currentUser.can('read', collection) // true or false
// can() would just look at the endpoint and compare to my perms object.
You could extend your router to wrap your routes callbacks to perform a validity check before allowing the action.
var Router = Backbone.Router.extend({
routes: {
"app/*perm": "go"
},
route: function(route, name, callback) {
if (!callback) callback = this[name];
var f = function() {
var perms = this.authorized(Backbone.history.getFragment());
if (perms === true) {
callback.apply(this, arguments);
} else {
this.trigger('denied', perms);
}
};
return Backbone.Router.prototype.route.call(this, route, name, f);
},
authorized: function(path) {
// check if the path is authorized
},
go: function(perm) {
// perform action
}
});
If the path is authorized, the route performs as usual, a denied event is triggered if not.
The authorized
method could be based on a list of paths mapped to your permissions objects, something like this
var permissions = {
'api/pages': {
create: true, read: true, update: true, destroy: true
},
'api/links': {
create: false, read: true, update: false, destroy: false
}
}
var Router = Backbone.Router.extend({
routes: {
"app/*perm": "go"
},
// protected paths, with the corresponding entry in the permissions object
permissionsMap: {
"app/pages": 'api/pages',
"app/links": 'api/links',
},
route: function(route, name, callback) {
// see above
},
// returns true if the path is allowed
// returns an object with the path and the permission key used if not
authorized: function(path) {
var paths, match, permkey, perms;
// find an entry for the current path
paths = _.keys(this.permissionsMap);
match = _.find(paths, function(p) {
return path.indexOf(p)===0;
});
if (!match) return true;
//check if the read permission is allowed
permkey = this.permissionsMap[match];
if (!permissions[permkey]) return true;
if (permissions[permkey].read) return true;
return {
path: path,
permission: permkey
};
},
go: function(perm) {}
});
And a demo http://jsfiddle.net/t2vMA/1/
Nikoshr's answer gave me something to run with. I didn't think to actually override route
itself. But here's my solution. I should have mentioned it in the question - but sometimes a router action requires more than one collection.
The code on this is really rough, and needs tests - but it works! Fiddle here.
Here are the relevant portions - These two methods take care of the authorization.
authorize: function(namedRoute) {
if (this.permissions && this.collections) {
var perms = this.permissions[namedRoute];
if (!perms) {
perms = {};
// if nothing is specified for a particular route, we
// assume read access required for all registered controllers.
_.each(_.keys(this.collections), function(key) {
return perms[key] = [];
});
}
var authorized = _.chain(perms)
.map(function(reqPerms, collKey) {
var collection = this.collections[collKey],
permKey = _.result(collection, 'url');
// We implicitly check for 'read'
if (!_.contains('read')) {
reqPerms.push('read');
}
return _.every(reqPerms, function(ability) {
return userPermissions[permKey][ability];
});
}, this)
.every(function(auth){ return auth; })
.value();
return authorized;
}
return true;
},
route: function(route, name, callback) {
if (!callback) { callback = this[name]; }
var action = function() {
// allow anonymous routes through auth check.
if (!name || this.authorize(name)) {
callback.apply(this, arguments);
} else {
this.trigger('denied');
}
}
Backbone.Router.prototype.route.call(this, route, name, action);
return this;
}
And each controller/router inherits from the perm router, where the permissions for each action are mapped like so:
// Setup
routes: {
'list' : 'list',
'list/:id' : 'detail',
'create' : 'create'
},
// Collection are registered so we can
// keep track of what actions use them
collections: {
pages: new PagesCollection([{id:1, title: 'stuff'}]),
links: new LinksCollection([{id:1, link: 'things'}])
},
// If a router method is not defined,
// 'read' access is assumed to be
// required for all registered collections.
permissions: {
detail: {
pages: ['update'],
links: ['update']
},
create: {
pages: ['create'],
links: ['create', 'update']
}
},
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With