fortune icon indicating copy to clipboard operation
fortune copied to clipboard

How to limit access to resources?

Open hamez0r opened this issue 4 years ago • 5 comments

How can one limit access to resources, based on application logic? I couldn't find anything in documentation about this, I'm not even sure it's possible.

For example User A should not be able to access resources belonging to User B. In the typical web app backed by an ORM, that's pretty straight forward.

I'm using fortune-json-api and MongoDB adapter. From what I understand, to interfere with what's going on during a request, my only option is using hooks.

The only other solution I can think of is using classic Express app, manually define routes, and use the store like I would use an ORM (but doesn't this defeat the purpose?). And since I want to stick to JSON:API, I probably need to find a serializer as well.

Cheers!

Edit

I found #270 in the meantime. I'll try to find out if something similar works for my case.

hamez0r avatar Aug 23 '19 10:08 hamez0r

Hey @hamez0r, sorry about delay. Of course it is possible, there might just not be clear documentation or examples for this use case.

For example User A should not be able to access resources belonging to User B. In the typical web app backed by an ORM, that's pretty straight forward.

This may be straightforward to implement using a framework, but you're also discounting how much work the framework is actually doing. Generally speaking, a user will have an authentication token which the server must lookup which user it belongs to, then lookup whether a record is associated to that user, sometimes through multiple associations such as user => group => resource.

Assuming that one already has figured out an authentication scheme, implementing authorization isn't too hard:

// this is for reading a record, writing to a record would be almost identical.
function outputHook(context, record) {
  return context.transaction.find('User', [record.user]).then(result => {
    if (result.payload.records[0].id !== authenticatedUserId) {
      throw new fortune.errors.UnauthorizedError(`Can't read this!`)
    }
    return record
  })
}

gr0uch avatar Aug 26 '19 22:08 gr0uch

Hey, thanks for your reply! I do understand the framework does a lot of work, it's actually really nice and I'm trying to uncover all its functionalities.

Your example works well with targeted records (GET /record/id), but I can't seem to find a way to restrict access to GET /records.

In the typical app with multiple users, each holding their own data, a request to GET /records should only return the records associated with that users.

By the time the output hook is called, all records have already been retrieved from the DB, and, AFAIK, returning null is not allowed.

Do you have any hints for this?

Thanks in advance!

hamez0r avatar Sep 03 '19 10:09 hamez0r

In the typical app with multiple users, each holding their own data, a request to GET /records should only return the records associated with that users. By the time the output hook is called, all records have already been retrieved from the DB, and, AFAIK, returning null is not allowed.

So to implement this efficiently, you probably want to rewrite whatever request is coming in as matching a certain user, basically this query option:

{
  match: {
    user: 'userId'
  }
}

Here's an oversimplified example:

const originalRequest = store.request
store.request = function (contextRequest) {
  if (contextRequest.type === 'Resource') {
    contextRequest.options.match = { user: authenticatedUserId }
  }
  return originalRequest.call(this, contextRequest)
}

However, it would be simpler query (and less work) to go in the reverse direction: just get the user and include its related records.

store.find('User', [authenticatedUserId], null, [['resources']])

This way would also be easier to implement when you need to get nested related records.

gr0uch avatar Sep 04 '19 00:09 gr0uch

I faced a similar problem today. At first sight, the input/output hooks didn't seem ideal. They are tied to the adapter and are triggered when you call models from within the backend. This could have unintended side effects when you modify your response. Furthemore, the standard input/output hooks don't seem to keep track of all fields of the request, which were needed to identify the users.

To me, it seems that fetching data from the DB is different from presenting data to the client.

So anyway, I ended adding a custom hook in the serialization.

const jsonApi = require('fortune-json-api');
module.exports = Serializer  => {
  const JsonApiSerializer = jsonApi(Serializer);
  return class Custom extends JsonApiSerializer {

    async processResponse(contextResponse, request, response) {
      await modify(contextResponse, request, response); //I introduced the hook to e.g. filter data to which the user is entitled. 
      return super.processResponse(contextResponse, request, response);
    }
  };
};

I was wondering what your thoughts would be, that's why I post it here. I am well aware this is not strictly speaking serialization, but I am missing the place right before the data gets passed to the serializer.

cecemel avatar Feb 25 '20 19:02 cecemel

@cecemel,

To me, it seems that fetching data from the DB is different from presenting data to the client.

Yes, this is by design.

So you can do it that way by modifying the serializer. The reason why I didn't suggest doing that is because it's entirely dependent on the application protocol used.

It's better to monkey-patch the request method. Why? Because you can re-use the same logic no matter what protocol is used, like websocket, http, or just direct API call.

gr0uch avatar Feb 25 '20 22:02 gr0uch