angoose
Version:
Angoose is a Remote Method Invocation module that comes with built-in mongoose/angular support. Now you can call server side module in browser just like you're in the server side!
266 lines (238 loc) • 10.4 kB
JavaScript
var angoose = require("../lib/angoose");
var toolbox = require("../lib/util/toolbox");
var EXTENSION = 'angoose-authorization';
var MODEL_NAME = 'PermissionModel';
var COLLECTION_NAME = 'angoose_perms';
var cached = null;
var authExt = {
name: EXTENSION,
preAuthorize: preAuth,
postAuthorize: postAuth,
preRedact: redact,
postInvoke: postInvoke,
beforeCreateBundle: beforeCreateBundle
};
module.exports = angoose.extension('AngooseAuthorization', authExt);
function preAuth(next){
logger().trace("in preAuth", session().$authenticatedUser);
var authUser = session().$authenticatedUser;
if(authUser){
logger().trace("Found user in session", authUser);
//angoose.getContext().setUser( session().$authenticatedUser );
angoose.getContext().setPrincipal( new angoose.Principal( authUser.userId, authUser.roles) );
}
else{
angoose.getContext().setPrincipal( new angoose.Principal( 'guest', 'guest' ));
}
next();
};
function logger(){
var extensionOptions = angoose.config()[EXTENSION] ;
angoose.getLogger('angoose-authorization').setLevel((extensionOptions && extensionOptions.logging) || 'INFO');
return angoose.getLogger('angoose-authorization')
}
function postAuth(next, invocation){
var extensionOptions = angoose.config()[EXTENSION] ;
var ctx = angoose.getContext();
var user = ctx.getPrincipal();
invocation.allowed = true;
/** admin user bypass */
var superuser = extensionOptions && extensionOptions.superuser
if(user.getUserId() === superuser ) return next(); // always allow super user admin
var superrole = ( extensionOptions && extensionOptions.superrole ) || 'admin';
if(user && user.getRoles().indexOf(superrole) >=0) return next();
logger().trace("in auth.postAuth:", invocation.clazz, invocation.method);
if(invocation.method == 'signin' || invocation.method=="signout") return next();
var mod = angoose.module( invocation.clazz );
var category = mod.config(EXTENSION +".category") || invocation.clazz;
var group = toolbox.camelcase(getGroup(mod, invocation.method)) || invocation.method;
var roles = user.getRoles() ? user.getRoles(): [];
roles = Array.isArray(roles)? roles: [ roles ];
if(roles.indexOf('guest') <0) roles.push('guest');
if(roles.indexOf('authenticated') <0) roles.push('authenticated');
var allowed = isAllowed( category +"."+ group, roles, function(allowed){
logger().trace("isAllowed: ", category, group, allowed);
invocation.allowed = allowed;
next();
});
};
// redaction
function redact(next){
logger().trace("in auth.preRedact");
next();
};
function isAllowed(action, roles, callback){
logger().trace("Checking ", roles, " for action", action);
getMatrix(function(perms){
for(var i=0;i<roles.length;i++){
var permissions = perms && perms[ roles[i] ];
if(permissions && permissions.get( action ) === true ) return callback(true);
};
callback(false);
});
}
function getMatrix(callback){
if(cached){
return callback(cached);
}
angoose(MODEL_NAME).find(function(err, perms){
if(err) logger().error("Failed to load permission roles", err);
cached = {};
for(var i=0; perms && i<perms.length; i++){
var perm = perms[i];
cached[perm.role] = perm;
}
callback(cached);
});
}
function schemaInterceptor(next, schema){ // shouldn't the err argument be here?
//console.log("in post prepareSchema",schema);
if(schema && (schema.methods || schema.statics) ){
var methodNames = (schema.methods && Object.keys(schema.methods)) || [];
methodNames.concat( (schema.statics && Object.keys(schema.statics)) || []);
for(var i=0;methodNames && i<methodNames.length;i++){
var mName = methodNames[i];
var path = schema.moduleName +"." + mName;
var opts = {path: path, options:{type:'Boolean'} };
authItems[path] = opts;
};
};
next(null, schema); // we shouldn't need to provide the arguments here, bug in hooks module?
};
function getGroup(module, methodName){
var configPath = EXTENSION+"."+methodName+".group";
var grp = module.config(configPath) || ""
if(!grp && isModel(module)){
//populate,find,findOne,findById,findByIdAndRemove,findByIdAndUpdate,findOneAndRemove,findOneAndUpdate,update,remove,count,geoNear,geoSearch,aggregate
// mongoose groups: View, Modify, Create, Remove
if('populate,find,findOne,findById,geoNear,geoSearch,aggregate,count,'.indexOf(methodName)>=0) return 'View';
if('update,save'.indexOf(methodName )>=0) return 'Modify';
if('remove'.indexOf(methodName )>=0) return 'Delete';
if('create'.indexOf(methodName )>=0) return 'Create';
if('findByIdAndRemove,findByIdAndUpdate,findOneAndRemove,findOneAndUpdate'.indexOf(methodName) >=0) return "find-and-modify"
}
return grp
}
function getLabel(module, methodName){
var configPath = EXTENSION+"."+methodName+".label";
return module.config(configPath) || toolbox.camelcase(methodName, true)
}
function getCategory(module){
var configPath = EXTENSION+".category";
return module.config(configPath) || module.config('name');
}
function isModel(module){
return module.config('baseClass') == 'Model';
}
function beforeCreateBundle( client){
// generating PermissionModel schema used on UI
logger().debug("in beforeCreateBundle");
MODEL_NAME = angoose.config('angoose-authorization.model-name') || MODEL_NAME;
COLLECTION_NAME = angoose.config('angoose-authorization.collection-name') || COLLECTION_NAME;
var schemas = client.schemas;
var authSchema = getModelSchema();
// get list of all published methods
Object.keys(schemas).forEach(function(moduleName){
var schema = schemas[moduleName];
if(!schema || moduleName == 'SampleUser') return;
var methodNames = Object.keys(schema.methods).concat(Object.keys(schema.statics));
var mod = angoose.module(moduleName);
if(mod.config && mod.config("visibility") === false ) return;
var category = getCategory(mod);
for(var i=0;methodNames && i<methodNames.length;i++){
var mName = methodNames[i];
if(mName == 'config' || mName == 'getSchema') continue;
var fn = schema.methods && schema.methods[mName];
fn = fn || (schema.statics && schema.statics[mName]);
if(toolbox.methodType(fn) != 'remote') continue;
var group = getGroup(mod, mName);
if(!group){
var path = category +"." + mName;
var field = {};
field[path] = {type:Boolean, label: getLabel(mod,mName)};
authSchema.add(field);
}
else if(!authSchema.path(group)){
// add schema path for the permission group
var path = category +"." + toolbox.camelcase(group);
var field = {};
field[path] = {type:Boolean, label: toolbox.camelcase(group, true)};
authSchema.add(field);
}
};
});
var mongooseModel = angoose.getMongoose().modelNames().indexOf(MODEL_NAME)>=0? angoose.getMongoose().model(MODEL_NAME):angoose.getMongoose().model(MODEL_NAME, authSchema) ;
var permModule = angoose.module(MODEL_NAME, mongooseModel);
new angoose.Bundle().exportModule(client, MODEL_NAME); // toolbox.exportModuleMethods(MODEL_NAME, permModule);
logger().debug("Added mongoose model", MODEL_NAME);
setupInitialRoles();
};
function postInvoke(next, invocation){
// this is bizzare, if main method fails, this will be called with arguments meant for pre()
//var invocation = angoose.getContext().getInvocation();
if( ['signin', 'signout'].indexOf(invocation.method ) <0 ) return next();;
if(invocation.method == 'signout'){
if(angoose.getContext().getRequest().session)
angoose.getContext().getRequest().session.$authenticatedUser = null;
logger().debug("User logged out");
}
var data = invocation.result;
logger().debug("Intercepting login methods", invocation.method, data);
if(!data || !data.userId )
return next();;
if(invocation.method == 'signin'){
angoose.getContext().getRequest().session.$authenticatedUser = {userId: data.userId, roles: data.roles } ;
logger().debug("User authenticated", angoose.getContext().getRequest().session.$authenticatedUser );
}
next();
};
function moduleSetup(next){
next();
}
function session(){
return angoose.getContext().getRequest().session || {};
};
function getModelSchema(){
var schema = new angoose.getMongoose().Schema({
role: {type:String, label:'Role', required:true, tags:['default-list'], unique:true },
desc: {type:String, label:'Description',tags:['default-list']}
}, {collection: COLLECTION_NAME});
schema.pre('save', function(next){
cached = null; // invalidate the cache
next();
});
return schema;
}
function setupInitialRoles(){
var model= angoose.module(MODEL_NAME);
model.findOne({role:'admin'}, function(err, role){
if(role) return;
var u = new model({
role:'admin',
desc:"System default role with all permissions"
});
u.save(function(err){
logger().debug("Added default role: admin");
});
});
model.findOne({role:'authenticated'}, function(err, role){
if(role) return;
var u = new model({
role:'authenticated',
desc:"System default role for authenticated user"
});
u.save(function(err){
logger().debug("Added default role: authenticated");
});
});
model.findOne({role:'guest'}, function(err, role){
if(role) return;
var u = new model({
role:'guest',
desc:"System default role for guest user"
});
u.save(function(err){
logger().debug("Added default role: guest");
});
});
};