From 39a34b40e1f14cc20aa76684b14802d377d7de4b Mon Sep 17 00:00:00 2001 From: Charly Landet Date: Fri, 24 Apr 2026 14:03:40 +0200 Subject: [PATCH] Fix Security 'this' variable on ExtendType fields Fixes #653 Related to #792 When using #[Security] on a field declared in an #[ExtendType], the 'this' variable was incorrectly set to the resolver instance instead of the source object. This breaks authorization logic relying on the domain object. Fix: preserve $source when injectSource=true. Adds test coverage to ensure correct behavior. --- src/Middlewares/SecurityFieldMiddleware.php | 7 ++++++- src/Middlewares/SecurityInputFieldMiddleware.php | 7 ++++++- .../Integration/Types/ExtendedContactType.php | 11 +++++++++++ tests/Integration/EndToEndTest.php | 3 +++ 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/Middlewares/SecurityFieldMiddleware.php b/src/Middlewares/SecurityFieldMiddleware.php index 920071c02d..d6cdea541c 100644 --- a/src/Middlewares/SecurityFieldMiddleware.php +++ b/src/Middlewares/SecurityFieldMiddleware.php @@ -83,7 +83,12 @@ public function process( $injectSource = $queryFieldDescriptor->isInjectSource(); $queryFieldDescriptor = $queryFieldDescriptor->withResolver(function (object|null $source, ...$args) use ($originalResolver, $securityAnnotations, $resolver, $failWith, $parameters, $queryFieldDescriptor, $injectSource) { - $variables = $this->getVariables($args, $parameters, $originalResolver->executionSource($source), $injectSource); + $variables = $this->getVariables( + $args, + $parameters, + $injectSource ? $source : $originalResolver->executionSource($source), + $injectSource, + ); foreach ($securityAnnotations as $annotation) { try { diff --git a/src/Middlewares/SecurityInputFieldMiddleware.php b/src/Middlewares/SecurityInputFieldMiddleware.php index 8c9a5632ce..ab1da69d5a 100644 --- a/src/Middlewares/SecurityInputFieldMiddleware.php +++ b/src/Middlewares/SecurityInputFieldMiddleware.php @@ -51,7 +51,12 @@ public function process(InputFieldDescriptor $inputFieldDescriptor, InputFieldHa $injectSource = $inputFieldDescriptor->isInjectSource(); $inputFieldDescriptor = $inputFieldDescriptor->withResolver(function (object|null $source, ...$args) use ($originalResolver, $securityAnnotations, $resolver, $parameters, $inputFieldDescriptor, $injectSource) { - $variables = $this->getVariables($args, $parameters, $originalResolver->executionSource($source), $injectSource); + $variables = $this->getVariables( + $args, + $parameters, + $injectSource ? $source : $originalResolver->executionSource($source), + $injectSource, + ); foreach ($securityAnnotations as $annotation) { try { diff --git a/tests/Fixtures/Integration/Types/ExtendedContactType.php b/tests/Fixtures/Integration/Types/ExtendedContactType.php index 10d69db74f..46ae8281dc 100644 --- a/tests/Fixtures/Integration/Types/ExtendedContactType.php +++ b/tests/Fixtures/Integration/Types/ExtendedContactType.php @@ -48,4 +48,15 @@ public function extendedSecretName(Contact $contact): string|null { return $contact->getName(); } + + /** + * Regression: in #[Security] on an #[ExtendType] field, `this` must be + * the source object, not the resolver instance. + */ + #[Field] + #[Security("user && this.getName() == 'Joe'", failWith: null)] + public function sourceAwareSecretName(Contact $contact): string|null + { + return $contact->getName(); + } } diff --git a/tests/Integration/EndToEndTest.php b/tests/Integration/EndToEndTest.php index 040f2efdca..31eba39313 100644 --- a/tests/Integration/EndToEndTest.php +++ b/tests/Integration/EndToEndTest.php @@ -1360,6 +1360,7 @@ public function testEndToEndSecurityOnExtendTypeField(): void contacts { name extendedSecretName + sourceAwareSecretName } } '; @@ -1368,6 +1369,7 @@ public function testEndToEndSecurityOnExtendTypeField(): void $data = $this->getSuccessResult($result); $this->assertSame('Joe', $data['contacts'][0]['name']); $this->assertNull($data['contacts'][0]['extendedSecretName']); + $this->assertNull($data['contacts'][0]['sourceAwareSecretName']); // Logged-in user with bar=42 → the Security expression passes and the field returns the name. $container = $this->createContainer([ @@ -1394,6 +1396,7 @@ public function getUser(): object|null $result = GraphQL::executeQuery($schema, $queryString); $data = $this->getSuccessResult($result); $this->assertSame('Joe', $data['contacts'][0]['extendedSecretName']); + $this->assertSame('Joe', $data['contacts'][0]['sourceAwareSecretName']); } public function testEndToEndUnions(): void