Query filters and client permissions


#1

Hello, guys!

I’m thinking about one more ambiguous moment. It’s not about {json:api} semantic, but related to query filters which are very powerful tool. Thanks to @michaelhibay for suggesting me to start use them, my APIs became much friendly!

I have imaginary resource account which represents bank account. I want to allow users to see only their own account resources. They can do it via /users/{user}?include=accounts easily.

There is no issues right before you decide to allow user to get their accounts via it’s own endpoint with query filter. Each bank account linked with user resource using relationship holder. So this URI /accounts?filter[holder][id]={user} will returns list of bank accounts related to {user}.

  1. How do you think, is it okay to throw 422 or 403 status code when user tries to call /accounts endpoint without query filter (otherwise it will get other user’s accounts)? Or other status code should be thrown?
  2. What if there are other filters which are allowed to be used by moderators only, what response should be sent if user will try to filter accounts by this special filter, 422 or 403 again?
  3. If user will try to filter accounts by 2 ids /accounts?filter[holder][id]={user},{other-user}, should I throw 403 if {other-user} is not allowed to be seen by this user or display his own accounts and ignore second id? I think first option is more preferable.

Thanks in advance!
AK


#2

This is certainly an interesting problem. The first thing I would ask right away is have you considered returning an empty collection instead of trying to disallow particular users from calling certain URLs? Adding a trigger like that, would actually be an auth information leak.

Why can’t GET /accounts simply be the get my accounts query? Remember /accounts as a collection isn’t guaranteeing every resource will be returned, just the resources the user has access to. Since you know who is logged in, this would be the get all my accounts. I’m leaning more and more to 404 as the response for unauthorized access as 403 leads to an information leak, and you have to know a resource does exist before you get the same leak with a 404 response.

  1. See above.
  2. I think the best way is to just trim the response collection of resources the user is allowed to access, since they can’t access anyone else’s stuff, this will be an empty collection, which is the best response.
  3. Empty collection.

This all changes of course depending on your security configuration and requirements. Good luck!


#3

@michaelhibay thank you for an interesting proposal. I have thought about it long time ago but rejected it because I’ve thought that response should be consistent. If two users call /accounts endpoint they should get the same output.

But now I’ve start thinking that this is controversial decision because I have pretty similar case for resource endpoint /accounts/{account}. Resource owner can see all the fields, other users can see only public data and relationships.

I should think about drawbacks and side effects more and return with final decision.


#4

We’ve discussed this question with frontend and backend teams.

For frontend it’s really nice to have an ability to get collection of entities you are allowed to see, without knowledge how to build an URI and what filters you need to include to get complete list of resources. Otherwise they need to know that right now you are allowed to see accounts of user=1, user=4 and team=32. That leads to access rights registry on frontend side which will know how to convert it to request query.

For backend it’s painful because:

  • It complicates logic, because there could be situations where you can see not only own resources, but resources of other users\groups which you are allowed to see at this moment of time.
  • Root endpoints will be harder to cache, because each user will have own result set. If access rights are changing you need to rebuild cache from a scratch.
  • It will increase database load.

Nevertheless we are going to try to implement this case, because APIs made for clients, not for servers.

  • /accounts will return collection of all accounts which user is allowed to see.
  • /accounts?filter[holder][id]={user} will return collection filtered by holder.id with id {user}.
    • If there is no such data found - empty collection will be returned.
  • /accounts?filter[holder][id]={user},{other-user} will return collection filtered by holder.id with ids {user} and {other-user}.
    • If there is no {user} and {other-user} found - return empty collection.
    • If there is only {user} found - return collection with {user} resource only (this means that {user},{other-user} means {user} OR {other-user}, not {user} AND {other-user}.
  • All requests above will return status 200 OK even if nothing has been found, because empty collection will be returned.

If there is something that I missed, your comments are highly appreciated!

Special thanks to Michael Hibay for his professional overview!


#5

Glad to help! My view is the API exists to encapsulate the business logic to lower the requirements on the front end developers and consumers to allow them to leverage the services correctly. One additional note, if you’re concerned about API developers correctly constructing the URLs, you could always return those as links with semantic rel names.

The first filter link could be something like ..."rel":"searchByUserId", "href":".....".... Your second filter semantics is also ambiguous, and while explained in your post would probably be better served streamlining your filter syntax to make it clear. A search for some of the strategies listed in the forum should be pretty easy to see a few of the best the community has coalesced around.

Good luck!


#6

If it helps at all, the OData query language seems to work well as a way to filter. So, if your server can implement an OData query language parser, it’s pretty powerful.

Based on your original example, one can do:

GET /accounts?filter=holder/id eq '{user}'

… or your other example:

GET /accounts?filter=holder/id in ('{user}', '{other-user}')

Because filter is just reserved, there isn’t anything in the specification that says one can’t choose how to filter.


#7

Thanks for sharing it, @jgornick!
What I don’t like in OData is whitespaces in URI.