JSONAPI::Authorization (JA) is unique in the way it considers relationship changes to change the underlying models. Whenever an incoming request changes associated resources, JA will authorize those operations are OK.
As JA runs the authorization checks before any changes are made (even to in-memory objects), Pundit policies don't have the information needed to authorize changes to relationships. This is why JA provides special hooks to authorize relationship changes and falls back to checking #update? on all the related records.
Caveat: In case a relationship is modifiable through multiple ways it is your responsibility to ensure consistency.
For example if you have a many-to-many relationship with users and projects make sure that
ProjectPolicy#add_to_users?(users) and UserPolicy#add_to_projects?(projects) match up.
Table of contents
has-onerelationships- Example setup for
has-oneexamples PATCH /articles/article-1/relationships/author- Changing a
has-onerelationship
- Changing a
DELETE /articles/article-1/relationships/author- Removing a
has-onerelationship
- Removing a
PATCH /articles/article-1/with differentauthorrelationship- Changing resource and replacing a
has-onerelationship
- Changing resource and replacing a
PATCH /articles/article-1/with nullauthorrelationship- Changing resource and removing a
has-onerelationship
- Changing resource and removing a
POST /articleswith anauthorrelationship- Creating a resource with a
has-onerelationship
- Creating a resource with a
- Example setup for
has-manyrelationships- Example setup for
has-manyexamples POST /articles/article-1/relationships/comments- Adding to a
has-manyrelationship
- Adding to a
DELETE /articles/article-1/relationships/comments- Removing from a
has-manyrelationship
- Removing from a
PATCH /articles/article-1/relationships/commentswith differentcomments- Replacing a
has-manyrelationship
- Replacing a
PATCH /articles/article-1/relationships/commentswith emptycomments- Removing a
has-manyrelationship
- Removing a
PATCH /articles/article-1with differentcommentsrelationship- Changing resource and replacing a
has-manyrelationship
- Changing resource and replacing a
PATCH /articles/article-1with emptycommentsrelationship- Changing resource and removing a
has-manyrelationship
- Changing resource and removing a
POST /articleswith acommentsrelationship- Creating a resource with a
has-manyrelationship
- Creating a resource with a
- Example setup for
The examples for has-one relationship authorization use these models and resources:
class Article < ActiveRecord::Base
belongs_to :author, class_name: 'User'
end
class ArticleResource < JSONAPI::Resource
include JSONAPI::Authorization::PunditScopedResource
has_one :author, class_name: 'User'
endclass User < ActiveRecord::Base
has_many :articles, foreign_key: :author_id
end
class UserResource < JSONAPI::Resource
include JSONAPI::Authorization::PunditScopedResource
has_many :articles
endChanging a has-one relationship with a relationship operation
Setup:
user_1 = User.create(id: 'user-1')
article_1 = Article.create(id: 'article-1', author: user_1)
user_2 = User.create(id: 'user-2')
PATCH /articles/article-1/relationships/author{ "type": "users", "id": "user-2" }
ArticlePolicy.new(current_user, article_1).replace_author?(user_2)
ArticlePolicy.new(current_user, article_1).update?UserPolicy.new(current_user, user_2).update?
Note: Currently JA does not fallback to authorizing UserPolicy#update? on user_1 that is about to be dissociated. This will likely be changed in the future.
Removing a has-one relationship with a relationship operation
Setup:
user_1 = User.create(id: 'user-1')
article_1 = Article.create(id: 'article-1', author: user_1)
DELETE /articles/article-1/relationships/author(empty body)
ArticlePolicy.new(current_user, article_1).remove_author?
ArticlePolicy.new(current_user, article_1).update?
Note: Currently JA does not fallback to authorizing UserPolicy#update? on user_1 that is about to be dissociated. This will likely be changed in the future.
Changing resource and replacing a has-one relationship
Setup:
user_1 = User.create(id: 'user-1')
article_1 = Article.create(id: 'article-1', author: user_1)
user_2 = User.create(id: 'user-2')
PATCH /articles/article-1{ "type": "articles", "id": "article-1", "relationships": { "author": { "data": { "type": "users", "id": "user-2" } } } }
ArticlePolicy.new(current_user, article_1).update?
ArticlePolicy.new(current_user, article_1).replace_author?(user_2)
ArticlePolicy.new(current_user, article_1).update?UserPolicy.new(current_user, user_2).update?
Note: Currently JA does not fallback to authorizing UserPolicy#update? on user_1 that is about to be dissociated. This will likely be changed in the future.
Changing resource and removing a has-one relationship
Setup:
user_1 = User.create(id: 'user-1')
article_1 = Article.create(id: 'article-1', author: user_1)
PATCH /articles/article-1{ "type": "articles", "id": "article-1", "relationships": { "author": { "data": null } } }
ArticlePolicy.new(current_user, article_1).update?
ArticlePolicy.new(current_user, article_1).remove_author?
ArticlePolicy.new(current_user, article_1).update?
Note: Currently JA does not fallback to authorizing UserPolicy#update? on user_1 that is about to be dissociated. This will likely be changed in the future.
Creating a resource with a has-one relationship
Setup:
user_1 = User.create(id: 'user-1')
POST /articles{ "type": "articles", "relationships": { "author": { "data": { "type": "users", "id": "user-1" } } } }
ArticlePolicy.new(current_user, Article).create?
Note: The second parameter for the policy is the Article class, not the new record. This is because JA runs the authorization checks before any changes are made, even changes to in-memory objects.
ArticlePolicy.new(current_user, Article).create_with_author?(user_1)
UserPolicy.new(current_user, user_1).update?
The examples for has-many relationship authorization use these models and resources:
class Article < ActiveRecord::Base
has_many :comments
end
class ArticleResource < JSONAPI::Resource
include JSONAPI::Authorization::PunditScopedResource
# `acts_as_set` allows replacing all comments at once
has_many :comments, acts_as_set: true
endclass Comment < ActiveRecord::Base
belongs_to :article
end
class CommentResource < JSONAPI::Resource
include JSONAPI::Authorization::PunditScopedResource
has_one :article
endAdding to a has-many relationship
Setup:
comment_1 = Comment.create(id: 'comment-1')
article_1 = Article.create(id: 'article-1', comments: [comment_1])
comment_2 = Comment.create(id: 'comment-2')
comment_3 = Comment.create(id: 'comment-3')
POST /articles/article-1/relationships/comments{ "data": [ { "type": "comments", "id": "comment-2" }, { "type": "comments", "id": "comment-3" } ] }
ArticlePolicy.new(current_user, article_1).add_to_comments?([comment_2, comment_3])
ArticlePolicy.new(current_user, article_1).update?CommentPolicy.new(current_user, comment_2).update?CommentPolicy.new(current_user, comment_3).update?
Removing from a has-many relationship
Setup:
comment_1 = Comment.create(id: 'comment-1')
comment_2 = Comment.create(id: 'comment-2')
comment_3 = Comment.create(id: 'comment-3')
article_1 = Article.create(id: 'article-1', comments: [comment_1, comment_2, comment_3])
DELETE /articles/article-1/relationships/comments{ "data": [ { "type": "comments", "id": "comment-1" }, { "type": "comments", "id": "comment-2" } ] }
ArticlePolicy.new(current_user, article_1).remove_from_comments?([comment_1, comment_2])
ArticlePolicy.new(current_user, article_1).update?CommentPolicy.new(current_user, comment_1).update?CommentPolicy.new(current_user, comment_2).update?
Replacing a has-many relationship
Setup:
comment_1 = Comment.create(id: 'comment-1')
article_1 = Article.create(id: 'article-1', comments: [comment_1])
comment_2 = Comment.create(id: 'comment-2')
comment_3 = Comment.create(id: 'comment-3')
PATCH /articles/article-1/relationships/comments{ "data": [ { "type": "comments", "id": "comment-2" }, { "type": "comments", "id": "comment-3" } ] }
ArticlePolicy.new(current_user, article_1).replace_comments?([comment_2, comment_3])
ArticlePolicy.new(current_user, article_1).update?CommentPolicy.new(current_user, comment_2).update?CommentPolicy.new(current_user, comment_3).update?
Note: Currently JA does not fallback to authorizing CommentPolicy#update? on comment_1 that is about to be dissociated. This will likely be changed in the future.
Removing a has-many relationship
Setup:
comment_1 = Comment.create(id: 'comment-1')
article_1 = Article.create(id: 'article-1', comments: [comment_1])
PATCH /articles/article-1/relationships/comments{ "data": [] }
ArticlePolicy.new(current_user, article_1).replace_comments?([])
TODO: We should probably call remove_comments? (with no arguments) instead. See venuu#73 for more details and implementation progress.
ArticlePolicy.new(current_user, article_1).update?
Note: Currently JA does not fallback to authorizing CommentPolicy#update? on comment_1 that is about to be dissociated. This will likely be changed in the future.
Changing resource and replacing a has-many relationship
Setup:
comment_1 = Comment.create(id: 'comment-1')
article_1 = Article.create(id: 'article-1', comments: [comment_1])
comment_2 = Comment.create(id: 'comment-2')
comment_3 = Comment.create(id: 'comment-3')
PATCH /articles/article-1{ "type": "articles", "id": "article-1", "relationships": { "comments": { "data": [ { "type": "comments", "id": "comment-2" }, { "type": "comments", "id": "comment-3" } ] } } }
ArticlePolicy.new(current_user, article_1).update?
ArticlePolicy.new(current_user, article_1).replace_comments?([comment_2, comment_3])
ArticlePolicy.new(current_user, article_1).update?CommentPolicy.new(current_user, comment_2).update?CommentPolicy.new(current_user, comment_3).update?
Note: Currently JA does not fallback to authorizing CommentPolicy#update? on comment_1 that is about to be dissociated. This will likely be changed in the future.
Changing resource and removing a has-many relationship
Setup:
comment_1 = Comment.create(id: 'comment-1')
article_1 = Article.create(id: 'article-1', comments: [comment_1])
PATCH /articles/article-1{ "type": "articles", "id": "article-1", "relationships": { "comments": { "data": [] } } }
ArticlePolicy.new(current_user, article_1).update?
ArticlePolicy.new(current_user, article_1).replace_comments?([])
TODO: We should probably call remove_comments? (with no arguments) instead. See venuu#73 for more details and implementation progress.
ArticlePolicy.new(current_user, article_1).update?
Note: Currently JA does not fallback to authorizing CommentPolicy#update? on comment_1 that is about to be dissociated. This will likely be changed in the future.
Creating a resource with a has-many relationship
Setup:
comment_1 = Comment.create(id: 'comment-1')
comment_2 = Comment.create(id: 'comment-2')
POST /articles{ "type": "articles", "relationships": { "comments": { "data": [ { "type": "comments", "id": "comment-1" }, { "type": "comments", "id": "comment-2" } ] } } }
ArticlePolicy.new(current_user, Article).create?
Note: The second parameter for the policy is the Article class, not the new record. This is because JA runs the authorization checks before any changes are made, even changes to in-memory objects.
ArticlePolicy.new(current_user, Article).create_with_comments?([comment_1, comment_2])
CommentPolicy.new(current_user, comment_1).update?CommentPolicy.new(current_user, comment_2).update?