Express.js/Mongoose user roles and permissions - node.js

I am creating a fairly simple site with Node, Express and Mongoose. The site needs to have have user roles and permissions. My thoughts are that i'll validate permissions based on user interaction with the data base.
In mongoose is there a way to determine the type of CRUD operation currently being carried out possibly by a user?

I've found a solution. It would be great to hear peoples opinions on this.
I have a permissions config object which defines each role and their permissions.
Permissions config object
roles.admin = {
id: "admin",
name: "Admin",
description: "",
resource : [
{
id : 'blog',
permissions: ['create', 'read', 'update', 'delete']
},
{
id : 'user',
permissions: ['create', 'read', 'update', 'delete']
},
{
id : 'journal',
permissions: ['create', 'read', 'update', 'delete']
},
]
};
roles.editor = {
id: "editor",
name: "Editor",
description: "",
resource : [
{
id : 'blog',
permissions: ['create', 'read', 'update', 'delete']
},
{
id : 'user',
permissions: ['read']
},
{
id : 'journal',
permissions: ['create', 'read', 'update']
},
]
};
Middleware function
var roles = require('./config');
var permissions = (function () {
var getRoles = function (role) {
var rolesArr = [];
if (typeof role === 'object' && Array.isArray(role)) {
// Returns selected roles
for (var i = 0, len = role.length; i < len; i++) {
rolesArr.push(roles[role[i]]);
};
return rolesArr;
} else if (typeof role === 'string' || !role) {
// Returns all roles
if (!role) {
for (var role in roles) {
rolesArr.push(roles[role]);
};
}
// Returns single role
rolesArr.push(roles[role]);
return rolesArr;
}
},
check = function (action, resource, loginRequired) {
return function(req, res, next) {
var isAuth = req.isAuthenticated();
// If user is required to be logged in & isn't
if (loginRequired && !isAuth) {
return next(new Error("You must be logged in to view this area"));
}
if (isAuth || !loginRequired) {
var authRole = isAuth ? req.user.role : 'user',
role = get(authRole),
hasPermission = false;
(function () {
for (var i = 0, len = role[0].resource.length; i < len; i++){
if (role[0].resource[i].id === resource && role[0].resource[i].permissions.indexOf(action) !== -1) {
hasPermission = true;
return;
}
};
})();
if (hasPermission) {
next();
} else {
return next(new Error("You are trying to " + action + " a " + resource + " and do not have the correct permissions."));
}
}
}
}
return {
get : function (role) {
var roles = getRoles(role);
return roles;
},
check : function (action, resource, loginRequired) {
return check(action, resource, loginRequired);
}
}
})();
module.exports = permissions;
Then i created a middleware function, when the check method gets called it gets the users role from the req object (req.user.role). It then looks at the params passed to the middleware and cross references them with those in the permissions config object.
Route with middlware
app.get('/journal', `**permissions.check('read', 'journal')**`, function (req, res) {
// do stuff
};

This is my implementation. The code is reusable for client and server. I use it for my express/angular website
Reduce code duplicate, better consistence between client/server
Bonus benefit: on client's adapter, we can simply return true to grant max access to test the robustness of server (since hackers and easily overcome client side restrict )
in app/both/both.js
var accessList = {
//note: same name as controller's function name
assignEditor: 'assignEditor'
,adminPage: 'adminPage'
,editorPage: 'editorPage'
,profilePage: 'profilePage'
,createArticle: 'createArticle'
,updateArticle: 'updateArticle'
,deleteArticle: 'deleteArticle'
,undeleteArticle: 'undeleteArticle'
,banArticle: 'banArticle'
,unbanArticle: 'unbanArticle'
,createComment: 'createComment'
,updateComment: 'updateComment'
,deleteComment: 'deleteComment'
,undeleteComment: 'undeleteComment'
,banComment: 'banComment'
,unbanComment: 'unbanComment'
,updateProfile: 'updateProfile'
}
exports.accessList = accessList
var resourceList = {
//Note: same name as req.resource name
profile: 'profile'
,article: 'article'
,comment: 'comment'
}
exports.resourceList = resourceList
var roleList = {
admin: 'admin'
,editor: 'editor'
,entityCreator: 'entityCreator'
,profileOwner: 'profileOwner' //creator or profile owner
,normal: 'normal' //normal user, signed in
,visitor: 'visitor' //not signed in, not used, open pages are uncontrolled
}
var permissionList = {}
permissionList[accessList.assignEditor] = [roleList.admin]
permissionList[accessList.adminPage] = [roleList.admin]
permissionList[accessList.editorPage] = [roleList.admin, roleList.editor]
permissionList[accessList.profilePage] = [roleList.admin, roleList.editor, roleList.normal]
permissionList[accessList.createArticle] = [roleList.admin, roleList.editor, roleList.normal]
permissionList[accessList.updateArticle] = [roleList.admin, roleList.editor, roleList.entityCreator]
permissionList[accessList.deleteArticle] = [roleList.admin, roleList.editor, roleList.entityCreator]
permissionList[accessList.undeleteArticle] = [roleList.admin, roleList.editor, roleList.entityCreator]
permissionList[accessList.banArticle] = [roleList.admin, roleList.editor]
permissionList[accessList.unbanArticle] = [roleList.admin, roleList.editor]
permissionList[accessList.createComment] = [roleList.admin, roleList.editor, roleList.normal]
permissionList[accessList.updateComment] = [roleList.admin, roleList.editor, roleList.entityCreator]
permissionList[accessList.deleteComment] = [roleList.admin, roleList.editor, roleList.entityCreator]
permissionList[accessList.undeleteComment] = [roleList.admin, roleList.editor, roleList.entityCreator]
permissionList[accessList.banComment] = [roleList.admin, roleList.editor]
permissionList[accessList.unbanComment] = [roleList.admin, roleList.editor]
permissionList[accessList.updateProfile] = [roleList.admin, roleList.profileOwner]
var getRoles = function(access, resource, isAuthenticated, entity, user) {
var roles = [roleList.visitor]
if (isAuthenticated) {
roles = [roleList.normal]
if (user.username === 'admin')
roles = [roleList.admin]
else if (user.type === 'editor')
roles = [roleList.editor]
if (resource) {
if (resource === resourceList.profile) {
//Note: on server _id is a object, client _id is string, which does not have equals method
if (entity && entity._id.toString() === user._id.toString())
roles.push(roleList.profileOwner)
}
else if (resource === resourceList.article) {
if (entity && entity.statusMeta.createdBy._id.toString() === user._id.toString())
roles.push(roleList.entityCreator)
}
else if (resource === resourceList.comment) {
if (entity && entity.statusMeta.createdBy._id.toString() === user._id.toString())
roles.push(roleList.entityCreator)
}
}
}
return roles
}
exports.havePermission = function(access, resource, isAuthenticated, entity, user) {
var roles = getRoles(access, resource, isAuthenticated, entity, user)
//Note: we can implement black list here as well, like IP Ban
if (!permissionList[access])
return true
for (var i = 0; i < roles.length; i++) {
var role = roles[i]
if (permissionList[access].indexOf(role) !== -1)
return true
}
return false
}
Then on app/server/helper.js (act as adapter)
var both = require(dir.both + '/both.js')
exports.accessList = both.accessList
exports.resourceList = both.resourceList
exports.havePermission = function(access, resource, req) {
return both.havePermission(access, resource, req.isAuthenticated(), req[resource], req.user)
}
//todo: use this function in other places
exports.getPermissionError = function(message) {
var err = new Error(message || 'you do not have the permission')
err.status = 403
return err
}
exports.getAuthenticationError = function(message) {
var err = new Error(message || 'please sign in')
err.status = 401
return err
}
exports.requiresPermission = function(access, resource) {
return function(req, res, next) {
if (exports.havePermission(access, resource, req))
return next()
else {
if (!req.isAuthenticated())
return next(exports.getAuthenticationError())
else
return next(exports.getPermissionError())
}
}
}
on app/client/helper.js, also act as adapter.
exports.accessList = both.accessList
exports.resourceList = both.resourceList
exports.havePermission = function(access, resource, userService, entity) {
//Note: In debugging, we can grant client helper all access, and test robustness of server
return both.havePermission(access, resource, userService.isAuthenticated(), entity, userService.user)
}

I personnally took inspiration from ghost. In my config there is the perms, and permissions.jsexport a canThisfunction that take the current logged user. Here is the whole project
Part of my config file
"user_groups": {
"admin": {
"full_name": "Administrators",
"description": "Adminsitators.",
"allowedActions": "all"
},
"modo": {
"full_name": "Moderators",
"description": "Moderators.",
"allowedActions": ["mod:*", "comment:*", "user:delete browse add banish edit"]
},
"user": {
"full_name": "User",
"description": "User.",
"allowedActions": ["mod:browse add star", "comment:browse add", "user:browse"]
},
"guest": {
"full_name": "Guest",
"description": "Guest.",
"allowedActions": ["mod:browse", "comment:browse", "user:browse add"]
}
},
mongoose = require("mongoose")
###
This utility function determine whether an user can do this or this
using the permissions. e. g. "mod" "delete"
#param userId the id of the user
#param object the current object name ("mod", "user"...)
#param action to be executed on the object (delete, edit, browse...)
#param owner the optional owner id of the object to be "actionned"
###
# **Important this is a promise but to make a lighter code I removed it**
exports.canThis = (userId, object, action, ownerId, callback) ->
User = mongoose.model("User")
if typeof ownerId is "function"
callback = ownerId
ownerId = undefined
if userId is ""
return process(undefined, object, action, ownerId, callback)
User.findById(userId, (err, user) ->
if err then return callback err
process(user, object, action, ownerId, callback)
)
process = (user, object, action, ownerId, callback) ->
if user then role = user.role or "user"
group = config.user_groups[role or "guest"]
if not group then return callback(new Error "No suitable group")
# Parses the perms
actions = group.allowedActions
for objAction in actions when objAction.indexOf object is 0
# We get all the allowed actions for the object and group
act = objAction.split(":")[1]
obj = objAction.split(":")[0]
if act.split(" ").indexOf(action) isnt -1 and obj is object
return callback true
callback false
config = require "../config"
Usage example:
exports.edit = (userid, name) ->
# Q promise
deferred = Q.defer()
# default value
can = false
# We check wheteher it can or not
canThis(userid, "user", "edit").then((can)->
if not userid
return deferred.reject(error.throwError "", "UNAUTHORIZED")
User = mongoose.model "User"
User.findOne({username: name}).select("username location website public_email company bio").exec()
).then((user) ->
# Can the current user do that?
if not user._id.equals(userid) and can is false
return deferred.reject(error.throwError "", "UNAUTHORIZED")
# Done!
deferred.resolve user
).fail((err) ->
deferred.reject err
)
deferred.promise
Perhaps what I've done isn't good, but it works well as far as I can see.

Check the Node module permission for that matter. It's pretty simple concept, I hope they'll allow all CRUD methods too.

Yes, you can access that through the request argument.
app.use(function(req,res,next){
console.log(req.method);
});
http://nodejs.org/api/http.html#http_message_method
Edit:
Misread your question. It would probably just be better to assign user permissions and allow access to the database based upon the permissions. I don't understand what you mean by validate by means of interaction with the database. If you are already allowing them to interact with the database and they don't have the proper permissions to do so, isn't that a security issue?

Related

node express caching issue

I have an app which sends a form schema with the data for page rendering.
The form schema comes from a require call to a configuration - this is in javascript object notation. Depending on the user's permission level, the function massageSchema then for example removes protected fields from the schema, prior to the schema being sent.
This all works. However, if I log out and log in as a different user, the schema relevant to the previous user is sent.
If I stop/start the node instance, the correct schema then gets sent.
So I've deduced that this appears to be a caching issue, but I have no clue as to how to address it.
The router code:
router.get('/:table/:id', authUser, resolveTableName, authMethodTable, function(req, res, next) {
getTableModule(req.params.table)
.then(mod => {
// Massage the schema
mod.formSchema = massageSchema(mod, req.session.user);
...
db.one( sql, [ req.params.table, res.locals.table.idAttribute, req.params.id ])
.then( row => {
res.render("record", {
data: row,
user: req.session.user,
table: req.params.table,
module: mod,
referer: req.get('Referer')
})
....
The massageSchema function:
module.exports = function(mod, user) {
var rv = {};
var orig = mod.formSchema ? mod.formSchema : mod.schema;
// Remove unallowed fields
for(var i in orig) {
if(orig[i].display) {
if(orig[i].display == 'admin' && (user.role == 'admin' || user.role == 'master')) {
rv[i] = orig[i];
} else if(orig[i].display == 'editor' &&
(user.role == 'editor' || user.role == 'admin' || user.role == 'master')) {
rv[i] = orig[i];
}
} else {
rv[i] = orig[i];
}
}
return rv;
};
Why is this happening? What to do?
I'm guessing mod is part of some module.exports?. In JavaScript, objects are always passed by reference, including module.exports mod.formSchema = massageSchema(mod, req.session.user) is actually modifying the module's exported object.
try let schema = massageSchema(mod, req.session.user) instead

Update Contact Details using Azure AD Graph & Node

I am using Node.js and azure-graph to create a user in Azure. It works as expected for basic fields like Name, etc. However, jobTitle and other fields that are usually found in the portal cannot be updated. Any pointers?
let msRest = require('ms-rest-azure');
let azureGraph = require('azure-graph');
let tenantId = common.configDefaults.azure_tenant;
let clientId = common.configDefaults.azure_client_id;
let clientPwd = common.configDefaults.azure_client_secret;
let create_user_in_azure_ad = function (user, cb) {
msRest.loginWithServicePrincipalSecret(clientId, clientPwd, tenantId, {
tokenAudience: 'graph'
}, function (err, credentials, subscriptions) {
if (err) {
done(err.message, null);
} else {
// Create Azure Graph Client to access
let client = new azureGraph(credentials, tenantId);
let password = common.generatePassword(10);
let userParams = {
accountEnabled: true,
userPrincipalName: user.email_official, //please add your domain over here
displayName: user.display_name,
mailNickname: user.email_official.split("#")[0],
jobTitle: "A FANCY TITLE",
passwordProfile: {
password: password,
forceChangePasswordNextLogin: true
},
};
// Now, we can create the User in Active Directory
client.users.create(userParams, function (err, done) {
if (err) {
cb(err, null);
} else {
// The user is created now with a password. Return this information
cb(null, {user: user, password: password});
}
});
}
});
};
Though I'm not aware of Node.js, but I think you can create/update jobTitle by updating contact of a specific user.

Can`t login with seeded database configuration using asp.net identity

I have a problem with login, even though i see the tables are filled with my seeded user info from Configuration.cs:
protected override void Seed(www.Models.ApplicationDbContext context)
{
if (!context.Roles.Any(x => x.Name == "admin"))
{
var roleStore = new RoleStore<IdentityRole>(context);
var roleManager = new RoleManager<IdentityRole>(roleStore);
var role = new IdentityRole { Name = "admin" };
roleManager.Create(role);
}
if (!context.Users.Any(x => x.UserName == "admin" && x.Email == "admin#admin.com"))
{
var userStore = new UserStore<ApplicationUser>(context);
var userManager = new UserManager<ApplicationUser>(userStore);
var user = new ApplicationUser { UserName = "admin", Email = "admin#admin.com" };
var hasher = new PasswordHasher();
userManager.Create(user, "MySecret5");
userManager.AddToRole(user.Id, "admin");
}
}
and when i try to login i get error "Invalid login attempt".
What am i missing?
EDIT:
Im in the process of learning all stuff about asp.net so im pretty big noob now :( so i found this example to be working for me, and if anyone else needs it here it is:
protected override void Seed(www.Models.ApplicationDbContext context)
{
var userStore = new UserStore<ApplicationUser>(context);
var userManager = new UserManager<ApplicationUser>(userStore);
if (!context.Users.Any(x => x.UserName == "admin#v.com"))
{
var user = new ApplicationUser { UserName = "admin#v.com", Email = "admin#v.com" };
userManager.Create(user, "Password5%");
context.Roles.AddOrUpdate(x => x.Name, new IdentityRole { Name = "admin" });
context.SaveChanges();
userManager.AddToRole(user.Id, "admin");
}
}
And thanks for all your help and time.
It seems that the problem with logging in with seeded users is associated with MVC inaccuracy, look at:
var result = await SignInManager.PasswordSignInAsync(model.Emali, model.Password, model.RememberMe, shouldLockout: false);
if we seed this user:
var user = new ApplicationUser { UserName = "SomeName", Email = "admin#v.com" };
result will be == false, but if we change model.Emali in result to model.UserName then login end with success - of course now when we log in, we must give the UserName e.g:
UserName: SomeName;
Passwort: Password5%;
This worked flawlessly for me.
protected override void Seed(www.Models.ApplicationDbContext context)
{
var userStore = new UserStore<ApplicationUser>(context);
var userManager = new UserManager<ApplicationUser>(userStore);
if (!context.Users.Any(x => x.UserName == "admin#v.com"))
{
var user = new ApplicationUser { UserName = "admin#v.com", Email = "admin#v.com" };
userManager.Create(user, "Password5%");
context.Roles.AddOrUpdate(x => x.Name, new IdentityRole { Name = "admin" });
context.SaveChanges();
userManager.AddToRole(user.Id, "admin");
}
}

MVC5 Identity Seed Users in database

I am getting an error that says: "UserId not found." when trying to seed multiple users into my database.
Here is my seed method:
protected override void Seed(newBestPlay.Models.ApplicationDbContext context)
{
// This method will be called after migrating to the latest version.
InitialCreate create = new InitialCreate();
create.Down();
create.Up();
context.Configuration.LazyLoadingEnabled = true;
if (!context.Roles.Any(r => r.Name == "Admin"))
{
var store = new RoleStore<IdentityRole>(context);
var manager = new RoleManager<IdentityRole>(store);
var role = new IdentityRole { Name = "Admin" };
manager.Create(role);
}
if (!context.Roles.Any(r => r.Name == "User"))
{
var store = new RoleStore<IdentityRole>(context);
var manager = new RoleManager<IdentityRole>(store);
var role = new IdentityRole { Name = "User" };
manager.Create(role);
}
if (!context.Users.Any(u => u.UserName == "user1"))
{
var store = new UserStore<ApplicationUser>(context);
var manager = new UserManager<ApplicationUser>(store);
var user = new ApplicationUser { UserName = "user1", Email = "email1" };
manager.Create(user, "ChangeItAsap!");
manager.AddToRole(user.Id, "Admin");
}
if (!context.Users.Any(u => u.UserName == "user2"))
{
var store = new UserStore<ApplicationUser>(context);
var manager = new UserManager<ApplicationUser>(store);
var user = new ApplicationUser { UserName = "user2", Email = "email2" };
manager.Create(user, "ChangeItAsap!");
manager.AddToRole(user.Id, "Admin");
}
}
It is failing on that last "manager.AddToRole" line. I figured out the second user isn't even getting added to the database, so it can't find a user.Id since it never got added.
Figured it out. It was not allowing dashes in my username. My username(email) has a dash in the domain part. I had to add in
this.UserValidator = new UserValidator<ApplicationUser>(this) { AllowOnlyAlphanumericUserNames = false };
into my IdentityConfig.cs file into the ApplicationUserManager constructor so it now looks like this:
public class ApplicationUserManager : UserManager<ApplicationUser>
{
public ApplicationUserManager(IUserStore<ApplicationUser> store)
: base(store)
{
this.UserValidator = new UserValidator<ApplicationUser>(this) { AllowOnlyAlphanumericUserNames = false };
}
.....
add this into your seed method:
var manager = new UserManager<ApplicationUser>(store);
manager.UserValidator = new UserValidator<ApplicationUser>(manager)
{
AllowOnlyAlphanumericUserNames = false,
};

Xamarin.Auth with Twitter

I am trying to implement login with Facebook and Twitter. I got it working with Facebook but don't know how to do it with Twitter. Are there any examples? My Facebook implementation is like this:
var auth = new OAuth2Authenticator(
clientId: "*****************",
scope: "email",
authorizeUrl: new System.Uri("https://m.facebook.com/dialog/oauth/"),
redirectUrl: new System.Uri("http://www.facebook.com/connect/login_success.html"));
StartActivity(auth.GetUI(this));
auth.Completed += (senderFb, eventArgs) =>
{
if (eventArgs.IsAuthenticated)
{
AccountStore.Create(this).Save(eventArgs.Account, "Facebook");
// Now that we're logged in, make a OAuth2 request to get the user's info.
var request = new OAuth2Request("GET", new System.Uri("https://graph.facebook.com/me"), null, eventArgs.Account);
request.GetResponseAsync().ContinueWith(t =>
{
if (!t.IsFaulted && !t.IsCanceled)
{
var obj = JsonValue.Parse(t.Result.GetResponseText());
if (obj != null)
{
var user = new UserProfile
{
FirstName = obj["first_name"],
LastName = obj["last_name"],
FacebookProfileLink = obj["link"],
FacebookToken = eventArgs.Account.Properties["access_token"],
Gender = obj["gender"] == "female" ? "Female" : "Male",
EmailAddress = obj["email"],
DisplayName = obj["name"],
Name = obj["name"],
LoginName = obj["email"]
};
SignUpUser(user);
}
}
}, uiScheduler);
}
};
You can use oauth1 like this.
this.Authenticator = new OAuth1Authenticator (ConsumerKey, ConsumerSecret, RequestTokenUrl, AuthorizeUrl, AccessTokenUrl, DummyCallBackUrl, (IDictionary<string, string> accountProperties) => {
string screen_name = "";
if (accountProperties.TryGetValue("screen_name", out screen_name)) {
Account a = new Account(screen_name, accountProperties);
AuthenticatorCompletedEventArgs e = new AuthenticatorCompletedEventArgs(a);
CompleteAuthentication(e);
}
return null;
});
twitter api dosen't fully support OAuth2

Resources