Skip to content

Add a "me" query and modify login return type #30

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions Controller/GraphQL/LoginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use TheCodingMachine\GraphQLite\Annotations\Mutation;
Expand Down Expand Up @@ -55,12 +56,12 @@ public function __construct(UserProviderInterface $userProvider, UserPasswordEnc
/**
* @Mutation()
*/
public function login(string $userName, string $password, Request $request): bool
public function login(string $userName, string $password, Request $request): UserInterface
{
try {
$user = $this->userProvider->loadUserByUsername($userName);
} catch (UsernameNotFoundException $e) {
// FIXME: should we return false instead???
// FIXME: should we return null instead???
throw InvalidUserPasswordException::create($e);
}

Expand All @@ -83,7 +84,7 @@ public function login(string $userName, string $password, Request $request): boo
$event = new InteractiveLoginEvent($request, $token);
$this->eventDispatcher->dispatch($event, 'security.interactive_login');

return true;
return $user;
}

/**
Expand Down
51 changes: 51 additions & 0 deletions Controller/GraphQL/MeController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php
namespace TheCodingMachine\Graphqlite\Bundle\Controller\GraphQL;


use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use TheCodingMachine\GraphQLite\Annotations\Mutation;
use TheCodingMachine\GraphQLite\Annotations\Query;
use TheCodingMachine\Graphqlite\Bundle\Types\BasicUser;

class MeController
{
/**
* @var TokenStorageInterface
*/
private $tokenStorage;

public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}

/**
* @Query()
*/
public function me(): ?UserInterface
{
$token = $this->tokenStorage->getToken();
if ($token === null) {
return null;
}

$user = $token->getUser();

if (!$user instanceof UserInterface) {
// getUser() can be an object with a toString or a string
$userName = (string) $user;
$user = new BasicUser($userName);
}

return $user;
}
}
1 change: 1 addition & 0 deletions DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public function getConfigTreeBuilder()
->arrayNode('security')
->children()
->enumNode('enable_login')->values(['on', 'off', 'auto'])->defaultValue('auto')->info('Enable to automatically create a login/logout mutation. "on": enable, "auto": enable if security bundle is available.')->end()
->enumNode('enable_me')->values(['on', 'off', 'auto'])->defaultValue('auto')->info('Enable to automatically create a "me" query to fetch the current user. "on": enable, "auto": enable if security bundle is available.')->end()
->scalarNode('firewall_name')->defaultValue('main')->info('The name of the firewall to use for login')->end()
->end()
->end()
Expand Down
40 changes: 39 additions & 1 deletion DependencyInjection/GraphqliteCompilerPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
use TheCodingMachine\GraphQLite\Annotations\Parameter;
use TheCodingMachine\GraphQLite\Annotations\Query;
use TheCodingMachine\Graphqlite\Bundle\Controller\GraphQL\LoginController;
use TheCodingMachine\Graphqlite\Bundle\Controller\GraphQL\MeController;
use TheCodingMachine\GraphQLite\FieldsBuilder;
use TheCodingMachine\GraphQLite\FieldsBuilderFactory;
use TheCodingMachine\GraphQLite\GraphQLException;
Expand Down Expand Up @@ -115,7 +116,6 @@ public function process(ContainerBuilder $container)
// If the security is disabled, let's remove the LoginController
if ($disableLogin === true) {
$container->removeDefinition(LoginController::class);
$container->removeDefinition(AggregateControllerQueryProviderFactory::class);
}

if ($container->getParameter('graphqlite.security.enable_login') === 'on') {
Expand All @@ -132,6 +132,36 @@ public function process(ContainerBuilder $container)
$provider = $container->findDefinition('security.firewall.map.config.'.$firewallName)->getArgument(5);

$container->findDefinition(LoginController::class)->setArgument(0, new Reference($provider));

$this->registerController(LoginController::class, $container);
}

