Skip to content

Commit 2d7eb06

Browse files
authored
Don't conceal errors in resolvers for canFind, canModel and canQuery directives (#2686)
1 parent 84e43f6 commit 2d7eb06

10 files changed

+180
-6
lines changed

src/Auth/BaseCanDirective.php

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Nuwave\Lighthouse\Auth;
44

55
use Illuminate\Contracts\Auth\Access\Gate;
6+
use Nuwave\Lighthouse\Auth\Contracts\ResolvesDuringAuthorization;
67
use Nuwave\Lighthouse\Exceptions\AuthorizationException;
78
use Nuwave\Lighthouse\Execution\ResolveInfo;
89
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
@@ -86,9 +87,18 @@ public function handleField(FieldValue $fieldValue): void
8687
$gate = $this->gate->forUser($context->user());
8788
$checkArguments = $this->buildCheckArguments($args);
8889
$authorizeModel = fn (mixed $model) => $this->authorizeModel($gate, $ability, $model, $checkArguments);
90+
$hasResolved = false;
91+
$trackedResolver = function () use (&$hasResolved, $resolver) {
92+
$hasResolved = true;
93+
94+
return $resolver(...func_get_args());
95+
};
8996

9097
try {
91-
return $this->authorizeRequest($root, $args, $context, $resolveInfo, $resolver, $authorizeModel);
98+
$resolved = $this->authorizeRequest($root, $args, $context, $resolveInfo, $trackedResolver, $authorizeModel);
99+
if ($hasResolved) {
100+
return $resolved;
101+
}
92102
} catch (\Throwable $throwable) {
93103
$action = $this->directiveArgValue('action');
94104
if ($action === 'EXCEPTION_NOT_AUTHORIZED') {
@@ -101,11 +111,18 @@ public function handleField(FieldValue $fieldValue): void
101111

102112
throw $throwable;
103113
}
114+
115+
// Try to resolve the field outside the authorization try-catch block to avoid catching resolver exceptions.
116+
return $resolver($root, $args, $context, $resolveInfo);
104117
});
105118
}
106119

107120
/**
108-
* Authorizes request and resolves the field.
121+
* Authorizes request and optionally resolves the field.
122+
*
123+
* This method is called inside the authorization try-catch block.
124+
* Resolving the field here is optional, it will be done outside the try-catch block if not resolved here.
125+
* Resolving inside this method means any exceptions thrown by the resolver will be caught and handled according to the `action` argument.
109126
*
110127
* @phpstan-import-type Resolver from \Nuwave\Lighthouse\Schema\Values\FieldValue as Resolver
111128
*

src/Auth/CanFindDirective.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ protected function authorizeRequest(mixed $root, array $args, GraphQLContext $co
6565
$authorize($model);
6666
}
6767

68-
return $resolver($root, $args, $context, $resolveInfo);
68+
return null;
6969
}
7070

7171
/**

src/Auth/CanModelDirective.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,6 @@ protected function authorizeRequest(mixed $root, array $args, GraphQLContext $co
3333
{
3434
$authorize($this->getModelClass());
3535

36-
return $resolver($root, $args, $context, $resolveInfo);
36+
return null;
3737
}
3838
}

src/Auth/CanQueryDirective.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,6 @@ protected function authorizeRequest(mixed $root, array $args, GraphQLContext $co
4545
$authorize($model);
4646
}
4747

48-
return $resolver($root, $args, $context, $resolveInfo);
48+
return null;
4949
}
5050
}

src/Auth/CanRootDirective.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,6 @@ protected function authorizeRequest(mixed $root, array $args, GraphQLContext $co
2727
{
2828
$authorize($root);
2929

30-
return $resolver($root, $args, $context, $resolveInfo);
30+
return null;
3131
}
3232
}

tests/Integration/Auth/CanFindDirectiveDBTest.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Tests\Utils\Models\Post;
1111
use Tests\Utils\Models\Task;
1212
use Tests\Utils\Models\User;
13+
use Tests\Utils\Mutations\ThrowWhenInvoked;
1314
use Tests\Utils\Policies\UserPolicy;
1415

1516
final class CanFindDirectiveDBTest extends DBTestCase
@@ -210,6 +211,37 @@ public function testFailsToFindSpecificModelConcealException(): void
210211
]);
211212
}
212213

214+
public function testDoesntConcealResolverException(): void
215+
{
216+
$admin = new User();
217+
$admin->name = UserPolicy::ADMIN;
218+
$this->be($admin);
219+
220+
$user = factory(User::class)->create();
221+
assert($user instanceof User);
222+
223+
$this->schema = /** @lang GraphQL */ '
224+
type Mutation {
225+
throwWhenInvoked(id: ID!): User
226+
@canFind(ability: "view", find: "id", action: EXCEPTION_NOT_AUTHORIZED)
227+
}
228+
229+
type User {
230+
name: String!
231+
}
232+
' . self::PLACEHOLDER_QUERY;
233+
234+
$this->graphQL(/** @lang GraphQL */ '
235+
mutation ($id: ID!) {
236+
throwWhenInvoked(id: $id) {
237+
name
238+
}
239+
}
240+
', [
241+
'id' => $user->getKey(),
242+
])->assertGraphQLErrorMessage(ThrowWhenInvoked::ERROR_MESSAGE);
243+
}
244+
213245
public function testFailsToFindSpecificModelWithFindOrFailFalse(): void
214246
{
215247
$user = new User();
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Integration\Auth;
4+
5+
use Tests\DBTestCase;
6+
use Tests\Utils\Models\User;
7+
use Tests\Utils\Mutations\ThrowWhenInvoked;
8+
use Tests\Utils\Policies\UserPolicy;
9+
10+
final class CanModelDirectiveDBTest extends DBTestCase
11+
{
12+
public function testDoesntConcealResolverException(): void
13+
{
14+
$admin = new User();
15+
$admin->name = UserPolicy::ADMIN;
16+
$this->be($admin);
17+
18+
$this->schema = /** @lang GraphQL */ '
19+
type Mutation {
20+
throwWhenInvoked: Task
21+
@canModel(ability: "adminOnly", action: EXCEPTION_NOT_AUTHORIZED)
22+
}
23+
24+
type Task {
25+
name: String!
26+
}
27+
' . self::PLACEHOLDER_QUERY;
28+
29+
$this->graphQL(/** @lang GraphQL */ '
30+
mutation {
31+
throwWhenInvoked {
32+
name
33+
}
34+
}
35+
')->assertGraphQLErrorMessage(ThrowWhenInvoked::ERROR_MESSAGE);
36+
}
37+
}

tests/Integration/Auth/CanQueryDirectiveDBTest.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Tests\Utils\Models\Post;
77
use Tests\Utils\Models\Task;
88
use Tests\Utils\Models\User;
9+
use Tests\Utils\Mutations\ThrowWhenInvoked;
910
use Tests\Utils\Policies\UserPolicy;
1011

1112
final class CanQueryDirectiveDBTest extends DBTestCase
@@ -83,6 +84,37 @@ public function testFailsToFindSpecificModelWithQuery(): void
8384
]);
8485
}
8586

