Skip to content

Commit

Permalink
Log in subscribers when broadcasting a subscription update (#1306)
Browse files Browse the repository at this point in the history
  • Loading branch information
stayallive authored and spawnia committed Apr 19, 2020
1 parent 552f576 commit de1c2e8
Show file tree
Hide file tree
Showing 14 changed files with 348 additions and 92 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
13 changes: 11 additions & 2 deletions src/Subscriptions/Contracts/SubscriptionIterator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
69 changes: 69 additions & 0 deletions src/Subscriptions/Iterators/AuthenticatingSyncIterator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

namespace Nuwave\Lighthouse\Subscriptions\Iterators;

use Closure;
use Exception;
use Illuminate\Contracts\Auth\Factory;
use Illuminate\Contracts\Config\Repository;
use Illuminate\Support\Collection;
use Nuwave\Lighthouse\Subscriptions\Contracts\SubscriptionIterator;
use Nuwave\Lighthouse\Subscriptions\Subscriber;
use Nuwave\Lighthouse\Subscriptions\SubscriptionGuard;

/**
* Logs in the subscriber as their subscription is resolved.
*/
class AuthenticatingSyncIterator implements SubscriptionIterator
{
/**
* @var \Illuminate\Contracts\Config\Repository
*/
private $configRepository;

/**
* @var \Illuminate\Contracts\Auth\Factory
*/
private $authFactory;

public function __construct(Repository $configRepository, Factory $authFactory)
{
$this->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);
}
}
13 changes: 5 additions & 8 deletions src/Subscriptions/Iterators/SyncIterator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
}
Expand Down
29 changes: 29 additions & 0 deletions src/Subscriptions/SubscriptionGuard.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace Nuwave\Lighthouse\Subscriptions;

use Illuminate\Auth\GuardHelpers;
use Illuminate\Contracts\Auth\Guard;
use RuntimeException;

class SubscriptionGuard implements Guard
{
use GuardHelpers;

public const GUARD_NAME = 'lighthouse_subscriptions';

public function user()
{
return $this->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.');
}
}
17 changes: 17 additions & 0 deletions src/Subscriptions/SubscriptionServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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;
});
}
}

/**
Expand Down
12 changes: 1 addition & 11 deletions tests/Unit/Subscriptions/BroadcastManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
117 changes: 117 additions & 0 deletions tests/Unit/Subscriptions/Iterators/AuthenticatingSyncIteratorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php

namespace Tests\Unit\Subscriptions\Iterators;

use Illuminate\Auth\AuthManager;
use Illuminate\Contracts\Auth\Authenticatable;
use Mockery;
use Mockery\MockInterface;
use Nuwave\Lighthouse\Subscriptions\Iterators\AuthenticatingSyncIterator;
use Nuwave\Lighthouse\Subscriptions\Subscriber;
use Nuwave\Lighthouse\Subscriptions\SubscriptionGuard;

class AuthenticatingSyncIteratorTest extends IteratorTest
{
public function testIsWellBehavedIterator(): void
{
$iterator = $this->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()
{
}
}
Loading

0 comments on commit de1c2e8

Please sign in to comment.