Search code examples
javascriptloopbackjs

Loopback 3: check if a model exists AND it's mine


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 get the ID from an IoT device
  • I send that ID to the API
    • If the device isn't registered in the API, I can claim the device as mine
    • If it is registered and it's not mine, then I should be told I can't interact with the device
    • If it's already registered AND it is mine, then I get a response that lets me know I had already claimed this one.

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.


Solution

  • 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:

    • I'm passing the deviceId I want to look for
    • The response object so I can set custom headers
    • The context to get the currently logged in user.

    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.