# 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**
* `has-one` relationships
- [Example setup for `has-one` examples](#example-setup-has-one)
- [`PATCH /articles/article-1/relationships/author`](#change-has-one-relationship-op)
* Changing a `has-one` relationship
- [`DELETE /articles/article-1/relationships/author`](#remove-has-one-relationship-op)
* Removing a `has-one` relationship
- [`PATCH /articles/article-1/` with different `author` relationship](#change-and-replace-has-one-resource-op)
* Changing resource and replacing a `has-one` relationship
- [`PATCH /articles/article-1/` with null `author` relationship](#change-and-remove-has-one-resource-op)
* Changing resource and removing a `has-one` relationship
- [`POST /articles` with an `author` relationship](#create-has-one-resource-op)
* Creating a resource with a `has-one` relationship
* `has-many` relationships
- [Example setup for `has-many` examples](#example-setup-has-many)
- [`POST /articles/article-1/relationships/comments`](#add-to-has-many-relationship-op)
* Adding to a `has-many` relationship
- [`DELETE /articles/article-1/relationships/comments`](#remove-from-has-many-relationship-op)
* Removing from a `has-many` relationship
- [`PATCH /articles/article-1/relationships/comments` with different `comments`](#replace-has-many-relationship-op)
* Replacing a `has-many` relationship
- [`PATCH /articles/article-1/relationships/comments` with empty `comments`](#remove-has-many-relationship-op)
* Removing a `has-many` relationship
- [`PATCH /articles/article-1` with different `comments` relationship](#change-and-replace-has-many-resource-op)
* Changing resource and replacing a `has-many` relationship
- [`PATCH /articles/article-1` with empty `comments` relationship](#change-and-remove-has-many-resource-op)
* Changing resource and removing a `has-many` relationship
- [`POST /articles` with a `comments` relationship](#create-has-many-resource-op)
* Creating a resource with a `has-many` relationship
[back to top ↑](#doc-top)
## `has-one` relationships
### Example setup for `has-one` examples
The examples for `has-one` relationship authorization use these models and resources:
```rb
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
```
```rb
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 ↑](#doc-top)
### `PATCH /articles/article-1/relationships/author`
_Changing a `has-one` relationship with a relationship operation_
Setup:
```rb
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`
>
> ```json
> {
> "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 ↑](#doc-top)
### `DELETE /articles/article-1/relationships/author`
_Removing a `has-one` relationship with a relationship operation_
Setup:
```rb
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 ↑](#doc-top)
### `PATCH /articles/article-1/` with different `author` relationship
_Changing resource and replacing a `has-one` relationship_
Setup:
```rb
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`
>
> ```json
> {
> "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 ↑](#doc-top)
### `PATCH /articles/article-1/` with null `author` relationship
_Changing resource and removing a `has-one` relationship_
Setup:
```rb
user_1 = User.create(id: 'user-1')
article_1 = Article.create(id: 'article-1', author: user_1)
```
> `PATCH /articles/article-1`
>
> ```json
> {
> "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 ↑](#doc-top)
### `POST /articles` with an `author` relationship
_Creating a resource with a `has-one` relationship_
Setup:
```rb
user_1 = User.create(id: 'user-1')
```
> `POST /articles`
>
> ```json
> {
> "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 ↑](#doc-top)
## `has-many` relationships
### Example setup for `has-many` examples
The examples for `has-many` relationship authorization use these models and resources:
```rb
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
```
```rb
class Comment < ActiveRecord::Base
belongs_to :article
end
class CommentResource < JSONAPI::Resource
include JSONAPI::Authorization::PunditScopedResource
has_one :article
end
```
[back to top ↑](#doc-top)
### `POST /articles/article-1/relationships/comments`
_Adding to a `has-many` relationship_
Setup:
```rb
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`
>
> ```json
> {
> "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 ↑](#doc-top)
### `DELETE /articles/article-1/relationships/comments`
_Removing from a `has-many` relationship_
Setup:
```rb
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`
>
> ```json
> {
> "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 ↑](#doc-top)
### `PATCH /articles/article-1/relationships/comments` with different `comments`
_Replacing a `has-many` relationship_
Setup:
```rb
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`
>
> ```json
> {
> "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 ↑](#doc-top)
### `PATCH /articles/article-1/relationships/comments` with empty `comments`
_Removing a `has-many` relationship_
Setup:
```rb
comment_1 = Comment.create(id: 'comment-1')
article_1 = Article.create(id: 'article-1', comments: [comment_1])
```
> `PATCH /articles/article-1/relationships/comments`
>
> ```json
> {
> "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 https://github.com/venuu/jsonapi-authorization/issues/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 ↑](#doc-top)
### `PATCH /articles/article-1` with different `comments` relationship
_Changing resource and replacing a `has-many` relationship_
Setup:
```rb
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`
>
> ```json
> {
> "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 ↑](#doc-top)
### `PATCH /articles/article-1` with empty `comments` relationship
_Changing resource and removing a `has-many` relationship_
Setup:
```rb
comment_1 = Comment.create(id: 'comment-1')
article_1 = Article.create(id: 'article-1', comments: [comment_1])
```
> `PATCH /articles/article-1`
>
> ```json
> {
> "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 https://github.com/venuu/jsonapi-authorization/issues/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 ↑](#doc-top)
### `POST /articles` with a `comments` relationship
_Creating a resource with a `has-many` relationship_
Setup:
```rb
comment_1 = Comment.create(id: 'comment-1')
comment_2 = Comment.create(id: 'comment-2')
```
> `POST /articles`
>
> ```json
> {
> "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 ↑](#doc-top)