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-one
relationships- Example setup for
has-one
examples PATCH /articles/article-1/relationships/author
- Changing a
has-one
relationship
- Changing a
DELETE /articles/article-1/relationships/author
- Removing a
has-one
relationship
- Removing a
PATCH /articles/article-1/
with differentauthor
relationship- Changing resource and replacing a
has-one
relationship
- Changing resource and replacing a
PATCH /articles/article-1/
with nullauthor
relationship- Changing resource and removing a
has-one
relationship
- Changing resource and removing a
POST /articles
with anauthor
relationship- Creating a resource with a
has-one
relationship
- Creating a resource with a
- Example setup for
has-many
relationships- Example setup for
has-many
examples POST /articles/article-1/relationships/comments
- Adding to a
has-many
relationship
- Adding to a
DELETE /articles/article-1/relationships/comments
- Removing from a
has-many
relationship
- Removing from a
PATCH /articles/article-1/relationships/comments
with differentcomments
- Replacing a
has-many
relationship
- Replacing a
PATCH /articles/article-1/relationships/comments
with emptycomments
- Removing a
has-many
relationship
- Removing a
PATCH /articles/article-1
with differentcomments
relationship- Changing resource and replacing a
has-many
relationship
- Changing resource and replacing a
PATCH /articles/article-1
with emptycomments
relationship- Changing resource and removing a
has-many
relationship
- Changing resource and removing a
POST /articles
with acomments
relationship- Creating a resource with a
has-many
relationship
- 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'
end
class User < ActiveRecord::Base
has_many :articles, foreign_key: :author_id
end
class UserResource < JSONAPI::Resource
include JSONAPI::Authorization::PunditScopedResource
has_many :articles
end
Changing 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
end
class Comment < ActiveRecord::Base
belongs_to :article
end
class CommentResource < JSONAPI::Resource
include JSONAPI::Authorization::PunditScopedResource
has_one :article
end
Adding 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 #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 #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?