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
Related
I am new to loopback. So as a part of the learning process, started creating a sample API. Also extended User model as Customer and AccessToken as CustomerAccessToken. But the problem is that access token is not being generated, when we tried to login data that is already send using a POST request.
Response body when login using credentials:
{
"id": "oiMDjErGGVkeSMtnt1SfHzGuERZf6OCId5FUulvir6A04htUbIV656FOBlXn9vDS",
"ttl": 1209600,
"created": "2019-05-09T09:41:05.184Z",
"userId": "5cd3f59594d45186b411bb02"
}
No accessToken is generated. I have removed the inbuilt User and AccessToken model and used extended ones.
model-config.json
{
"CustomerAccessToken": {
"dataSource": "db",
"public": false
}
"Customer": {
"dataSource": "db",
"public": true
}
}
customer.json
"relations": {
"accessTokens": {
"type": "hasMany",
"model": "CustomerAccessToken",
"foreignKey": "userId",
"options": {
"disableInclude": true
}
}
}
customerAccessToken.json
{
"name": "CustomerAccessToken",
"base": "AccessToken",
"properties": {},
"validations": [],
"relations": {
"user": {
"type": "belongsTo",
"model": "Customer",
"foreignKey": "userId"
}
},
"acls": [],
"methods": {}
}
One more query: Will login functionality work, when we try to extend User & AccessToken models or Do I need to write login functionality in the customer.js file in order to make it work.
Any help would be really appreciated.
Try to use the DEBUG variable to get more details about the error. from loopback root call
export DEBUG=loopback:security:access-context
npm start
to run the server in the debug mode. Then you can check what exactly is going on - which method is called and why you don't have access.
Probably it's ACL setting: by default, you cannot get users data in loopback, so you may want to add ACL rule to the Customer entity. I suggest to use loopback-cli for that - it's less error-prone. Then you select a model where you want to add a new rule and following the step-by-step instruction. You can read more about ACL here
Do not use CustomerAccessToken unless you want to implement multiple user models.
This is the configuration that works great for me:
model-config.json
{
"User": {
"dataSource": "mysql",
"public": false
},
"AccessToken": {
"dataSource": "mysql",
"public": false,
"relations": {
"Customer": {
"type": "belongsTo",
"model": "Customer",
"foreignKey": "userId"
}
}
},
"Customer": {
"dataSource": "mysql",
"public": true
}
}
server.js
app.use(loopback.token({
model: app.models.accessToken,
currentUserLiteral: 'me'
}))
Customer model must extend the User model:
customer.json
{
"name": "Customer",
"plural": "customers",
"base": "User"
}
In addition to Antonio Trapanis response. If you want to use your own AccessToken model to have to set it in server.js
app.use(loopback.token({
model: app.models.CustomerAccessToken,
}));
I followed this tutorial.
https://youtu.be/Jx39u8IssRg
I received Token in response body when logged in with 'user' through user controller.
I am able to to hit the end points of to-do model by passing that 'token' in the headers as 'Bearer Token'. I used "postman" to hit the to-do end points
( I was unable to insert/attach the received token into the to-do requests of loopback4 app)
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.
is there any way to query related model's related model like from post \Get:
{"order": "created_at DESC","include":[{"relation": "user"}]
But, in my user model there is a relation hasone with settings model. I want to get that also, while querying from post \Get rest api. I've tried with:
{ "include": { "relation": "user","include": {"relation":"settings"}}}
but no luck.
I've create nested relationship to related to your question.
example : teamRole.json
TeamRole > belongTo > User and Team
"validations": [],
"relations": {
"team": {
"type": "belongsTo",
"model": "Team",
"foreignKey": ""
},
"user": {
"type": "belongsTo",
"model": "User",
"foreignKey": ""
}
}
Retrieve results
app.models.TeamRole.findOne({
where: {
userId: user.id
},
include:[ {
relation: 'team'
},
{
relation: 'user'
} ]
},function(err,team,user){
//retrieve relational data here
});
Try this approach, hope this will helpful.
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'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.