87+
public function testDoesntConcealResolverException(): void
88+
{
89+
$admin = new User();
90+
$admin->name = UserPolicy::ADMIN;
91+
$this->be($admin);
92+
93+
$user = factory(User::class)->create();
94+
assert($user instanceof User);
95+
96+
$this->schema = /** @lang GraphQL */ '
97+
type Mutation {
98+
throwWhenInvoked(id: ID!): User
99+
@canQuery(ability: "view", action: EXCEPTION_NOT_AUTHORIZED)
100+
}
101+
102+
type User {
103+
name: String!
104+
}
105+
' . self::PLACEHOLDER_QUERY;
106+
107+
$this->graphQL(/** @lang GraphQL */ '
108+
mutation ($id: ID!) {
109+
throwWhenInvoked(id: $id) {
110+
name
111+
}
112+
}
113+
', [
114+
'id' => $user->getKey(),
115+
])->assertGraphQLErrorMessage(ThrowWhenInvoked::ERROR_MESSAGE);
116+
}
117+
86118
public function testHandleMultipleModelsWithQuery(): void
87119
{
88120
$admin = new User();
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Integration\Auth;
4+
5+
use Tests\DBTestCase;
6+
use Tests\Utils\Models\User;
7+
use Tests\Utils\Mutations\ThrowWhenInvoked;
8+
use Tests\Utils\Policies\UserPolicy;
9+
10+
final class CanRootDirectiveDBTest extends DBTestCase
11+
{
12+
public function testDoesntConcealResolverException(): void
13+
{
14+
$admin = new User();
15+
$admin->name = UserPolicy::ADMIN;
16+
$this->be($admin);
17+
18+
$this->schema = /** @lang GraphQL */ '
19+
type Mutation {
20+
throwWhenInvoked: Task
21+
}
22+
23+
type Task {
24+
name: String! @canRoot(ability: "adminOnly", action: EXCEPTION_NOT_AUTHORIZED)
25+
}
26+
' . self::PLACEHOLDER_QUERY;
27+
28+
$this->graphQL(/** @lang GraphQL */ '
29+
mutation {
30+
throwWhenInvoked {
31+
name
32+
}
33+
}
34+
')->assertGraphQLErrorMessage(ThrowWhenInvoked::ERROR_MESSAGE);
35+
}
36+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Tests\Utils\Mutations;
4+
5+
use Nuwave\Lighthouse\Exceptions\AuthorizationException;
6+
7+
final class ThrowWhenInvoked
8+
{
9+
public const ERROR_MESSAGE = 'Custom error message from ThrowWhenInvoked mutation.';
10+
11+
/**
12+
* @param array<string, mixed> $args
13+
*
14+
* @return array<string, mixed>
15+
*/
16+
public function __invoke(mixed $root, array $args): array
17+
{
18+
throw new AuthorizationException(self::ERROR_MESSAGE);
19+
}
20+
}

0 commit comments

Comments
 (0)