Sails.js policy not working for a single controller - sails.js

I have 4 Policies in sails.js, SuperUser, Admin, User and SuOrAdmin and 3 models with the blueprint controllers, this is the policies config:
'*': 'SuperUser',
User:{
'*': 'SuOrAdmin',
findOne: 'User'
},
Empresa:{
'*': 'SuperUser',
findOne: 'Admin',
findOne: 'User'
},
Noticia:{
'*': 'SuOrAdmin',
find: 'User',
findOne: 'User'
}
when i log in with the SuperUser i can CRUD all the models except the find method of Noticia, but when i log in with an Admin i can CRUD the Noticia model, this is the SuOrAdmin policy:
module.exports = function(req, res, next) {
// User is allowed, proceed to the next policy,
// or if this is the last policy, the controller
if ( (req.session.user && req.session.user.admin) || (req.session.SuperUser) ) {
return next();
}
// User is not allowed
// (default res.forbidden() behavior can be overridden in `config/403.js`)
return res.json(403, {error: 'You are not permitted to perform this action.'});
};
Can somebody help me please, i have been stucked 2 days in this problem.
#mikermcneil

You can apply multiple policies to a single function as below
Noticia:{
'*': 'superAdmin',
find: ['isUser','isSuperAdmin','isAdmin'],
...
}
In some cases this may still not be the easiest approach.
An alternative simpler way in your case could be that you include each level in the previous one, so a higher level user can always use a lower level user permissions, for example, these are your levels
isUser
isAdmin
isSuperAdmin
In the isUser method , first check if its an admin, return next, then check your user logic
module.exports = function isUser(req, res, next) {
if(is_admin)return next(); //you need to provide logic for is_admin here
//remaining user check logic below
....
}
Similarly in the Admin method, first check if its SuperAdmin and return next, then check your admin logic
module.exports = function isAdmin(req, res, next) {
if(is_su)return next(); //you need to provide logic for is_su here
//remaining admin check logic below
....
}

definitely check out #arkoak's answer as far as an approach. I'm not sure if this will solve your complete problem or not, but here's something else that might help.
Currently, you're mapping models on the LHS of your policies config. The reason it's working is because we have some decent guessing logic in-place-- but actually you want to be using controllers. Policies are just configurable middleware that sit between incoming requests from users and your controller actions.
So for instance, instead of User as the key, try:
UserController:{
'*': 'SuOrAdmin',
findOne: 'User'
}
The other thing I'd mention is to clarify that policies do not cascade-- that is, if you have:
'*': 'foo',
NoticiaController: { '*': 'bar' }
...then the actions of NoticiaController will only be protected under bar (not foo AND bar- catch my drift?)
As for your exact problem of not being able to find from Noticia as a superadmin, I believe it's because your policies are mutually exclusive, like what #arkoak suggested.
Hope that helps!

Related

Should I use information stored in a cookie for indexing purposes?

