Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
vendor/
composer.lock
.env
.idea
17 changes: 8 additions & 9 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"description": "Graphael: GraphQL Server builder",
"homepage": "http://www.github.com/linkorb/graphael",
"keywords": ["graphael", "graphql", "linkorb"],
"type": "application",
"type": "symfony-bundle",
"authors": [
{
"name": "Joost Faassen",
Expand All @@ -12,21 +12,20 @@
}
],
"require": {
"php": ">=7.2",
"symfony/options-resolver": "~3.0",
"symfony/dependency-injection": "~3.3",
"webonyx/graphql-php": "^0.13.8",
"php": ">=8.0",
"webonyx/graphql-php": "^15.2",
"linkorb/connector": "~1.0",
"firebase/php-jwt": "~5.0",
"pwa/time-elapsed": "^1.0",
"symfony/security-core": "^4.2 || ^5.0 ",
"symfony/http-foundation": "^4.2 || ^5.0",
"symfony/security-core": "^6",
"ext-pdo": "*",
"doctrine/cache": "^1.10"
"doctrine/cache": "^1.10",
"symfony/framework-bundle": "^6",
"symfony/security-bundle": "^6.0"
},
"autoload": {
"psr-4": {
"Graphael\\": "src/"
"LinkORB\\Bundle\\GraphaelBundle\\": "src/"
}
},
"license": "MIT"
Expand Down
70 changes: 70 additions & 0 deletions config/services.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
parameters:
graphael.default_jwt_algo: 'RS256'
graphael.default_admin_role: 'ROLE_ADMIN'
graphael.default_role: 'ROLE_AUTHENTICATED'
graphael.jwt_algo: '%env(default:graphael.default_jwt_algo:string:GRAPHAEL_JWT_ALGO)%'
graphael.jwt_key: '%env(file:GRAPHAEL_JWT_KEY)%'
graphael.jwt_username_claim: '%env(string:GRAPHAEL_JWT_USERNAME_CLAIM)%'
graphael.jwt_roles_claim: '%env(string:GRAPHAEL_JWT_ROLES_CLAIM)%'
graphael.admin_role: '%env(default:graphael.default_admin_role:string:GRAPHAEL_ADMIN_ROLE)%'
graphael.jwt_default_role: '%env(default:graphael.default_role:string:GRAPHAEL_JWT_DEFAULT_ROLE)%'
graphael.cache_driver: '%env(string:GRAPHAEL_CACHE_DRIVER)%'
graphael.cache_driver_file_path: '%env(string:GRAPHAEL_CACHE_DRIVER_FILE_PATH)%'
graphael.pdo_url: '%env(string:GRAPHAEL_PDO_URL)%'

services:
_defaults:
autowire: true
public: true

LinkORB\Bundle\GraphaelBundle\Controller\:
resource: '../src/Controller/'
tags: ['controller.service_arguments']

Connector\Connector:

PDO:
class: PDO
factory: ['LinkORB\Bundle\GraphaelBundle\Security\Factory\ConnectorFactory', 'createConnector']
arguments:
$pdoUrl: '%graphael.pdo_url%'

LinkORB\Bundle\GraphaelBundle\Services\DependencyInjection\ContainerTypeRegistry:
arguments: ['@service_container']

LinkORB\Bundle\GraphaelBundle\Services\Server:
class: LinkORB\Bundle\GraphaelBundle\Services\Server
LinkORB\Bundle\GraphaelBundle\Services\FieldResolver:

LinkORB\Bundle\GraphaelBundle\Services\DependencyInjection\TypeRegistryInterface:
alias: LinkORB\Bundle\GraphaelBundle\Services\DependencyInjection\ContainerTypeRegistry
public: true

LinkORB\Bundle\GraphaelBundle\Security\UserProvider\JwtDataMapperInterface:
alias: LinkORB\Bundle\GraphaelBundle\Security\UserProvider\DefaultJwtDataMapper
public: true

LinkORB\Bundle\GraphaelBundle\Security\JwtCertManager\JwtCertManagerInterface:
alias: LinkORB\Bundle\GraphaelBundle\Security\JwtCertManager\JwtCertManager
public: true

LinkORB\Bundle\GraphaelBundle\Security\JwtFactory:
arguments:
$jwtEnabled: '%graphael.jwt_key%'
$adminRole: '%graphael.jwt_default_role%'

LinkORB\Bundle\GraphaelBundle\Security\UserProvider\DefaultJwtDataMapper:

LinkORB\Bundle\GraphaelBundle\Security\UserProvider\JwtUserProvider:

