diff --git a/DependencyInjection/GraphqliteCompilerPass.php b/DependencyInjection/GraphqliteCompilerPass.php index a5e9fe6..6b69fae 100644 --- a/DependencyInjection/GraphqliteCompilerPass.php +++ b/DependencyInjection/GraphqliteCompilerPass.php @@ -40,7 +40,6 @@ use TheCodingMachine\GraphQLite\Annotations\Mutation; use TheCodingMachine\GraphQLite\Annotations\Parameter; use TheCodingMachine\GraphQLite\Annotations\Query; -use TheCodingMachine\Graphqlite\Bundle\QueryProviders\ControllerQueryProvider; use TheCodingMachine\GraphQLite\FieldsBuilder; use TheCodingMachine\GraphQLite\FieldsBuilderFactory; use TheCodingMachine\GraphQLite\GraphQLException; @@ -52,6 +51,7 @@ use TheCodingMachine\GraphQLite\Mappers\Root\CompositeRootTypeMapper; use TheCodingMachine\GraphQLite\Mappers\StaticTypeMapper; use TheCodingMachine\GraphQLite\NamingStrategy; +use TheCodingMachine\GraphQLite\SchemaFactory; use TheCodingMachine\GraphQLite\TypeGenerator; use TheCodingMachine\GraphQLite\Types\MutableObjectType; use TheCodingMachine\GraphQLite\Types\ResolvableInputObjectType; @@ -86,10 +86,7 @@ public function process(ContainerBuilder $container) $globTtl = 2; } - /** - * @var array> - */ - $classToServicesMap = []; + $schemaFactory = $container->getDefinition(SchemaFactory::class); foreach ($container->getDefinitions() as $id => $definition) { if ($definition->isAbstract() || $definition->getClass() === null) { @@ -125,63 +122,19 @@ public function process(ContainerBuilder $container) } foreach ($controllersNamespaces as $controllersNamespace) { + $schemaFactory->addMethodCall('addControllerNamespace', [ $controllersNamespace ]); foreach ($this->getClassList($controllersNamespace) as $className => $refClass) { $this->makePublicInjectedServices($refClass, $reader, $container); } } foreach ($typesNamespaces as $typeNamespace) { + $schemaFactory->addMethodCall('addTypeNamespace', [ $typeNamespace ]); foreach ($this->getClassList($typeNamespace) as $className => $refClass) { $this->makePublicInjectedServices($refClass, $reader, $container); } } - foreach ($container->findTaggedServiceIds('graphql.annotated.controller') as $id => $tag) { - $definition = $container->findDefinition($id); - $class = $definition->getClass(); - if ($class === null) { - throw new \RuntimeException(sprintf('Service %s has no class defined.', $id)); - } - - $reflectionClass = new ReflectionClass($class); - $isController = false; - $method = null; - foreach ($reflectionClass->getMethods() as $method) { - $query = $reader->getRequestAnnotation($method, Query::class); - if ($query !== null) { - $isController = true; - break; - } - $mutation = $reader->getRequestAnnotation($method, Mutation::class); - if ($mutation !== null) { - $isController = true; - break; - } - } - - if ($isController) { - // Let's create a QueryProvider from this controller - $controllerIdentifier = $class.'__QueryProvider'; - $queryProvider = new Definition(ControllerQueryProvider::class); - $queryProvider->setPrivate(true); - $queryProvider->setFactory([self::class, 'createQueryProvider']); - $queryProvider->addArgument(new Reference($id)); - $queryProvider->addArgument(new Reference(FieldsBuilder::class)); - $queryProvider->addTag('graphql.queryprovider'); - $container->setDefinition($controllerIdentifier, $queryProvider); - } - } - - foreach ($typesNamespaces as $typesNamespace) { - $definition = new Definition(GlobTypeMapper::class); - $definition->addArgument($typesNamespace); - $definition->setArgument('$globTtl', $globTtl); - $definition->setAutowired(true); - $definition->addTag('graphql.type_mapper'); - $container->setDefinition('globTypeMapper_'.str_replace('\\', '__', $typesNamespace), $definition); - } - - // Register custom output types $taggedServices = $container->findTaggedServiceIds('graphql.output_type'); @@ -210,12 +163,27 @@ public function process(ContainerBuilder $container) $definition->addMethodCall('setNotMappedTypes', [$customNotMappedTypes]); } - // Register type mappers - $typeMapperServices = $container->findTaggedServiceIds('graphql.type_mapper'); - $compositeTypeMapper = $container->getDefinition(CompositeTypeMapper::class); - foreach ($typeMapperServices as $id => $tags) { + // Register graphql.queryprovider + $this->mapAdderToTag('graphql.queryprovider', 'addQueryProvider', $container, $schemaFactory); + $this->mapAdderToTag('graphql.root_type_mapper', 'addRootTypeMapper', $container, $schemaFactory); + $this->mapAdderToTag('graphql.parameter_mapper', 'addParameterMapper', $container, $schemaFactory); + $this->mapAdderToTag('graphql.field_middleware', 'addFieldMiddleware', $container, $schemaFactory); + $this->mapAdderToTag('graphql.type_mapper', 'addTypeMapper', $container, $schemaFactory); + } + + /** + * Register a method call on SchemaFactory for each tagged service, passing the service in parameter. + * + * @param string $tag + * @param string $methodName + */ + private function mapAdderToTag(string $tag, string $methodName, ContainerBuilder $container, Definition $schemaFactory): void + { + $taggedServices = $container->findTaggedServiceIds($tag); + + foreach ($taggedServices as $id => $tags) { // add the transport service to the TransportChain service - $compositeTypeMapper->addMethodCall('addTypeMapper', [new Reference($id)]); + $schemaFactory->addMethodCall($methodName, [new Reference($id)]); } } @@ -296,14 +264,6 @@ private static function getParametersByName(ReflectionMethod $method): array return $parameters; } - /** - * @param object $controller - */ - public static function createQueryProvider($controller, FieldsBuilder $fieldsBuilder): ControllerQueryProvider - { - return new ControllerQueryProvider($controller, $fieldsBuilder); - } - /** * Returns a cached Doctrine annotation reader. * Note: we cannot get the annotation reader service in the container as we are in a compiler pass. diff --git a/QueryProviders/ControllerQueryProvider.php b/QueryProviders/ControllerQueryProvider.php deleted file mode 100644 index b959043..0000000 --- a/QueryProviders/ControllerQueryProvider.php +++ /dev/null @@ -1,47 +0,0 @@ -controller = $controller; - $this->fieldsBuilder = $fieldsBuilder; - } - - /** - * @return QueryField[] - */ - public function getQueries(): array - { - return $this->fieldsBuilder->getQueries($this->controller); - } - - /** - * @return QueryField[] - */ - public function getMutations(): array - { - return $this->fieldsBuilder->getMutations($this->controller); - } -} diff --git a/Resources/config/container/graphqlite.xml b/Resources/config/container/graphqlite.xml index 177a4f7..f49f80c 100644 --- a/Resources/config/container/graphqlite.xml +++ b/Resources/config/container/graphqlite.xml @@ -12,66 +12,29 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + - - - - + + - - - + - %graphqlite.annotations.error_mode% - - - - @@ -84,22 +47,6 @@ - - - - - - - - - - - - - - - - @@ -114,22 +61,6 @@ - - - - - - - - - - - - - - - - diff --git a/Security/AuthenticationService.php b/Security/AuthenticationService.php index 58fd96f..d833ca7 100644 --- a/Security/AuthenticationService.php +++ b/Security/AuthenticationService.php @@ -3,7 +3,6 @@ namespace TheCodingMachine\Graphqlite\Bundle\Security; - use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use TheCodingMachine\GraphQLite\Security\AuthenticationServiceInterface; diff --git a/Security/AuthorizationService.php b/Security/AuthorizationService.php index a002aed..dcfd0b4 100644 --- a/Security/AuthorizationService.php +++ b/Security/AuthorizationService.php @@ -4,6 +4,7 @@ namespace TheCodingMachine\Graphqlite\Bundle\Security; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use TheCodingMachine\GraphQLite\Security\AuthorizationServiceInterface; @@ -13,10 +14,15 @@ class AuthorizationService implements AuthorizationServiceInterface * @var AuthorizationCheckerInterface|null */ private $authorizationChecker; + /** + * @var TokenStorageInterface|null + */ + private $tokenStorage; - public function __construct(?AuthorizationCheckerInterface $authorizationChecker) + public function __construct(?AuthorizationCheckerInterface $authorizationChecker, ?TokenStorageInterface $tokenStorage) { $this->authorizationChecker = $authorizationChecker; + $this->tokenStorage = $tokenStorage; } /** @@ -27,10 +33,15 @@ public function __construct(?AuthorizationCheckerInterface $authorizationChecker */ public function isAllowed(string $right): bool { - if ($this->authorizationChecker === null) { + if ($this->authorizationChecker === null || $this->tokenStorage === null) { throw new \LogicException('The SecurityBundle is not registered in your application. Try running "composer require symfony/security-bundle".'); } + $token = $this->tokenStorage->getToken(); + if (null === $token) { + return false; + } + return $this->authorizationChecker->isGranted($right); } } diff --git a/Tests/Fixtures/Controller/TestGraphqlController.php b/Tests/Fixtures/Controller/TestGraphqlController.php index bca2f63..3c8d304 100644 --- a/Tests/Fixtures/Controller/TestGraphqlController.php +++ b/Tests/Fixtures/Controller/TestGraphqlController.php @@ -6,6 +6,9 @@ use GraphQL\Error\Error; use Porpaginas\Arrays\ArrayResult; +use TheCodingMachine\GraphQLite\Annotations\FailWith; +use TheCodingMachine\GraphQLite\Annotations\Logged; +use TheCodingMachine\GraphQLite\Annotations\Right; use TheCodingMachine\Graphqlite\Bundle\Tests\Fixtures\Entities\Contact; use TheCodingMachine\Graphqlite\Bundle\Tests\Fixtures\Entities\Product; use TheCodingMachine\GraphQLite\Annotations\Mutation; @@ -66,4 +69,37 @@ public function triggerException(int $code = 0): string { throw new MyException('Boom', $code); } + + /** + * @Query() + * @Logged() + * @FailWith(null) + * @return string + */ + public function loggedQuery(): string + { + return 'foo'; + } + + /** + * @Query() + * @Right("ROLE_ADMIN") + * @FailWith(null) + * @return string + */ + public function withAdminRight(): string + { + return 'foo'; + } + + /** + * @Query() + * @Right("ROLE_USER") + * @FailWith(null) + * @return string + */ + public function withUserRight(): string + { + return 'foo'; + } } diff --git a/Tests/FunctionalTest.php b/Tests/FunctionalTest.php index 3435fa4..1586b57 100644 --- a/Tests/FunctionalTest.php +++ b/Tests/FunctionalTest.php @@ -4,8 +4,16 @@ use function json_decode; use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; +use function spl_object_hash; +use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; +use Symfony\Component\Security\Core\User\User; +use TheCodingMachine\Graphqlite\Bundle\Controller\GraphqliteController; +use TheCodingMachine\Graphqlite\Bundle\Security\AuthenticationService; use TheCodingMachine\GraphQLite\Schema; +use function var_dump; class FunctionalTest extends TestCase { @@ -125,4 +133,63 @@ public function testErrors() $this->assertSame(404, $response->getStatusCode(), $response->getContent()); } + + public function testLoggedMiddleware() + { + $kernel = new GraphqliteTestingKernel('test', true); + $kernel->boot(); + + $request = Request::create('/graphql', 'GET', ['query' => ' + { + loggedQuery + }']); + + $response = $kernel->handle($request); + + $result = json_decode($response->getContent(), true); + + $this->assertSame([ + 'data' => [ + 'loggedQuery' => null + ] + ], $result); + } + + public function testLoggedMiddleware2() + { + $kernel = new GraphqliteTestingKernel('test', true); + $kernel->boot(); + + $request = Request::create('/graphql', 'GET', ['query' => ' + { + loggedQuery + withAdminRight + withUserRight + }']); + + $this->logIn($kernel->getContainer()); + + // Test again, bypassing the kernel (cause this triggers a reboot of the container that disconnects the user) + $response = $kernel->getContainer()->get(GraphqliteController::class)->handleRequest($request); + + + $result = json_decode($response->getContent(), true); + + $this->assertSame([ + 'data' => [ + 'loggedQuery' => 'foo', + 'withAdminRight' => null, + 'withUserRight' => 'foo', + ] + ], $result); + + } + + private function logIn(ContainerInterface $container) + { + // put a token into the storage so the final calls can function + $user = new User('foo', 'pass'); + $token = new UsernamePasswordToken($user, '', 'provider', ['ROLE_USER']); + $container->get('security.token_storage')->setToken($token); + } } diff --git a/Tests/GraphqliteTestingKernel.php b/Tests/GraphqliteTestingKernel.php index fcfe8dc..7cc78e2 100644 --- a/Tests/GraphqliteTestingKernel.php +++ b/Tests/GraphqliteTestingKernel.php @@ -4,7 +4,9 @@ namespace TheCodingMachine\Graphqlite\Bundle\Tests; +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; +use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Kernel; @@ -25,7 +27,8 @@ public function __construct() public function registerBundles() { return [ - new \Symfony\Bundle\FrameworkBundle\FrameworkBundle(), + new FrameworkBundle(), + new SecurityBundle(), new GraphqliteBundle(), ]; } @@ -36,6 +39,16 @@ public function configureContainer(ContainerBuilder $c, LoaderInterface $loader) $container->loadFromExtension('framework', array( 'secret' => 'S0ME_SECRET', )); + $container->loadFromExtension('security', array( + 'providers' => [ + 'in_memory' => ['memory' => null], + ], + 'firewalls' => [ + 'main' => [ + 'anonymous' => true + ] + ] + )); $container->loadFromExtension('graphqlite', array( 'namespace' => [ 'controllers' => ['TheCodingMachine\\Graphqlite\\Bundle\\Tests\\Fixtures\\Controller\\'],