Replies: 3 comments
-
I love your solution! I'd totally like to see this approach natively supported by cancancan. Thanks for sharing 🙏 |
Beta Was this translation helpful? Give feedback.
-
This is what i've been looking for, thank you for sharing this solution ❤️. |
Beta Was this translation helpful? Give feedback.
-
A bit late to the party, but I brought some feedback on the idea: in one of the projects with about ~200 models adding an ability per model or per controller, as suggested here, would be a suicide - might as well go with Pundit and join the Anonymous Burnouts Club straight away. Instead, one ability class per role is what turned out to be the best solution. This eliminates the need for suggested Solutions 3 and 4 completely. Naming abilities by "controller/action" and/or "controller/ui_element" adds extra sanity points, too. Another, simpler project, got away with a pretty basic, progressive mode: 6 roles, 6 access levels, each more permissive than previous:
Basically a textbook example. |
Beta Was this translation helpful? Give feedback.
-
For a recent project I switched from user based abilities to model based abilities like Alessandro suggested, however ran into a few issues so came up with some work arounds. I wrote a blog post about it but thought I'd paste it here in case it helps others.
I love CanCanCan, it’s a powerful authorization library for Rails initially created the Ryan Bates and later adopted by the Rails community to support and maintain. It’s my default authorization gem for all my Rails projects.
I also like to follow STI pattern for authentication, for example having a base User with Admin, etc subclasses. To go along with these user types, I usually define UserAbility, AdminAbility, etc to encapsulate all specific user’s abilities in one file.
This works great, although as a project grows these user ability files tend to get large. And if you are using any kind of _ids queries like user.post_ids when defining your abilities, you can see a performance hit since these query will be executed every time and not just when checking that specific ability.
So I recently to adopt the strategy outlined by Alessandro Rodi to separate abilities per model. For example where previously I had UserAbility or AdminAbility, I would now have PostAbility, CommentAbility, etc. However after implementing this pattern, I ran into a few problems so came up with some solutions.
Problem 1: If-Else Checks In Every Ability File
Since I have different user types, I’d need if-else checks inside every ability which gets repetitive and not great for readability.
Solution 1: Add Base ModelAbility Class
To avoid these if-else checks in every ability, I defined a ModelAbility base class which handles this check then calls a method named for each user type.
This keeps the implementing ability pretty clean and readable, which helps a lot when you have many user types.
Problem 2: Setting current_ability In Every Controller
To be able to use these model abilities, we need to set the current_ability according to the active controller being requested. For example the PostsController would load the PostAbility.
This seems a bit error prone, for example if you forgot to define the current_ability in one of your controllers then your permission checks wouldn’t work.
Solution 2: Load current_ability In ApplicationController
Instead of setting the current_ability in every controller, we can instead set it once in the base ApplicationController. Now the current_ability is set for the active controller without us having to manually set it in every controller.
Problem 3: Not Loading Parent Abilities
One issue with Solution 2 is that is only loads the ability for the current controller, however you may want to also load abilities for parent resources in the route. For the route /blogs/1/posts/2 you might need to load both BlogAbility and PostAbility.
Solution 3: Load Parent Abilities In TheRoute
One solution is to obtain all controller names in the route, then loop through and merge those abilities with the current_ability.
Although Rails has a controller_name helper, it doesn’t have a controller_names equivalent. So I wrote this method which skips ids both integers and uiids in the route.
Problem 4: Not Loading Child Abilities
Although Solution 3 will load the current controller model as well as parent controller models, it does not load any child model abilities. For example, you might need to load Comments so would also need to merge CommentAbility.
Solution 4: Extend can? Helper
If we extend the can? helper, we can intercept when any of our views check for abilities, then merge that ability if it hasn’t already been loaded. And to avoid merging existing abilities, we memoize a model_abilities hash where these are stored.
Here's a gist with the final solution https://gist.github.com/dalezak/ab763b41f2fb086c39ef42018879f31e. I'd love to hear others thoughts, feedback or ways to improvement this.
Beta Was this translation helpful? Give feedback.
All reactions