diff --git a/CHANGELOG.md b/CHANGELOG.md index ac01cfa8ca..9394aebfdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ You can find and compare releases at the [GitHub release page](https://github.co - Improve subscription context serializer https://github.com/nuwave/lighthouse/pull/1283 - Allow replacing the `SubscriptionRegistry` implementation using the container https://github.com/nuwave/lighthouse/pull/1286 - Report errors that are not client-safe through Laravel's `ExceptionHandler` https://github.com/nuwave/lighthouse/pull/1303 +- Log in subscribers when broadcasting a subscription update, so that calls to `auth()->user()` return + the authenticated user instead of `null` https://github.com/nuwave/lighthouse/pull/1306 ## 4.11.0 diff --git a/composer.json b/composer.json index 6b0591dff2..a8e4096592 100644 --- a/composer.json +++ b/composer.json @@ -26,6 +26,7 @@ "php": ">= 7.1", "ext-json": "*", "nesbot/carbon": "^1.26.0 || ^2.0", + "illuminate/auth": "5.5.* || 5.6.* || 5.7.* || 5.8.* || ^6.0 || ^7.0", "illuminate/contracts": "5.5.* || 5.6.* || 5.7.* || 5.8.* || ^6.0 || ^7.0", "illuminate/http": "5.5.* || 5.6.* || 5.7.* || 5.8.* || ^6.0 || ^7.0", "illuminate/pagination": "5.5.* || 5.6.* || 5.7.* || 5.8.* || ^6.0 || ^7.0", diff --git a/src/Subscriptions/Contracts/SubscriptionIterator.php b/src/Subscriptions/Contracts/SubscriptionIterator.php index a1bc354798..4af7f19e4c 100644 --- a/src/Subscriptions/Contracts/SubscriptionIterator.php +++ b/src/Subscriptions/Contracts/SubscriptionIterator.php @@ -8,9 +8,18 @@ interface SubscriptionIterator { /** - * Process collection of items. + * Process subscribers through the given callbacks. * + * @param \Illuminate\Support\Collection<\Nuwave\Lighthouse\Subscriptions\Subscriber> $subscribers + * The subscribers that receive the current subscription. + * + * @param \Closure $handleSubscriber + * Receives each subscriber in the passed in collection. + * function(\Nuwave\Lighthouse\Subscriptions\Subscriber $subscriber) + * + * @param \Closure|null $handleError + * Is called when $handleSubscriber throws. * @return void */ - public function process(Collection $items, Closure $cb, Closure $error = null); + public function process(Collection $subscribers, Closure $handleSubscriber, Closure $handleError = null); } diff --git a/src/Subscriptions/Iterators/AuthenticatingSyncIterator.php b/src/Subscriptions/Iterators/AuthenticatingSyncIterator.php new file mode 100644 index 0000000000..e7f58d0b56 --- /dev/null +++ b/src/Subscriptions/Iterators/AuthenticatingSyncIterator.php @@ -0,0 +1,69 @@ +authFactory = $authFactory; + $this->configRepository = $configRepository; + } + + public function process(Collection $subscribers, Closure $handleSubscriber, Closure $handleError = null): void + { + // Store the previous default guard name so we can restore it after we're done + $previousGuardName = $this->configRepository->get('auth.defaults.guard'); + + // Set our subscription guard as the default guard for the application + $this->authFactory->shouldUse(SubscriptionGuard::GUARD_NAME); + + /** @var \Nuwave\Lighthouse\Subscriptions\SubscriptionGuard $guard */ + $guard = $this->authFactory->guard(SubscriptionGuard::GUARD_NAME); + + $subscribers->each(static function (Subscriber $item) use ($handleSubscriber, $handleError, $guard): void { + // If there is an authenticated user set in the context, set that user as the authenticated user + if ($item->context->user()) { + $guard->setUser($item->context->user()); + } + + try { + $handleSubscriber($item); + } catch (Exception $e) { + if (! $handleError) { + throw $e; + } + + $handleError($e); + } finally { + // Unset the authenticated user after each iteration to restore the guard to a unauthenticated state + $guard->reset(); + } + }); + + // Restore the previous default guard name + $this->authFactory->shouldUse($previousGuardName); + } +} diff --git a/src/Subscriptions/Iterators/SyncIterator.php b/src/Subscriptions/Iterators/SyncIterator.php index 408b20f840..c5dfdebbcf 100644 --- a/src/Subscriptions/Iterators/SyncIterator.php +++ b/src/Subscriptions/Iterators/SyncIterator.php @@ -9,20 +9,17 @@ class SyncIterator implements SubscriptionIterator { - /** - * Process collection of items. - */ - public function process(Collection $items, Closure $cb, Closure $error = null): void + public function process(Collection $subscribers, Closure $handleSubscriber, Closure $handleError = null): void { - $items->each(function ($item) use ($cb, $error): void { + $subscribers->each(static function ($item) use ($handleSubscriber, $handleError): void { try { - $cb($item); + $handleSubscriber($item); } catch (Exception $e) { - if (! $error) { + if (! $handleError) { throw $e; } - $error($e); + $handleError($e); } }); } diff --git a/src/Subscriptions/SubscriptionGuard.php b/src/Subscriptions/SubscriptionGuard.php new file mode 100644 index 0000000000..d9b837cc76 --- /dev/null +++ b/src/Subscriptions/SubscriptionGuard.php @@ -0,0 +1,29 @@ +user; + } + + public function reset() + { + $this->user = null; + } + + public function validate(array $credentials = []) + { + throw new RuntimeException('The Lighthouse subscription guard cannot be used for credential based authentication.'); + } +} diff --git a/src/Subscriptions/SubscriptionServiceProvider.php b/src/Subscriptions/SubscriptionServiceProvider.php index 1394c5949d..c7baeded69 100644 --- a/src/Subscriptions/SubscriptionServiceProvider.php +++ b/src/Subscriptions/SubscriptionServiceProvider.php @@ -2,6 +2,7 @@ namespace Nuwave\Lighthouse\Subscriptions; +use Illuminate\Auth\AuthManager; use Illuminate\Contracts\Config\Repository as ConfigRepository; use Illuminate\Contracts\Events\Dispatcher as EventsDispatcher; use Illuminate\Support\ServiceProvider; @@ -16,6 +17,7 @@ use Nuwave\Lighthouse\Subscriptions\Contracts\SubscriptionIterator; use Nuwave\Lighthouse\Subscriptions\Events\BroadcastSubscriptionEvent; use Nuwave\Lighthouse\Subscriptions\Events\BroadcastSubscriptionListener; +use Nuwave\Lighthouse\Subscriptions\Iterators\AuthenticatingSyncIterator; use Nuwave\Lighthouse\Subscriptions\Iterators\SyncIterator; use Nuwave\Lighthouse\Support\Contracts\ProvidesSubscriptionResolver; @@ -50,6 +52,21 @@ public function boot(EventsDispatcher $eventsDispatcher, ConfigRepository $confi $this->app->make('router') ); } + + // If authentication is used, we can log in subscribers when broadcasting an update + if ($this->app->bound(AuthManager::class)) { + config([ + 'auth.guards.'.SubscriptionGuard::GUARD_NAME => [ + 'driver' => SubscriptionGuard::GUARD_NAME, + ], + ]); + + $this->app->bind(SubscriptionIterator::class, AuthenticatingSyncIterator::class); + + $this->app->make(AuthManager::class)->extend(SubscriptionGuard::GUARD_NAME, static function () { + return new SubscriptionGuard; + }); + } } /** diff --git a/tests/Unit/Subscriptions/BroadcastManagerTest.php b/tests/Unit/Subscriptions/BroadcastManagerTest.php index 64f50cb8cc..b810049b5b 100644 --- a/tests/Unit/Subscriptions/BroadcastManagerTest.php +++ b/tests/Unit/Subscriptions/BroadcastManagerTest.php @@ -9,24 +9,14 @@ use Nuwave\Lighthouse\Subscriptions\BroadcastManager; use Nuwave\Lighthouse\Subscriptions\Contracts\Broadcaster; use Nuwave\Lighthouse\Subscriptions\Subscriber; -use Nuwave\Lighthouse\Subscriptions\SubscriptionServiceProvider; -use Tests\TestCase; -class BroadcastManagerTest extends TestCase +class BroadcastManagerTest extends SubscriptionTestCase { /** * @var \Nuwave\Lighthouse\Subscriptions\BroadcastManager */ protected $broadcastManager; - protected function getPackageProviders($app) - { - return array_merge( - parent::getPackageProviders($app), - [SubscriptionServiceProvider::class] - ); - } - /** * Set up test environment. */ diff --git a/tests/Unit/Subscriptions/Iterators/AuthenticatingSyncIteratorTest.php b/tests/Unit/Subscriptions/Iterators/AuthenticatingSyncIteratorTest.php new file mode 100644 index 0000000000..820007a9af --- /dev/null +++ b/tests/Unit/Subscriptions/Iterators/AuthenticatingSyncIteratorTest.php @@ -0,0 +1,117 @@ +app->make(AuthenticatingSyncIterator::class); + + $this->assertIteratesOverItemsWithCallback($iterator); + $this->assertPassesExceptionToHandler($iterator); + } + + public function testSetsAndResetsGuardContextAfterEachIteration(): void + { + $subscriberCount = 3; + + // Give each subscriber a user stub with an ID based on the index of the subscriber in the collection + $subscribers = $this + ->subscribers($subscriberCount) + ->map(static function (Subscriber $subscriber, int $index): Subscriber { + $subscriber->context->user = new AuthenticatingSyncIteratorAuthenticatableStub($index + 1); + + return $subscriber; + }); + + $guard = Mockery::mock(SubscriptionGuard::class, static function (MockInterface $mock) use ($subscribers) { + $subscribers->each(static function (Subscriber $subscriber) use ($mock) { + $user = $subscriber->context->user(); + + $mock + ->shouldReceive('setUser') + ->with($user) + ->once(); + + $mock + ->shouldReceive('user') + ->andReturn($user) + ->once(); + + $mock + ->shouldReceive('reset') + ->once(); + }); + }); + + $authManager = $this->app->make(AuthManager::class); + + $authManager->extend(SubscriptionGuard::GUARD_NAME, static function () use ($guard) { + return $guard; + }); + + $processedItems = []; + $authenticatedUsers = []; + $guardBeforeIteration = $authManager->guard(); + + $iterator = $this->app->make(AuthenticatingSyncIterator::class); + $iterator->process( + $subscribers, + static function (Subscriber $subscriber) use (&$processedItems, &$authenticatedUsers, $authManager): void { + $processedItems[] = $subscriber; + $authenticatedUsers[] = $authManager->user(); + } + ); + + $this->assertCount($subscriberCount, $processedItems); + $this->assertSame($subscribers->pluck('context.user')->all(), $authenticatedUsers); + $this->assertSame($guardBeforeIteration, $authManager->guard()); + } +} + +class AuthenticatingSyncIteratorAuthenticatableStub implements Authenticatable +{ + /** + * @var int + */ + private $id; + + public function __construct(int $id) + { + $this->id = $id; + } + + public function getAuthIdentifierName() + { + } + + public function getAuthIdentifier() + { + return $this->id; + } + + public function getAuthPassword() + { + } + + public function getRememberToken() + { + } + + public function setRememberToken($value) + { + } + + public function getRememberTokenName() + { + } +} diff --git a/tests/Unit/Subscriptions/Iterators/IteratorTest.php b/tests/Unit/Subscriptions/Iterators/IteratorTest.php new file mode 100644 index 0000000000..982f701038 --- /dev/null +++ b/tests/Unit/Subscriptions/Iterators/IteratorTest.php @@ -0,0 +1,71 @@ +process( + $this->subscribers($count), + static function (Subscriber $subscriber) use (&$subscribers): void { + $subscribers[] = $subscriber; + } + ); + + $this->assertCount($count, $subscribers); + } + + public function assertPassesExceptionToHandler(SubscriptionIterator $iterator): void + { + $exceptionToThrow = new Exception('test_exception'); + + /** @var \Exception|null $exceptionThrown */ + $exceptionThrown = null; + + $iterator->process( + $this->subscribers(1), + static function () use ($exceptionToThrow): void { + throw $exceptionToThrow; + }, + static function (Exception $e) use (&$exceptionThrown): void { + $exceptionThrown = $e; + } + ); + + $this->assertSame($exceptionToThrow, $exceptionThrown); + } + + /** + * @return \Illuminate\Support\Collection<\Nuwave\Lighthouse\Subscriptions\Subscriber> + */ + protected function subscribers(int $count): Collection + { + return Collection::times($count, [$this, 'generateSubscriber']); + } + + public function generateSubscriber(): Subscriber + { + $resolveInfo = $this->createMock(ResolveInfo::class); + + $resolveInfo->operation = (object) [ + 'name' => (object) [ + 'value' => 'lighthouse', + ], + ]; + + return new Subscriber([], new Context(new Request), $resolveInfo); + } +} diff --git a/tests/Unit/Subscriptions/Iterators/SyncIteratorTest.php b/tests/Unit/Subscriptions/Iterators/SyncIteratorTest.php index 60d6361d91..7f1c47b2f8 100644 --- a/tests/Unit/Subscriptions/Iterators/SyncIteratorTest.php +++ b/tests/Unit/Subscriptions/Iterators/SyncIteratorTest.php @@ -2,67 +2,15 @@ namespace Tests\Unit\Subscriptions\Iterators; -use Exception; -use Illuminate\Support\Collection; use Nuwave\Lighthouse\Subscriptions\Iterators\SyncIterator; -use Tests\TestCase; -class SyncIteratorTest extends TestCase +class SyncIteratorTest extends IteratorTest { - /** - * @var string - */ - public const EXCEPTION_MESSAGE = 'test_exception'; - - /** - * @var \Nuwave\Lighthouse\Subscriptions\Iterators\SyncIterator - */ - protected $iterator; - - protected function setUp(): void - { - parent::setUp(); - - $this->iterator = new SyncIterator; - } - - public function testCanIterateOverItemsWithCallback(): void + public function testIsWellBehavedIterator(): void { - $items = []; + $iterator = new SyncIterator; - $this->iterator->process( - $this->items(), - function ($item) use (&$items): void { - $items[] = $item; - } - ); - - $this->assertCount(3, $items); - } - - public function testCanPassExceptionToHandler(): void - { - /** @var \Exception|null $exception */ - $exception = null; - - $this->iterator->process( - $this->items(), - function (): void { - throw new Exception(self::EXCEPTION_MESSAGE); - }, - function (Exception $e) use (&$exception): void { - $exception = $e; - } - ); - - $this->assertSame(self::EXCEPTION_MESSAGE, $exception->getMessage()); - } - - /** - * @return \Illuminate\Support\Collection - */ - protected function items(): Collection - { - return new Collection([1, 2, 3]); + $this->assertIteratesOverItemsWithCallback($iterator); + $this->assertPassesExceptionToHandler($iterator); } } diff --git a/tests/Unit/Subscriptions/StorageManagerTest.php b/tests/Unit/Subscriptions/StorageManagerTest.php index 307a723b56..750a934440 100644 --- a/tests/Unit/Subscriptions/StorageManagerTest.php +++ b/tests/Unit/Subscriptions/StorageManagerTest.php @@ -6,24 +6,14 @@ use GraphQL\Utils\AST; use Nuwave\Lighthouse\Subscriptions\StorageManager; use Nuwave\Lighthouse\Subscriptions\Subscriber; -use Nuwave\Lighthouse\Subscriptions\SubscriptionServiceProvider; -use Tests\TestCase; -class StorageManagerTest extends TestCase +class StorageManagerTest extends SubscriptionTestCase { /** * @var \Nuwave\Lighthouse\Subscriptions\StorageManager */ protected $storage; - protected function getPackageProviders($app) - { - return array_merge( - parent::getPackageProviders($app), - [SubscriptionServiceProvider::class] - ); - } - protected function setUp(): void { parent::setUp(); diff --git a/tests/Unit/Execution/Utils/SubscriptionTest.php b/tests/Unit/Subscriptions/SubscriptionTest.php similarity index 96% rename from tests/Unit/Execution/Utils/SubscriptionTest.php rename to tests/Unit/Subscriptions/SubscriptionTest.php index 0e741c4006..508ace15ac 100644 --- a/tests/Unit/Execution/Utils/SubscriptionTest.php +++ b/tests/Unit/Subscriptions/SubscriptionTest.php @@ -1,6 +1,6 @@