$disableMe = false;
if ($container->getParameter('graphqlite.security.enable_me') === 'auto'
&& !$container->has(TokenStorageInterface::class)) {
$disableMe = true;
}
if ($container->getParameter('graphqlite.security.enable_me') === 'off') {
$disableMe = true;
}
// If the security is disabled, let's remove the LoginController
if ($disableMe === true) {
$container->removeDefinition(MeController::class);
}

if ($container->getParameter('graphqlite.security.enable_me') === 'on') {
if (!$container->has(TokenStorageInterface::class)) {
throw new GraphQLException('In order to enable the "me" query (via the graphqlite.security.enable_me parameter), you need to install the security bundle.');
}
}

if ($disableMe === false) {
$this->registerController(MeController::class, $container);
}

// Perf improvement: let's remove the AggregateControllerQueryProviderFactory if it is empty.
if (empty($container->findDefinition(AggregateControllerQueryProviderFactory::class)->getArgument(0))) {
$container->removeDefinition(AggregateControllerQueryProviderFactory::class);
}


Expand Down Expand Up @@ -220,6 +250,14 @@ public function process(ContainerBuilder $container)
$this->mapAdderToTag('graphql.type_mapper_factory', 'addTypeMapperFactory', $container, $schemaFactory);
}

private function registerController(string $controllerClassName, ContainerBuilder $container): void
{
$aggregateQueryProvider = $container->findDefinition(AggregateControllerQueryProviderFactory::class);
$controllersList = $aggregateQueryProvider->getArgument(0);
$controllersList[] = $controllerClassName;
$aggregateQueryProvider->setArgument(0, $controllersList);
}

/**
* Register a method call on SchemaFactory for each tagged service, passing the service in parameter.
*
Expand Down
2 changes: 2 additions & 0 deletions DependencyInjection/GraphqliteExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,12 @@ public function load(array $configs, ContainerBuilder $container)
}

$enableLogin = $configs[0]['security']['enable_login'] ?? 'auto';
$enableMe = $configs[0]['security']['enable_me'] ?? 'auto';

$container->setParameter('graphqlite.namespace.controllers', $namespaceController);
$container->setParameter('graphqlite.namespace.types', $namespaceType);
$container->setParameter('graphqlite.security.enable_login', $enableLogin);
$container->setParameter('graphqlite.security.enable_me', $enableMe);
$container->setParameter('graphqlite.security.firewall_name', $configs[0]['security']['firewall_name'] ?? 'main');

$loader->load('graphqlite.xml');
Expand Down
12 changes: 11 additions & 1 deletion Resources/config/container/graphqlite.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@

<service id="TheCodingMachine\GraphQLite\AggregateControllerQueryProviderFactory">
<argument type="collection">
<argument>TheCodingMachine\Graphqlite\Bundle\Controller\GraphQL\LoginController</argument>
</argument>
<tag name="graphql.queryprovider_factory" />
</service>
Expand Down Expand Up @@ -77,5 +76,16 @@
<service id="TheCodingMachine\Graphqlite\Bundle\Controller\GraphQL\LoginController" public="true">
<argument key="$firewallName">%graphqlite.security.firewall_name%</argument>
</service>

<service id="TheCodingMachine\Graphqlite\Bundle\Controller\GraphQL\MeController" public="true" />

<service id="TheCodingMachine\Graphqlite\Bundle\Types\UserType" public="true"/>

<service id="TheCodingMachine\GraphQLite\Mappers\StaticClassListTypeMapperFactory">
<argument type="collection">
<argument>TheCodingMachine\Graphqlite\Bundle\Types\UserType</argument>
</argument>
<tag name="graphql.type_mapper_factory"/>
</service>
</services>
</container>
114 changes: 111 additions & 3 deletions Tests/FunctionalTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,10 @@ public function testLoginQuery(): void

$request = Request::create('/graphql', 'POST', ['query' => '
mutation login {
login(userName: "foo", password: "bar")
login(userName: "foo", password: "bar") {
userName
roles
}
}']);

$response = $kernel->handle($request);
Expand All @@ -229,9 +232,47 @@ public function testLoginQuery(): void

$this->assertSame([
'data' => [
'login' => true
'login' => [
'userName' => 'foo',
'roles' => [
'ROLE_USER'
]
]
]
], $result);
}

public function testMeQuery(): void
{
$kernel = new GraphqliteTestingKernel();
$kernel->boot();

$session = new Session(new MockArraySessionStorage());
$container = $kernel->getContainer();
$container->set('session', $session);

$request = Request::create('/graphql', 'POST', ['query' => '
{
me {
userName
roles
}
}
']);

$response = $kernel->handle($request);

$result = json_decode($response->getContent(), true);

$this->assertSame([
'data' => [
'me' => [
'userName' => 'anon.',
'roles' => [],
]
]
], $result);

}

public function testNoLoginNoSessionQuery(): void
Expand All @@ -241,7 +282,9 @@ public function testNoLoginNoSessionQuery(): void

$request = Request::create('/graphql', 'POST', ['query' => '
mutation login {
login(userName: "foo", password: "bar")
login(userName: "foo", password: "bar") {
userName
}
}']);

$response = $kernel->handle($request);
Expand All @@ -259,6 +302,14 @@ public function testForceLoginNoSession(): void
$kernel->boot();
}

public function testForceMeNoSecurity(): void
{
$kernel = new GraphqliteTestingKernel(false, 'off', false, 'on');
$this->expectException(GraphQLException::class);
$this->expectExceptionMessage('In order to enable the "me" query (via the graphqlite.security.enable_me parameter), you need to install the security bundle.');
$kernel->boot();
}

public function testForceLoginNoSecurity(): void
{
$kernel = new GraphqliteTestingKernel(true, 'on', false);
Expand All @@ -267,6 +318,63 @@ public function testForceLoginNoSecurity(): void
$kernel->boot();
}

/*public function testAutoMeNoSecurity(): void
{
$kernel = new GraphqliteTestingKernel(true, null, false);
$kernel->boot();

$session = new Session(new MockArraySessionStorage());
$container = $kernel->getContainer();
$container->set('session', $session);

$request = Request::create('/graphql', 'POST', ['query' => '
{
me {
userName
roles
}
}
']);

$response = $kernel->handle($request);

$result = json_decode($response->getContent(), true);

$this->assertSame([
'data' => [
'me' => [
'userName' => 'anon.',
'roles' => [],
]
]
], $result);
}*/

public function testAllOff(): void
{
$kernel = new GraphqliteTestingKernel(true, 'off', true, 'off');
$kernel->boot();

$session = new Session(new MockArraySessionStorage());
$container = $kernel->getContainer();
$container->set('session', $session);

$request = Request::create('/graphql', 'POST', ['query' => '
{
me {
userName
roles
}
}
']);

$response = $kernel->handle($request);

$result = json_decode($response->getContent(), true);

$this->assertSame('Cannot query field "me" on type "Query".', $result['errors'][0]['message']);
}

private function logIn(ContainerInterface $container)
{
// put a token into the storage so the final calls can function
Expand Down
13 changes: 12 additions & 1 deletion Tests/GraphqliteTestingKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,18 @@ class GraphqliteTestingKernel extends Kernel
* @var bool
*/
private $enableSecurity;
/**
* @var string|null
*/
private $enableMe;

public function __construct(bool $enableSession = true, ?string $enableLogin = null, bool $enableSecurity = true)
public function __construct(bool $enableSession = true, ?string $enableLogin = null, bool $enableSecurity = true, ?string $enableMe = null)
{
parent::__construct('test', true);
$this->enableSession = $enableSession;
$this->enableLogin = $enableLogin;
$this->enableSecurity = $enableSecurity;
$this->enableMe = $enableMe;
}

public function registerBundles()
Expand Down Expand Up @@ -115,6 +120,12 @@ public function configureContainer(ContainerBuilder $c, LoaderInterface $loader)
];
}

if ($this->enableMe) {
$graphqliteConf['security'] = [
'enable_me' => $this->enableMe,
];
}

$container->loadFromExtension('graphqlite', $graphqliteConf);
});
$confDir = $this->getProjectDir().'/Tests/Fixtures/config';
Expand Down
Loading