-
Notifications
You must be signed in to change notification settings - Fork 60
Handling relationship policy checking in replace_fields #51
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
Handling relationship policy checking in replace_fields #51
Conversation
Update both the authorizer and the default pundit policy to follow same pattern as the other relationship endpoints (sending the related record as an argument to the policy method).
authorize_replace_fields
| def remove_to_one_relationship(source_record, related_record, relationship_type) | ||
| relationship_method = "remove_#{relationship_type}?" | ||
| authorize_relationship_operation(source_record, relationship_method) | ||
| authorize_relationship_operation(source_record, relationship_method, related_record) |
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.
Removing an association doesn't need the old records, as it will be available directly from the record inside the Pundit policies. Wasn't that what you discussed with @matthias-g in #40 (comment)?
| query: add_method_name, | ||
| record: records, | ||
| policy: policy | ||
| end |
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 think you could use the authorize_relationship_operation helper method a lot in here. It would hopefully cut down the duplication in here a lot :)
| when ->(relationship) { relationship.polymorphic } | ||
| polymorphic_type = data[:records].class.name.downcase | ||
| "#{prefix}_#{relationship.class_name.downcase}_#{polymorphic_type}?" | ||
| when ->(relationship) { relationship.class == JSONAPI::Relationship::ToOne } |
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 think these case-statements could be written like so?
case relationship
when relationship.polymorphic
# ...
when JSONAPI::Relationship::ToOne
# ...
else
# ...
endI haven't seen this sort of lambda usage before myself 😅
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.
In order to call a method on the case object (like .polymorphic and .class) I have to do this via a lambda. when relationship.polymorphic and relationship.class == JSONAPI::Relationship::HasOne always come back false even when they should be true. Perhaps I am missing something here? Please let me know. It only works properly for me when using lambdas. (ref)
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 it so that you are now using the class name of the actual record to figure out the policy method name? If I understood correctly, in @matthias-g's PR we went with using the relation name from the resource class itself as-is instead of figuring out the class name behind the association. I think if we'd do the same in here, we could make the code much more simpler and wouldn't have to worry about polymorphic associations as much? |
|
Thanks for the PR! This feature is what is missing indeed and we'd like to get it to the 1.0 release 😊 . Let's figure out how to test this with the current test suite once we agree on a direction where this is going :) |
|
Thanks, ill review asap and clean things up. Will post back soon! |
helper, refactorings.
f1a3461 to
2d8222c
Compare
|
I think we will need to call the @matthias-g, what do you think about the case when we're PATCHing away an entire relationship? Here's the setup: Article.new(id: 1, comments: [comment_with_id_1, comment_with_id_2]).saveAnd we'd send this JSON API request:
What authorization methods should such an operation use? If we didn't have any other methods besides the
Would that make sense? If we think this is the solution we should use, we'll need to have specs checking that these are the calls that are being made and authorized against. |
|
The build is currently failing because of code style issues: I'm sorry that we don't have a nicer way of running the lint with CI yet 😞 . See #34 for some history regarding these lint failures. |
Sorry but I don't quite follow on this. What situation(s) are you talking about specifically? If we're talking about the replace_fields_with_context I have a feeling I can refactor that down using some of those helper methods that @matthias-g created, but I've been sick and haven't had time/energy to dig deeper into that just yet. I am looking into it though. |
This is how it is handled in this branch. |
Hmm I guess I'm just a bit confused yet as to what Pundit policy method will be called when the resource specifies a custom Here's the setup: Comment.new(id: 1, reviewing_user: user_with_id_2).saveclass CommentResource < JSONAPI::Resource
include JSONAPI::Authorization::PunditScopedResource
has_many :tags
has_one :article
has_one :reviewer, relation_name: "reviewing_user", class_name: "User"
endAnd we'd send this JSON API request:
"data": {
"type": "articles",
"id": "1",
"relationships": {
"reviewer": {
"data": {
"type": "user",
"id": "3"
}
}
}
}Will we call I think we might be better off if we'd just pick the relation name from the relationship definition, not the What do you think? |
Ok, I will investigate and report back. |
I agree. But I see two things to consider:
|
I don't think we will ever be at that stage, considering the authorization happens before any changes to models are made. I'm not quite sure what you're saying, so maybe you could elaborate this second part? |
Oh, sorry, that was indeed not clear. class ArticlePolicy
def add_to_comments?(comments)
(record.comments.count + comments.count) < 3
end
endthe |
- Utilizes `authorize_replace_to_many_relationship` and `authorize_replace_to_one_relationship` helpers. - Removes helpers no longer used after refactor.
c2e459f to
b09fbb3
Compare
|
Hmm I think you might need to also call |
|
My understanding is that all of that logic goes inside of the |
|
Mm but in the new resource case, we don't yet have a user_1 = User.new(id: 'user-1').save!
comment_1 = Comment.new(id: 'c-1', content: 'Is this the real life?').save!
comment_2 = Comment.new(id: 'c-2', content: 'Is this just fantasy?').save!
{
"type": "articles",
"relationships": {
"author": {
"data": {
"type": "users",
"id": "user-1"
}
},
"comments": {
"data": [
{ "type": "comments", "id": "c-1" },
{ "type": "comments", "id": "c-2" }
]
}
}
}This will need to be addressed somehow... I'm afraid that we don't actually have any methods we could use for this case, though, as all of the relationship operation methods expect that we already have a record instance! Damn. Should we call these methods in this case? (NOTE: The names are arbitrary, I don't like these)
I'm afraid that we haven't actually discussed this case at all. 😕 What should we authorize in this case? 😧 |
|
The case in the above comment might need new methods to handle the creation of resources because this is basically how the policy = ArticlePolicy.new(current_user, Article)Note that we don't yet have an instance of |
|
I don't think a new action is necessarily required but it is an option. # ArticlePolicy.rb
def initialize(user, record)
@user = user
@record = record.is_a? Class ? record.new() || record
end
def replace_comments?(new_comments)
remove_from_comments?(record.comments)
add_to_comments?(new_comments)
endCould we just do this? |
I'm afraid not 😞 We could however modify the scope of this PR to just handle the |
|
Ah, I suppose an new instance would respond with an empty array and the Ok, I have no problem with reducing the scope to just |
Yes, but we don't want to put that burden on our users. Nobody likes the |
|
Ok I figured that was the reasoning. I will clean out |
Done, see #56 :) |
6efb32d to
a39f903
Compare
One failing spec seems to me to be an issue with the instance double... I'm not quite sure how to resolve it. It's this spec, which is pending anyway, but still. # ./spec/requests/tricky_operations_spec.rb:132
context 'limited by Comments policy scope' do
let(:comments_policy_scope) { Comment.where("id NOT IN (?)", new_comments.map(&:id)) }
before { allow_operation('replace_fields', article, related_records_with_context) }
it do
pending 'DISCUSS: Should this error out somehow?'
is_expected.to be_not_found
end
end |
a39f903 to
082d9eb
Compare
valscion
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.
Thanks for this, the code looks pretty straightforward to me! I left a few questions inline.
Could you also merge the master branch here, as I discovered that CI didn't actually run the specs (:flushed:) and I just merged #57 to make them run again.
| let(:related_records_with_context) do | ||
| Array.new(1) { | ||
| Hash[:relation_name, :comments, :relation_type, :to_many, :records, related_records] | ||
| } |
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.
Could this be written using literal array & hash syntax? Like so:
let(:related_records_with_context) do
[{
relation_name: :comments,
relation_type: :to_many,
records: related_records
}]
endI don't know about you, but I have a bit of a hard time figuring out how the hash looks like if I use the Hash[] constructor
| let(:related_records_with_context) do | ||
| Array.new(1) { | ||
| Hash[:relation_name, :comments, :relation_type, :to_many, :records, new_comments] | ||
| } |
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.
Same thing in here as in https://github.com/venuu/jsonapi-authorization/pull/51/files#r109847602
| else | ||
| resource_class = resource_class_for_relationship(assoc_name) | ||
| primary_key = resource_class._primary_key.to_sym | ||
| resource_class._model_class.find_by(primary_key => assoc_value) |
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.
Hmm is there a reason why this third branch looks so different to the Hash and Array case? I would've expected here to be similar code using resource_class.find_by_key instead of going to the _model_class and using .find_by manually.
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.
No, that was existing code from #related_models I believe that i co-opted.
All changes made.
… into relationship-processing-enhancements
|
Looking great! Do you think this would benefit from having the same specs for the case when the policy classes don't specify the context 'where replace_<type>? not defined' do
# CommentPolicy does not define #replace_article?, so #update? should determine authorization
let(:source_record) { comments(:comment_1) }
let(:related_records) { Article.new }
subject(:method_call) do
-> { authorizer.replace_to_one_relationship(source_record, related_record, :article) }
end
context 'authorized for update? on record' do
before { allow_action(source_record, 'update?') }
it { is_expected.not_to raise_error }
end
context 'unauthorized for update? on record' do
before { disallow_action(source_record, 'update?') }
it { is_expected.to raise_error(::Pundit::NotAuthorizedError) }
end
endWe can do that in another PR, though, as this is getting quite long. Thanks for your help! |
|
Absolutely, I will file a new PR to add them. And then later this week leave my thoughts on Thank you @valscion for your incredible patience and helping me to get something contributable finished. I learned a lot! |
|
Thank you for enduring all the way through this long PR. It's been a pleasure seeing you learn new things and you being able to contribute to this project |

FYI: This breaks tests. I haven't had time to fix them, but wanted to just put this code out to start discussion in the mean time.
This will check permissions on new records being added. It uses the same method syntax as @matthias-g's #40 recently merged PR. i.e.
add_comment,remove_comment,add_to_users,remove_from_users, etc. It also creates a new method syntax for polymorphic source records.For example, if comments are polymorphic, in the comment policy you could check if the user has permissions to comment on an article with
add_commentable_article?(article). This would cover payloads like:Also, the
remove_to_one_relationshipmethod was not updated to match the rest of the endpoints in @matthias-g 's branch - so it seemed to me - and I updated it here.Again, need to update the test suite to handle this way of doing things and do more tests around polymorphic scenarios, etc. But it's something to start more conversation on perhaps.
Also this is my first non trivial code contribution to an OSS project so please let me know how I can best help out with this! This is the code I was offering to share in issue #30.