Loopback $owner doesn't works for findById - node.js

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.

Related

Role in loopback not working properly getting error 401

Hello I'm new to loopback and I'm stucked on the Role creation and use.So basically what I'm trying to do is to create 2 roles and based on these roles I want to restrict some users to access some resources.The problem is that on every attempt to get some information from the api I'm getting this
{
"error": {
"statusCode": 401,
"name": "Error",
"message": "Authorization Required",
"code": "AUTHORIZATION_REQUIRED",
"stack": "Error: Authorization Required\n at C:\\Users\\HP\\Desktop\\battle-horse\\battle-horse\\node_modules\\loopback\\lib\\application.js:433:21\n at C:\\Users\\HP\\Desktop\\battle-horse\\battle-horse\\node_modules\\loopback\\lib\\model.js:359:7\n at C:\\Users\\HP\\Desktop\\battle-horse\\battle-horse\\node_modules\\loopback\\common\\models\\acl.js:536:16\n at C:\\Users\\HP\\Desktop\\battle-horse\\battle-horse\\node_modules\\async\\dist\\async.js:3888:9\n at C:\\Users\\HP\\Desktop\\battle-horse\\battle-horse\\node_modules\\async\\dist\\async.js:473:16\n at iteratorCallback (C:\\Users\\HP\\Desktop\\battle-horse\\battle-horse\\node_modules\\async\\dist\\async.js:1064:13)\n at C:\\Users\\HP\\Desktop\\battle-horse\\battle-horse\\node_modules\\async\\dist\\async.js:969:16\n at C:\\Users\\HP\\Desktop\\battle-horse\\battle-horse\\node_modules\\async\\dist\\async.js:3885:13\n at C:\\Users\\HP\\Desktop\\battle-horse\\battle-horse\\node_modules\\loopback\\common\\models\\acl.js:518:17\n at C:\\Users\\HP\\Desktop\\battle-horse\\battle-horse\\node_modules\\loopback\\common\\models\\role.js:447:21\n at _combinedTickCallback (internal/process/next_tick.js:131:7)\n at process._tickCallback (internal/process/next_tick.js:180:9)"
}
}
In my application I have 2 models:
1.Client (which extends build in User Model) and has role ```bs_client```
2.Admin(which also extends the build in User Model)
Note that these models were created using loopback cli and has no relationship created yet.
lb model
I'm using Mongodb as database and here is my datasource file
"mongodb": {
"host": "",
"port": 0,
"url": "mongodb+srv://general:234234##/#####?retryWrites=true&w=majority",
"database": "database",
"password": "password",
"name": "mongodb",
"user": "general",
"useNewUrlParser": true,
"includeSubDomains": true,
"useUnifiedTopology": true,
"connector": "mongodb"
}
It seems that the data is being added correctly in my collections (Role, Rolemapping, Client and Access Token).
I'm assigning role to each client dynamically upon creation using this
Client.observe('after save', function setRole(ctx, next) {
if (ctx.instance) {
if (ctx.isNewInstance) {
// look up role based on type
//
app.models.Role.find({where: {name: 'bs_client'}}, function(err, role) {
if (err) { return console.log(err); }
if (role) {
app.models.RoleMapping.create({
principalType: app.models.RoleMapping.User,
principalId: ctx.instance.id,
roleId: role.id,
}, function(err, roleMapping) {
if (err) { return console.log(err); }
console.log('User assigned RoleID ' + role.id + ' (' + ctx.instance.type + ')');
});
};
});
}
} next();
});
and here is my model-config.json
{
"_meta": {
"sources": [
"loopback/common/models",
"loopback/server/models",
"../common/models",
"./models"
],
"mixins": [
"loopback/common/mixins",
"loopback/server/mixins",
"../common/mixins",
"./mixins"
]
},
"User": {
"dataSource": "mongodb",
"public": false
},
"AccessToken": {
"dataSource": "mongodb",
"public": false
},
"ACL": {
"dataSource": "mongodb",
"public": false
},
"RoleMapping": {
"dataSource": "mongodb",
"public": true,
"options": {
"strictObjectIDCoercion": true
}
},
"Role": {
"dataSource": "mongodb",
"public": true
},
"Email": {
"dataSource": "Email"
},
"Client": {
"dataSource": "mongodb",
"public": true
},
}
and in client.json
"acls": [
{
"accessType": "*",
"principalType": "CLIENT",
"principalId": "bs_client",
"permission": "DENY"
},
{
"accessType": "READ",
"principalType": "CLIENT",
"principalId": "bs_client",
"permission": "ALLOW"
},
{
"accessType": "EXECUTE",
"principalType": "CLIENT",
"principalId": "$authenticated",
"permission": "ALLOW",
"property": "create"
},
{
"accessType": "WRITE",
"principalType": "CLIENT",
"principalId": "bs_client",
"permission": "ALLOW"
}
],
Following https://loopback.io/doc/en/lb3/Model-property-reference.html, everything should be working fine, why I'm not able to retrieve "clients" using the configuration above.
Thanks in advance.
This line should look like this everywhere in "acls": "principalType": "ROLE",
example ACL:
"acls": [
{
"accessType": "*",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "DENY"
},
{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$authenticated",
"permission": "ALLOW"
},
{
"accessType": "*",
"principalType": "ROLE",
"principalId": "admin",
"permission": "ALLOW"
}
],

