-
Notifications
You must be signed in to change notification settings - Fork 59
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
API proposal for authorizing relationships #30
Comments
Pinging people that are affected by this: @venuu/core @NuckChorris @aaronbhansen @vevix @DNA @billxinli @GRardB @arcreative @RSSchermer Please let me know what you think of this proposal! Any input would be greatly appreciated 💞 (even a +1 reaction to the proposal is enough, if you agree with it 😄) |
Also @alyssais and @barelyknown from https://github.com/togglepro/pundit-resources if you have had to work around this same issue I'm super interested to hear how you've managed to solve it! |
I don't know off the top of my head what pundit-resources does in this circumstance. It's entirely possible that it handles it incorrectly. I don't know if it's something we've come across. |
With this proposal, the authorization can differ by point of view. For example, creating a comment model directly ( If we wanted them to return the same answer, we would probably do something like this: class UserPolicy
def associate_with_comments?(comments)
comment.map { |comment| CommentPolicy.new(comment, user).update? }
end
end But that should be really simple to generate, so why do we even write it? Oh, right, because...
But it turns out to be quite easy to get the inverse of an association in ActiveRecord: just What if we generated the Policy name based on the So, what if we just made a best-effort attempt to find the inverse? Attempt to use AR reflection to get the name of the associated record, and otherwise fall back to the less-awesome I guess the code would look something like this (warning: do not use this code without a whitelist on the constantize): # Assuming 'record' accesses the main record of the request
# And 'policy' accesses that class's policy
# And 'current_user' accesses the current user
relationships_json.each do |key, associated|
associated_class = associated[:type].classify.constantize
associated_record = associated_class.find(associated[:id])
associated_policy = Pundit::PolicyFinder.new(associated_class).policy
inverse = associated_class.try(:reflect_on_association, key)&.inverse_of
if inverse.present?
associated_record[inverse.name] = record
unless associated_policy.new(associated_record, current_user).update?
raise 'Not allowed'
end
else
unless policy.new(record, current_user).send("associate_with#{key}?", associated_record)
raise 'Not allowed'
end
end
end I'm not certain this works for all associations (I just wrote it right now in the GitHub comment box) but I suspect it could be made to cover all cases fairly easily. My view is that JA should do whatever it can (even if it means adding complexity to JA itself) to enforce a consistent view of authorization, to hew as closely as possible to the existing Pundit techniques, and abstract the request away from the policy. Obviously, this won't be perfect, since json:api has a lot more surface area than the traditional server-rendered apps that Pundit was originally designed for, but I don't think adding more methods is necessary in most cases. |
Just asking whether I understood correctly:
So the way it differs from the current behavior of JA is the step 2 above. For the unfortunate case where we wouldn't be able to query for the inverse assocation, we'd fallback to the How would this translate to the case, where you want to authorize for sending a comment to a post as a normal user? Clearly you wouldn't be authorized to update the post so what would be called in that case? {
"data": {
"id": "comment-1",
"type": "comments",
"relationships": {
"post": {
"data": {
"id": "post-1", "type": "post"
}
}
}
}
} Would the answer be in this case that we'd somehow set You're absolutely right in that the current proposal risks having different behavior depending on from which side the association is authorized from 😕. I'm not sure anymore on how much we should try to shield us away from the surprising mutations of ActiveRecord. What would happen if we'd just rely on transactions always being able to rollback the changes and just handle the authorization in an Here would be an example case where users aren't allowed to comment on draft posts. Does this make sense? class CommentPolicy
attr_reader :user, :record
def initialize(user, record)
@user = user
@record = record
end
def create?
!record.post.draft?
end
end |
I'm getting quite confused here. Could we try to identify some approaches that are flying around here and in our heads more concisely and give examples on how they'd look like? Say we have two models, class Article < ActiveRecord::Base
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :article
end And we have our resources: class ArticleResource < BaseResource
has_many :comments
end
class CommentResource < BaseResource
attribute :body
has_one :article
end We have an
{
"data": {
"id": "comment-uuid-1",
"type": "comments",
"attributes": { "body": "foobar" },
"relationships": {
"article": {
"data": {
"id": "article-1",
"type": "article"
}
}
}
}
} (A) Current approach 1policy(comment_1).create?
policy(article_1).update? (B)
|
We could opt-in to the approach G above and not force any naming convention to the users by letting them supply the association policy class name themselves. class ArticleResource < BaseResource
has_many :comments
authorize_relationship :comments, with: 'ArticleCommentAssociationPolicy'
end
class CommentResource < BaseResource
has_one :article
authorize_relationship :article, with: 'ArticleCommentAssociationPolicy'
end
class ArticleCommentAssociationPolicy
def initialize(user, article:, comment:)
@user = user
@article = article
@comment = comment
end
def associate?
ArticlePolicy.new(@user, @article).show?
end
end
|
Going back to my original ticket, what I was hoping to do is have some simple permission checking to let me have a hook into creating the association instead of calling update? on the related model. I think the solutions above are trying to solve this, but add a lot of extra methods and complexity. I would rather have most the examples above reversed. Taking option C above as an example
I feel like it makes more sense to reverse how you authorize the creation of a relationship. In the example above, if you have a single relationship point, an article has multiple comments, the example makes sense. But when you have something like a user object that is on multiple relationships (article, comment, tag, etc), you know have to implement that same check over and over on all the child policies. My original idea was to do something like this.
While its similar to the example above, I think having the parent check the relationship makes the most sense. What if I had a user object on the article and comment, then I have to implement the same method on each child policy.
Where in all the examples I have locally, I know from the user object if that association is allowed.
One parent association point to check all the related classes. And if you had a base pundit class, you could still default allow_relationship? back to update? until the method is implemented. Maybe others can better define more use cases, but for me the state of the parent object usually defined if I allowed child objects to be related. If the user wasn't a Author for example, then they shouldn't be able to create an article or tag. If they are a user or an author then they can create comments. You could allow a cascading type of check
Mostly I want to avoid trying to implement the same policy over 7 different models, when I know from the parent relationship if that relationship can be created. The user object is even easier to show this as you could just check if the user_id on the association is the same as the current user and then allow the model.
|
Could you clarify what you mean by "parent" here? It's not immediately obvious to me. Is it possible to, without knowing the business logic of the app, look at a one-to-one relationship between |
On a one-to-many, a has-one, or many-to-many you can always go back to the the belongs-to relationship reference. If the model has a belongs-to relationship, it is a child of a parent. In the one-to-many example, the parent would be the one who has the relationship you're trying to add.
A user has many articles. Article belongs to a user (parent). Check on the User policy if the relationship is allowed. In the has one example, you still can know the parent.
A product has one product_details, a product_details belongs to one product (parent). I think its easiest to just give responsibility to the product and have it decide if the product_detail is allowed to exist as a relationship. Since the has-one relationship is a relationship pointer anyways with no FK's on product. In the many-to-many example, it seems more complex as there isn't a model with a belongs to represented in code, but there still is a belongs to relationship implied. If its a has-many-and-belongs-to-many with just a mapping table and no model representing the intermediate table, there is still a database table that has a belongs to relationship to both sides.
A user has many companies, and a company has many users. The users_companies table isn't represented as a model in rails, but the users_companies still has a belongs-to to a user and a belongs-to to a company. Since the users_companies tables isn't represented in rails code as a model, you would need to check the user policy if the company relationship can be added, and the inverse, check the company policy if the user relationship can be added. I also think that many-to-many tables are the exception, and most fall under has-many, and belongs-to. If the users / companies many-to-many was a has-many-through relationship, then when creating the has-many-through relationship, its more clear that you should still check the parents. You create a new users_company relationship and it has
In this situation a belongs to exists as a relationship on the model in rails. User has many users-companies, and users-companies belongs to user. All it comes down to is, anytime you are creating a new record, if it has a belongs-to FK, that record is a child of the parent it belongs to. Go check if the child can be created from the parent it references. If you take the example of an e-commerce ordering system. You have the following tables laid out, you have two ways of checking if that association is allowed. One you check the parent, or two you check on each child. Let me lay out both. The table relationships
In the original examples, if you wanted to check if you could create child objects under the parents you would need to add the following methods.
As you can see from that example, as you have nested tables, you have to add the same methods over and over. If you decided to change whats allowed, you need to make sure each policy method is updated to ensure the same results. If you had the parent take responsibility for checking.
In this example, you have 3 methods which cover any child being added. Instead of re-implementing the same method up to 3 times in the previous example. Hopefully that clarifies my thought process on why I think the parent should be responsible for checking authorization of a child. |
Given the same scenario as in #30 (comment), my proposal (#30 (comment)) would look like this, without the downside listed in (D) of requiring transactions:
CommentPolicy merely needs to validate its belongs_to associations. Which, for most models, will be one. No trouble there. So let's try a more complex scenario. Given the following models (the resources should be fairly obvious): class Article
has_many :comments, as: 'topic'
end
class Review
has_many :comments, as: 'topic'
end
class Comment
belongs_to :topic, polymorphic: true
end And this request {
"data": {
"id": "review-uuid-1",
"type": "review",
"attributes": { "body": "foobar" },
"relationships": {
"comments": [
{
"data": {
"id": "comment-1",
"type": "comment"
}
},
{
"data": {
"id": "comment-1",
"type": "comment"
}
}
]
}
}
}
This way, the default is to just ask the model declaring When you aren't using ActiveRecord (or if AR Reflection fails us, though you should declare an |
This seems backwards. The User shouldn't know whether they can create a Comment, the Comment should know whether it can be created by a User. Otherwise, when User can create 700 things, the UserPolicy is gonna be huge while the CommentPolicy is 5LOC If you want to defer to the User to determine this, it should be explicit, because you're very much going against the grain on separation. |
I disagree. Why would the user, article, etc not know if you can add a relationship. If the article isn't published, then you shouldn't be able to publish a component. That is a restriction of the article, not the comment. The comment shouldn't check itself for article permissions, the article should be the one that gives permission to the comment to publish. If a user wants to publish a comment, and the user isn't active, the comment shouldn't check itself for user permissions, the user should know if its in a valid state to allow that comment through. My point was, if you have a user on 4 different tables, Article, Review, Comment, History, etc and you want to perform the same user checks on each one, why write the same code over and over. The user policy could have overrides as I specified for specific relationships, but there is a base permission check that is shared when adding any relationship. A modified example:
Why would you want to have those same checks on each of the child policies that have a user relationship instead of going to the parent (user) to do some global checks? You have the same permissions you need to check thats global for any relationship being added as a user belongs-to.
Then you could add specific overrides for child relationships like article.
|
Because it's an action on the Comment, not on the Article. The Article doesn't necessarily have a has_many, but the Comment definitely has a belongs_to. You're suggesting that the User should have responsibility for checking whether a Comment can be created? That's patently absurd. If you want to share the code, don't just try and shove the responsibility onto the wrong object — create a generic method on the User such as It's one line, and it's 100% logical to do si.
This is probably not a good idea in any situation, because suddenly when you add another model named If you put it in the correct object (the def update?
return false unless record.user.active?
end And then SettingsPolicy has this: def update?
!!current_user
end Super simple, it's very clear what's going on, and there's no possibility of confusion by POV. |
No, I'm suggesting that there are still permission that need to be checked if you can create a comment, and that is contained entirely within the scope of the CommentPolicy.create?, except for relationships. If you want to associate a comment with a user, the UserPolicy should take responsibility for allowing that relationship or not. It works for any situation, has-many, has-one, many to many. Its a relational operation, everything has a relation and a responsibility. You're saying that with a parent / child relation, you should let the children decide to do whatever they want? If the Parent has no responsibility at that point, why is it a parent relationship. From a security stand point, adding security on the child models in an opt-in process, you have to ensure that you opt-in to any policy relationship permission. You must create relationship permission checks on each policy you create. If you forgot to opt-int you now have created a security hole. If the parent is responsible, its an opt-out process. Everything has the same security requirements unless you specifically opt-out. Yes, you could separate the concerns into a module and include it on each child policy, but if you forget to add a association permission on a new policy you now are defaulting to the parents update? permission anyway. So it goes back to the parent by default, why not start and maintain relationship security checks on the parent. Even from your own comment, you spell out what I'm trying to avoid.
Yes that is correct, CommentPolicy.create? is called when trying to create a comment, but also UserPolicy.update? because of the user relationship, which is the problem as it exists today.
Exactly one of the problems I'm trying to avoid. Rather than re-implement the same methods over and over with possibly differences, have a single source of truth. Continuing on:
The rest of your solution is to try and fall back to the parent relationship dynamically and check if update? is allowed. Your whole comment is what I'm suggesting except instead of calling update?, which is the entirely the wrong method to check in the first place, call an association permission check. Why is calling the parent relationship policy update? permission the entirely wrong method to check. It's because the current user may not have access to update? an Article, but they can add an associated comment to that article, hence the allow_relationship? check on article. Why would you default back to update? when its clearly the wrong solution. Updating an article and creating a relationship are two different concerns. Even your last comment aren't even targeting the right security settings in your example.
From your previous comments, you said that the CommentPolicy or ArticlePolicy should check back to the inverse relationship and check the update? permission, and if the inverse can't be found then check the association method. Your CommentPolicy and ArticlePolicy don't even make sense. If you are creating a new Comment as JSON authorization stands today, it would check the comment policy for create? and then would call ArticlePoilcy for update? You aren't checking the article policies user to see if that user is active to create a comment, you need to check your own comments user relationship. It doesn't matter about the articles user, only thing that matters is your user. The baseline change that needs to happen is to try and avoided calling update? on a parent relation when creating a child model. There is a clear difference between being able to update? an article and being able to create a relationship off of article. |
Our proposals are quite opposite, in fact: you're proposing the association be handled by the associated side while I propose it's handled on the side with the foreign key. My proposal is merely that a foreign key is naught but another attribute to check — if you want to build your solution as an abstraction on top of that, be my guest. The simplest and most Pundit-like API should be the one used by default in JA, if you ask me. And that is, quite clearly, my solution. Associations are not some magical thing which needs to be treated differently. I understand that you feel that the parent is the real decision-maker here (and I don't disagree, generally I defer to a method on the parent when checking this), but this risks ballooning the parent's concerns to be huge, and leaving the child completely emaciated. I mean, what happens when you have seven different has_many associations on a User? Suddenly you have seven codepaths in that policy and none in the child policies. Let's not talk about knowledge (because we both agree that's in the User model), let's talk about responsibility. When a User wants to create a Comment, is the User model responsible for validating that Comment? Hell no it isn't, so why should it be responsible for checking if the Comment is doable, just because it happens that the authorization for a Comment calls on the User? By that logic maybe I should just straight take it to the Role model, since the User model is just gonna call on the Role model anyways. It's abundantly clear that this approach does not scale beyond a simple project. And what happens when you have a The entire point of Pundit is to say "given X object, can Y user do Z?", and you're throwing that out the window because you want to simplify your own solution (which honestly doesn't even make sense, because you would be authorizing the current_user, not the associated user!) |
Pundit works well in the non api world, but not in an api one. You can do so many more things through an api call. When its not a structured json api controller, you have specific methods for specific operations which pundit can account for. create_user, make_admin, etc, etc. Each having its own policy authorization.
This is wrong, they do need to be specifically checked. What if you call the api with curl and pass in a different user_id. You now have created an object for a different user.
This is a total valid authorization. You need to call the parent for each of those children. This doesn't mean you'll have zero responsibilities in child policies as I show below.
As i explained earlier, there is always a has_many_through table, something still belongs to something. The has_many table has two belongs-to references to both parent tables. Json authorization has two responsibilities it needs to handle, I'll try to make them both abundantly clear.
Without one of those responsibilities, its half an authorization system. You have a User Object with 3 roles You have an Lets create the three policies and point out where is responsibility needs to happen.
Now that we've defined the policies, lets play through some scenarios. Scenarios:
In your example, you are totally missing Responsibility 2. Checking relationships is as much of a authorization as Responsibility 1 |
Actually, no. Pundit works great in the API world too, as long as you're not trying to find a problem for your solution. To suggest that Pundit policies should be (or were) traditionally coupled with the controller is absurd. That breaks separation of concerns on so many levels. The job of a policy is to say "Can X do Y to Z?" which does not take context into account. If your policies have special cases like that, you're using it wrong. Pundit handles CRUD perfectly, it's really 👌 and there's no reason for JA to be different, except that it is a little tough to automatically find which policy should be validating an association, which in traditional Pundit would be exactly as I suggest — the child model! I proposed a solution to this: we attempt to find the child side of any
Yes, there's a clear child and two clear parents. You don't answer which parent gets called to validate the child, and that's a very serious problem. Do both parents get called? How do we rig that? Why? Why not use my solution?
And I keep saying these are one and the same. Associations are naught but attributes, and any differences should be hidden from the user. Where do you even draw the line, you're calling And no, this isn't the role of JA. JA exists to hook Pundit with JR, and nothing more. We should not be opinionated about how you structure your Pundit stuff. If you want to build it this weird way that you want to, you have every right to call out to the other objects or even build your own abstraction. JA's job is to try and fit JR's operations onto Pundit authorizations as accurately as possible But Pundit handles everything we need just fine.
This is not a reasonable solution. What happens when you have 7 child associations on User? Because we already have that on Hummingbird. And they all need different logic. Yeah, doesn't work too well. So we would need something like this:
And so on. For 7 different associations. In a fucking case statement. No. Just no. This is not a good solution, at all. Meanwhile, in my solution:
Because, as it turns out, updating a foreign key isn't special or magical. It's just normal. |
At this point we aren't to convince the other. We are also the only ones commenting, so the point is somewhat mute. It's not worth discussing further. I think @valscion sound pick a solution and move forward. If he does what I suggested in my original ticket, separate pundit and the authorization methods into an abstract class, then that can be set in the config for JSON Api Authorization like many of the services in JSON Resources, anyone can implement their own services. It would be great to see Pundit and the authorization methods put behind an abstract service that can be configured and changed in your projects config file. Pick a solution from here and implement it as the default. It would be even nicer to have the pundit requirement be optional so you could install only the configurations you wanted, one for the generic authorization service, and then the authorization implementation. |
Well, that was some unpleasant escalation to wake up to. 😕 People, it should be perfectly possible to discuss the pros and cons of any given solution, without being disrespectful to your peers. This kind of aggressive back-and-forth only serves to muddy the waters, and takes us further away from finding a solution. I encourage everyone to take a deep breath, and read through the proposals once more. They all have shortcomings, that's why we have this discussion. Rather than defending your own solution with tooth and nail, see if you can find something you like among the rest. |
@lime I agree. I was merely trying to explain a custom built system that we already have in place, but doesn't exist in JSON API Authorizations today. It probably has shortcomings, but we haven't run into them yet. It provides clear testing of permissions and full sanitization. It provides not only operation level permissions (create / update / view, etc), but attribute and relational sanitization. We we love to use it in JSON API Authorizations, but the abstraction to do so don't exist. I was trying to share our domain knowledge in how we fixed the current problem, which is what this ticket is about. Again, I don't care what solution is picked, my original ticket was to make things abstract enough in JSON Api Authorizations so we could have our own security implementations. I was only trying to fully explain how our custom implementation worked in production today through examples. Our working process is, sanitize absolutely as much data as you can when you receive an input operation (create, update, delete), then when you receive an output operation (view / index), you can trust your existing data and have very minimal output sanitization. Since input operations are only a minor portion of your sites operations in terms of processing power / requests, it makes sense to do as much work on input as possible, and minimize work on output since viewing data is far more common. Maximize server resources for viewing by trusting that your input was valid. I'm completely fine if others don't agree that there are different levels of security that need to happen (permission, attribute, and relational). |
Can I confirm that there still isn't any way to create a relationship with a resource unless you're also allowed to update the target of the relationship? That seems like an essential feature, that must come up in every non-trivial application. Could we not just have an option to skip the update? check on certain relationships? |
Yeah, unfortunately such an option is not yet implemented.
The problem comes from what does "just have an option to skip the update" look like, and that it hasn't yet been implemented. This is precisely what we have been trying to discuss in this issue :) |
Yes I went to patch it myself, and can see that it's not that simple. Necessary, but non-obvious how to make it nicely configurable. For my own purposes I've just added this to my initializers:
And in application_policy.rb:
And then in the policy of the resource where it's become an issue for me:
But that's no kind of solution. Adding it here in case someone really lazy just wants to sidestep that particular validation in a hurry. |
That's definitely an improvement over just creating a custom class where no authorization is ran for the related records. Did you see that you can actually create a completely custom authorizer class for your purposes and configure require 'jsonapi-authorization'
class MyCustomAuthorizer < JSONAPI::Authorization::DefaultPunditAuthorizer
def create_resource(source_class, related_records)
::Pundit.authorize(user, source_class, 'create?')
related_records.each do |record|
::Pundit.authorize(user, record, 'relate?')
end
end
end
# ...and in an initializer, do this:
JSONAPI::Authorization.configure do |config|
config.authorizer = MyCustomAuthorizer
end |
I'm taking a stab at testing out different approaches to these suggestions in here. The approach I want to take relies on always using a whitelist instead of blacklist. This means that:
So even at the cost of some policies becoming huge, I want to keep each method as small and unambiguous as possible. That means methods named like this: class CommentPolicy
def allow_relationship_article?(article)
# ...
end
end I haven't yet decided on which side of the association should handle the authorization. I'll have to restructure the spec dummy application a lot to include different kinds of relations so that I can be sure each case is tested properly 💦. Based on my initial testings, it should be possible to get a nice approach for these authorization checks. Relationship objects coming from JR do know who owns the foreign key and allows jumping between the associated resources with relative ease. This just might be quite big of an effort... |
I just updated this issue's description to highlight the direction I'd like this to go. Resolving this issue is not a high priority for our team yet, as our API is currently only used internally, and we're perfectly fine with just authorizing for The principle of JA is basically: Prefer being overly restrictive rather than too permissive by accident. What follows is that we want to have:
|
I've thought about this issue and I've come up with a proposal that looks (too?) simple. For easier discussion of the changes I've submitted a PR (#40) instead of posting the code here. |
Ok so, after thinking this over, here are my thoughts on this. The JSONAPI spec basically treats relationships as resources themselves. This is why we get into trouble with Pundit coverage for those pesky associations. Usually, in a standard rails app the manipulating of those relationships is just within a controller action that we can authorize for. Here, they are WITHOUT. If that makes sense... I feel like Solution (G) from the above is the best route to go.
Probably would need a scope as well, but whatever. These seem like the only two actions we need on them. For PATCH requests, you would pass in the OLD related records through #destroy then the NEW related records through CREATE. As far as I know, the entire request is wrapped in a transaction by JR, so if auth fails at some point everything will rollback and an error returned to the client. I have put together a working version of (C) in our application but I dislike immensely having TWO separate policies to check the same relationship. It's just asking for trouble eventually. I think I will try to put something together on this to see how it works out. I'll report back. |
This makes a lot of sense to me - authorising associations correctly is important enough (and, often, confusing enough) that giving it it's own policy file seems appropriate. |
Begrudgingly, I think I like it too. "Begrudgingly" because 1000 policies, but "like" because it does seem to give the requisite flexibility. I'm currently in nightmare land where I'm throwing Pundit exceptions in the resource because I just can't get access to context or params, which is needed in a lot of cases for my application. The only problem I see is that you would still need the inverse policy as well...right? If you have |
Regarding a dual policy, its one relationship, so one policy ought to cover it fine. Its just a POV difference from one model to the other. |
Yeah presumably it would depend how they were defined? module V1
class CatResource < BaseResource
relationship :mouse, to: :one would imply a But you wouldn't want a module V1
class MouseResource < BaseResource
relationship :cat, to: :one |
Seems good to me then. A problem I'm running into just today is regarding a model where creation requires that either the In my case, the relationships don't only need to know about the two records, but also about each other, which opens up a whole new can of worms. I can chalk this up to being uncommon/unorthodox, but I would love to have a solution that just feels beautiful like [most of] the rest of Rails. |
Yes, I think that option G does seem to be the most powerful option that allows any abstraction to be built on top of it. I think we'll want to test out how G would look like in a real-world application and what challenges it entails. I also envision that as it is such a new concept compared to the basic pundit classes, we'd want to make it opt-in with a configuration option -- similar to how you can now override the class used for authorization. Or we could make it configurable on the resource level, too. This would give us hopefully the best of both worlds -- the default authorizer gets you up and running quickly and safely while the relationship policy classes would allow you to consolidate your relationship authorizations to one, conventionally named pundit-ish class. This way you wouldn't have to define a huge number of new policy classes unless you think it would clean up your application. In our application for instance, we don't allow a relationship to be mutated from both sides of the relation. This way we'll likely only need to authorize those relationship changes only in one policy class instead of two. I feel that the best way to go forward will be for me and @lime to draft a roadmap issue soon to open up a discussion of what the future could look like and to allow comments to be consolidated in one place |
I like the idea behind solution (G) to interpret relationships as resources themselves. What if we have a setup with two relationships that relate the same classes? class Concert < ActiveRecord::Base
has_many :attendees, class_name: 'User'
has_many :performers, class_name: 'User'
end
class User < ActiveRecord::Base
has_many :concerts_as_attendee, class_name: 'Concert'
has_many :concerts_as_performer, class_name: 'Concert'
end (An alternative would be to use a single relationship like Would the policies be called |
That's a very good question 🤔 . Definitely a use case worth considering before committing to any particular direction. |
It seems like we should be able to figure things out via reflection on the models, then build the class name, regardless of which end of the relationship we're doing the calculations. However I sympathize a lot with the idea @valscion put forth about this being a sort of opt-in, more complex layer. I think @matthias-g covers most things well enough, and I've been able to do something similar in POST and PATCH routes, just hitting those same policy actions (add/replace/remove)... the caveat being making sure policies covering the same relationship match. I'm going to spend the weekend thinking about it! |
Sound like a good idea! Do you think it would be more clear to discuss the G case specifically in a new issue? I'm afraid that this issue might get cluttered if we want to dive deeper to one solution, and having a new issue might allow us to have more constructive dialogue |
I just want to bring in a way to handle this with the current approach of JA. It's rather a design pattern than a clear solution.
module ConcertUserRelationship extend ActiveSupport::Concern
included do
def allow_add_attendee_to_concert?(attendee, concert)
concert.published && (user == attendee || user.admin?)
end
def allow_remove_attendee_from_concert?(attendee, concert)
user == attendee || user.admin?
end
# also allow_add_performer_to_concert? and allow_remove_performer_from_concert?
end
end
class ConcertPolicy < ApplicationPolicy
include ConcertUserRelationship
def add_to_attendees?(users)
users.all? { |user| allow_add_attendee_to_concert?(user, record) }
end
def remove_from_attendees?(user)
allow_remove_attendee_from_concert?(user, record)
end
# also add_to_performers? and remove_from_performers?
end
class UserPolicy < ApplicationPolicy
include ConcertUserRelationship
def add_to_concerts_as_attendee?(concerts)
concerts.all? { |concert| allow_add_attendee_to_concert?(record, concert) }
end
def remove_from_concerts_as_attendee?(concert)
allow_remove_attendee_from_concert?(record, concert)
end
# also add_to_concerts_as_performer? and remove_from_concerts_as_performer?
end |
I like that. Seems like something we should have already thought about when confronting the 'make sure both policies have the same logic' ... In fact I think I'll refactor our app a touch to use this. |
Such a design pattern could easily work out OK for most of the time. We might be better off delaying the implementation of an association policy class and only implement one when we have discovered real world usage pains, instead of just ones we might have. It might also make sense to document such a pattern in the README under the association policy caveats :) |
What do you think of the following which is in the roadmap issue #48 ?
Do you think it would be a good first step to implement the |
I will try my best to put a PR together before the end of the week, next week at the latest - with the code from our project for |
Relationship authorization has been implemented and this issue can be closed. https://github.com/venuu/jsonapi-authorization/blob/v1.0.0.beta2/docs/relationship-authorization.md |
UPDATE on 2016-10-29:
The approach I want to take on authorizing relationships relies on always using a whitelist instead of blacklist. This means that:
allow_relationship?
method that would handle all relationships.update?
after trying to set a foreign key won't work, as it is a blacklist approach.So even at the cost of some policies becoming huge, I want to keep each method as small and unambiguous as possible. That means methods named like this:
I haven't yet made up my mind on which side of the association the authorization should happen, but I'd like that choice to be abstracted away from the
AuthorizingProcessor
. It could live insideDefaultPunditAuthorizer
, if it would make sense, or we could come up with a new place for that logic to live in.See my comment about this for more details, and to continue the discussion.
Open this to read the old proposal that kicked off the conversation
For a create request that looks like this:
We authorize with these methods:
PostPolicy.new(post_1, current_user).create?
PostPolicy.new(post_1, current_user).associate_with_author(user_1)?
PostPolicy.new(post_1, current_user).associate_with_comments([comment_1, comment_2])?
PostPolicy.new(post_1, current_user).associate_with_tags([tag_1, tag_2])?
For this request, no matter if it's an update or a create call, we are able to only use
PostPolicy
to authorize for every association.I can't see there being any other way as we _cannot_ change the
UserPolicy#associate_with_comments
call to these without a huge scope creep:CommentPolicy.new(comment_1, user).associate_with_post(post_1)?
CommentPolicy.new(comment_2, user).associate_with_post(post_1)?
Who knows, the
Comment
resource might even call that association with a completely different name thanpost
underneath!If that request would be an update request, it would only change the
PostPolicy#create?
call to bePostPolicy#update?
and the association checks would remain the same.I know this goes against Pundit architecture, but I can't really see there being any other way because we aren't able to call
post_1.author = author_1
norpost_1.comments = [comment_1, comment_2]
before authorizing as it would implicitly save the association immediately. So we aren't able to just use thepost_1.author
value inPostPolicy#update?
to check for authorization, as it isn't set yet.has_one
association reference — When are objects saved? (Ruby on Rails Guides)has_many
association reference — When are objects saved? (Ruby on Rails Guides)The text was updated successfully, but these errors were encountered: