wasp-lang / wasp

The fastest way to develop full-stack web apps with React & Node.js.
https://wasp-lang.dev
MIT License
13.87k stars 1.19k forks source link

RFC: Support for Permissions (Access Control) #584

Open Martinsos opened 2 years ago

Martinsos commented 2 years ago

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

  1. Centralized, well-structured, easy to review access control logic.
    • Recommended by OWASP, specifically they recommend ABAC over RBAC.
  2. Deny access by default.
    • Recommended by OWASP, idea is that you don't by accident provide access that shouldn't be provided.
    • We can't deny everything by default though, it would be impractical.
  3. Minimize possibility of developer mistakes where too much access is given.
    • Use case 3.1.: Piece of Entity (database data) is private, while the rest is public (in given context), and developer by accident shares the whole Entity when they shouldn't.
    • Use case 3.2.: Developer by accident allows user to read/modify Entity they shouldn't have access to.

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

  1. Wasp by accident makes Wasp Operation public, due to forgetting to check if user is authenticated.

    • Requirement (3).
    • This check we usually do by putting 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.
  2. Developer by accident sends the hashed password of the user to the frontend.

    • Requirement (3.1.).
    • They want to send the user's profile, which is represented by User Entity, and forget to exclude that field.
  3. Developer by accident sends private user's profile data to another user.

    • Requirement (3.1.).
    • They want to send the summarized version of user's profile, which is represented by User Entity, and forget to exclude / not include some private fields like address, age or similar.
    • In RealWorld, we have this situation (https://github.com/wasp-lang/wasp/blob/main/examples/realworld/ext/user/queries.js#L22) where for 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 to User, 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.
  4. Developer by accident allows user to delete any article, even if it was made by another user.

    • Requirement (3.2.).
    • Developer forgets to check if user is owner of the article, and just deletes any article that user specifies by id.
    • Lot of good examples here: https://github.com/wasp-lang/wasp/blob/main/examples/realworld/ext/article/actions.js . In these examples, we specify in Prisma query that entities need to have owner set to the user that is performing the operation (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”?
  5. Developer by accident allows user access to article they created but no longer have rights to read.

    • Requirement (3.2.).
    • Let's say we have an app where user can create articles as part of an organization, but if he leaves that organization, he looses access to them. Developer might forget to check for organization membership and allow developer to read the article because they created it.

Possible solutions

  1. Specify which Wasp Pages require authenticated user to view them.

    • We already have this implemented!
    • This is ergonomics / ux feature, it doesn't really secure anything.
  2. Specify which Wasp Operations are public / private.

    • We can make them private (require authenticated user) by default (Deny by default) and allow developer to specify which are public (don't require authenticated user).
    • Pros: Simple to implement in Wasp.
    • Cons: Brings some value but not a ton.
  3. Provide a way to specify access control rules in structured, centralized manner.

    • Could be ABAC, or RBAC.
    • Even it not very well integrated with the rest of Wasp (Entities, Operations), it can still bring value due to bringing some best practices and providing structure.
    • This would probably come down to using some existing ABAC framework and integrating it into Wasp, or by doing our own solution if simple enough.
    • Couple different approaches, from simpler to more complex:
      1. Once they define rules via Wasp, developers could call them manually from their Wasp Operations (their JS implementation). So, rules would be exposed as JS functions, and would be used in Wasp Operations to check access control, by developers, manually.
      2. Additionally, we could also allow developers to declaratively, via Wasp Lang, attach rules to specific Wasp Operations. Then Wasp would make sure to run attached rule(s) before running a specific Operation.
      3. Finally, we could allow attaching the rules to Entities and their fields. Hard part here is figuring out in Wasp when to run these -> we would need some additional knowledge (data schema?) of what specific Operations return in order to be able to enforce these automatically. More about this bellow, in solution (4), as we are entering that territory now.
    • Pros:
      • We don't need to make up stuff, we follow what others are doing.
      • We probably don’t need to modify Wasp too much, since we don’t need deep integration with rest of Wasp, possibly a bit with Operations.
      • Could bring a lot of value, due to us introducing best practices and making it easy to do access control.
    • Cons:
      • We need to pick a way to do access control. Luckily there are not so many common ways to do it.
      • Doesn’t utilize all Wasp features, like Entities and similar, feels more like an add on, or half of the full solution.
      • Might be a lot of work to implement, not sure yet.
      • ABAC in its core just gives a nice way to write rules and then check the result of those rules, but it doesn’t ensure that following code actually asked for correct permissions! So if we also want that, we will have to figure out how to do that, do some more advanced integration / abstractions, where access control checks are more tightly connected to the operations that are using these checks.
    • Additional materials:
      1. GraphQL Shield (https://graphql-shield.vercel.app/docs#example), a library that provides primitives for constructing access control rules, and then also a way to apply them to queries, mutations and also fields. Kind of like ABAC for GraphQL, and actually an implementation for GraphQL of what we want to do here. Main advantage they have over us at the moment is the fact that they have GraphQL data schema so they can attach rules to data / fields.
  4. Define access rules per Entity (fields).

    • Allow developer to define access rules for individual Entity fields.
    • Either Wasp automatically enforces these at right time, or developer can manually invoke the access control check. If Wasp does it automatically, it means it needs to know more about the operation that is being performed -> for example it might need to know that Operation returns specific Entity, which means we need to introduce some kind of data schema for Operations. Or maybe some other kind of abstraction that gives more knowledge to Wasp (like Aggregate/Item), to be able to figure out when to call which checks. If we let developer do it manually, we would be generating logic for them, and they would be invoking the check when they want. So we generate smth like Task.checkAccess(task, user) or Task.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, ...).
    • Pros
      • Utilizes Wasp’s strengths - Entity.
      • Provides decent amount of value, is common in other access control solutions / ORMs.
      • There is a half-solution that we can use to get something out faster.
      • If we go for full solution, it will force us to elevate other parts of Wasp to a next level (Operations, Entities) → so whole Wasp will become more powerful!
    • Cons
      • If we go for full solution, it will force us to elevate other parts of Wasp to a next level (Operations, Entities) → that could be very demanding resource-wise!
      • Might not be as valuable as it seems at first → how often will devs benefit from this in practice, how important is it really? Compared to e.g. introducing a proper ABAC support in Wasp?
    • Additional materials:
      • Blog post on how to do something similar in GraphQL, by using field directives -> basically you add attributes to fields and then those are used in middleware for resolving those fields, so permissions can be enforced there: https://www.prisma.io/blog/graphql-directive-permissions-authorization-made-easy-54c076b5368e . Similar to our idea with field-level permissions, but they have a clear place to enforce those, which are field resolvers, while for us it is not so clear where to enforce it, we are missing that level of "data schema". Looking at this it almost becomes tempting to use GraphQL in Wasp. Or, we should introduce the concept of "data schema" almost certainly.
      • GraphQL Shield also works for this, check solution (3) for details on it.

Summary

Crux of access control seems to come down to the following:

  1. Providing a nice, centralized way to define access rules / logic.
  2. Providing a nice, easy way to attach these rules / logic to operations (Wasp Operations) and data (Entities, Entity fields, ...), so that these rules / logic can be executed automatically and efficiently when needed (as smartly positioned middleware).
  3. Once rule is approved, ensure that actual code indeed acts in accordance with what was allowed by the rule. This is most complex part of the story and not commonly seen in other solutions out there, and almost certainly requires some heavy restrictions / abstractions in order to be able to enforce it. If we can do this at some point that would be awesome, but I would look at it as more of a wishlist item, not neccesary.

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:

  1. Let's implement solution (2) -> make all Wasp Operations private by default, allow user to set authRequired: false to make an Operation public.
  2. Let's postpone solutions (3) and (4) for a bit, until we have a better idea of how to fit them into the rest of Wasp. Of all of these, I think it might make sense to progress on (3) first, with its "manual" solution. I would like though that we relatively soon put more thought into finding a solution that deeply integrates with Wasp, be it via having data schema for Operations, or doing some kind of CRUD abstraction like Aggregate/Item, or something completely different. Some auth libraries to study for this, both for inspiration and as possible implementation of ABAC:
    • https://casbin.org/ -> flexible polyglot auth library.
    • https://casl.js.org -> Auth library for JS, isomorphic and can support ABAC from what it seems. Looks interesting. It has official support for React, and official support for Prisma, and for other popular stuff!
shayneczyzewski commented 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?

  1. Let's implement solution (2) -> make all Wasp Operations private by default, allow user to set authRequired: false to make an Operation public.

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!

Martinsos commented 2 years ago

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

Martinsos commented 2 years ago

We had a nice brainstorming on this, here are some ideas/thoughts from @shayneczyzewski @sodic @matijaSos @maksim36ua :

  1. RBAC is widely used, but if fine granularity is needed, it can become too limiting.
  2. Usual mechanism is middleware that gets inserted to do access control logic (Ruby on Rails via class inheritance, Node (Express) via middleware functions, Python via decorators, ...).
  3. Auth0 has good middleware story, it is easy to insert into existing project, we might want to look at them for inspiration.
  4. Supabase has row-level based security, that might be interesting as a concept.
  5. While we could attach middleware directly to specific Operation, we could also go the opposite direction and specify for middleware which Operations it affects -> attach it to a group of Operations. So we could map either operation -> middleware(s) or middleware -> operation(s).
  6. "Crazy" idea: Wasp could, once it has enough knowledge, list/show all the routes/operations/pages/other resources that specific user/role can access! So it could give developer a good idea of who has access to what.
  7. For adding access control to Entity fields, we could maybe add ".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.
  8. Idea for possible next step is to focus on the "middleware" part of the story as a start, so point (2) from the Summary in the RFC, which is how to attach the access control logic to stuff it guards (Operations, Entities). We can do this before introducing point (1) from Summary in RFC, which is a way to centralize and structure the rules. To take this step further, we might want to look into general way to insert middleware for Operations / Entities, instead of specifically inserting access control middleware - I am not sure what is better yet and what could be the differences though.

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.

shayneczyzewski commented 2 years ago

Permissions recap

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

My closing thoughts

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.

Martinsos commented 2 years ago

Makes sense to me!

Just to recap a bit, I would say we are looking at couple of main directions right now:

  1. General middleware for Wasp Operations, that can then also be used for ACL/permissions logic.
  2. We integrate CASL library with Wasp and allow devs to use CASL to define their access control rules (checks), while Wasp then based on some conventions / rules will make sure to apply those on correct places. Specifically, CASL rule could be tied to Operation or Entity, and Wasp will then run them when that Operation is called or when that Entity is accessed via Prisma. They could still define rules with custom targets in CASL and call any of the rules manually, btw. This sounds pretty cool, especially for Entities, and helps devs feel confident that they won't by mistake do a thing they shouldn't. But, the part with ensuring that Prisma calls obey the rules requires significant effort, so this is what makes it hard. We explored this whole direction here https://github.com/wasp-lang/wasp/tree/martin-shayne-casl-perms/examples/wauth , and I tried to more concretely explain this approach here: https://github.com/wasp-lang/wasp/blob/f0b2d0d1be9aba6ff731d48ca7596ac4e747418e/examples/wauth/ext/acl.js#L55-L169 .
  3. Similar to (2), but automatic applying of checks/rules by Wasp would be based on some kind of schema introduced at the Operation level (what Operations return). This would give us more knowledge and therefore more power probably, but Operations Schema is a major undertaking that is not yet clear, so we can't push on this one 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!

shayneczyzewski commented 2 years ago

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)

Martinsos commented 5 months ago

User saying they would love to have RBAC, similar to casbin: https://discord.com/channels/686873244791210014/867713251479519232/1248925089311756382 .