Skip to content

Latest commit

 

History

History
601 lines (419 loc) · 15.9 KB

relationship-authorization.md

File metadata and controls

601 lines (419 loc) · 15.9 KB

Authorization of operations touching relationships

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

back to top ↑

has-one relationships

Example setup for has-one examples

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

back to top ↑

PATCH /articles/article-1/relationships/author

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"
}

Custom relationship authorization method

  • ArticlePolicy.new(current_user, article_1).replace_author?(user_2)

Fallback

  • 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.

back to top ↑

DELETE /articles/article-1/relationships/author

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)

Custom relationship authorization method

  • ArticlePolicy.new(current_user, article_1).remove_author?

Fallback

  • 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.

back to top ↑

PATCH /articles/article-1/ with different author relationship

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"
      }
    }
  }
}

Always calls

  • ArticlePolicy.new(current_user, article_1).update?

Custom relationship authorization method

  • ArticlePolicy.new(current_user, article_1).replace_author?(user_2)

Fallback

  • 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.

back to top ↑

PATCH /articles/article-1/ with null author relationship

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
    }
  }
}

Always calls

  • ArticlePolicy.new(current_user, article_1).update?

Custom relationship authorization method

  • ArticlePolicy.new(current_user, article_1).remove_author?

Fallback

  • 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.

back to top ↑

POST /articles with an author relationship

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"
      }
    }
  }
}

Always calls

  • 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.

Custom relationship authorization method

  • ArticlePolicy.new(current_user, Article).create_with_author?(user_1)

Fallback

  • UserPolicy.new(current_user, user_1).update?

back to top ↑

has-many relationships

Example setup for has-many examples

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

back to top ↑

POST /articles/article-1/relationships/comments

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" }
  ]
}

Custom relationship authorization method

  • ArticlePolicy.new(current_user, article_1).add_to_comments?([comment_2, comment_3])

Fallback

  • ArticlePolicy.new(current_user, article_1).update?
  • CommentPolicy.new(current_user, comment_2).update?
  • CommentPolicy.new(current_user, comment_3).update?

back to top ↑

DELETE /articles/article-1/relationships/comments

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" }
  ]
}

Custom relationship authorization method

  • ArticlePolicy.new(current_user, article_1).remove_from_comments?([comment_1, comment_2])

Fallback

  • ArticlePolicy.new(current_user, article_1).update?
  • CommentPolicy.new(current_user, comment_1).update?
  • CommentPolicy.new(current_user, comment_2).update?

back to top ↑

PATCH /articles/article-1/relationships/comments with different comments

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" }
  ]
}

Custom relationship authorization method

  • ArticlePolicy.new(current_user, article_1).replace_comments?([comment_2, comment_3])

Fallback

  • 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.

back to top ↑

PATCH /articles/article-1/relationships/comments with empty comments

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": []
}

Custom relationship authorization method

  • 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.

Fallback

  • 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.

back to top ↑

PATCH /articles/article-1 with different comments relationship

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" }
      ]
    }
  }
}

Always calls

  • ArticlePolicy.new(current_user, article_1).update?

Custom relationship authorization method

  • ArticlePolicy.new(current_user, article_1).replace_comments?([comment_2, comment_3])

Fallback

  • 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.

back to top ↑

PATCH /articles/article-1 with empty comments relationship

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": []
    }
  }
}

Always calls

  • ArticlePolicy.new(current_user, article_1).update?

Custom relationship authorization method

  • 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.

Fallback

  • 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.

back to top ↑

POST /articles with a comments relationship

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" }
      ]
    }
  }
}

Always calls

  • 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.

Custom relationship authorization method

  • ArticlePolicy.new(current_user, Article).create_with_comments?([comment_1, comment_2])

Fallback

  • CommentPolicy.new(current_user, comment_1).update?
  • CommentPolicy.new(current_user, comment_2).update?

back to top ↑