I've been reading the loopback docs about roles. They state the following:
To qualify a $owner, the target model needs to have a belongsTo
relation to the User model (or a model extends from User) and property
matching the foreign key of the target model instance. The check for
$owner is only performed for a remote method that has ':id' on the
path, for example, GET /api/users/:id.
However, what happens when I have a "hasMany" relation and want to perform an action on some object like this:
PUT myusers/123/news/456
This would be my user.json:
{
"name": "MyUser",
"plural": "myusers",
"base": "User",
"idInjection": true,
"options": {
"validateUpsert": true
},
"properties": {},
"validations": [],
"relations": {
"news": {
"type": "hasMany",
"model": "News",
"foreignKey": ""
}
},
"acls": [],
"methods": []
}
Based on this, this and this. I've changed the MyUser entity to Writer entity, because I like it.
As the Writer entity has many News, the News relation and ACL should be something like this (news.json).
"relations": {
"writer": {
"type":"belongsTo",
"model":"Writer",
"foreignKey":"writer_id"
}
},
"acls": [
{ // Nobody has access to nothing
"accessType": "*",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "DENY"
},
{ // But everyone can read everything
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
},
{ // And authenticated users can create news
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$authenticated",
"permission": "ALLOW",
"property": "create"
},
{ // And the owner of a news can update it
"accessType": "WRITE",
"principalType": "ROLE",
"principalId": "$owner",
"permission": "ALLOW"
}
],
And the Writer entity has the same ACL rules but this relations (writer.json)
"relations": {
"news": {
"type": "hasMany",
"model": "News",
"foreignKey": "writer_id"
}
}
What really happens here is that, when you create a Writer, you must specify email and password becouse he inherited from User model. So if you want to perform the
PUT writers/123/news/456
You must be logged has a Writer which can be done in this endpoint: /api/writers/login (with email+password). This endpoint is going to give you the Writer token and then you'll be able to perform the update on the News if you has the $owner token on your header, url or form.
On the other hand, you can also get the user who is doing the HTTP Request and put that user has the news owner with a hook.
Hope it helps. Regards.
Related
I'm trying to build a simple blog with loopback. I want to extend get Posts with the amount of comments.
I have two possible ways in my mind.
1) Extend the response of the get-posts by a count of the comments, this would be my favorite way, but I have no idea how to extend the reposne.
2) I have tried to observe the comment saving and to get the posts-model, but I can't change it.
post.json
{
"name": "post",
"base": "PersistedModel",
"idInjection": true,
"options": {
"validateUpsert": true
},
"properties": {
"title": {
"type": "string",
"required": true
},
"content": {
"type": "string",
"required": true
}
"published": {
"type": "boolean",
"required": true,
"default": false
}
"commentCount": {
"type": "number",
"default": 0
}
},
"validations": [],
"relations": {
"user": {
"type": "belongsTo",
"model": "user",
"foreignKey": ""
},
"comments": {
"type": "hasMany",
"model": "comment",
"foreignKey": ""
}
},
"acls": [
{
"accessType": "*",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "DENY"
},
{
"accessType": "*",
"principalType": "ROLE",
"principalId": "admin",
"permission": "ALLOW"
},
{
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "admin",
"permission": "ALLOW",
"property": "find"
},
{
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$authenticated",
"permission": "ALLOW",
"property": "create"
},
{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$owner",
"permission": "ALLOW"
},
{
"principalType": "ROLE",
"principalId": "$authenticated",
"permission": "ALLOW",
"property": [
"__create__comments",
"__get__comments"
]
},
{
"principalType": "ROLE",
"principalId": "$owner",
"permission": "ALLOW",
"property": "__delete__comments"
}
],
"methods": {}
}
comment.json
{
"name": "comment",
"base": "PersistedModel",
"idInjection": true,
"options": {
"validateUpsert": true
},
"properties": {
"content": {
"type": "string",
"required": true
}
},
"validations": [],
"relations": {
"user": {
"type": "belongsTo",
"model": "user",
"foreignKey": ""
},
"idea": {
"type": "belongsTo",
"model": "post",
"foreignKey": ""
}
},
"acls": [
{
"accessType": "*",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "DENY"
},
{
"accessType": "*",
"principalType": "ROLE",
"principalId": "admin",
"permission": "ALLOW"
},
{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$authenticated",
"permission": "ALLOW"
},
{
"accessType": "*",
"principalType": "ROLE",
"principalId": "$owner",
"permission": "ALLOW"
}
],
"methods": {}
}
comment.js ##
var loopback = require('loopback');
module.exports = function(Comment) {
Comment.observe('after save', function(ctx, userInstance, next) {
var postId = ctx.instance.postId;
// loopback.getModel('post').definition.rawProperties.commentCount... something... something...
});
};
I'm still very new to loopback and I don't know what is the best way to achieve the solution. Maybe you have a third, better way? Or maybe anyone can help me to complete the comment.js.
Fisrt, in your comment.json, you've written idea instead of post:
"post": { //change here
"type": "belongsTo",
"model": "post",
"foreignKey": ""
}
Secondly, you simply add one commentCount in the post linked to your comment in your after save method and then update the attributes of your post:
'use strict';
var app = require('../../server/server');
var models = app.models;
var Post;
// pattern to get your models on start event
app.on('started', function () {
Post = models.post;
});
module.exports = function(Comment) {
Comment.observe('after save', function(ctx, next) {
// only add a commentCount if it's a new instance
if (ctx.instance && ctx.isNewInstance && ctx.instance.postId) {
Post.findOne({where: {id: ctx.instance.postId}}, function (err, post) {
if (!err) {
post.updateAttributes({commentCount: post.commentCount++});
}
});
}
next();
});
};
Another solution would be to create a customGet endpoint in your post.js file:
'use strict';
module.exports = function(Post) {
Post.customGet = function (postId, cb) {
Post.findOne({where: {id: postId}, include: 'comments'}, function (err, post) {
if(err){
return cb(err, {});
}
post.commentCount = post.comments.length;
return cb(err, post);
});
}
Post.remoteMethod('customGet', {
description: 'New endpoint with the commentCount',
accepts: {arg: 'postId', type: 'string'},
returns: {arg: 'post', type: 'object'},
http: {verb: 'get'}
});
};
You can improve this method a bit but you get the idea.
I would like to get some informations about my user with loopback.
For that I created a "user" model related with "accessToken" model until now a POST on /user, a POST on /user/login and a POST on /user/logout is working.
I added on /common/models/user.json
{
"name": "user",
"base": "User",
"idInjection": true,
"options": {
"validateUpsert": true
},
"properties": {},
"validations": [],
"relations": {
"accessTokens": {
"type": "hasMany",
"model": "accessToken",
"foreignKey": "userId"
}
},
"acls": [
{
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$authenticated",
"permission": "ALLOW",
"property": "logout"
},
{
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$owner",
"permission": "ALLOW",
"property": "findById"
}
],
"methods": {}
}
And when I do a GET on /user/{id} I got :
{
"error": {
"statusCode": 401,
"name": "Error",
"message": "Autorisation requise",
"code": "AUTHORIZATION_REQUIRED",
"stack": "Error: Autorisation requise\n at..."
}
}
I guess I didn't understand acl/relation very well
This could be because you are only allowing $owner to findById:
To qualify a $owner, the target model needs to have a belongsTo relation to the User model (or a model that extends User) and property matching the foreign key of the target model instance. The check for $owner is performed only for a remote method that has ‘:id’ on the path, for example, GET /api/users/:id.
Make sure the accessToken you are providing is the owner of the id of the user you are looking for.
If you are not sure, try to replace:
"principalId": "$owner" with "principalId": "$authenticated", then you'll know if that's your problem.
I am new to loopback. I am trying to learn & implement ACL.
I have one "PersistedModel" named 'Page'. I am using two different models as 'Employee' and 'Customer', both based on built-in 'User' model.
Relation:
A page belongs to an 'Employee' as well as a 'Customer'. Both should be owners for a page. Customer & Employee both has many Pages. So, I have added following in relations:
"customer": {
"type": "belongsTo",
"model": "Customer",
"foreignKey": "customerId"
},
"employee": {
"type": "belongsTo",
"model": "Employee",
"foreignKey": "employeeId"
}
ACL:
I want 'WRITE' permission only for owners. So, I have added following in acls:
{
"accessType": "WRITE",
"principalType": "ROLE",
"principalId": "$owner",
"permission": "ALLOW"
}
When I try a patch request, owner customer's request gets executed successfully. But, owner employee's request gets 'Authorization error'.
What am I doing wrong here?
LoopBack documentation is updated recently and they have added these two lines in a notice: (http://loopback.io/doc/en/lb3/Using-built-in-models.html#user-model)
LoopBack does not support multiple models based on the User model in a single application. That is, you cannot have more than one model derived from the built-in User model in a single app.
So basically, I should not have created two different models those are based on 'User' model. :(
Loopback only checks for one owner relation here.
For two owner you need to write your own custom role and register by role resolver
As loopback said, you should one extend model from User model, define role (from Role model), and assign role to any user or participant via RoleMapping model (it's built-in model).
for example:
Extend model from user model and named to MyUser.
Create 'Customer' and 'Employee' role.
Add relation:
"customer": {
"type": "belongsTo",
"model": "MyUser",
"foreignKey": "customerId"
},
"employee": {
"type": "belongsTo",
"model": "MyUser",
"foreignKey": "employeeId"
}
in MyUser model, add the following ACL:
{
"accessType": "WRITE",
"principalType": "ROLE",
"principalId": "Customer",
"permission": "ALLOW"
},
{
"accessType": "WRITE",
"principalType": "ROLE",
"principalId": "Employee",
"permission": "ALLOW"
}
As per this commit (loopback 3.13.0 released on Sept 28, 2017), you can set {ownerRelations: true} to
The container Model looks like this:
{
"name": "container",
"base": "Model",
...
"acls": [
{
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "DENY",
"accessType": "*"
},
{
"principalType": "ROLE",
"principalId": "admin",
"permission": "ALLOW",
"accessType": "WRITE"
}
]
}
When I post an image to it it returns a 401 error. I'm sure that I'm logged in as an 'admin' user because I make two requests with the same access token and the same acl in the json model. For the first (not container) it works, but for the containers one not.
Is there a problem with the container ACL?
-----Edit-----
Starting with DEBUG=loopback:security:acl it returns:
---ACL---
model container
property *
principalType ROLE
principalId $everyone
accessType *
permission DENY
with score: 7495
---ACL---
model container
property *
principalType ROLE
principalId admin
accessType WRITE
permission ALLOW
with score: -1
"property": "*" is default in acl.
For the first request (not container) it returns the same but admin-write-allow has o higher score than $everyone-*-deny.
Setting "score": -2 doesn't work.
You are not defining a property (a method basically), so I guess the ACl resolution gives higher weight to your first then second.
"acls": [
{
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "DENY",
"accessType": "*"
},
{
"principalType": "ROLE",
"principalId": "admin",
"property": "*", // Add this line
"permission": "ALLOW",
"accessType": "WRITE"
}
]
But then, ACL can be tough to figure out sometimes. I would recommend using debug string to see exactly what the ACL system has resolved:
On windows:
set DEBUG=loopback:security:acl && node .
EDIT:
The issue was actually coming from the accessType (requested endpoint was EXECUTE instead of WRITE), thus ACL not resolved as expected.
I'm trying to execute this request:
PUT /api/cars/564d8e792583afef310affe3/categories/rel/suv-idcat
This works fine if I'm logged in as administrator, but if I'm logged in as another role, I get a 401 response.
My Car model has the following ACLs:
...
{
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "admin",
"permission": "ALLOW",
"property": "__create__categories"
},
{
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "admin",
"permission": "ALLOW",
"property": "__updateById__categories"
},
{
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "admin",
"permission": "ALLOW",
"property": "__destroyById__categories"
}
...
Now, if I add this same ACL rules for a specific role:
{
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "StoreAdmin",
"permission": "ALLOW",
"property": "__create__categories"
},
{
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "StoreAdmin",
"permission": "ALLOW",
"property": "__updateById__categories"
},
{
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "StoreAdmin",
"permission": "ALLOW",
"property": "__destroyById__categories"
}
...
If I tap the endpoint logged in as a StoreAdmin user, then I get a 401 error response.
P.S. I've already taken a look at this: https://docs.strongloop.com/display/public/LB/Accessing+related+models, but there is no "hasAndBelongsTo" relation
I'll answer myself. It turns out that for the hasManyAndBelongsTo relation, the method names are not the same as the ones stated in the documentation for a hasMany relation. In fact, it is not even documented.
By running the app in debug mode: DEBUG=loopback:security:* I found out that the real method name was __link__categories.