RFC Authsome
Opening a discussion about how authsome modes could potentially be easier and smaller.
Here's the basic ideas:
On the server
The problem:
- Every operation inside a resolver will pass through the authsome mode. This results in a lot of operations. eg. If inside my resolver, I need to get a book, then get all the divisions for that book and patch one of them, my mode will check if I have permission to read books, if I have permission to read divisions, then if I have permission to update divisions. Each of these could potentially be repeating calls (eg. get this user's teams, check if team A is one of them etc.). I also need to go back to the authsome mode for every operation I write.
- Connectors have a different api to objection, making writing code confusing some times, or even worse, somewhat limited compared to all operations that one can do in objection.
- A workaround for the above would be to use objection whenever I don't care about authorizing and use connectors whenever I do (eg. only use connectors for the update). But this results in the use of two different apis (objection & connectors) in the same resolver, which is less than ideal, and perhaps too easy to miss.
The proposal:
- The server can become the only source of truth for authorization.
- There can be a one-to-one relationship between resolvers and entries to the auth mode. Given the example above, the only thing I need to check is if the user has permission to run "UPDATE_DIVISION" or not. This runs once.
- Each resolver can start with an auth service call. Even better, graphql middleware can be capturing that this resolver is about to be called (or has just been called -- useful for filtering results).
- Each auth entry in this setup only requires
(operation, context)
as arguments (eg.UPDATE_DIVISION, ctx
). The user id is provided by the context, and the object is irrelevant in this setup. - We can expose all actual models - not connectors - in the context. In this way, both resolvers and the authsome mode have full access to whatever they can possibly need from the data. An alternative would be to not pass them, but simply import them. Though I'd personally prefer having them in the context, minimizing imports, if someone were to use the alternative approach, they wouldn't be missing out on anything, as authorization wouldn't be bound to connectors.
- Every time team membership changes, publish the change through a subscription, filtering who gets that change (ie. a client should only get results relevant to the currently logged in user).
On the client
The problem:
- Wrapping components with
Authorize
will request permission from the cache on every render of the surrounding component, which can end up being a lot of times, even if nothing has changed authorization-wise. This can have a significant performance impact. - Complex authorization requests can be hard to reason about when using
Authorize
, potentially leading to two or more nested ones. - Wrapping components with
Authorize
reduces reusability, as we're kind of hard-coding authorization inside the UI (including which operation of the mode to look up). This is probably not a problem for small components, but can definitely become an issue for medium-sized UI elements.
The proposal:
- Client-side we only get one auth object on login (mostly booleans, but could be whatever is needed). Whenever the values change on the server, this object gets updated through the aforementioned subscription.
- Since the auth values are given by the server, there is no need to worry about sensitive data being sent to the client for authorization. A result does not need to be limited by the contents of
user.teams
, the server has access to all teams.
There is no reason that I can think of that would make any of the above break backward compatibility.
It can also be implemented incrementally, instead of doing all of the above at once.
But it would IMHO end up with modes that are smaller and easier to reason about.