universal-permissions
v1.0.0
Published
Super easy to use javascript `can`-style permission management library. Not relying on prototypes — share same permissions across client and server.
Maintainers
Readme
Universal Permissions
Super easy to use javascript can-style permission management library. Not relying on prototypes — share same permissions across client and server.
Usage
/* permissions.js */
import Permissions from 'universal-permissons'
import * as definitions from './definitions'
export const { can, set, remove } = new Permissions(definitions);Definitions
Definitions can be passed inside an object to Permissions constructor, or/and can be set/removed dynamically.
For example, you can use a separate module with multiple exports, and then import it as shown above:
/* definitions.js */
export const post = {
edit: (viewer, post) => (
viewer.id === post.authorId
),
see: true
};
export const comment = {
edit: (viewer, comment) => (
viewer.id === comment.authorId && !comment.blocked
),
delete: comment.edit,
create: (viewer) => viewer.id
};
Here exported objects correspond to permission type, keys such as 'edit', 'delete', etc. correspond to permission action, and properties correspond to permission definition.
As you can see, definition can be of any type, but if it's a function, it will recieve viewer, and entity objects as params.
Make sure to always return something from functional definition, otherwise action will be always unpermitted.
For some reason, you may want to set/delete/replace definitions during runtime:
import { set, remove } from './permissions'
set('comment', 'like', (viewer) => viewer.id );
set('post', ['edit', 'delete'], (viewer, post) => (
viewer.id === post.authorId && !post.protected
)); // 'edit' replaced
remove('comment', 'create');
can
Then anywhere you want, you can find out whether you can perform an action on given object calling can:
import { can } from './permissions'
const viewer = { id: 1 };
const comment = { authorId: 1, text: 'Hello' };
can(viewer, 'edit', { comment }) // true
can({ id: 2 }, 'edit', { comment }) // false
can(null, 'create', 'comment') // false
can(viewer, 'create', 'comment') // true
Last argument can be an object of shape { type: entity } or a string, representing type. It is your responsibility to pass proper entity to functional definitions. For example, this will, of course, return false for definitions defined above:
const viewer = { id: 1 };
const post = { authorId: 1 };
can(viewer, 'edit', 'post') // false
can(viewer, 'edit', post) // will throw because of
// unknown type 'authorId'
Client-side can example
Not the best style of doing things, but you can get the idea:
import { can } from './permissions'
import store from './store'
const { viewer } = store;
fetch('/api/comment/2')
.then(res => res.json())
.then(comment => {
comment.editable = can(viewer, 'edit', { comment })
comment.deletable = can(viewer, 'delete', { comment })
})Server-side can example
One can imagine such Express setup:
import express from 'express';
import Comment from './models/User'
import { can } from './permissions'
let app = express();
/* Here should be used some auth middleware,
* providing req.user, ex. passport */
const getComment = ({ params: id }, res, next) => {
Comment.findById(id)
.then(comment => {
req.comment = comment;
next();
})
.catch(error => next(error));
};
const ifCan = (action, type) => {
return (req, res, next) => {
if (can(req.user, action, { type: req[type] })) {
return next();
}
res.status(403).end('Forbidden');
}
}
/* We update only if we CAN, otherwise we see an error */
app.get('/api/comment/:id/edit',
getComment,
ifCan('edit', 'post'),
({ body }, res, next) => {
Post
.update(body)
.then(post => res.json(post))
.catch(error => next(error))
;
});
API
Coming soon.
Contributing
MIT license, you are welcome.
