Using Loopback ACL's I'm used to set model access rules so that only the owner can check the model details or modify them:
{
"accessType": "*",
"principalType": "ROLE",
"principalId": "$owner",
"permission": "ALLOW"
},
{
"accessType": "*",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "DENY"
},
However, there is a model in one of my projects in which the logic is not that straightforward. The flow is something like:
I can't seem to do this with status codes alone.
*Attempt one: using *exists**
If I enable any authenticated user to check wether a model exists or not:
{
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$authenticated",
"permission": "ALLOW",
"property": "exists"
}
Then it will return status code 200 if the model exists and status code 404 if it doesn't. So I'd need to perform a second query (e.g. using findById
) to know if it's mine or not (in which case I'd get a 403 status code)
Attempt two: using findById
Since only the owner can interact with the model, querying for a model that exists and it's not mine will return 403. Fine enough.
However, querying for a model that doesn't exist will also return status code 403. I guess it is due to the fact that a model that doesn't exists has no owner, therefore I'm not allowed to query it's details.
Attempt three: enabling findById for other authenticated users
Every model to which the device belongs to has its proper ACL in place, so I thought if I enabled findById
it wouldn't display related entities. Turns out I was wrong. If I enable it:
{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$authenticated",
"permission": "ALLOW",
"property": "findById"
}
It returns 404 if the model doesn't exists, and it returns every darned detail of other related entities if it does, eventhough is belongs to another user.
So there is it. It seems I'm off to make a special remote method to handle this case, but I'm puzzled, this sounds like a fairly common scenario and I just wanted to get 404 / 403 / 200 to allow the frontend to act accordingly.
Ok, in case anybody comes to this problem, I finally did it with a remote method.
It takes the context as a parameter from the request, so in the model json file it is defined as:
"methods": {
"hasclaimed": {
"accepts": [{
"arg": "deviceId",
"type": "number",
"required": true
},
{
"arg": "res",
"type": "object",
"http": {
"source": "res"
}
},
{
"arg": "options",
"type": "object",
"http": "optionsFromRequest"
}
],
"returns": {
"arg": "data",
"type": "object",
"root": true
},
"http": {
"verb": "GET",
"path": "/hasclaimed/:deviceId"
}
}
}
And in the javascript file as:
Customer.hasclaimed = function(deviceId, res, options) {
const Device = this.app.models.Device;
return Device.findById(deviceId, null, options).then(device => {
const token = options && options.accessToken;
const userId = token && token.userId;
if (!userId) {
res.status(401);
return {};
}
if (!device) {
res.status(204);
return {};
}
console.log('(%s) %j', userId, device.idCustomer);
if (device.idCustomer !== userId) {
res.status(403);
return {};
}
res.status(200);
return device;
});
};
So basically:
Using the customer endpoint, I call GET customer/hasclaimed/:deviceId
If the user is not logged in it will receive a 401. Past that point, an available device will return 204 (I didn't want to return 404 since the endpoint is right, it's just that there's no device to display) An existent device will return 200 (with its attributes) to the owner, and 403 it the device exists but it's not his.