Row level permissions in sails.js - mysql

I need row-level permissions in sails. I already looked at this and at other threads, but everybody says to just use policies.
But what I need isn't just limiting the ability to do e.g. find/update all elements of a table but to have permissions per database row that consider associations for permissions.
It would be great if the permissions support blueprints like the ember data adapter, nested create/update/find (with populate) and sockets.
So for example:
GET /:model/:id
should return and populate with such entries where certain associated conditions are met.
So for example, we have 4 models:
User (columns: id, name, email, pwd_hash, ...)
Project (columns: id, client, name, ...)
UserAssignment (columns: id, user, project, user_perms, ...)
Client (columns: id, name, ...)
User and Project are linked through UserAssignment - an advanced MM-Table. (Users may have special user_perms to different projects, such as read,write,manage). And a Project always has one Client.
Here's the corresponding sails models:
// User.js
attributes: {
name: 'string'
}
// Project.js
attributes: {
name: 'string',
client: {
model: 'client'
},
userAssignments: {
collection: 'userAssignment',
via: 'project'
}
}
// UserAssignment.js
attributes: {
userPerms: 'integer',
user: {
model:'user'
},
project: {
model:'project'
}
}
// Client.js
attributes: {
name: 'string',
projects: {
collection: 'project',
via: 'client'
}
}
So lets say the User with the ID=1 wants to access a list of Clients he is allowed to see, he calls
GET /clients/
Speaking in SQL:
SELECT client.*
FROM client
INNER JOIN project ON project.client = client.id
INNER JOIN user_assignment ON project.id = user_assignment.project
WHERE user_assignment.user = 1 and user_perms > 4
GROUP BY client.id;
And then also if we have certain Project managers, they can update associated UserAssignments etc.
Basically I thought the permissions could be based on role associations.
I tried several ways to implement this functionality. I tried _permission_read, _permission_write columns for all rows and other stuff like using populates for this but nothing seems to work right.
The above example is just a excerpt of many different kinds of models which I can filter based on SQL couldn't do nicely with Sails/Waterline.
Do I need custom SQL queries for this?
Is it possible to do this neatly with Sails?
Do I misunderstand policies and is there a way to implement such requirements with them?
Or shall I use SQL views instead of tables?
Thanks in advance!

Related

Return softDeleted relation rows

I have a table, called "bsService", where I save my created services. Those services have some relations, like categories, activities and others.
I'm trying to get services where categories was softDeleted. Example: service 1 relates to category 1, I softDeleted category 1, and now this service doesn’t return on findAll even if add 'withDeleted: true' on the query.
Here's my findAll method. I want all data even if a relation is softDeleted.
findAll = async (
where?: WhereConditions,
transactionEntityManager: EntityManager = getManager(),
order?: 'ASC' | 'DESC',
withDeleted?: boolean,
): Promise<BSService[]> => transactionEntityManager.find(BSService, {
withDeleted,
where,
relations: ['sla', 'activity', 'activity.category', 'department', 'department.company', 'attendance', 'logs', 'logs.user', 'requestingAgent', 'alocatedAgent', 'category', 'requestingAgent.jobs', 'alocatedAgent.jobs'],
order: {
updateAt: order,
},
});
The 'withDeleted' becomes true, depending on which page client is using. For that example, it is always true.
withDeleted in .find() only applies to the top layer, not relations.
Here's someone with a similar issue.
Here's a PR that shows how you can solve it by using a query builder with .withDeleted() before the relations you want to include. There's some discussion about supporting this with .find() but it seems like it won't be any time soon.
await manager
.getRepository(BSService)
.createQueryBuilder('bsservice')
.withDeleted() // Above innerJoinAndSelects
.innerJoinAndSelect('bsservice.sla', 'sla')
.innerJoinAndSelect('bsservice.activity', 'activity')
// ... more inner joins ...
.getMany();

How to get associated elements in Sequelize for specific case

I have models for Users, Projects and Tasks
Users and Tasks has association Many-to-Many
Projects and Tasks has association One-to-Many (each Project has many Tasks)
When receiving Tasks for specified User, I want to get associated Project for each task.
May anybody help to find solution how yo to this.
My code is below:
// receiving of user from database
const user = await User.findOne({where: {id: request.userId}})
// receiving of tasks related to user
const tasks = await user.getTasks(
// here i've tried to add following code, but it does not works
include: { model: Project, as: 'project' }
)
response.status(201).json( tasks )
Any help would be thankful
In Sequelize, when dealing with 'many' associations, might become a little different. You said you already have this association:
Project.model
Project.hasMany(Task)
Now you need to implement the other way inside your Task.model:
Task.belongsTo(Project);
In the Sequelize Documentation you'll find this:
To create a One-To-Many relationship, the hasMany and belongsTo
associations are used together;
I have fixed problem. Correct code below.
const user = await User.findByPk( request.userId )
if(!user) return
const tasks = await user.getTasks(
{ include: [
{ model: Project, as: 'project' }
] }
)

