MongoDB + Node JS + Role Based Access Control (RBAC) - javascript

Am currently learning MEAN stack, developing a simple TODO's app and want to implement Role Based Access Control (RBAC) for that. How do i set up roles & permission on MongoDB.
I want 3 roles (roles may look funny but this is purely to learn) :
GOD
SUPER HERO
MAN
GOD - similar to super admin, can do anything in the application. C,R,U,D permissions for TODO's and for other users too. Can Create a TODO & assign it to any SUPER HERO or MAN directly. Update or Delete either a TODO or a User at any point in time.
SUPER HERO - similar to admin, has super power to do anything on his personal Data - C,R,U,D for TODO's. Can't create any users. Can only Read & add comments for TODO's created by GOD & assigned to him/her.
MAN - Can only Read and add comments to TODO's assigned to him/her.
To sum it up :
GOD - C,R,U,D [Global Level]
SUPER HERO - C,R,U,D [Private] + R,U [Assigned to him]
MAN - R,U [Assigned to him]
I understand that i need to have USERS & ROLES collections. Where ROLES inturn should have PERMISSIONS etc. How do i wire them all ?

I like names given to roles - GOD, SUPER HERO & MAN, easy to understand.
As you are using MEAN stack and much of routes validation happens on node, i would prefer keeping roles table simple.
Roles :
{
_id : 1,
name : GOD,
golbalPerms : true
},
{
_id : 2,
name : SUPER HERO,
privatePerms : true
},
{
_id : 3,
name : MAN
}
Users :
{
_id : 111,
name : Jesus,
roleId : 1
},
{
_id : 222,
name : BatMan,
roleId : 2
},
{
_id : 333,
name : Jack,
roleId : 3
}
When user logs in and sending user object back to client, make sure to replace roleId with corresponding role object from DB.
Coming to code on Node JS :
By completely understanding your usecase we can divide them into following methods -
CreateUser
CreateTodo
DeleteTodo
ReadTodo
UpdateTodo
CommentTodo
AssignTodo
Lets go step by step, CreateUser.
Routes code snippet :
app.all('/users', users.requiresLogin);
// Users Routes
app.route('/users')
.post(users.hasPerms('globalPerms'), users.create);
In your Controller you can validate based on the input globalPerms, if validated allow to create user by calling next() else return with corresponding error message.
Now CreateTodo && DeleteTodo :
Both of them pretty much work on same logic with a small trick.
Routes code snippet :
app.all('/todos', users.requiresLogin);
// Users Routes
app.route('/todos')
.post(users.hasPerms('globalPerms','privatePerms'), todos.create);
.delete(users.hasPerms('globalPerms','privatePerms'), todos.delete);
For creating a Todo, globalPerms are with GOD & privatePerms are with SUPER HERO, both of them can be allowed.
Trick here will be in todos.delete method, just ensure user.id === todos.createById else SUPER HERO may go on to delete Todos created by GOD.
ReadTodo :
When a TODO is created it should have a createById stored likewise when a TODO is assigned to someone then assignedTo and assignedBy should be recorded too.
This makes lot of other operations easy to handle.
user.role.globalPerms - give GOD all TODO's data.
user.role.privatePerms - give TODO's either createdBy him/her or assigned to him/her.
user.role.globalPerms === undefined && user.role.privatePerms === undefined - its MAN and give TODO's which are only assignedTo him.
UpdateTodo & CommentTodo :
This is exact replica of what ReadTODO does so DIY
Last one, AssignTodo :
Simple one, loggedInUser.id === todos.createdById then he can assign it to anyone.
Two things to keep in mind here :
As assigning part mostly happens on your UI (Angular) front, i have given that approach of checking loggedInUser.id === todos.createdById. Logged in user any ways will see all TODO's by read operation and can assign it to anyone he/she likes.
Make sure a SUPER HERO can only assign a TODO to himself or other SUPER HERO or to a MAN but not to GOD. How you show Assign to options on UI front is out of scope of this question. This is just a heads up.
Hope this was clear.
NOTE : There was no necessity to give permissions to MAN in Roles collection & we managed all possible operations with out that.

