diff --git a/docs/master/api-reference/directives.md b/docs/master/api-reference/directives.md index 09d532c5b2..ee1fd019fb 100644 --- a/docs/master/api-reference/directives.md +++ b/docs/master/api-reference/directives.md @@ -621,7 +621,7 @@ directive @can( Check the policy against the model instances returned by the field resolver. Only use this if the field does not mutate data, it is run before checking. - Mutually exclusive with `query` and `find`. + Mutually exclusive with `query`, `find`, and `root`. """ resolved: Boolean! = false @@ -648,7 +648,7 @@ directive @can( Query for specific model instances to check the policy against, using arguments with directives that add constraints to the query builder, such as `@eq`. - Mutually exclusive with `resolved` and `find`. + Mutually exclusive with `resolved`, `find`, and `root`. """ query: Boolean! = false @@ -663,7 +663,7 @@ directive @can( You may pass the string in dot notation to use nested inputs. - Mutually exclusive with `resolved` and `query`. + Mutually exclusive with `resolved`, `query`, and `root`. """ find: String @@ -671,6 +671,13 @@ directive @can( Should the query fail when the models of `find` were not found? """ findOrFail: Boolean! = true + + """ + If your policy should check against the root value. + + Mutually exclusive with `resolved`, `query`, and `find`. + """ + root: Boolean! = false ) repeatable on FIELD_DEFINITION """ diff --git a/src/Auth/CanDirective.php b/src/Auth/CanDirective.php index 4e23aec373..a80a3dfce0 100644 --- a/src/Auth/CanDirective.php +++ b/src/Auth/CanDirective.php @@ -53,7 +53,7 @@ public static function definition(): string Check the policy against the model instances returned by the field resolver. Only use this if the field does not mutate data, it is run before checking. - Mutually exclusive with `query` and `find`. + Mutually exclusive with `query`, `find`, and `root`. """ resolved: Boolean! = false @@ -80,7 +80,7 @@ public static function definition(): string Query for specific model instances to check the policy against, using arguments with directives that add constraints to the query builder, such as `@eq`. - Mutually exclusive with `resolved` and `find`. + Mutually exclusive with `resolved`, `find`, and `root`. """ query: Boolean! = false @@ -95,7 +95,7 @@ public static function definition(): string You may pass the string in dot notation to use nested inputs. - Mutually exclusive with `resolved` and `query`. + Mutually exclusive with `resolved`, `query`, and `root`. """ find: String @@ -103,6 +103,13 @@ public static function definition(): string Should the query fail when the models of `find` were not found? """ findOrFail: Boolean! = true + + """ + If your policy should check against the root value. + + Mutually exclusive with `resolved`, `query`, and `find`. + """ + root: Boolean! = false ) repeatable on FIELD_DEFINITION """ @@ -167,6 +174,10 @@ protected function modelsToCheck(mixed $root, array $args, GraphQLContext $conte ->get(); } + if ($this->directiveArgValue('root')) { + return [$root]; + } + if ($find = $this->directiveArgValue('find')) { $findValue = Arr::get($args, $find) ?? throw self::missingKeyToFindModel($find); @@ -276,7 +287,7 @@ protected function buildCheckArguments(array $args): array public function manipulateFieldDefinition(DocumentAST &$documentAST, FieldDefinitionNode &$fieldDefinition, ObjectTypeDefinitionNode|InterfaceTypeDefinitionNode &$parentType): void { - $this->validateMutuallyExclusiveArguments(['resolved', 'query', 'find']); + $this->validateMutuallyExclusiveArguments(['resolved', 'query', 'find', 'root']); if ($this->directiveHasArgument('resolved') && $parentType->name->value === RootType::MUTATION) { throw self::resolvedIsUnsafeInMutations($fieldDefinition->name->value); diff --git a/tests/Unit/Auth/CanDirectiveTest.php b/tests/Unit/Auth/CanDirectiveTest.php index f78baa5cad..46b9c4b9bf 100644 --- a/tests/Unit/Auth/CanDirectiveTest.php +++ b/tests/Unit/Auth/CanDirectiveTest.php @@ -281,6 +281,42 @@ public function testInjectArgsPassesClientArgumentToPolicy(): void ]); } + public function testChecksAgainstRootModel(): void + { + $this->be(new User()); + + $this->mockResolver(fn (): User => $this->resolveUser()); + + $this->schema = /** @lang GraphQL */ ' + type Query { + user(foo: String): User! @mock + } + + type User { + name: String @can(ability: "view", root: true) + email: String @can(ability: "superAdminOnly", root: true) + } + '; + + $this->graphQL(/** @lang GraphQL */ ' + { + user(foo: "bar") { + name + email + } + } + ')->assertJson([ + 'data' => [ + 'user' => [ + 'name' => 'foo', + 'email' => null, + ], + ], + ])->assertJsonFragment([ + 'message' => 'Only super admins allowed', + ]); + } + public function testInjectedArgsAndStaticArgs(): void { $this->be(new User()); @@ -322,6 +358,7 @@ public static function resolveUser(): User { $user = new User(); $user->name = 'foo'; + $user->email = 'test@example.com'; return $user; }