I am creating my first major app, and I thought of a way to optimize query performance. I am not sure though whether I should go through with it.
Here is a description of my approach.
Every time a user account is created, that user is assigned a random number between 1 and 10, which is stored in their user document. The number is called a number-Id. Here is how the user schema will look like:
let User = new Schema({
/// I left out all the other fields for clairty sake
numberId: {
type: Number,
default: Math.floor(Math.random() * 11),
index: true
}
}
Every time a user creates a blogpost and post, their number-Id is referenced inside the document of that blogpost and post. This is to make querying much faster by indexing the users number-id. Here is how the document of a blogpost would look like in MongoD:
{
"title": "my Blog Post",
"_id": "ObjectId("594824b2828d7b15ecd7b6a5")",
/// Here is the numberId of the user who posted the blogpost, it is added
/// to the document of the blogpost when it is created.
"postersNumberId": 2
/// the Id of the user who posted the blogpost
"postersId": "59481f901f0c7d249cf6b050"
}
Let's say I want to get all the blogposts made by a specific user. I can optimize my query much faster by using the number-Id of the user in question as an index, given that their number-Id is referenced in all the blogposts and comment posts they make.
BlogPost.find({postersId: user_id, postersNumberId: user.numberId});
It seems like this approach warrants that I store the users number-id in req.user in order for it to be readily available whenever I need it to optimize queries. So that means I would have to store the users data in a cookie via passport:
passport.serializeUser(function(user, done){
done(null, user._id);
});
passport.deserializeUser(function(id, done) {
User.findById(id, function (err, user){
if (err || !user) return done(err, null);
done(null, user);
});
});
Given this approach, I could now use all the information stored in the cookie, particularly the numberId, to optimize queries that retrieve the comments and blogposts a user makes:
BlogPost.find({postersId: req.user_id, postersNumberId: req.user.numberId});
However, I am using json-web-tokens to authenticate the user rather than cookies. So I will have to use a cookie to store the number-Id for indexing purposes in addition to using JWT for authentication. I've heard, however, that having cookies is bad for scalability, so I am worried that storing the users number-Id in req.user will eventually impact performance.
Should I continue with this approach, or no? What are the performance implications?
In addition to authentication JWT has a payload, which can be used to store additional information within the generated token itself:
var jwt = require('jsonwebtoken');
var token = jwt.sign({
data: {
numberId: 7
}
}, 'jwtSecret', {
expiresIn: '1h'
});
For retrieval:
jwt.verify(token, 'jwtSecret', function(err, decoded) {
if (err) {
console.log(err)
} else {
console.log(decoded);
//{ data: { numberId: 7 }, iat: 1498350787, exp: 1498354387 }
}
});

Make user settings available in req

I know that passport exposes the current user (I think using the 'passport' policy) in req.user. I want to do a similar thing for settings of a particular user, which are stored in a separate collection (so that they are available in req.settings). How can I do this?
The serializeUser method is responsible for that https://github.com/jaredhanson/passport#sessions
passport.serializeUser(function(user, done) {
done(null, user.id);
});
So instead of just serializing the user.id you could serialize the the settings, too:
passport.serializeUser(function(user, done) {
var sessionUser = { _id: user._id, name: user.name, settings: [...] }
done(null, sessionUser);
});
You might want to read Safe to store complete user info in session with Sails.js?

How do I specify a field that can be validated but not stored in Waterline?

I'm looking at the example in the Waterline docs here.
var User = Waterline.Collection.extend({
types: {
// snip
password: function(password) {
return password === this.passwordConfirmation;
});
},
attributes: {
// snip
password: {
type: 'string',
password: true
},
passwordConfirmation: {
type: 'string'
}
}
});
Is there a way to tell Waterline that passwordConfirmation is not part of the schema so that it is not created if migrate is set to alter or drop, or if using a schemaless DB engine?
The actual use case I want is for a clear text password field to validated from the request, but use beforeCreate to populate a hash field that is actually stored (but not allowing the password property to be stored in the process).
Thanks.
Waterline doesn't support transient fields that are validated but not persisted. You can add the schema: true property to your model which will have it filter out any attributes that aren't explicitly declared, but still make them available in lifecycle callbacks. You'd have to do the validation for those attributes manually (in beforeCreate() or beforeValidate() for example), and you'd lose the ability to add arbitrary fields to schemaless dbs, but it's not necessarily a bad solution.
For your case though, I don't see why it's exactly necessary. Why not just hash the password in beforeCreate and save it back to password?
beforeCreate: function (values, cb) {
values.password = hash(values.password);
return cb();
}

Sailsjs and Associations