This is a very broad question which can be solved in many ways.
You have added that you are using MEAN stack therefore I'll restrict my question to that.
One thing that you haven't included in the whole question is what kind of authentication architecture are you using. Let's say you are using token based authentication, generally people these days use it.
We have 3 types of users.
You have different options available to differentiate between type of tokens as well.
Different Collection (mongoDB) or Redis sets where they will be stored
The encrypted token will have type of the user as well etc.. (This will come in handy if you don't need to store tokens on the backend, you can just decrypt and check)
It will completely depend on use case.
Now, before allowing any user's entry to user specific route make sure that you are checking the token first.
Example
app.post('/godlevelroute', godtokencheck, callrouteandfunction);
app.post('/superherolevelroute', superheroroute, callrouteandfunction);
You must send token in header from angular and then you can take the data out from the header and then you can check if that specific user has permission to go through that route or not.
Let's say a god level user is logged in then he'll have the godleveltoken with him and we'll check that first before allowing him to access that route or else you can just show error message.
This can be your sample token checking function on server end
function checkToken(req, res, next) {
var token = req.headers['accesstoken']; //access token from header
//now depending upon which system you are following you can run a check
};
Node Module Suggestion : https://www.npmjs.com/package/jsonwebtoken
Now coming to frontend part. You are using angular based on what you have written, you can intercept the token before showing any page.
You can go through this blog to get a pictorial representation of what I have tried to explain. Click Here

Possible approach-> have role embedded in user collection/schema:
users document shall have the following:
{
_id : "email#mail.com",
name: "lorem ipsum",
role: "MAN"
}
As far as your post describes, only god can make and assign TODOs.
Roles Collection may hold the following:
{
_id : "MAN",
globalPerm: [],
privatePerm: [],
assignedPerm: ["r","u"],
},
{
_id : "SUPER_HERO",
globalPerm: [],
privatePerm: ["c","r","u","d"],
assignedPerm: ["c","r","u","d"],
},
{
_id : "GOD",
globalPerm: ["c","r","u","d"],
privatePerm: ["c","r","u","d"],
assignedPerm: ["c","r","u","d"],
}
Node JS Middlewares
After getting correct permission values for a user, you might want to use middlewares.
Sample express HTTP request route:
app.post('/updateTodo', permissions.check('privatePerm', 'c'), function (req, res) {
// do stuff
};
permissions.check is called before actually executing function body to update TODO.
Hence if a user tries to update a todo, it will first verify the corresponding permissions.

Related

Discord JS - Reverse lookup to obtain userID from their display name

We're currently building a bot that will review specific messages and perform follow-up administrative actions on it. One of the actions will read a message and extract a named user (string - not link), search for it and return the ID of the user - which is then used elsewhere.
if (message.channel.id === register) {
// The post is in the register channel
if (message.content.includes('doing business with you')) {
//it's a successful claim
Rclaimedby = message.content.match(/\*\*(.*?)\*\*/)[1];
const Ruser = client.users.cache.find(user => user.username == Rclaimedby).id;
The text snippet will just have the individuals name as a string, which the regex will extract. If the individual doesn't have a nickname set, it'll work and the rest of the code works. However, as soon as that name isn't their "normal" name, it doesn't work and crashes with a TypeError.
I need to be able to use that stripped name and find the ID regardless of whether they have a nickname or not, but I've been searching for days (and a LOT of trial and error) and can't find a way around it.
To help, the message content would say something like:
(...) doing business with you normalname (...) or
(...) doing business with you randomnickname (...)
Help?
Like you mentioned in your comment, the key difference between extracting a user's ID from their displayName would mean you would have to get the GuildMember object first (it's server-specific) and then get the user property from that.
All in all, it just requires a little bit of re-maneuvering around the variables in order to get what you want since the displayName property is specific to each server.
Process:
Get the server ID
From the server ID, get the server
Get the members property from the server
Find the member by matching it with the given randomnickname, which I've set as displayNickname in this case.
Get the user property from the GuildMember object and grab the ID from there.
Code:
This is merely something you can work off of.
if (command === "memberinfo") {
let serverID = "800743801526157383"; //server id; manually input your own
let displayNickname = args[0];
message.channel.send(`Sent Nickname: ${displayNickname}`);
let serverMembers = client.guilds.cache.get(serverID).members //defines server and returns members
let matchedMember = serverMembers.cache.find(m => m.displayName === displayNickname);
message.channel.send(`Matched Member ${matchedMember.user.username}.`)
console.log(`The user's ID: ${matchedMember.user.id}`);
}
Resources:
SO Question - Fetching user from displayName
Discord.js Documentation

Efficient way making "Like" function without duplication?

I am making a "Like" function (facebook or instagram kind of), but not sure what is the right way to do it.
I could think of 2 ways.... (User cannot like same article twice)
A. "User" data has an array of "Article" IDs...
// simplified user schema MongoDB
const UserSchema ={
id:ObjectID,
username:String,
likes:[{type:ObjectID,ref:"Article"}]
}
// simplified article schema
const AriticleSchema = {
id:ObjectID,
title:String,
content:String,
likes:Number,
}
B. "Article" data has an array of "User" IDs...
// simplified user schema MongoDB
const UserSchema ={
id:ObjectID,
username:String,
}
// simplified article schema
const AriticleSchema = {
id:ObjectID,
title:String,
content:String,
likes:[{type:ObjectID,ref:"User"}],
}
I tried both ways and they all worked fine when I only have few users and few articles.
but What if I have thousands of "User"s and thousands of "Article"s? I am worrying that everytime I request "User" data or "Article" data(let's say several at a time), I also have to bring arrays of thousands? I think there must be better way to do this...
Do you know how people or companies do this? I want to know the concept of how "Like" function works.
Thank you.
** Adding some details **
I want "User" can login for reading articles, and press "like button" to like it. Article "like" will go up by 1 every time unique user likes it (no duplicate). Somebody who already liked the " article" they can "unlike" it or "user" will see they already liked the "article", which means we gotta know that "user" like this "article" or not. Other people dosen't need to know.
I'll extrapolate on a solution by going through the requirements you elaborated step by step.
User can login
This will require some system for authorization. You could perhaps use another Mongo table dedicated to this sort of thing - at the minimum it should link some authorization token to a user id.
const AuthSchema = {
user_id: ObjectID,
auth_token: string,
}
The way you get this auth token is through various means - really depends on how you auth your users, e.g. phone auth or username/password. All that's a bit beyond the scope of this answer.
Article "like" will go up by 1 every time unique user likes it (no duplicate). Somebody who already liked the " article" can "unlike" it or "user" will see they already liked the "article"
A Redis Set would model this quite well. Every time a user likes an article, add their user_id into a Redis Set for that article. The key for such set could look like this:
article:${article_id}:likes
Get the number of people who liked the article by taking the size of its set (full of unique user_ids). Whenever someone unlikes an article, remove their user_id from the set.
Further reading:
Redis Sets and other Redis datatypes
Redis sadd command.

Best way to manage Chat channels in Firebase

In my main page I have a list of users and i'd like to choose and open a channel to chat with one of them.
I am thinking if use the id is the best way and control an access of a channel like USERID1-USERID2.
But of course, user 2 can open the same channel too, so I'd like to find something more easy to control.
Please, if you want to help me, give me an example in javascript using a firebase url/array.
Thank you!
A common way to handle such 1:1 chat rooms is to generate the room URL based on the user ids. As you already mention, a problem with this is that either user can initiate the chat and in both cases they should end up in the same room.
You can solve this by ordering the user ids lexicographically in the compound key. For example with user names, instead of ids:
var user1 = "Frank"; // UID of user 1
var user2 = "Eusthace"; // UID of user 2
var roomName = 'chat_'+(user1<user2 ? user1+'_'+user2 : user2+'_'+user1);
console.log(user1+', '+user2+' => '+ roomName);
user1 = "Eusthace";
user2 = "Frank";
var roomName = 'chat_'+(user1<user2 ? user1+'_'+user2 : user2+'_'+user1);
console.log(user1+', '+user2+' => '+ roomName);
<script src="https://getfirebug.com/firebug-lite-debug.js"></script>
A common follow-up questions seems to be how to show a list of chat rooms for the current user. The above code does not address that. As is common in NoSQL databases, you need to augment your data model to allow this use-case. If you want to show a list of chat rooms for the current user, you should model your data to allow that. The easiest way to do this is to add a list of chat rooms for each user to the data model:
"userChatrooms" : {
"Frank" : {
"Eusthace_Frank": true
},
"Eusthace" : {
"Eusthace_Frank": true
}
}
If you're worried about the length of the keys, you can consider using a hash codes of the combined UIDs instead of the full UIDs.
This last JSON structure above then also helps to secure access to the room, as you can write your security rules to only allow users access for whom the room is listed under their userChatrooms node:
{
"rules": {
"chatrooms": {
"$chatroomid": {
".read": "
root.child('userChatrooms').child(auth.uid).child(chatroomid).exists()
"
}
}
}
}
In a typical database schema each Channel / ChatGroup has its own node with unique $key (created by Firebase). It shouldn't matter which user opened the channel first but once the node (& corresponding $key) is created, you can just use that as channel id.
Hashing / MD5 strategy of course is other way to do it but then you also have to store that "route" info as well as $key on the same node - which is duplication IMO (unless Im missing something).
We decided on hashing users uid's, which means you can look up any existing conversation,if you know the other persons uid.
Each conversation also stores a list of the uids for their security rules, so even if you can guess the hash, you are protected.
Hashing with js-sha256 module worked for me with directions of Frank van Puffelen and Eduard.
import SHA256 from 'crypto-js/sha256'
let agentId = 312
let userId = 567
let chatHash = SHA256('agent:' + agentId + '_user:' + userId)

Which is worse? One more db acess or one outdated value?

PROBLEM
I'm running an application with AngularJS, Node JS, Express and MongoDB. I'm acessing MongoDB trought mongoose. My problem is that I have a lists of swords, each forged by someone. But, to access someone's profile, I need to use the ID of someone. The ID is unique. But I can't display the link as something link '352384b685326vyad6'. So, when someone create's a sword, I will store his or her name within the sword info.
To display a list of swords, for example, I could do something like this:
<div ng-repeat='sword in swords'>
<p> Sword name: {{sword.name}} </p>
<p> Author: <a href='#/user/{{sword.createdByID}}'> {{sword.createdByName}} </a> </p>
</div>
But, if the User changes his name, the sword will not update his creator name accordingly. What should I do? I have thought of some solutions, but I don't know which and if any could solve this in a good manner.
When someone changes own name, I could make a POST request with the new username and the ID, updating all sword that has createdByID equals to user ID. But I see this too strange.
SwordModel.find({ createdByID: req.body.id}, [...]);
When loading the swords in the controller via GET request, make another GET request for each sword and update sword.username based on the sword.createdById.
UserModel.findById(req.body.id), [...]);
Forget UX and use ugly links.
I want to know how can I maintain the username of each sword updated without affecting too much my DB Thanks for any advice.
MODELS - Just for reference.
sword.js
var mongoose = require('mongoose');
var SwordModel = mongoose.model('SwordModel',
{
name: String, //Sword's name
createdById: String, //ID of the user who created.
createdByName: String //Name of the user
});
module.exports = mongoose.model('SwordModel', SwordModel);
user.js
var mongoose = require('mongoose');
var UserModel = mongoose.model('UserModel',
{
name: String, //Name of the user.
ID: String //ID of the user.
});
module.exports = mongoose.model('UserModel', UserModel);
You can do it by referencing the User inside Swords if you make use of Mongoose schema.
After you make a reference to another schema you can use populate method to get the desired results.
Example (May be not exactly, but something similar to following) :
Sword Schema:
var swordSchema = new Schema({
name: String,
createdBy: {type: mongoose.Schema.Types.ObjectId, ref: 'User'}
});
Make model using the schema :
var swordModel = mongoose.model('Sword', swordSchema);
Find what you are looking for using populate.
See full documentation for populate here - http://mongoosejs.com/docs/populate.html
EDIT: note that I am recommending to keep user's name only in User model and just reference it.
I think we have less of a technical problem and more of a conceptual problem here.
Constraints
I take it for granted that...
users may change their usernames
usernames are unique
you only need to provide a link to a user, displayed by name
Furthermore, I will use plain JSON and MongoDB and trust that you can translate this to Mongoose.
The solution
Albeit users can change their usernames, this is not going to happen often. The more common use case is that you need to link to a username. So we first need to find out how to efficiently deal with that use case.
Since you only need the name of the smith for a given sword, there is nothing wrong with a sword model like
{
_id: new ObjectId(),
name: "Libertas",
smith: "Foobar"
}
to efficiently find "Foobar" in the users collection, we simply add an index here (if not already done):
db.users.createIndex({username:1}, {unique:true})
and your service can query efficiently by using
db.users.find({name: "Foobar"})
No need to save the _id of the user within the sword document, but still you can query for it efficiently.
Dealing with a change in the username is a rather rarely executed use case, so optimizing here does not make sense. However, if a user changes his or her username, your service can easily achieve that through
db.swords.update(
{ smith:"Foobar" },
{ $set:{ smith: "CoolNewUsername" },
{ multi: true, writeConcern: { w:1, j:true }
)
The last line of the above needs a little explanation. The multi: true option tells MongoDB to change all documents matching {smith: "Foobar"}, not only the first one found, that's easy to understand. But why to set the write concern to journaled? The first reason for it is that regardless of the write concern configured for the connection (which may even be unacknowledged), we need those changes to be durable. However, we usually do not need to have the changes to be propagated to more replica set members, so the chosen write concern gives you the best performance while still you can be sure that the changes were synced to a disk. If you need higher durability, of course you can set the write concern to {w:2} or {w:"majority"}.
Advantages
For the most common use case of this relationship (displaying a link to the user who forged a given sword), all information needed to do this is included in the sword's document, preventing possibly unnecessary queries.
Still, the smith of a given sword can be queried efficiently if a user clicks said link.
Changing a users name is possible and can be achieved pretty efficiently and durable
Disadvantages
The main disadvantage here is that you actually have to modify all affected swords when a user changes his or her username, whereas with a Mongoose reference that would be unnecessary. However, since this is a rare use case and using populate would result in the whole user document to be loaded where only the users name is needed, I see this disadvantage as negligible. Basically you are trading to cut down the queries needed for a common use case by half against the need for a manual update which occurs rather rarely.
I fail to see any other disadvantage.

Meteor - Allow multiple users to edit a post

I'm not able to use the node server debugger so I'm posting here to see if I can get a nudge in the right direction.
I am trying to allow multiple users to edit documents created by any of the users within their specific company. My code is below. Any help would be appreciated.
(Server)
ComponentsCollection.allow({
// Passing in the user object (has profile object {company: "1234"}
// Passing in document (has companyId field that is equal to "1234"
update: function(userObject, components) {
return ownsDocument(userObject, components);
}
});
(Server)
// check to ensure user editing document created/owned by the company
ownsDocument = function(userObject, doc) {
return userObject.profile.company === doc.companyId;
}
The error I'm getting is: Exception while invoking method '/components/update' TypeError: Cannot read property 'company' of undefined
I'm trying to be as secure as possible, though am doing some checks before presenting any data to the user, so I'm not sure if this additional check is necessary. Any advice on security for allowing multiple users to edit documents created by the company would be awesome. Thanks in advance. -Chris
Update (solution):
// check that the userId specified owns the documents
ownsDocument = function(userId, doc) {
// Gets the user form the userId being passed in
var userObject = Meteor.users.findOne(userId);
// Checking if the user is associated with the company that created the document being modified
// Returns true/false respectively
return doc.companyId === userObject.profile.companyId;
}
Looking at the docs, it looks like the first argument to the allow/deny functions is a user ID, not a user document. So you'll have to do Meteor.users.findOne(userId) to get to the document first.
Do keep in mind that users can write to their own profile subdocument, so if you don't disable that, users will be able to change their own company, allowing them to edit any post. You should move company outside of profile.
(If you can't use a proper debugger, old-fashioned console.log still works. Adding console.log(userObject) to ownsDocument probably would have revealed the solution.)

Categories

Resources