-
Notifications
You must be signed in to change notification settings - Fork 634
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
Allows additional methods to be used with Scopes #443
Conversation
Nice addition. Might wish to call it |
The methods I'm adding the argument to already have an argument called |
Maybe |
@davidbiehl I have little hope that this will ever be merged so I am forking and working from there. |
For anyone that stumbles on this, you can also just include the following method in your def policy_scope(scope, method = :resolve)
policy_scope = PolicyFinder.new(scope).scope
policy_scope.new(current_user, scope).public_send(method) if policy_scope
end |
@davidbiehl thanks for your workaround. I had to add def policy_scope(scope, method = :resolve)
@_pundit_policy_scoped = true
policy_scope = PolicyFinder.new(scope).scope
policy_scope.new(current_user, scope).public_send(method) if policy_scope
end |
@davidbiehl Sorry for this PR being left alone for soo long. I'm a bit curious if you have some more specific use cases for this? My first thought is that Pundit should do the base scoping and not cater for different views. Like, if I'm allowed to see my organization's articles then that's the base scope. Whether I only can see published articles in some views and unpublished in another is not Pundit's responsibility (I'm open to be convinced about this :) ). I mean, something like this seems appropriate for me: # In ArticlePolicy
def resolve
scope.where(organization_id: user.organization_id)
end
# In FooController
policy_scope(Article).published
# In BarController
policy_scope(Article).unpublished
# In BazController
policy_scope(Article) I don't see the benefit of passing another argument which controls the extra scoping for different scenarios. What do you think? |
@Linuus consider the following example.
Suppose in your example a user can have roles assigned on per-article basis, e.g. 'READER', 'EDITOR', 'AUTHOR' and so on. It is handled by an
Then the "base scope" would be something like
With that base scope,
will return a list of readable articles, no problem. The problem is that to go from
but that could be more expensive performance-wise and that would mean we would also have to pay the JOIN performance penalty for the base scope. And you also need to take care if a user can have multiple roles for an article, because then you need to deduplicate the scope too. UPD: In addition to that, now the Now, if only at the moment of
And avoid any performance penalties or deduplication problems. |
@immerrr why use a scope for the delete action? You can just authorize that specific article in the |
For example, I'm implementing a multi-choice form to batch-delete articles, then I want a scope to be able to display the list of articles that a user can destroy and maybe paginate through it. And I mean, I can check objects one by one with |
Here's a potential simple example: date range params passed in from the request. So you want to limit the results based on the date range first and foremost and then apply some Pundit-controlled policy restrictions as well - but every user type will definitely be scoped down by those date params. You'd have to apply a sort of "where dates in" query to the model first (in the controller) and then pass that subset of results into the Pundit scope. If I understand correctly, that would be (at least one) additional separate query, whereas if those params could be passed along into Pundit it could possibly all be handled in one larger query. Incidentally, this happens to be exactly what I'm trying to do and how I found this issue :) |
What about add scopes similarly to the https://actionpolicy.evilmartians.io/#/scoping ? |
No. Well, it depends on your ORM, but most ORMs are lazy so there will only be one query executed in the end.
IMO this is too complex of a feature to be in Pundit. It kind of goes against the core philosophy of the library. However, I don't think I'm the lead maintainer anymore so ultimately I guess it's up to @dgmstuart to decide. |
I'm just starting to get my head around this and I have a couple of small thoughts for starters:
The fact that we're passing class names rather than instances perhaps makes that less clean than it might otherwise be (eg. I don't see how we could have one |
Ah wait, the suggestion is around having a variable called |
Possibly dumb question: For @immerrr's example does the following make any sense? What are the tradeoffs? class ReadScope
attr_reader :user, :articles
def initialize(user, articles)
@user = user
@articles = articles
end
def resolve
if user.admin?
articles.all
else
articles.where('EXISTS(SELECT 1 FROM article_users WHERE article_id = articles.id AND user_id = ?)', user.id)
end
end
end class DeleteScope
attr_reader :user, :articles
def initialize(user, articles)
@user = user
@articles = articles
end
def resolve
if user.admin?
articles.all
else
articles.where('EXISTS(SELECT 1 FROM article_users WHERE article_id = articles.id AND user_id = ? AND role = ?)', user.id, 'AUTHOR')
end
end
end readable_articles = policy_scope(Article, policy_scope_class: ArticlePolicy::ReadScope)
deletable_articles = policy_scope(Article, policy_scope_class: ArticlePolicy::DeleteScope) ...although there seems to be some mismatch between the README which includes the example: @publications = policy_scope(publication_class, policy_scope_class: PublicationPolicy::Scope) ...and the actual source which says: def policy_scope(user, scope) ( Line 84 in 5b0d709
...so maybe this doesn't exist anymore 😞 |
It's not stupid at all, it could work, although there's an immediate code duplication optimization, the attr_reader :user, :articles
def initialize(user, articles)
@user = user
@articles = articles
end part is asking to be shared between the policy classes. Or if it is just one class, to be shared between methods with no extra actions :) |
@immerrr is there more duplication between these two classes than between two |
I guess it depends on the policy itself, I mean the policy in question obviously shares this snippet between two resolve methods:
But none of this is a big deal really. The way you emphasize "different" resources could be the reason why we went for different approaches intuitively. I think it's because I don't see them as different resources, the resource for me here is the same, the list of objects passed in as the input scope. Then the policies look like filters rather than new subresources to me. And the policy scopes are a natural optimization of what otherwise would look something like
But I don't insist. For me personally, it would be great to see this feature land, the way it is implemented doesn't matter that much. |
@immerrr I think I've caused some confusion: when I say different resources I do literally mean different - eg. Wouldn't this bit be duplicated regardless of whether this was implemented in the same class or two different classes? if user.admin?
articles.all
else According to the documentation, the solution I outlined (which is actually the same as this, described in the original issue: #368 (comment)), should already be possible in pundit (but whether the documentation is correct is another matter). |
Ah, ok, sorry.
Originally I had it in my head as something like def resolve_for_action(action: :show?)
if user.admin?
articles.all
else
...
end
end And in that case there would probably less duplication, but like I said, I'm fine with separate methods or separate classes too, you would split the single method if/when it would grow anyway. Re: the solution you outlined: I get how from the maintainer's perspective sometimes less features is better than more features, both in terms of effort and interface complexity. As the example from #368 shows, your approach can be achieved without any pundit interference at all: @publications = policy_scope(publication_class, policy_scope_class: PublicationPolicy::Scope)
# with
@publications = PublicationPolicy::Scope.new(current_user, publication_class).resolve That approach does not require any extra pundit code, and oddly enough it ends up being a few characters shorter. AFAICS this PR is more about a way to bring feature parity between authorize post, :update?
# vs
policy_scope Post, :update? The way it is implemented IMHO doesn't really matter as long as the implementation details, like policy class names, don't leak. |
There is no mismatch. That's not the correct method you are looking at. Here is the source: https://github.com/varvet/pundit/blob/master/lib/pundit.rb#L250-L253 |
Ah I see - there's a class method and an instance method with the same name. |
Yes. So you can do |
OK - I think I'm getting there with understanding this - thanks everyone for your patience. Here's my understanding of the issue:
Conclusion 1: Different scopes corresponding to different policy rules is a well-formed concept. Let's refer to these as 'scope rules' Conclusion 2: Scope rules should only relate to specific policy rules (otherwise there's no reason for them to live within Pundit rather than in the model) Conclusion 3: The behaviour of
I wonder if it would be misleading to aim to give these such similar interfaces since they don't actually behave the same way? (cc @immerrr) Conclusion 4: Since a specific scope rule and policy rule are actually two representations of the same rule, ideally we would infer the scope rule from the policy rule rather than defining it explicitly, but I don't see how that's possible since pundit rules are arbitrary ruby and we can't assume much about what's backing the policy scope either. Given some data we could perhaps check that the policy and scope rules match up, but I'm not sure if that's a useful/practical thing to do: post = policy_scope(Posts, :updateable).first
authorize(post, :update?) # should always be true
|
Great summary, thanks @dgmstuart ! Sorry for the delay.
Yeah, the "new"/"create" pair (C of CRUD) is a strange thing to do for a Scope that's supposed to operate on persisted objects, but the rest should fit quite well.
In that sense
Perhaps it could it be done the other way around? E.g. auto-generating a policy rule from a scope rule by doing something like Happy Holidays! |
No, that's not what I'm saying. "filtering" is an ambiguous word. This is what I mean when I'm using that word:
Pundit is designed to help with 1:
Pundit should probably not get involved in 2: To put it another way, the intention is that the definition of |
Thank your kindly reply and patient to explain your design decision. I'm afraid I still hope So this PR, IMHO merged to the master will be helpful. 😄 |
Why not to add filtering to the pundit as it would be helpful to filter-out unauthorized attributes posted via HTTP using strong_parameters? |
I'm not sure I understand what you mean: Let's say we have records A, B, C and D, and we have a user who is authorised in general to see A, B and C. Let's say we have a page on which we only want the user to be able to see A and B. They're authorised to see C, but we don't want to include it on this page for some other reason. How can that reason be only based on the user? Don't we need to use some information about the records in order to decide which ones to show? |
We always control access based on user and his access tables. we do need some information about the user, like his title, job_level, company or departments, but it can be calculated on the User#calculate_operation_access_code I need change the example a little bit: we have records A, B, C and D in the one table, and we have a user who is authorised in general to see A, B only and it used in many place Now we having a little special page, he should be able to see A,B and C. (notice he still can not access D.) Then can not using the general resolve in pundit because C is already filled out in general, and if we not using That's the one reason I want to merge this PR, another reason is some what optioned, pundit as |
@Eric-Guo If they're authorized to see record C in any context, then by definition they're authorized to see it. If you want to hide that record in some contexts then that's not authorization and it's not something Pundit should be concerned with. Your concept of "authorized in general" doesn't make sense to me - sorry. |
Thanks for your input everyone, but I'm closing this PR. I'll very happily re-open if someone is able to present (or construct) a simple example of a plausible use case that I can understand. |
@dgmstuart please look to the activepolicy scoping examples: https://actionpolicy.evilmartians.io/#/scoping |
@dmitry thanks, I've read them. Currently the reason for closing is that I haven't seen a use case that I can understand, for which this feature seems like a good solution. I don't see one in those docs either: it's all pretty abstract no? All I'm asking for is a simple plausible example where two different scopes required in the same policy. All I can say for certain about the actionpolicy feature is that it seems pretty complex, and as Linus said above, against the design approach of this gem. Bear in mind that asking for new features is asking for me to be part of maintaining those features for the foreseeable future. |
@dgmstuart few examples:
|
@dmitry I'm sorry I'm afraid I don't understand the first point. Could you write a simple code example? In the second case, it sounds like ID and business license are treated differently for the purposes of authorization, in which case shouldn't they have different policies? Or if I've misunderstood, please write a simple code example: it's much easier to understand by looking at code. |
First case.Strong parameters scope usage could look like: def create
attributes = policy_scope(params, :params, class: PostPolicy)
@post = Post.create(attributes)
end Definition: class ParamsScope
def initialize(user, params)
@user = user
@params = params
end
# could be reused as nested class
def resolve
params.permit(permitted)
end
protected
def permitted
if user.admin?
[:user_id, :title, :text]
else
[:text]
end
end
end Second caseI agree you can create shared class (DocumentPolicy) which can be reused as nested class for IDPolicy and BusinessLicensePolicy classes. One of the real examples, is when you are logged in as a super user and you want to control over subuser's documents. In that case you need to have special context scope, as you are not accessing directly from the user's account to the it's documents, but from the super account. |
@dmitry |
@dmitry Second case sounds completely implementable in Pundit currently. I'm showing an example with different policies, but I don't think you actually need these: it seems much simpler to just pass the relevant scope into the class Admin::DocumentsController < ApplicationController
def index
@documents = policy_scope(Document)
end
end
class DocumentsController < ApplicationController
def index
@documents = policy_scope(Document)
end
end
class IDCardsController < ApplicationController
def index
@documents = policy_scope(Document, policy_scope_class: IDCardPolicy::Scope)
# but actually this isn't even necessary and you can just do this for exactly the same result:
@documents = policy_scope(Document.id_cards)
end
end
class DocumentPolicy < ApplicationPolicy
class Scope < Scope
def resolve
if user.admin?
scope.all
else
scope.where(owner_id: user.id)
end
end
end
def show?
record.type = "licence" || user.admin? || record.owner_id = user.id
end
end
class LicencePolicy < ApplicationPolicy
class Scope < Scope
def resolve
scope.licences # assuming that this is a scope on Documents that returns all the licences.
end
end
def show?
true
end
end
class IDCardPolicy < ApplicationPolicy
class Scope < Scope
def resolve
if user.admin?
scope.id_cards # assuming that the Document model has a scope which returns all the ID cards
else
scope.id_cards.where(owner_id: user.id)
end
end
end
def show?
user.admin? || record.owner_id = user.id
end
end |
A proposal for issue #368.
Adds an additional argument to the
policy_scope
methods that specifies the method on the scope that should be called. Defaults to:resolve
for backward compatibility.This makes Scopes more flexible by allowing multiple scopes to be defined on a
Scope
class and making them available for use in controllers or views with existing helper methods.