How to use .findAll and find records from two different tables and a self reference?

I'm working with two tables in particular. Users and Friends. Users has a bunch of information that defines the User whereas Friends has two columns aside from id: user_id and friend_id where both of them are a reference to the User table.
I'm trying to find all of the users friends in as little calls to the db as possible and I currently have 2. One to retrieve the id of a user first from a request, then another to Friends where I compare the IDs from the first call and then a third call that passes the array of friends and find all of them in the Users table. This already feels like overkill and I think that with associations, there has to be a better way.
Modification of the tables unfortunately is not an option.
One thing that I saw from "http://docs.sequelizejs.com/manual/querying.html#relations---associations"
I tried but got an interesting error.. when trying to repurpose the code snippet in the link under Relations/Associations, I get "user is associated to friends multiple times. To identify the correct association, you must use the 'as' keyword to specify the alias of the association you want to include."
const userRecord = await User.findOne({
where: { id }
})
const friendsIDs = await Friends.findAll({
attributes: ["friend_id"],
where: {
user_id: userRecord.id
}
}).then(results => results.map(result => result.friend_id));
const Sequelize = require("sequelize");
const Op = Sequelize.Op;
return await User.findAll({
where: {
id: { [Op.in]: friendsIDs }
},
});
Above for my use case works. I'm just wondering if there are ways to cut down the number of calls to the db.
Turns out Sequelize handles this for you if you have the proper associations in place so yes, it was a one liner user.getFriends() for me.

Strongloop Loopback Native SQL

I have the following situation in a loopback application:
I have a 'Person' class that extends from built in 'User'
I have students ('Person' class with Role 'student')
I have teachers ('Person' class with Role 'teacher')
I have classgroups (ClassGroup class related to Person class with hasManyAndBelongsTo)
Students and Teachers are added to classgroups in the same way. Teachers can have multiple classgroups, students not. If I want to see all the students that are in the same classgroups, as a teacher, I use a remote method on Person. In that method I use the following SQL (21 is the id of the teacher):
SELECT * FROM `Person`
JOIN `ClassGroupPerson`
ON `Person`.`id` = `ClassGroupPerson`.`personId`
JOIN `RoleMapping`
ON `RoleMapping`.`principalType` = "USER"
AND `RoleMapping`.`principalId` = `Person`.`id`
JOIN `Role`
ON `Role`.`name` = "student"
AND `Role`.`id` = `RoleMapping`.`roleId`
WHERE
`ClassGroupPerson`.`classGroupId` IN (
SELECT `classGroupId`
FROM `ClassGroupPerson`
WHERE `personId` = 21
)
In the documentation, it says, that using native Mysql is discouraged (http://docs.strongloop.com/display/public/LB/Executing+native+SQL).
How would I achieve the same result in 'Loopback-style'. I've tried with filters, include, hasManyTrough, ... -stuff but I'm not able to achieve the same result?
Extra question: the result from this are JSON objects, how can I convert those easily into Person objects?
EDIT
With the following code in my remote method, I'm getting close:
Person.find({
include: {
relation: 'classgroups',
scope: {
include: {
relation: 'people'
}
}
},
where: {
id: id
}
}, function(err, people){
cb(err, people);
});
I have as a result: my teacher, with his classgroups included, and with each classgroup, I have the connected persons (teachers and students). The problem that remains, is that the teacher is also included in the classrooms, so I should be able to filter the result by Role = 'student', and I don't see how to do that?

How to count association size with waterline/sails?

Using sails 0.10.5/waterline 0.10.15:
I cannot find an answer to a simple question: how to count the elements of an association without using populate() (which would load all data).
Let take a simple many2many relation with via:
User:
attributes: {
following: {
collection: 'user',
via: 'follower',
dominant: true
},
follower: {
collection: 'user',
via: 'following'
}
Now I need the size of the collections.
Currently I try
User.findById(1).populateAll().exec(function(err, user) {
// count of followings -> user.following.length;
// count of followers-> user.follower.length;
}
which leads to loading the collections.
I'm missing a count function at collection level to avoid population/loading of data.
Is there a possibility to access the (auto generated) join tables to run a count-query directly on the join?
Something like:
User.findById(1).count({'followings'}).exec(function(err, followings) {
...}
or
UserFollowingFollow_FollowFollowing.countByUserFollowingFollowId(1).
exec(function(err, followings) {
...}
Waterline does offer the count query method and it can be used like this to solve your problem:
User.count().where({follower: followerId})
.exec(function(err, numberOfFollowings) {
//numberOfFollowings will be the integer that you need
})
followerId is the id that you are passing to User.findOne() in your example.
You can also read the Waterline documentation about this.