diff --git a/src/Eloquent/Concerns/Authorizable.php b/src/Eloquent/Concerns/Authorizable.php index 24d7bda1..7afbe0a7 100644 --- a/src/Eloquent/Concerns/Authorizable.php +++ b/src/Eloquent/Concerns/Authorizable.php @@ -99,6 +99,32 @@ public function authorizedToAdd(Model $model): bool return $this->authorized($method, [$model]); } + /** + * Determine if the current user can add the given model to the model or throw an exception. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @throws \Illuminate\Auth\Access\AuthorizationException + */ + public function authorizeToRemove(Model $model): void + { + $method = 'remove'.str_singular(class_basename($model)); + + $this->authorize($method, [$model]); + } + + /** + * Determine if the current user can add the given model to the model. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @return bool + */ + public function authorizedToRemove(Model $model): bool + { + $method = 'remove'.str_singular(class_basename($model)); + + return $this->authorized($method, [$model]); + } + /** * Determine if the current user can attach the given model to the model. * diff --git a/src/Eloquent/Concerns/InteractsWithRelations.php b/src/Eloquent/Concerns/InteractsWithRelations.php index ee450a6b..835b9b8a 100644 --- a/src/Eloquent/Concerns/InteractsWithRelations.php +++ b/src/Eloquent/Concerns/InteractsWithRelations.php @@ -94,9 +94,17 @@ protected function fillConnections(array $connections) * @param Relations\BelongsTo $relation * @param mixed $id * @return void + * @throws \Illuminate\Auth\Access\AuthorizationException */ protected function connectBelongsToRelation(Relations\BelongsTo $relation, $id) { + $current = $relation->first(); + + if ($current) { + $currentSchema = $this->registry->getSchemaForModel($current); + $currentSchema->authorizeToRemove($this->getModel()); + } + if (! $id) { $relation->dissociate(); @@ -119,9 +127,17 @@ protected function connectBelongsToRelation(Relations\BelongsTo $relation, $id) * @param Relations\BelongsTo $relation * @param array $attributes * @return void + * @throws \Illuminate\Auth\Access\AuthorizationException */ protected function fillBelongsToRelation(Relations\BelongsTo $relation, $attributes = []) { + $current = $relation->first(); + + if ($current) { + $currentSchema = $this->registry->getSchemaForModel($current); + $currentSchema->authorizeToRemove($this->getModel()); + } + if (! $attributes) { $relation->dissociate(); @@ -145,9 +161,16 @@ protected function fillBelongsToRelation(Relations\BelongsTo $relation, $attribu * @param Relations\HasOne $relation * @param string $id * @return void + * @throws \Illuminate\Auth\Access\AuthorizationException */ protected function connectHasOneRelation(Relations\HasOne $relation, $id) { + $current = $relation->first(); + + if ($current) { + $this->authorizeToRemove($current); + } + if (! $id) { $relation->delete(); @@ -171,6 +194,12 @@ protected function connectHasOneRelation(Relations\HasOne $relation, $id) */ protected function fillHasOneRelation(Relations\HasOne $relation, $attributes) { + $current = $relation->first(); + + if ($current) { + $this->authorizeToRemove($current); + } + if (! $attributes) { $relation->delete(); @@ -195,9 +224,17 @@ protected function fillHasOneRelation(Relations\HasOne $relation, $attributes) * @param Relations\HasMany $relation * @param array $ids * @return void + * @throws \Illuminate\Auth\Access\AuthorizationException */ protected function connectHasManyRelation(Relations\HasMany $relation, array $ids) { + $models = $relation->get(); + + foreach ($models as $model) { + $currentSchema = $this->registry->getSchemaForModel($model); + $currentSchema->authorizeToRemove($this->getModel()); + } + $this->queue(function () use ($relation, $ids) { $models = $relation->getModel()->findMany($ids); @@ -215,9 +252,17 @@ protected function connectHasManyRelation(Relations\HasMany $relation, array $id * @param Relations\HasMany $relation * @param array $values * @return void + * @throws \Illuminate\Auth\Access\AuthorizationException */ protected function fillHasManyRelation(Relations\HasMany $relation, array $values) { + $models = $relation->get(); + + foreach ($models as $model) { + $currentSchema = $this->registry->getSchemaForModel($model); + $currentSchema->authorizeToRemove($this->getModel()); + } + $this->queue(function () use ($relation, $values) { $related = $relation->getRelated(); $relation->delete(); diff --git a/tests/Feature/AuthorizationTest.php b/tests/Feature/AuthorizationTest.php index 1e62b4f9..33bb7506 100644 --- a/tests/Feature/AuthorizationTest.php +++ b/tests/Feature/AuthorizationTest.php @@ -275,6 +275,27 @@ public function it_cant_save_has_one_if_not_authorized() $this->assertNull($user->phone); } + /** @test */ + public function it_cant_remove_has_one_if_not_authorized() + { + $user = factory(User::class)->create(); + factory(Phone::class)->create(['user_id' => $user->id]); + + $_SERVER['graphql.user.removePhone'] = false; + + $this->withExceptionHandling()->graphql('mutation($id: ID!, $input: UpdateUserInput!) { updateUser(id: $id, input: $input) { id } }', [ + 'id' => $user->id, + 'input' => [ + 'phone' => null, + ], + ]); + + unset($_SERVER['graphql.user.removePhone']); + + $user = User::first(); + $this->assertNotNull($user->phone); + } + /** @test */ public function it_cant_create_and_add_has_many_if_not_authorized_to_create() { @@ -348,6 +369,27 @@ public function it_cant_add_has_many_if_not_authorized() $this->assertTrue($user->articles->isEmpty()); } + /** @test */ + public function it_cant_remove_has_many_if_not_authorized() + { + $user = factory(User::class)->create(); + factory(Article::class)->create(['user_id' => $user->id]); + + $_SERVER['graphql.user.removeArticle'] = false; + + $this->withExceptionHandling()->graphql('mutation($id: ID!, $input: UpdateUserInput!) { updateUser(id: $id, input: $input) { id } }', [ + 'id' => $user->id, + 'input' => [ + 'articles' => [], + ], + ]); + + unset($_SERVER['graphql.user.removeArticle']); + + $user = User::first(); + $this->assertTrue($user->articles->isNotEmpty()); + } + /** @test */ public function it_cant_create_and_add_belongs_to_if_not_authorized_to_create() { @@ -417,6 +459,27 @@ public function it_cant_add_belongs_to_if_not_authorized() $this->assertNull($article->user); } + /** @test */ + public function it_cant_remove_belongs_to_if_not_authorized() + { + factory(User::class)->create(); + $article = factory(Article::class)->create(); + + $_SERVER['graphql.user.removeArticle'] = false; + + $this->withExceptionHandling()->graphql('mutation($id: ID!, $input: UpdateArticleInput!) { updateArticle(id: $id, input: $input) { id } }', [ + 'id' => $article->id, + 'input' => [ + 'user' => null, + ], + ]); + + unset($_SERVER['graphql.user.removeArticle']); + + $article = Article::first(); + $this->assertNotNull($article->user); + } + /** @test */ public function it_cant_attach_belongs_to_many_if_not_authorized() { diff --git a/tests/Fixtures/Policies/PhonePolicy.php b/tests/Fixtures/Policies/PhonePolicy.php index 3907e7da..3caa0e78 100644 --- a/tests/Fixtures/Policies/PhonePolicy.php +++ b/tests/Fixtures/Policies/PhonePolicy.php @@ -18,4 +18,9 @@ public function delete(): bool { return $_SERVER['graphql.phone.deletable'] ?? true; } + + public function removeUser(): bool + { + return $_SERVER['graphql.phone.removeUser'] ?? true; + } } diff --git a/tests/Fixtures/Policies/UserPolicy.php b/tests/Fixtures/Policies/UserPolicy.php index 38e525eb..2daaae9b 100644 --- a/tests/Fixtures/Policies/UserPolicy.php +++ b/tests/Fixtures/Policies/UserPolicy.php @@ -24,11 +24,21 @@ public function addPhone(): bool return $_SERVER['graphql.user.addPhone'] ?? true; } + public function removePhone(): bool + { + return $_SERVER['graphql.user.removePhone'] ?? true; + } + public function addArticle(): bool { return $_SERVER['graphql.user.addArticle'] ?? true; } + public function removeArticle(): bool + { + return $_SERVER['graphql.user.removeArticle'] ?? true; + } + public function addComment(): bool { return $_SERVER['graphql.user.addComment'] ?? true;