Open Martinsos opened 2 years ago
Overall, I think this is a very nice structure and plan, @Martinsos ! The requirements make sense, and the examples solidified their relevance. Here are a few thoughts on the next steps:
Under Requirement 2 you note that:
- We can't deny everything by default though, it would be impractical.
However, isn't this the proposal for next step?
- Let's implement solution (2) -> make all Wasp Operations private by default, allow user to set
authRequired: false
to make an Operation public.
Just curious what you had in mind regarding the impracticality in the first note.
Also, I wonder if we need go give people a global flag for backwards compatibility that we can phase out, to make it less onerous for existing apps to upgrade?
Lastly, I know we will have requirements for the next steps, but is your thinking that all action
and query
will get authRequired
field like page
do now, but just default to true
and there is some Express middleware check added? Do we forsee any confusion here with page
using the same thing but with different defaults for each (and behaviours, but that may be expected)?
In thinking ahead, I do think something like Casbin plus an auth middleware leveraging it to protect API routes makes sense. The entity field part feels much fuzzier and harder. However, in both cases I think the trick will be finding the right place/way to declare it in Wasp. Some future sci-fi DSL may help in solution (3). Great job!
@shayneczyzewski thanks!
Deny everything by default -> by everything I meant pages in frontend, operations in backend, all entity fields, maybe something else. For example denying all entity fields by default would probably be impractical. Same goes for frontend pages. But operations, for them it could be ok.
Global flag -> yes that might be an option! Or we can make it a breaking change, right now Wasp apps are pretty small so I don't think it should be a problem to upgrade. We are in Alpha, so that is ok, we want to figure out good defaults, which we can't do if we try to ensure backward compatibility.
If I got it right, what you are saying is that different defaults on frontend vs server side might be confusing? Yes that is a good point. I still thought they make sense, but I do agree that they might feel a bit confusing! Not sure how to go about it. Make everything private by default, both Pages and Operations? Maybe that would be ok. Plus provide app-wide flags for setting default behaviour?
Rules framework + middleware for Operations -> yup, that does seem like the most clear direction. Sounds good, I can work more on some sci-fi DSL suggestions / pieces of code!
We had a nice brainstorming on this, here are some ideas/thoughts from @shayneczyzewski @sodic @matijaSos @maksim36ua :
.select()
" statements to entities that we inject via context into Operations -> something in that direction, basically ensure they are limited in what they can do. This can also maybe be done with monkey patching prisma code. Not sure if we can do this with Prisma really, but worth investigating a bit.Next step will be to draft a couple different directions (how to enable middleware, general middleware vs specific access control middleware, what goes into wasp and what goes into JS, how could we structure the rules, can we do something easily about entities and their fields, ...), together with some sci-fi code and examples, so that we have specific ideas to choose from.
@Martinsos and I spent some time brainstorming the permissions problem from multiple angles over a few sessions, and we did some prototyping with CASL as well. To recap, the challenge was to provide developers with an easy to use way to define permissions at the API level (Operations) and the Entity level, and determine what should be in Wasp DSL vs. what should be in JS. We also explored different views of the tradeoffs involved, for example manual vs. semi-automated vs. fully automated approaches, defining checks vs. using checks in DSL vs. JS, and even when the checks happen (e.g., before attempting to fetch vs. after). There was a lot covered, and the stream of consciousness notes can be found here.
For Operation-level authorization, different frameworks use different approaches. Rails, for instance, would allow you to get a lot of mileage out of just modifying your base ApplicationController
(but you can also use middleware). Express tends to favor middleware.
If we decide to use middleware to secure Operations, we have a few options:
1) Expose Express middleware directly
1) Pros: trivial to expose, allows devs to manipulate the request/response for things like Header
manipulation, custom logging/error handling, and even use 3rd party Express middleware (of which there is a lot)
2) Cons: tied directly to Express, low-level and requires boilerplate for the perms use case (but maybe helpers alleviate this)
2) Expose a slightly higher-level Wasp-specific middleware
1) Pros: can still do interesting things, no longer tied to Express
2) Cons: need to decide on use cases supported and design it
3) Expose a Wasp-specific, permissions-focused middleware
1) Pros: would be focused squarely on permissions, so we could add lots of niceties to make it easy to do
2) Cons: permissions-only (so less useful in general), need to design it
Other middleware considerations include determining if any of it can be leveraged for other application needs, like custom HTTP endpoints, web sockets, different authentication methods, etc.
We then spent some time writing sci-fi code for option (3) and thinking about what the permission check function interface could be, and how you could associate them with Operations. Part of the struggle here is we needed to pull info from the request/context (e.g., mapping args to real things you can check against, like "can the user update this specific Task?"), which became tedious to specify. Beyond that, the overarching question we kept hitting when trying various approaches was "why doesn't the dev just manually put these checks into their Operation functions?" Were the benefits gained by making the check function to Operation mapping in Wasp DSL better than that status-quo alternative? That is still an open question.
We then tried to think about enriching the Entities themselves so we could gain some of the benefits of what some GraphQL authorization libraries provide. They can do things more ergonomically at the API level since you know what the exact query/return shape are, but this requires having access to an Operation schema (which we currently lack in Wasp). They also do things on the Entity definitions for field-level resolver checks, which is something we could consider for basic checks (like RBAC-style).
Last, we tried looking at libraries for inspiration. Specifically, we were intrigued by CASL in that it provided centralized ABAC as well as a Prisma-aware query helpers, in order to see if centralizing the rules was a better starting point than the check functions themselves.
CASL does make it nice to define "abilities", so we could use it for both Operation checks and Entity checks. We tried to see what it felt like using vanilla CASL, and then trying to make it easier to use here: https://github.com/wasp-lang/wasp/tree/martin-shayne-casl-perms/examples/wauth
Part of this effort was trying to auto-wire in both the ability checks in Operations and the CASL Prisma where
helpers into the Prisma queries automatically via a JS Proxy, under a context.checked.Task
sort of namespace. This would mean anything you do will check the abilities automatically. This looked promising, but the Entity proxy approach gets complicated fast because:
WhereInput
(used in places where you fetch multiple results and more complex operators like AND/OR
are available, so things like updateMany()
), but it does not support WhereUniqueInput
(used by things like update()
).connect
, upsert
s, or create
with associations like so.My recommendation is we step back and ask if doing the checks in code, like they can now, is acceptable for Beta. If not, I propose the next step is we focus on providing support for library-agnostic (e.g., no CASL baked in) authorization of Operations via middleware. I would vote we delay explicit support for authorization on Entities at this time, until more use cases can be discussed and potential drawbacks above can be addressed.
TLDR: I think middleware would give us the most bang for the buck given our knowledge at this time, and something users will probably want/need anyways. In future phases I think we could refine this experience, add more explicit support for Entities, and hopefully move more of this into Wasp DSL.
Makes sense to me!
Just to recap a bit, I would say we are looking at couple of main directions right now:
I am excited about (2) because it sounds like it could actually work, and like it could provide decent value and be pretty cool and innovative! But, it does sound like decent amount of work + lot of holes to plug and it is not 100% clear that we can make it work well enough. So one hand I am excited to give it a try, but on the other hand it might consume more resources than we expect in order to get it working.
So with that in mind, I also agree it makes sense to purse some simpler approach for now, like middleware (1). But I do hope we would relatively soon do at least a bit more of a push in the (2) direction!
Thanks for the recap @Martinsos, I 100% agree. I, too, think we are pretty close to getting something like (2) working, but worry it could take up much of the month to iron out all the details, for example, and leave us lacking in other areas. Whereas we know we have a few rough spots we want to work on this month beforehand. But, that said, I agree we did prove it is both possible, ergonomic, and doable with enough though/effort around the Entity use case. I think this is definitely something we can do sooner rather than later! 👍🏻 (And even keep pushing some in the background)
User saying they would love to have RBAC, similar to casbin: https://discord.com/channels/686873244791210014/867713251479519232/1248925089311756382 .
Support for Permissions (Access Control) in Wasp
Motivation
When developing web apps, developers almost always need to implement some kind of access control, where they authenticate users/agents and then ensure that they only read data or perform actions that they are authorized for, nothing more.
We want to offer them an integrated, full-stack way in Wasp to handle most common use cases for access control, so that they can do it in easy and secure manner, while following best practices.
Requirements
In theory, we could collapse all 3 requirements above into the third requirement, which is minimizing chance of mistakes, since both requirements (1) and (2) serve that purpose, but I still separated them to make the discussion more practical and specific.
Some examples of developer mistakes that result in too much access given
Wasp by accident makes Wasp Operation public, due to forgetting to check if user is authenticated.
if (!context.user) { throw new HttpError(403) }
as the first line in the Operation. It is easy to forget to do this, happened to me.Developer by accident sends the hashed password of the user to the frontend.
Developer by accident sends private user's profile data to another user.
User
we specify which fields are to be selected when returned to the client. This way we pick interesting fields + avoid returning hashed password. This sounds like it could be some kind of "view" feature in Wasp. We also have function which calculates and adds additional fields toUser
, so that would be some kind of "derived" data. For "view" and "derived" concepts, solution is not in permissions, but for skipping password, it might be.Developer by accident allows user to delete any article, even if it was made by another user.
where: { < ... information what to delete, like id ... >, user: { id: [context.user.id](http://context.user.id/) }}
). But that piece of code is easy to mess up / forget! And we are repeating it over and over. How can we make that more robust, more intentional, remove repetition? Do we need some kind of ABAC + Prisma integration? Or some higher lvl abstraction, smth in the Item direction? Or is this just required complexity that we can’t avoid? Can we directly embed some piece of logic/middleware into Prisma that performs additional checks, ensuring Prisma doesn’t do operations on stuff it shouldn’t do operations on, kind of like “last check”?Developer by accident allows user access to article they created but no longer have rights to read.
Possible solutions
Specify which Wasp Pages require authenticated user to view them.
Specify which Wasp Operations are public / private.
Provide a way to specify access control rules in structured, centralized manner.
Define access rules per Entity (fields).
Task.checkAccess(task, user)
orTask.strip(task, user)
and they can call it on the entities before they return them from an Operation. Ok, they need to remember to call it manually, but we still gave them an easy way to centralize this logic for checking that no forbidden fields are returned / for stripping those forbidden fields, and for this we don’t need to yet introduce the concept of data schema for Operations. Even if we introduce later concept of data schema for Operations and do this stuff automatically, it could still be useful for developer to be able to call this manually in some situations. Before in life, we would for example implement .view() method on each Mongoose model and make sure to call that in order to get only the fields that are relevant to the client → but that served both for removing sensitive fields, and for also removing redundant(boring) fields (updatedAt, ...).Summary
Crux of access control seems to come down to the following:
Very crudely stated, ABAC is a standard for doing (1). GraphQL, with its schema, gives nice foundations for doing (2). Wasp currently has nice foundations for doing operations part of (2), but not data part of (2). (3) is relatively rare, we would need to probably go beyond Operations to have this, to CRUD level of abstractions, meaning something like Aggregate/Item solution, or automatically generated Operations, or smth like that. Or be smart in some other way, but we need to understand how to check if operation does what rule allows.
We could do (1) without the (2) by having system for defining rules but letting developers manually invoke the rules. We could do (1) with only half of the (2) -> operations, while not having integration with the data. We could do (2) without the (1) by attaching pieces of logic directly to operations / data, without having them organized in nice way.
Proposed next steps:
authRequired: false
to make an Operation public.