Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to limit access to resources? #317

Open
hamez0r opened this issue Aug 23, 2019 · 5 comments
Open

How to limit access to resources? #317

hamez0r opened this issue Aug 23, 2019 · 5 comments

Comments

@hamez0r
Copy link

hamez0r commented Aug 23, 2019

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.

@gr0uch
Copy link
Member

gr0uch commented Aug 26, 2019

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
  })
}

@hamez0r
Copy link
Author

hamez0r commented Sep 3, 2019

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!

@gr0uch
Copy link
Member

gr0uch commented Sep 4, 2019

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.

@cecemel
Copy link
Contributor

cecemel commented Feb 25, 2020

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.

@gr0uch
Copy link
Member

gr0uch commented Feb 25, 2020

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants