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\Definition\Plugin; |
13
|
|
|
|
14
|
|
|
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; |
15
|
|
|
use Ynlo\GraphQLBundle\Definition\ClassAwareDefinitionInterface; |
16
|
|
|
use Ynlo\GraphQLBundle\Definition\DefinitionInterface; |
17
|
|
|
use Ynlo\GraphQLBundle\Definition\ExecutableDefinitionInterface; |
18
|
|
|
use Ynlo\GraphQLBundle\Definition\FieldsAwareDefinitionInterface; |
19
|
|
|
use Ynlo\GraphQLBundle\Definition\ImplementorInterface; |
20
|
|
|
use Ynlo\GraphQLBundle\Definition\InterfaceDefinition; |
21
|
|
|
use Ynlo\GraphQLBundle\Definition\MutationDefinition; |
22
|
|
|
use Ynlo\GraphQLBundle\Definition\NodeAwareDefinitionInterface; |
23
|
|
|
use Ynlo\GraphQLBundle\Definition\QueryDefinition; |
24
|
|
|
use Ynlo\GraphQLBundle\Definition\Registry\DefinitionRegistry; |
25
|
|
|
use Ynlo\GraphQLBundle\Definition\Registry\Endpoint; |
26
|
|
|
use Ynlo\GraphQLBundle\Definition\SubscriptionDefinition; |
27
|
|
|
use Ynlo\GraphQLBundle\Definition\UnionDefinition; |
28
|
|
|
use Ynlo\GraphQLBundle\Model\NodeInterface; |
29
|
|
|
|
30
|
|
|
class EndpointsDefinitionPlugin extends AbstractDefinitionPlugin |
31
|
|
|
{ |
32
|
|
|
/** |
33
|
|
|
* @var array |
34
|
|
|
*/ |
35
|
|
|
private $endpointAlias = []; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* @var string |
39
|
|
|
*/ |
40
|
|
|
private $endpointDefault; |
41
|
|
|
|
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* EndpointsDefinitionPlugin constructor. |
45
|
|
|
* |
46
|
|
|
* @param array $endpointsConfig |
47
|
|
|
*/ |
48
|
3 |
|
public function __construct(array $endpointsConfig) |
49
|
|
|
{ |
50
|
3 |
|
$this->endpointAlias = $endpointsConfig['alias'] ?? []; |
51
|
3 |
|
$this->endpointDefault = $endpointsConfig['default'] ?? null; |
52
|
3 |
|
} |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* {@inheritDoc} |
56
|
|
|
*/ |
57
|
|
|
public function buildConfig(ArrayNodeDefinition $root): void |
58
|
|
|
{ |
59
|
|
|
$root |
60
|
|
|
->info('List of endpoints for queries and mutations') |
61
|
|
|
->scalarPrototype(); |
62
|
|
|
} |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* {@inheritDoc} |
66
|
|
|
*/ |
67
|
3 |
|
public function normalizeConfig(DefinitionInterface $definition, $config): array |
68
|
|
|
{ |
69
|
3 |
|
$endpoints = $config['endpoints'] ?? []; |
70
|
|
|
|
71
|
|
|
//allow set only one endpoint in a simple string |
72
|
3 |
|
if (\is_string($endpoints)) { |
73
|
2 |
|
$endpoints = [$endpoints]; |
74
|
|
|
} |
75
|
|
|
|
76
|
3 |
|
return $endpoints; |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
* {@inheritDoc} |
81
|
|
|
*/ |
82
|
1 |
|
public function configure(DefinitionInterface $definition, Endpoint $endpoint, array $config): void |
83
|
|
|
{ |
84
|
1 |
|
if ($endpoint->getName() === DefinitionRegistry::DEFAULT_ENDPOINT) { |
85
|
|
|
return; |
86
|
|
|
} |
87
|
|
|
|
88
|
|
|
//ignore safe operations |
89
|
1 |
|
if (\in_array($definition->getName(), ['node', 'nodes'])) { |
90
|
|
|
return; |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
//apply default endpoint to all operations |
94
|
1 |
|
$endpoints = $this->normalizeConfig($definition, $definition->getMeta('endpoints')); |
95
|
1 |
|
if (!$endpoints && $this->endpointDefault) { |
|
|
|
|
96
|
1 |
|
if ($definition instanceof QueryDefinition |
97
|
1 |
|
|| ($definition instanceof ClassAwareDefinitionInterface |
98
|
1 |
|
&& is_subclass_of($definition->getClass(), NodeInterface::class, true)) |
99
|
|
|
) { |
100
|
1 |
|
$definition->setMeta('endpoints', ['endpoints' => [$this->endpointDefault]]); |
101
|
|
|
} |
102
|
|
|
} |
103
|
1 |
|
} |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* {@inheritDoc} |
107
|
|
|
*/ |
108
|
1 |
|
public function configureEndpoint(Endpoint $endpoint): void |
109
|
|
|
{ |
110
|
1 |
|
if ($endpoint->getName() === DefinitionRegistry::DEFAULT_ENDPOINT) { |
111
|
|
|
return; |
112
|
|
|
} |
113
|
|
|
|
114
|
1 |
|
$forbiddenTypes = $this->getForbiddenTypes($endpoint); |
115
|
1 |
|
$this->processForbiddenTypes($endpoint, $forbiddenTypes); |
116
|
1 |
|
} |
117
|
|
|
|
118
|
1 |
|
public function isGranted(Endpoint $endpoint, DefinitionInterface $definition) |
119
|
|
|
{ |
120
|
1 |
|
$endpoints = $this->normalizeConfig($definition, $definition->getMeta('endpoints', [])); |
121
|
1 |
|
if ($endpoints) { |
|
|
|
|
122
|
1 |
|
$endpoints = $this->endpointsAliasToRealNames($endpoints); |
123
|
|
|
} |
124
|
|
|
|
125
|
1 |
|
return empty($endpoints) || \in_array($endpoint->getName(), $endpoints); |
126
|
|
|
} |
127
|
|
|
|
128
|
1 |
|
protected function processForbiddenTypes(Endpoint $endpoint, $forbiddenTypes) |
129
|
|
|
{ |
130
|
1 |
|
foreach ($endpoint->allQueries() as $queries) { |
131
|
1 |
|
$this->secureOperations($endpoint, $queries, $forbiddenTypes); |
132
|
|
|
} |
133
|
|
|
|
134
|
1 |
|
foreach ($endpoint->allMutations() as $mutations) { |
135
|
1 |
|
$this->secureOperations($endpoint, $mutations, $forbiddenTypes); |
136
|
|
|
} |
137
|
|
|
|
138
|
1 |
|
foreach ($endpoint->allSubscriptions() as $subscriptions) { |
139
|
|
|
$this->secureOperations($endpoint, $subscriptions, $forbiddenTypes); |
140
|
|
|
} |
141
|
|
|
|
142
|
1 |
|
foreach ($endpoint->allTypes() as $type) { |
143
|
|
|
//remove implementations of forbidden interfaces |
144
|
1 |
|
if ($type instanceof ImplementorInterface) { |
145
|
1 |
|
foreach ($type->getInterfaces() as $interface) { |
146
|
1 |
|
if (in_array($interface, $forbiddenTypes)) { |
147
|
1 |
|
$type->removeInterface($interface); |
148
|
|
|
} |
149
|
|
|
} |
150
|
|
|
} |
151
|
|
|
|
152
|
1 |
|
if ($type instanceof UnionDefinition) { |
153
|
|
|
foreach ($type->getTypes() as $subType) { |
154
|
|
|
//remove union sub-type related to forbidden type |
155
|
|
|
$fieldType = $endpoint->hasType($subType->getType()) ? $endpoint->getType($subType->getType()) : null; |
156
|
|
|
if (($fieldType && in_array($fieldType->getName(), $forbiddenTypes))) { |
157
|
|
|
$type->removeType($subType->getType()); |
158
|
|
|
} |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
//after delete sub-types related to forbidden objects, |
162
|
|
|
//verify if the union has at least one sub-type |
163
|
|
|
//otherwise mark this type as forbidden |
164
|
|
|
if (!$type->getTypes()) { |
165
|
|
|
$forbiddenTypes[] = $type->getName(); |
166
|
|
|
$this->processForbiddenTypes($endpoint, $forbiddenTypes); |
167
|
|
|
} |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
//remove fields related to forbidden interfaces |
171
|
1 |
|
if ($type instanceof FieldsAwareDefinitionInterface) { |
172
|
1 |
|
if ($type->getFields()) { |
173
|
1 |
|
foreach ($type->getFields() as $field) { |
174
|
|
|
//remove forbidden field |
175
|
1 |
|
if (!$this->isGranted($endpoint, $field)) { |
176
|
1 |
|
$type->removeField($field->getName()); |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
//remove field related to forbidden type |
180
|
1 |
|
$fieldType = $endpoint->hasType($field->getType()) ? $endpoint->getType($field->getType()) : null; |
181
|
1 |
|
$fieldNodeType = $endpoint->hasType($field->getMeta('node')) ? $endpoint->getType($field->getMeta('node')) : null; |
182
|
1 |
|
if (($fieldType && in_array($fieldType->getName(), $forbiddenTypes)) |
183
|
1 |
|
|| ($fieldNodeType && in_array($fieldNodeType->getName(), $forbiddenTypes))) { |
184
|
1 |
|
$type->removeField($field->getName()); |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
// remove field inherited from forbidden interface |
188
|
1 |
|
if ($field->getInheritedFrom()) { |
189
|
1 |
|
$inheritedFromArray = $field->getInheritedFrom(); |
190
|
1 |
|
foreach ($field->getInheritedFrom() as $index => $inheritedFrom) { |
191
|
1 |
|
if (in_array($inheritedFrom, $forbiddenTypes, true)) { |
192
|
|
|
// remove inheritance |
193
|
1 |
|
unset($inheritedFromArray[$index]); |
194
|
|
|
} |
195
|
|
|
} |
196
|
1 |
|
$field->setInheritedFrom($inheritedFromArray); |
197
|
|
|
// if all parents are removed, remove the field |
198
|
1 |
|
if (empty($inheritedFromArray)) { |
199
|
1 |
|
$type->removeField($field->getName()); |
200
|
|
|
} |
201
|
|
|
} |
202
|
|
|
|
203
|
1 |
|
foreach ($field->getArguments() as $argument) { |
204
|
|
|
//remove forbidden argument |
205
|
1 |
|
if (!$this->isGranted($endpoint, $argument)) { |
206
|
|
|
$field->removeArgument($argument->getName()); |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
//remove argument related to forbidden type |
210
|
1 |
|
$argumentType = $endpoint->hasType($argument->getType()) ? $endpoint->getType($argument->getType()) : null; |
211
|
1 |
|
if ($argumentType && \in_array($argumentType->getName(), $forbiddenTypes, true)) { |
212
|
|
|
$field->removeArgument($argument->getName()); |
213
|
|
|
} |
214
|
|
|
} |
215
|
|
|
} |
216
|
|
|
|
217
|
|
|
//after delete fields related to forbidden objects, |
218
|
|
|
//verify if the object has at least one field |
219
|
|
|
//otherwise mark this type as forbidden |
220
|
1 |
|
if (!$type->getFields()) { |
221
|
1 |
|
$forbiddenTypes[] = $type->getName(); |
222
|
1 |
|
$this->processForbiddenTypes($endpoint, $forbiddenTypes); |
223
|
|
|
} |
224
|
|
|
} |
225
|
|
|
} |
226
|
|
|
} |
227
|
|
|
|
228
|
|
|
/** @var InterfaceDefinition $type */ |
229
|
1 |
|
foreach ($endpoint->allInterfaces() as $type) { |
230
|
1 |
|
if ($type->getImplementors()) { |
231
|
1 |
|
foreach ($type->getImplementors() as $implementor) { |
232
|
1 |
|
if (in_array($implementor, $forbiddenTypes)) { |
233
|
1 |
|
$type->removeImplementor($implementor); |
234
|
|
|
} |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
//after delete forbidden implementors |
238
|
|
|
//verify if the interface has at least one implementor |
239
|
|
|
//otherwise mark this interface as forbidden |
240
|
1 |
|
if (!$type->getImplementors()) { |
241
|
1 |
|
$forbiddenTypes[] = $type->getName(); |
242
|
1 |
|
$this->processForbiddenTypes($endpoint, $forbiddenTypes); |
243
|
|
|
} |
244
|
|
|
} |
245
|
|
|
} |
246
|
|
|
|
247
|
1 |
|
foreach ($forbiddenTypes as $type) { |
248
|
1 |
|
$endpoint->removeType($type); |
249
|
|
|
} |
250
|
1 |
|
} |
251
|
|
|
|
252
|
|
|
/** |
253
|
|
|
* Remove |
254
|
|
|
* |
255
|
|
|
* @param Endpoint $endpoint |
256
|
|
|
* @param ExecutableDefinitionInterface $executableDefinition |
257
|
|
|
* @param array|string[] $forbiddenTypes |
258
|
|
|
*/ |
259
|
1 |
|
protected function secureOperations(Endpoint $endpoint, ExecutableDefinitionInterface $executableDefinition, $forbiddenTypes) |
260
|
|
|
{ |
261
|
1 |
|
$type = $endpoint->hasType($executableDefinition->getType()) ? $endpoint->getType($executableDefinition->getType()) : null; |
262
|
|
|
|
263
|
1 |
|
$node = null; |
264
|
|
|
//resolve the related node using interface |
265
|
1 |
|
if ($executableDefinition instanceof NodeAwareDefinitionInterface) { |
|
|
|
|
266
|
1 |
|
$node = $endpoint->hasType($executableDefinition->getNode()) ? $endpoint->getType($executableDefinition->getNode()) : null; |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
//resolve related node using metadata |
270
|
1 |
|
if (!$node) { |
271
|
1 |
|
$node = $endpoint->hasType($executableDefinition->getMeta('node')) ? $endpoint->getType($executableDefinition->getMeta('node')) : null; |
272
|
|
|
} |
273
|
|
|
|
274
|
1 |
|
$granted = true; |
275
|
1 |
|
$endpoints = $this->normalizeConfig($executableDefinition, $executableDefinition->getMeta('endpoints', [])); |
276
|
|
|
|
277
|
|
|
//if the operation has endpoints defined use that, |
278
|
|
|
//otherwise check by related type and node |
279
|
1 |
|
if ($endpoints) { |
|
|
|
|
280
|
1 |
|
$granted = $this->isGranted($endpoint, $executableDefinition); |
281
|
1 |
|
} elseif (($type && \in_array($type->getName(), $forbiddenTypes, true)) |
282
|
1 |
|
|| ($node && \in_array($node->getName(), $forbiddenTypes, true))) { |
283
|
1 |
|
$granted = false; |
284
|
|
|
} |
285
|
|
|
|
286
|
1 |
|
if (!$granted) { |
287
|
1 |
|
if ($executableDefinition instanceof MutationDefinition) { |
288
|
1 |
|
$endpoint->removeMutation($executableDefinition->getName()); |
289
|
1 |
|
} elseif ($executableDefinition instanceof SubscriptionDefinition) { |
290
|
|
|
$endpoint->removeSubscription($executableDefinition->getName()); |
291
|
|
|
} else { |
292
|
1 |
|
$endpoint->removeQuery($executableDefinition->getName()); |
293
|
|
|
} |
294
|
|
|
} else { |
295
|
1 |
|
foreach ($executableDefinition->getArguments() as $argument) { |
296
|
|
|
//remove forbidden argument |
297
|
|
|
if (!$this->isGranted($endpoint, $argument)) { |
298
|
|
|
$executableDefinition->removeArgument($argument->getName()); |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
//remove argument related to forbidden type |
302
|
|
|
$argumentType = $endpoint->hasType($argument->getType()) ? $endpoint->getType($argument->getType()) : null; |
303
|
|
|
if ($argumentType && \in_array($argumentType->getName(), $forbiddenTypes, true)) { |
304
|
|
|
$executableDefinition->removeArgument($argument->getName()); |
305
|
|
|
} |
306
|
|
|
} |
307
|
|
|
} |
308
|
1 |
|
} |
309
|
|
|
|
310
|
1 |
|
protected function getForbiddenTypes(Endpoint $endpoint) |
311
|
|
|
{ |
312
|
1 |
|
$forbiddenTypes = []; |
313
|
1 |
|
foreach ($endpoint->allTypes() as $type) { |
314
|
1 |
|
if (!$this->isGranted($endpoint, $type)) { |
315
|
1 |
|
$forbiddenTypes[] = $type->getName(); |
316
|
|
|
} |
317
|
|
|
} |
318
|
|
|
|
319
|
1 |
|
return $forbiddenTypes; |
320
|
|
|
} |
321
|
|
|
|
322
|
|
|
/** |
323
|
|
|
* Given array of endpoints (containing alias) return the array of specific endpoints (without aliases) |
324
|
|
|
* |
325
|
|
|
* ["all"] => ["admin", "frontend"] |
326
|
|
|
* |
327
|
|
|
* @param array $endpoints |
328
|
|
|
* |
329
|
|
|
* @return array |
330
|
|
|
*/ |
331
|
1 |
|
protected function endpointsAliasToRealNames($endpoints) |
332
|
|
|
{ |
333
|
1 |
|
foreach ($endpoints as $index => $endpointName) { |
334
|
1 |
|
foreach ($this->endpointAlias as $alias => $targets) { |
335
|
1 |
|
if ($alias === $endpointName) { |
336
|
1 |
|
$targets = $this->endpointsAliasToRealNames($targets); |
337
|
1 |
|
unset($endpoints[$index]); |
338
|
1 |
|
$endpoints = array_merge($endpoints, $targets); |
339
|
|
|
} |
340
|
|
|
} |
341
|
|
|
} |
342
|
|
|
|
343
|
1 |
|
return $endpoints; |
344
|
|
|
} |
345
|
|
|
} |
346
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.