-
Notifications
You must be signed in to change notification settings - Fork 1k
Move to CanCanCan for authorization #2023
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
This will let controllers override for specific circumstances
Access logic is not _entirely_ exported from the controller, unfortunately. For interface reasons, some actions which require admin have to be listed within the controller's deny_access method. This is required because, being a default-deny system, cancancan _cannot_ tell you the reason you were denied access; and so the "nice" feedback presenting next steps can't be gleaned from the exception
The OAuth capabilities are essentially user permissions that have been granted to the app. If the user authenticates through a non-oauth method, they are assumed to have granted all capabilities to the app
These are asking fundamentally different questions; Abilities are asking the application if the user has a role that allows the user to take a certain action Capabilities are asking if the user has granted the application to perform a certain type of action CanCanCan makes no distinction, however, so the `granted_capabilities` method is provided as a point that can be checked in rescue methods, so that one can _attempt_ to continue to provide the more informative error messages around permission refusals
…website into rubyforgood-authz
tomhughes
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume (given your question) that this can be used as is and the existing technology continues to work for unconverted methods?
If that is the case then I have no objection in principle to merging this and then proceeding with further refactoring separately.
| @@ -0,0 +1,57 @@ | |||
| # frozen_string_literal: true | |||
|
|
|||
| class Ability | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is app/model the right place for this? It doesn't look like a model and the CanCanCan documentation just refers to is as a "class" so I was expecting it to be in lib but I guess this is where the generator put it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this is where the generator puts it. I'm happy to put it where ever suits - perhaps in config? I'm not sure about lib, I consider that somewhere where stuff goes that (in theory) could be extracted into a gem, but I can see the logic there since we put various classes into lib already.
From https://github.com/CanCanCommunity/cancancan/blob/develop/lib/generators/cancan/ability/USAGE:
The cancan:ability generator creates an Ability class in the models
directory. You can move this file anywhere you want as long as it
is in the load path.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's tricky... I'm not sure rails even creates a lib directory these days, and I know it was removed from the default load path but we put it back in config/application.rb.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just weighing in, FWIW, Ive seen a folder created for these and seen them put in app/abilities/ before. It would also make splitting the ability file (which are likely to want to do later) much easier.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/lib is almost certainly the wrong place, as this isn't external / library code. I could see putting it in config, but it's active code that gets checked, and config isn't the first place I'd look. Ben's thought about app/abilities appeals, if the plan is to deconstruct the large object at some point ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did do some googling before and there seems to be approximately no consensus on where to put things that aren't one of the "standard" types of class.
I'd always seen lib as being for random bits of non-classifiable code and not just for external things (which wouldn't normally be in the repo at all) and in the early days when lib was on the autoload path I think that was a more common view but then it got removed.
I didn't even know could just create random directories under app to be honest - how does that work? Does rails add every directory there to the autoload path?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does.
there are some additional app directories that are something approaching a standard: app/presenters, app/forms, app/services (especially the last one) but it's pretty much open season for organizing things under the top layer of app. Rails will loop through those directories and add them to autoload at boot time (so, if you add a new app/foo class, you'd need to reboot your dev server, but otherwise, classes underneath are subject to Rails' loading magicks)
| end | ||
|
|
||
| def current_ability | ||
| Ability.new(current_user).merge(granted_capability) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This has lost the memoization of the default method, which has @current_ability ||= as a prefix - is that deliberate?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not deliberate by me. But adding in the memoization causes the tests to fail, and I would need to investigate why.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tests fail because in our controller tests, the controller state (e.g. instance variables) is not reset between each "request" - see rails/rails#24566 . Calling get in a controller test isn't really making an http request, it just calls the methods on the controller object which exists for the length of the test.
So for a simple example, site_controller_test#test_welcome calls get twice. The memoized result from the first call means that the second logged-in call is also forbidden. This is, ultimately, because you're not really meant to make multiple requests in a single controller testcase - that should be in integration or system tests. But we do this a lot in our tests.
app/models/capability.rb
Outdated
| @@ -0,0 +1,21 @@ | |||
| # frozen_string_literal: true | |||
|
|
|||
| class Capability | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Obviously similar comments apply here as with Ability but this one I think is something of your invention as I don't see it mentioned in the documentation?
I wonder if this is even needed - given we are overriding current_ability could we not pass both the user and token to Ability and have it do everything?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have any views on this, beyond what @cflipse wrote in the commit message when he split them out.
To be blunt, I don't yet have a good understanding of how all the tokens stuff works, so I'm happy to take direction here. My experience elsewhere is just with straightforward approaches around having a current_user with particular roles, and our token handling here is more complex.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's been a while. IIRC, the capabilities reflect permissions granted to the application -- so, if it's making a request to access your GPS, and you say "yes", then you've granted the app that capability. Another common example is when you OAuth login and the system asks for permission to read your contact lists.
This is, more or less, inverse of the CanCanCan's normal idea of an Ability, which is the app deciding if the user has permission to do something; Capability is the user deciding if the app has permission to do something. (Capability was an inherited name, I suspect that something better could be determined) The end result of the calculation is the same, but separating them helps to keep them from getting too crossed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The name capability comes from us - it was something we built on top of OAuth 1 to associate a token with a set of things it was allowed to do.
OAuth 2 has something similar built in but calls them scopes where when an application requests a token it indicates what scopes it wants access to.
…to tokens, logged in users and other users
The logic about missing tokens implying logged in users (and that all logged in users have access to any method protected by a token capability) is correct. However, I believe it is both confusing and brittle, and leaves a security-related door ajar for future foot-gun incidents. Instead, apply Abilities as normal, and keep the Capabilities involvement only for situations where a token is provided. This reduces the cognitive burden when considering Abilities in isolation.
|
I've pushed a few more changes, in particular a slight reworking of the token handling which I think makes the behaviour more obvious. I also tried to convert another couple of controllers, but realised that there's a few edge cases in each case which deserve their own PR, so I don't think I'll expand the scope of this any further yet! |
|
My brain is melting from trying to understand all this, but I'm almost on top of it now. I've used the same check as setup_user_auth, for consistency, and removed the user check from Capabilities. I hope this is a valid approach! |
|
I think so, and long term it should mean we can get rid of Fully achieving that will require also handling basic auth when setting up abilities. Plus we'll have to figure out what to do with the user blocks check... |
Sufficient permissions are granted by the basic authorisation, so this isn't testing anything.
|
I think this is ready for final review and/or merging now. |
Resolves #1626. Builds on and replaces #1904
I've taken #1904, brought it up to date, and resolved a couple of things that I'd noticed and added a few more refactorings, including the first use of the
can?in the views.At this point, do we want to merge what we have already and then refactor the rest of the controllers in subsequent PRs, or should we wait until we're ready with a comprehensive PR that covers all controllers?