Extend model with count of related model

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.

ACL troubles with loopback.io

I'm currently evaluating loopback.io for developing the API portion of a new project, and I'm having problems with setting the correct ACL entries.
What I wish to accomplish is given an auth token, the GET endpoints should only return objects owned by the user. For example, a request to /Shows?access_token=xxxxxx should return only objects owned by the user.
Below is my shows.json file, and my User model is named Podcaster. Any help would be appreciated.
{
"name": "Show",
"base": "PersistedModel",
"idInjection": true,
"options": {
"validateUpsert": true
},
"properties": {
"title": {
"type": "string",
"required": true
},
"description": {
"type": "string"
}
},
"validations": [],
"relations": {
"episodes": {
"type": "hasMany",
"model": "Episode",
"foreignKey": ""
},
"podcaster": {
"type": "belongsTo",
"model": "Podcaster",
"foreignKey": ""
}
},
"acls": [
{
"accessType": "WRITE",
"principalType": "ROLE",
"principalId": "$authenticated",
"permission": "ALLOW",
"property": "create"
},
{
"accessType": "*",
"principalType": "ROLE",
"principalId": "$owner",
"permission": "ALLOW"
},
{
"accessType": "*",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "DENY"
}
],
"methods": {}
}
It's not related to ACL's.
You want to change the business logic of the method. So the best practice is that you create a new method for getting shows owning by current user.
If you want to work your current owner ACl, you need to create a relation between user and show, and set ownerId in the show model.
{
"name": "Show",
"base": "PersistedModel",
"idInjection": true,
"options": {
"validateUpsert": true
},
"properties": {
"title": {
"type": "string",
"required": true
},
"description": {
"type": "string"
},
"description": {
"type": "string"
}
"ownerId": {
"type": "object"
}
},
"validations": [],
"relations": {
"owner": {
"type": "belongsTo",
"model": "user",
"foreignKey": "ownerId"
},
....

Loopback - $owner role with a "hasMany" relation

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.

Properties in Model.json

What are the properties that can be mentioned in the Model.json file of a model in loopback(Strongloop) ?
I was trying to figure out what extra properties can be added to the model.json file but in the documentation very less is given.
Certain new fields like – hidden, hiddenProperties, hiddenAPIProperties can be added to the model.json file which are not mentioned in the Loopback documentation……take a look at these
{
"name": "product",
"plural": "products",
"base": "persistedModel",
"strict": true,
"properties": {
"id": {
"type": "text",
"required": true,
"access": "readOnly",
"readOnlyType": "auto"
},
"created": {
"type": "text",
"required": true,
"access": "readOnly",
"readOnlyType": "createdTimestamp"
},
"lastUpdated": {
"type": "text",
"required": true,
"access": "readOnly",
"readOnlyType": "updatedTimestamp"
},
"requestIPAddress": {
"type": "text",
"required": true,
"access": "hidden"
},
"name": {
"type": "text",
"required": true,
},
"type": {
"type": "text",
"required": true
},
"manufacturerId": {
"required": true
},
"supplierId": {
"required": true
}
},
"hidden": [],
"hiddenProperties": [],
"hiddenAPIProperties": [],
"validations": [],
"relations": {
"manufacturer": {
"type": "belongsTo",
"model": "manufacturer",
"foreignKey": "manufacturerId"
},
"supplier": {
"type": "belongsTo",
"model": "supplier",
"foreignKey": "supplierId"
}
},
"acls": [
{
"accessType": "*",
"modelProperty": "manufacturerId"
"permission": "DENY",
"principalType": "ROLE",
"principalId": "$everyone"
},
{
"accessType": "*",
"modelProperty": "supplierId"
"permission": "DENY",
"principalType": "ROLE",
"principalId": "$everyone"
},
{
"property": "_READ_HIDDEN_PROPERTIES_",
"permission": "ALLOW",
"principalType": "ROLE",
"principalId": "admin"
},
{
"property": "_WRITE_HIDDEN_PROPERTIES_",
"permission": "ALLOW",
"principalType": "ROLE",
"principalId": "admin"
},
{
"accessType": "*",
"modelProperty": "manufacturerId"
"permission": "ALLOW",
"principalType": "ROLE",
"principalId": "admin"
},
{
"accessType": "*",
"modelProperty": "supplierId"
"permission": "ALLOW",
"principalType": "ROLE",
"principalId": "admin"
},
{
"accessType": "*",
"modelProperty": "manufacturerId"
"permission": "ALLOW",
"principalType": "ROLE",
"principalId": "productAdmin"
},
{
"accessType": "*",
"modelProperty": "supplierId"
"permission": "ALLOW",
"principalType": "ROLE",
"principalId": "productAdmin"
}
],
"methods": []
}

Resources