Getting into sails.js - enjoying the cleanliness of models, routes, and the recent addition of associations. My dilemma:
I have Users, and Groups. There is a many-many relationship between the two.
var User = {
attributes: {
username: 'string',
groups: {
collection: 'group',
via: 'users'
}
}
};
module.exports = User;
...
var Group = {
attributes: {
name: 'string',
users: {
collection: 'user',
via: 'groups',
dominant: true
}
}
};
module.exports = Group;
I'm having difficulty understanding how I would save a user and it's associated groups.
Can I access the 'join table' directly?
From an ajax call, how should I be sending in the list of group ids to my controller?
If via REST URL, is this already accounted for in blueprint functions via update?
If so - what does the URL look like? /user/update/1?groups=1,2,3 ?
Is all of this just not supported yet? Any insight is helpful, thanks.
Documentation for these blueprints is forthcoming, but to link two records that have a many-to-many association, you can use the following REST url:
POST /user/[userId]/groups
where the body of the post is:
{id: [groupId]}
assuming that id is the primary key of the Group model. Starting with v0.10-rc5, you can also simultaneously create and a add a new group to a user by sending data about the new group in the POST body, without an id:
{name: 'myGroup'}
You can currently only add one linked entity at a time.
To add an entity programmatically, use the add method:
User.findOne(123).exec(function(err, user) {
if (err) {return res.serverError(err);}
// Add group with ID 1 to user with ID 123
user.groups.add(1);
// Add brand new group to user with ID 123
user.groups.add({name: 'myGroup'});
// Save the user, committing the additions
user.save(function(err, user) {
if (err) {return res.serverError(err);}
return res.json(user);
});
});
Just to answer your question about accessing the join tables directly,
Yes you can do that if you are using Model.query function. You need to check the namees of the join tables from DB itself. Not sure if it is recommended or not but I have found myself in such situations sometimes when it was unavoidable.
There have been times when the logic I was trying to implement involved a lot many queries and it was required to be executed as an atomic transaction.
In those case, I encapsulated all the DB logic in a stored function and executed that using Model.query
var myQuery = "select some_db_function(" + <param> + ")";
Model.query(myQuery, function(err, result){
if(err) return res.json(err);
else{
result = result.rows[0].some_db_function;
return res.json(result);
}
});
postgres has been a great help here due to json datatype which allowed me to pass params as JSON and also return values as JSON

Is there a way to distinguish room leaders?

When a user creates a room, is that user given some sort of identifier that can be used to give them special/separate functions within the room?
I'm thinking specifically of a follow-the-leader-type scenario, where the room creator is the only one who can affect the page.
(I presume this is easily done via formal user authentication, but I'm curious as to what happens out of the box.)
There is currently no way to identify which user joined a room first. Currently, the only way to do this is to use a JWT that identifies which users are leaders or followers by their groups.
You can then leverage our ACL's to ensure that only the leader can modify certain data. For retrieving the users groups you can call get on the relevant user key for displaying the appropriate views inside your application.
On our roadmap is the ability to read group and user information from a key in the ACL meaning that when a user joins a room they could perform a set with overwrite false. If no other user had set the key they would automatically be a member of the group.
If you're not worried about proper user authentication, you could determine the user by leveraging set overwrite. The following is an example:
var leaderKey = room.key('leader');
function acquireLeader (cb) {
leaderKey.set(user.id, { overwrite: false, cascade: room.self() }, function (err) {
if (err instanceof goinstant.errors.CollisionError) {
return cb(null, false); // someone else is the leader
} else if (err) {
return cb(err); // something else went wrong
}
cb(null, true);
});
}
room.join(function(err) {
if (err) {
throw err;
}
acquireLeader(function(err, isLeader) {
if (err) {
throw err;
}
console.log('are you the leader?', isLeader);
leaderKey.on('remove', function (value, context) {
acquireLeader(function(err, isLeader) {
console.log('did you acquire leader?', isLeader);
});
});
});
});
The above leverages set overwrite and key cascading to ensure when a user joins the room they attempt to become the leader. If they key does not have a value their user id is stored in the leader key. If the key already has a value then someone else is the leader.
When the user leaves the room, the leader key will be removed and then leadership-election can occur again. Meaning someone will always be the leader!