LinkORB\Bundle\GraphaelBundle\Security\JwtCertManager\JwtCertManager:
arguments:
$publicCert: '%graphael.jwt_key%'

LinkORB\Bundle\GraphaelBundle\Security\JwtAuthenticator:
arguments:
$userProvider: '@LinkORB\Bundle\GraphaelBundle\Security\UserProvider\JwtUserProvider'
$jwtAlg: '%graphael.jwt_algo%'

LinkORB\Bundle\GraphaelBundle\Security\Authorization\UsernameVoter:
tags: ['security.voter']
109 changes: 109 additions & 0 deletions src/Controller/GraphController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php declare(strict_types=1);

namespace LinkORB\Bundle\GraphaelBundle\Controller;
use GraphQL\Error\InvariantViolation;
use GraphQL\Executor\ExecutionResult;
use GraphQL\Utils\Utils;
use LinkORB\Bundle\GraphaelBundle\Services\Server;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;

class GraphController
{
public function __construct(
private Server $server,
private LoggerInterface $logger,
) {}

public function __invoke(Request $request)
{
$response = new JsonResponse();
$response->headers->set('Access-Control-Allow-Origin', '*');

if ($request->isMethod('OPTIONS')) {
$response->headers->set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Authorization');
$response->headers->set('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
$response->setContent('{"status": "ok"}');

return $response;
}

$result = $this->server->executeRequest();
$httpStatus = $this->resolveHttpStatus($result);

if (count($this->logger->getHandlers())>0) {
$result->setErrorsHandler(function($errors) {
foreach ($errors as $error) {
$json = json_encode($error, JSON_UNESCAPED_SLASHES);
$data = [
'event' => [
'action' => 'graphael:error',
],
'log' => [
'level' => 'error',
'original' => json_encode(['error' => $json], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
],
];
$this->logger->error($error->getMessage() ?? 'Execution Error', $data);
}
return array_map('GraphQL\Error\FormattedError::createFromException', $errors);
});
}

$data = $result->toArray();
$json = json_encode($data, JSON_UNESCAPED_SLASHES);
$response->setContent($json);
$response->setStatusCode($httpStatus);

if ($httpStatus!=200) {
$data = [
'event' => [
'action' => 'graphael:error',
],
'log' => [
'level' => 'error',
'original' => 'HTTP' . $httpStatus . ': ' . json_encode(['error' => $json], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
],
];
$this->logger->error('HTTP Error', $data);
}

return $response;
}

/**
* @param ExecutionResult|mixed[] $result
*/
private function resolveHttpStatus($result): int
{
if (is_array($result) && isset($result[0])) {
foreach ($result as $index => $executionResultItem) {
if (!$executionResultItem instanceof ExecutionResult) {
throw new InvariantViolation(sprintf(
'Expecting every entry of batched query result to be instance of %s but entry at position %d is %s',
ExecutionResult::class,
$index,
Utils::printSafe($executionResultItem)
));
}
}
$httpStatus = 200;
} else {
if (!$result instanceof ExecutionResult) {
throw new InvariantViolation(sprintf(
'Expecting query result to be instance of %s but got %s',
ExecutionResult::class,
Utils::printSafe($result)
));
}
if ($result->data === null && count($result->errors) > 0) {
$httpStatus = 400;
} else {
$httpStatus = 200;
}
}

return $httpStatus;
}
}
34 changes: 34 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php declare(strict_types=1);

namespace LinkORB\Bundle\GraphaelBundle\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder('graphael');
$rootNode = $treeBuilder->getRootNode();

$rootNode
->children()
->scalarNode('jwt_algo')->defaultValue('%graphael.jwt_algo%')->end()
->scalarNode('jwt_key')->defaultValue('%graphael.jwt_key%')->end()
->scalarNode('jwt_username_claim')->defaultValue('%graphael.jwt_username_claim%')->end()
->scalarNode('jwt_roles_claim')->defaultValue('%graphael.jwt_roles_claim%')->end()
->scalarNode('admin_role')->defaultValue('%graphael.admin_role%')->end()
->scalarNode('jwt_default_role')->defaultValue('%graphael.jwt_default_role%')->end()
->scalarNode('cache_driver')->defaultValue('%graphael.cache_driver%')->end()
->scalarNode('cache_driver_file_path')->defaultValue('%graphael.cache_driver_file_path%')->end()
->scalarNode('pdo_url')->end()
->scalarNode('type_namespace')->isRequired()->cannotBeEmpty()->end()
->scalarNode('type_path')->isRequired()->cannotBeEmpty()->end()
->scalarNode('type_postfix')->isRequired()->cannotBeEmpty()->end()
->end()
->end();

return $treeBuilder;
}
}
111 changes: 111 additions & 0 deletions src/DependencyInjection/GraphaelExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php declare(strict_types=1);

namespace LinkORB\Bundle\GraphaelBundle\DependencyInjection;

use Doctrine\Common\Cache\ArrayCache;
use Doctrine\Common\Cache\Cache;
use Doctrine\Common\Cache\PhpFileCache;
use LinkORB\Bundle\GraphaelBundle\Services\FieldResolver;
use LinkORB\Bundle\GraphaelBundle\Services\Server;
use RuntimeException;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;

class GraphaelExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container): void
{
$configDir = new FileLocator(__DIR__ . '/../../config');
// Load the bundle's service declarations
$loader = new YamlFileLoader($container, $configDir);
$loader->load('services.yaml');

$configuration = new Configuration();
$options = $this->processConfiguration($configuration, $configs);

$this->processCacheDriver($options, $container);

if (file_exists($options['jwt_key'])) {
$jwtKey = file_get_contents($options['jwt_key']);
$options['jwt_key'] = $jwtKey;
$container->setParameter('graphael.jwt_key', $options['jwt_key']);
}

if ($options['pdo_url'] ?? null) {
$container->setParameter('graphael.pdo_url', $options['pdo_url']);
}

if (!isset($options['type_namespace'])) {
throw new RuntimeException("type_namespace not configured");
}
if (!isset($options['type_path'])) {
throw new RuntimeException("type_path not configured");
}

$this->processTypes($options, $container);

$this->initializeServer($options['type_namespace'], $options['type_postfix'], $container);
}

