Passed
Push — master ( 46c3ee...445208 )
by Rafael
04:28
created

Configuration::configureSecurity()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 24
ccs 0
cts 23
cp 0
rs 9.536
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 2
1
<?php
2
3
/*******************************************************************************
4
 *  This file is part of the GraphQL Bundle package.
5
 *
6
 *  (c) YnloUltratech <[email protected]>
7
 *
8
 *  For the full copyright and license information, please view the LICENSE
9
 *  file that was distributed with this source code.
10
 ******************************************************************************/
11
12
namespace Ynlo\GraphQLBundle\DependencyInjection;
13
14
use GraphQL\Validator\Rules\QueryComplexity;
15
use GraphQL\Validator\Rules\QueryDepth;
16
use Symfony\Component\Config\Definition\Builder\NodeBuilder;
17
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
18
use Symfony\Component\Config\Definition\ConfigurationInterface;
19
use Ynlo\GraphQLBundle\Encoder\SecureIDEncoder;
20
use Ynlo\GraphQLBundle\Error\DefaultErrorFormatter;
21
use Ynlo\GraphQLBundle\Error\DefaultErrorHandler;
22
23
/**
24
 * Class Configuration
25
 */
26
class Configuration implements ConfigurationInterface
27
{
28
    /**
29
     * {@inheritdoc}
30
     */
31
    public function getConfigTreeBuilder()
32
    {
33
        $treeBuilder = new TreeBuilder();
34
        /** @var NodeBuilder $rootNode */
35
        $rootNode = $treeBuilder->root('graphql')->addDefaultsIfNotSet()->children();
0 ignored issues
show
Bug introduced by
The method addDefaultsIfNotSet() does not exist on Symfony\Component\Config...\Builder\NodeDefinition. It seems like you code against a sub-type of Symfony\Component\Config...\Builder\NodeDefinition such as Symfony\Component\Config...der\ArrayNodeDefinition. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

35
        $rootNode = $treeBuilder->root('graphql')->/** @scrutinizer ignore-call */ addDefaultsIfNotSet()->children();
Loading history...
36
        $this->configureEndpoints($rootNode);
37
        $this->configureErrorHandling($rootNode);
38
        $this->configureCORS($rootNode);
39
        $this->configureGraphiQL($rootNode);
40
        $this->configurePlugins($rootNode);
41
        $this->configureSecurity($rootNode);
42
        $this->configureOthers($rootNode);
43
        $this->configureBCCompatibility($rootNode);
44
45
        return $treeBuilder;
46
    }
47
48
    protected function configureErrorHandling(NodeBuilder $root)
49
    {
50
        $errorHandling = $root->arrayNode('error_handling')
51
                              ->info('It is important to handle errors and when possible, report these errors back to your users for information. ')
52
                              ->addDefaultsIfNotSet()
53
                              ->children();
54
55
        $errorHandling->enumNode('validation_messages')
56
                      ->values(['error', 'payload', 'both'])
57
                      ->info('Where should be displayed validation messages.')
58
                      ->defaultValue('error');
59
60
        //@deprecated since v1.1 snd should be deleted in 2.0, this is a compatibility flag
61
        $errorHandling->booleanNode('jwt_auth_failure_compatibility')
62
                      ->info('Keep BC with oldest version of JWT Authentication errors')
63
                      ->setDeprecated('Since v1.1 and will will be removed in the next mayor release')
64
                      ->defaultFalse();
65
66
        $errorHandling->booleanNode('show_trace')->info('Show error trace in debug mode')->defaultFalse();
67
68
        $errorHandling->scalarNode('formatter')
69
                      ->info('Formatter is responsible for converting instances of Error to an array')
70
                      ->defaultValue(DefaultErrorFormatter::class);
71
72
        $errorHandling->scalarNode('handler')
73
                      ->info('Handler is useful for error filtering and logging.')
74
                      ->defaultValue(DefaultErrorHandler::class);
75
76
        $controlledErrors = $errorHandling
77
            ->arrayNode('controlled_errors')
78
            ->info('List of controlled errors')
79
            ->addDefaultsIfNotSet()
80
            ->children();
81
82
        $map = $controlledErrors->arrayNode('map')->useAttributeAsKey('code')->arrayPrototype()->children();
83
        $map->scalarNode('message')->isRequired();
84
        $map->scalarNode('description')->isRequired();
85
        $map->scalarNode('category')->defaultValue('user');
86
87
        $autoload = $controlledErrors
88
            ->arrayNode('autoload')
89
            ->info('Autoload exceptions implementing ControlledErrorInterface')
90
            ->addDefaultsIfNotSet()
91
            ->canBeDisabled()
92
            ->children();
93
94
        $autoload
95
            ->variableNode('locations')
96
            ->defaultValue(['Exception', 'Error'])
97
            ->info('Default folder to find exceptions and errors implementing controlled interface.')
98
            ->beforeNormalization()
99
            ->ifString()
100
            ->then(
101
                function ($v) {
102
                    return [$v];
103
                }
104
            )
105
            ->end();
106
107
        $autoload
108
            ->variableNode('whitelist')
109
            ->info('White listed classes')
110
            ->defaultValue(['/App\\\\[Exception|Error]/', '/\w+Bundle\\\\[Exception|Error]/'])
111
            ->beforeNormalization()
112
            ->ifString()
113
            ->then(
114
                function ($v) {
115
                    return [$v];
116
                }
117
            )
118
            ->end()
119
            ->validate()
120
            ->ifTrue(
121
                function (array $value) {
122
                    foreach ($value as $val) {
123
                        try {
124
                            preg_match($val, null);
125
                        } catch (\Exception $exception) {
126
                            return true;
127
                        }
128
                    }
129
                }
130
            )->thenInvalid('Invalid regular expression');
131
132
        $autoload
133
            ->variableNode('blacklist')
134
            ->info('Black listed classes')
135
            ->beforeNormalization()
136
            ->ifString()
137
            ->then(
138
                function ($v) {
139
                    return [$v];
140
                }
141
            )
142
            ->end()
143
            ->validate()
144
            ->ifTrue(
145
                function (array $value) {
146
                    foreach ($value as $val) {
147
                        try {
148
                            preg_match($val, null);
149
                        } catch (\Exception $exception) {
150
                            return true;
151
                        }
152
                    }
153
                }
154
            )->thenInvalid('Invalid regular expression');
155
    }
156
157
    protected function configureEndpoints(NodeBuilder $root)
158
    {
159
        $endpoints = $root->arrayNode('endpoints')
160
                          ->useAttributeAsKey('name')
161
                          ->validate()
162
                          ->ifTrue(
163
                              function ($v) {
164
                                  return array_key_exists('default', $v);
165
                              }
166
                          )->thenInvalid('"default" can\'t be used as endpoint name, the system internally use this endpoint name to store the entire schema.')
167
                          ->end()
168
                          ->arrayPrototype()
0 ignored issues
show
Bug introduced by
The method arrayPrototype() does not exist on Symfony\Component\Config...\Builder\NodeDefinition. It seems like you code against a sub-type of Symfony\Component\Config...\Builder\NodeDefinition such as Symfony\Component\Config...der\ArrayNodeDefinition. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

168
                          ->/** @scrutinizer ignore-call */ arrayPrototype()
Loading history...
169
                          ->children();
170
171
        $endpoints->arrayNode('roles')
172
                  ->beforeNormalization()
173
                  ->ifString()
174
                  ->then(
175
                      function ($v) {
176
                          return preg_split('/\s*,\s*/', $v);
177
                      }
178
                  )
179
                  ->end()
180
                  ->prototype('scalar')
181
                  ->end();
182
183
        $endpoints->scalarNode('host')->example('^api\.backend\.');
184
        $endpoints->scalarNode('path')->example('/backend');
185
186
        $root->arrayNode('endpoint_alias')
187
             ->info('Use alias to refer to multiple endpoints using only one name')
188
             ->useAttributeAsKey('name')
189
             ->beforeNormalization()
190
             ->ifString()
191
             ->then(
192
                 function ($v) {
193
                     return preg_split('/\s*,\s*/', $v);
194
                 }
195
             )
196
             ->end()
197
             ->variablePrototype();
0 ignored issues
show
Bug introduced by
The method variablePrototype() does not exist on Symfony\Component\Config...\Builder\NodeDefinition. It seems like you code against a sub-type of Symfony\Component\Config...\Builder\NodeDefinition such as Symfony\Component\Config...der\ArrayNodeDefinition. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

197
             ->/** @scrutinizer ignore-call */ variablePrototype();
Loading history...
198
199
        $root->scalarNode('endpoint_default')->info('Endpoint to apply to all definitions without explicit endpoint.');
200
201
    }
202
203
    protected function configureGraphiQL(NodeBuilder $root)
204
    {
205
        $graphiql = $root->arrayNode('graphiql')->addDefaultsIfNotSet()->children();
206
207
        $graphiql->scalarNode('title')
208
                 ->defaultValue('GraphQL API Explorer');
209
210
        $graphiql
211
            ->scalarNode('data_warning_message')
212
            ->defaultValue('Heads up! GraphQL Explorer makes use of your <strong>real</strong>, <strong>live</strong>, <strong>production</strong> data.');
213
        $graphiql->booleanNode('data_warning_dismissible')->defaultTrue();
214
        $graphiql->enumNode('data_warning_style')->values(['info', 'warning', 'danger'])->defaultValue('danger');
215
216
        $graphiql->scalarNode('template')->defaultValue('@YnloGraphQL/explorer.html.twig');
217
        $graphiql->scalarNode('default_query')->defaultNull()->info('An optional GraphQL string to use when no query exists from a previous session. If none is provided, GraphiQL will use its own default query.');
218
219
        $graphiql->scalarNode('favicon')->info('Url or path to favicon');
220
221
        $docs = $graphiql->arrayNode('documentation')->info('Display external API documentation link')->addDefaultsIfNotSet()->children();
222
        $docs->scalarNode('link')->info('Url, route or path.');
223
        $docs->scalarNode('btn_label')->defaultValue('Documentation');
224
        $docs->scalarNode('btn_class')->defaultValue('btn btn-outline-success');
225
226
        $authentication = $graphiql->arrayNode('authentication')->addDefaultsIfNotSet()->children();
227
        $authentication
228
            ->booleanNode('required')
229
            ->info(
230
                'The API require credentials to make any requests, 
231
if this value is FALSE and a provider is specified the authentication is optional.'
232
            )
233
            ->defaultFalse();
234
235
        $authentication->scalarNode('login_message')
236
                       ->defaultValue('Start exploring GraphQL API queries using your account’s data now.');
237
238
        $authenticationProvider = $authentication->arrayNode('provider')->children();
239
240
        //the updated version of `jwt` to use lexik authentication bundle
241
        $lexikJwt = $authenticationProvider->arrayNode('lexik_jwt')
242
                                           ->canBeEnabled()
243
                                           ->children();
244
245
        $lexikJwt->scalarNode('user_provider')
246
                 ->isRequired()
247
                 ->info('Name of the user provider to use');
248
249
        $lexikJwt->scalarNode('username_label')
250
                 ->defaultValue('Username');
251
252
        $lexikJwt->scalarNode('password_label')
253
                 ->defaultValue('Password');
254
255
        $authenticationProvider->scalarNode('custom')
256
                               ->defaultNull()
257
                               ->info('Configure custom service to use as authentication provider');
258
259
        //deprecated since v1.1 and should be deleted in v2.0
260
        $jwt = $authenticationProvider->arrayNode('jwt')
261
                                      ->setDeprecated('Use lexik_jwt instead, this provider will be removed in the next mayor release.')
262
                                      ->canBeEnabled()
263
                                      ->children();
264
265
        $jwtLogin = $jwt->arrayNode('login')->children();
266
267
        $jwtLogin->scalarNode('url')
268
                 ->info('Route name or URI to make the login process to retrieve the token.')
269
                 ->isRequired();
270
271
        $jwtLogin->scalarNode('username_parameter')
272
                 ->defaultValue('username');
273
274
        $jwtLogin->scalarNode('username_label')
275
                 ->defaultValue('Username');
276
277
        $jwtLogin->scalarNode('password_parameter')
278
                 ->defaultValue('password');
279
280
        $jwtLogin->scalarNode('password_label')
281
                 ->defaultValue('Password');
282
283
        $jwtLogin->enumNode('parameters_in')
284
                 ->values(['form', 'query', 'header'])
285
                 ->info('How pass parameters to request the token')
286
                 ->defaultValue('form');
287
288
        $jwtLogin->scalarNode('response_token_path')
289
                 ->defaultValue('token')
290
                 ->info('Where the token should be located in the response in case of JSON, set null if the response is the token.');
291
292
        $jwtRequests = $jwt->arrayNode('requests')->addDefaultsIfNotSet()->children();
293
294
        $jwtRequests->enumNode('token_in')
295
                    ->values(['query', 'header'])
296
                    ->info('Where should be located the token on every request')
297
                    ->defaultValue('header');
298
299
        $jwtRequests->scalarNode('token_name')
300
                    ->defaultValue('Authorization')
301
                    ->info('Name of the token in query or header name');
302
303
        $jwtRequests->scalarNode('token_template')
304
                    ->defaultValue('Bearer {token}')
305
                    ->info('Customize how the token should be send,  use the place holder {token} to replace for current token');
306
307
    }
308
309
    protected function configureCORS(NodeBuilder $root)
310
    {
311
        $cors = $root->arrayNode('cors')->canBeEnabled()->children();
312
        $cors->booleanNode('allow_credentials')->defaultTrue();
313
        $cors->variableNode('allow_headers')->defaultValue(['Origin', 'Content-Type', 'Accept', 'Authorization']);
314
        $cors->integerNode('max_age')->defaultValue(3600);
315
        $cors->variableNode('allow_methods')->defaultValue(['POST', 'GET', 'OPTIONS']);
316
        $cors->variableNode('allow_origins')->defaultValue(['*']);
317
    }
318
319
    protected function configurePlugins(NodeBuilder $root)
320
    {
321
        $this->configurePluginPaginationGlobalConfig($root);
322
        $this->configurePluginNamespaceGlobalConfig($root);
323
    }
324
325
    protected function configurePluginPaginationGlobalConfig(NodeBuilder $root)
326
    {
327
        $pagination = $root->arrayNode('pagination')->addDefaultsIfNotSet()->children();
328
        $pagination->integerNode('limit')
329
                   ->defaultValue(100)->info('Maximum limit allowed for all paginations');
330
    }
331
332
    protected function configurePluginNamespaceGlobalConfig(NodeBuilder $root)
333
    {
334
        $namespaces = $root->arrayNode('namespaces')
335
                           ->info(
336
                               'Group GraphQL schema using namespaced schemas. 
337
On large schemas is  helpful to keep schemas grouped by bundle and node'
338
                           )
339
                           ->canBeEnabled()
340
                           ->addDefaultsIfNotSet()
341
                           ->children();
342
343
        $bundles = $namespaces->arrayNode('bundles')
344
                              ->info('Group each bundle into a separate schema definition')
345
                              ->canBeDisabled()
346
                              ->addDefaultsIfNotSet()
347
                              ->children();
348
349
        $bundles->scalarNode('query_suffix')
350
                ->info('The following suffix will be used for bundle query groups')
351
                ->defaultValue('BundleQuery');
352
353
        $bundles->scalarNode('mutation_suffix')
354
                ->info('The following suffix will be used for bundle mutation groups')
355
                ->defaultValue('BundleMutation');
356
357
        $bundles->variableNode('ignore')
358
                ->info('The following bundles will be ignore for grouping, all definitions will be placed in the root query or mutation')
359
                ->defaultValue(['AppBundle']);
360
361
        $bundles->arrayNode('aliases')
362
                ->info(
363
                    'Define aliases for bundles to set definitions inside other desired bundle name. 
364
Can be used to group multiple bundles or publish a bundle with a different name'
365
                )
366
                ->example('SecurityBundle: AppBundle')
367
                ->useAttributeAsKey('name')
368
                ->prototype('scalar');
369
370
371
        $nodes = $namespaces->arrayNode('nodes')
372
                            ->info('Group queries and mutations of the same node into a node specific schema definition.')
373
                            ->addDefaultsIfNotSet()
374
                            ->canBeDisabled()
375
                            ->children();
376
377
        $nodes->scalarNode('query_suffix')
378
              ->info('The following suffix will be used to create the name for queries to the same node')
379
              ->defaultValue('Query');
380
381
        $nodes->scalarNode('mutation_suffix')
382
              ->info('The following suffix will be used to create the name for mutations to the same node')
383
              ->defaultValue('Mutation');
384
385
        $nodes->variableNode('ignore')
386
              ->info('The following nodes will be ignore for grouping, all definitions will be placed in the root query or mutation')
387
              ->defaultValue(['Node']);
388
389
        $nodes->arrayNode('aliases')
390
              ->info(
391
                  'Define aliases for nodes to set definitions inside other desired node name. 
392
Can be used to group multiple nodes or publish a node with a different group name'
393
              )
394
              ->example('InvoiceItem: Invoice')
395
              ->useAttributeAsKey('name')
396
              ->prototype('scalar');
397
    }
398
399
    private function configureSecurity(NodeBuilder $rootNode)
400
    {
401
        $securityNode = $rootNode
402
            ->arrayNode('security')
403
            ->canBeEnabled()
404
            ->children();
405
406
        $validationRulesNode = $securityNode
407
            ->arrayNode('validation_rules')
408
            ->addDefaultsIfNotSet()
409
            ->children();
410
        $validationRulesNode
411
            ->integerNode('query_complexity')
412
            ->info('Query complexity score before execution. (Recommended >= 200)')
413
            ->min(0)
414
            ->defaultValue(QueryComplexity::DISABLED);
415
        $validationRulesNode
416
            ->integerNode('query_depth')
417
            ->info('Max depth of the query. (Recommended >= 11)')
418
            ->min(0)
419
            ->defaultValue(QueryDepth::DISABLED);
420
        $validationRulesNode
421
            ->booleanNode('disable_introspection')
422
            ->defaultFalse();
423
    }
424
425
    private function configureOthers(NodeBuilder $rootNode)
426
    {
427
        $rootNode
428
            ->scalarNode('id_encoder')
429
            ->defaultValue(SecureIDEncoder::class)
430
            ->info('Service used to encode nodes identifiers, must implements IDEncoderInterface');
431
    }
432
433
    private function configureBCCompatibility(NodeBuilder $rootNode)
434
    {
435
        $bcNode = $rootNode
436
            ->arrayNode('bc')
437
            ->info('Backward compatibility layer to keep deprecated features during some time to upgrade API consumers progressively.')
438
            ->addDefaultsIfNotSet()
439
            ->children();
440
441
        $bcNode->variableNode('filters')
442
               ->info('Keep deprecated "filters" argument in collections')
443
               ->setDeprecated('v1.2')
444
               ->defaultFalse();
445
446
        $bcNode->variableNode('orderBy')
447
               ->info('Keep deprecated "orderBy" argument in collections')
448
               ->setDeprecated('v1.2')
449
               ->defaultFalse();
450
    }
451
}
452