private function processCacheDriver(array $options, ContainerBuilder $container): void
{
$cacheDriver = $options['cache_driver'] ?? null;
try {
$cacheDriver = $container->getParameterBag()->resolveValue($cacheDriver);
} catch (ParameterNotFoundException) {
// Value is set not as parameter, so let's use it as it is
}

switch ($cacheDriver) {
case 'file':
$container
->register(Cache::class, PhpFileCache::class)
->addArgument($options['cache_driver_file_path'])
;
break;
case ''; // default unconfigured to array
case 'array':
$container
->register(Cache::class, ArrayCache::class)
;
break;
default:
throw new RuntimeException("Unsupported or unconfigured cache driver: " . $options['cache_driver']);
}
}

private function processTypes(array $options, ContainerBuilder $container): void
{
// Auto register QueryTypes
foreach (glob($options['type_path'] . '/*Type.php') as $filename) {
$className = $options['type_namespace'] . '\\' . basename($filename, '.php');
if (!is_array(class_implements($className))) {
throw new RuntimeException("Can't register class (failed to load, or does not implement anything): " . $className);
}
if (is_subclass_of($className, 'GraphQL\\Type\\Definition\\Type')) {
$container->autowire($className, $className)->setPublic(true);
}
}
}

private function initializeServer(string $typeNamespace, string $typePostfix, ContainerBuilder $container): void
{
$container->getDefinition(Server::class)
->addArgument(new Reference($typeNamespace . '\QueryType'))
->addArgument(new Reference($typeNamespace . '\MutationType'))
->addArgument([])
->addArgument(new Reference(AuthorizationCheckerInterface::class))
->addArgument('%'.Server::CONTEXT_ADMIN_ROLE_KEY.'%')
->addArgument(new Reference('request_stack'))
->addArgument(new Reference(FieldResolver::class))
->addArgument(new Reference('service_container'))
->addArgument($typeNamespace)
->addArgument($typePostfix)
;
}
}
2 changes: 1 addition & 1 deletion src/Entity/Security/AuthorizationEntityInterface.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php declare(strict_types=1);

namespace Graphael\Entity\Security;
namespace LinkORB\Bundle\GraphaelBundle\Entity\Security;

interface AuthorizationEntityInterface
{
Expand Down
2 changes: 1 addition & 1 deletion src/Entity/Security/UsernameAuthorization.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php declare(strict_types=1);

namespace Graphael\Entity\Security;
namespace LinkORB\Bundle\GraphaelBundle\Entity\Security;

class UsernameAuthorization implements AuthorizationEntityInterface
{
Expand Down
2 changes: 1 addition & 1 deletion src/Exception/OmittedJwtTokenException.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php declare(strict_types=1);

namespace Graphael\Exception;
namespace LinkORB\Bundle\GraphaelBundle\Exception;

use Symfony\Component\Security\Core\Exception\AuthenticationException;

Expand Down
Loading