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\Registry\DefinitionRegistry; |
24
|
|
|
use Ynlo\GraphQLBundle\Definition\Registry\Endpoint; |
25
|
|
|
use Ynlo\GraphQLBundle\Model\NodeInterface; |
26
|
|
|
|
27
|
|
|
class EndpointsDefinitionPlugin extends AbstractDefinitionPlugin |
28
|
|
|
{ |
29
|
|
|
/** |
30
|
|
|
* @var array |
31
|
|
|
*/ |
32
|
|
|
private $endpointAlias = []; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* @var string |
36
|
|
|
*/ |
37
|
|
|
private $endpointDefault; |
38
|
|
|
|
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* EndpointsDefinitionPlugin constructor. |
42
|
|
|
* |
43
|
|
|
* @param array $endpointsConfig |
44
|
|
|
*/ |
45
|
1 |
|
public function __construct(array $endpointsConfig) |
46
|
|
|
{ |
47
|
1 |
|
$this->endpointAlias = $endpointsConfig['alias'] ?? []; |
48
|
1 |
|
$this->endpointDefault = $endpointsConfig['default'] ?? null; |
49
|
1 |
|
} |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* {@inheritDoc} |
53
|
|
|
*/ |
54
|
1 |
|
public function buildConfig(ArrayNodeDefinition $root): void |
55
|
|
|
{ |
56
|
|
|
$root |
57
|
1 |
|
->info('List of endpoints for queries and mutations') |
58
|
1 |
|
->scalarPrototype(); |
59
|
1 |
|
} |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* {@inheritDoc} |
63
|
|
|
*/ |
64
|
1 |
|
public function normalizeConfig(DefinitionInterface $definition, $config): array |
65
|
|
|
{ |
66
|
1 |
|
$endpoints = $config['endpoints'] ?? []; |
67
|
|
|
|
68
|
|
|
//allow set only one endpoint in a simple string |
69
|
1 |
|
if (\is_string($endpoints)) { |
70
|
1 |
|
$endpoints = [$endpoints]; |
71
|
|
|
} |
72
|
|
|
|
73
|
1 |
|
return $endpoints; |
74
|
|
|
} |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* {@inheritDoc} |
78
|
|
|
*/ |
79
|
1 |
|
public function configure(DefinitionInterface $definition, Endpoint $endpoint, array $config): void |
80
|
|
|
{ |
81
|
1 |
|
if ($endpoint->getName() === DefinitionRegistry::DEFAULT_ENDPOINT) { |
82
|
1 |
|
return; |
83
|
|
|
} |
84
|
|
|
|
85
|
|
|
//apply default endpoint to operations and nodes |
86
|
1 |
|
$endpoints = $this->normalizeConfig($definition, $definition->getMeta('endpoints')); |
87
|
1 |
|
if (!$endpoints && $this->endpointDefault) { |
|
|
|
|
88
|
1 |
|
if ($definition instanceof ExecutableDefinitionInterface |
89
|
1 |
|
|| ($definition instanceof ClassAwareDefinitionInterface |
90
|
1 |
|
&& is_subclass_of($definition->getClass(), NodeInterface::class, true)) |
91
|
|
|
) { |
92
|
1 |
|
$definition->setMeta('endpoints', ['endpoints' => [$this->endpointDefault]]); |
93
|
|
|
} |
94
|
|
|
} |
95
|
1 |
|
} |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* {@inheritDoc} |
99
|
|
|
*/ |
100
|
1 |
|
public function configureEndpoint(Endpoint $endpoint): void |
101
|
|
|
{ |
102
|
1 |
|
if ($endpoint->getName() === DefinitionRegistry::DEFAULT_ENDPOINT) { |
103
|
1 |
|
return; |
104
|
|
|
} |
105
|
|
|
|
106
|
1 |
|
$forbiddenTypes = $this->getForbiddenTypes($endpoint); |
107
|
1 |
|
$this->processForbiddenTypes($endpoint, $forbiddenTypes); |
108
|
1 |
|
} |
109
|
|
|
|
110
|
1 |
|
protected function processForbiddenTypes(Endpoint $endpoint, $forbiddenTypes) |
111
|
|
|
{ |
112
|
1 |
|
foreach ($endpoint->allQueries() as $queries) { |
113
|
1 |
|
$this->secureOperations($endpoint, $queries, $forbiddenTypes); |
114
|
|
|
} |
115
|
|
|
|
116
|
1 |
|
foreach ($endpoint->allMutations() as $mutations) { |
117
|
1 |
|
$this->secureOperations($endpoint, $mutations, $forbiddenTypes); |
118
|
|
|
} |
119
|
|
|
|
120
|
1 |
|
foreach ($endpoint->allTypes() as $type) { |
121
|
|
|
//remove implementations of forbidden interfaces |
122
|
1 |
|
if ($type instanceof ImplementorInterface) { |
123
|
1 |
|
foreach ($type->getInterfaces() as $interface) { |
124
|
1 |
|
if (in_array($interface, $forbiddenTypes)) { |
125
|
1 |
|
$type->removeInterface($interface); |
126
|
|
|
} |
127
|
|
|
} |
128
|
|
|
} |
129
|
|
|
//remove fields related to forbidden interfaces |
130
|
1 |
|
if ($type instanceof FieldsAwareDefinitionInterface) { |
131
|
1 |
|
if ($type->getFields()) { |
132
|
1 |
|
foreach ($type->getFields() as $field) { |
133
|
1 |
|
$fieldType = $endpoint->hasType($field->getType()) ? $endpoint->getType($field->getType()) : null; |
134
|
1 |
|
$fieldNodeType = $endpoint->hasType($field->getMeta('node')) ? $endpoint->getType($field->getMeta('node')) : null; |
135
|
1 |
|
if (($fieldType && in_array($fieldType->getName(), $forbiddenTypes)) |
136
|
1 |
|
|| ($fieldNodeType && in_array($fieldNodeType->getName(), $forbiddenTypes))) { |
137
|
1 |
|
$type->removeField($field->getName()); |
138
|
|
|
} |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
//after delete fields related to forbidden objects, |
142
|
|
|
//verify if the object has at least one field |
143
|
|
|
//otherwise mark this type as forbidden |
144
|
1 |
|
if (!$type->getFields()) { |
145
|
|
|
$forbiddenTypes[] = $type->getName(); |
146
|
1 |
|
$this->processForbiddenTypes($endpoint, $forbiddenTypes); |
147
|
|
|
} |
148
|
|
|
} |
149
|
|
|
} |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
/** @var InterfaceDefinition $type */ |
153
|
1 |
|
foreach ($endpoint->allInterfaces() as $type) { |
154
|
1 |
|
if ($type->getImplementors()) { |
155
|
1 |
|
foreach ($type->getImplementors() as $implementor) { |
156
|
1 |
|
if (in_array($implementor, $forbiddenTypes)) { |
157
|
1 |
|
$type->removeImplementor($implementor); |
158
|
|
|
} |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
//after delete forbidden implementors |
162
|
|
|
//verify if the interface has at least one implementor |
163
|
|
|
//otherwise mark this interface as forbidden |
164
|
1 |
|
if (!$type->getImplementors()) { |
165
|
|
|
$forbiddenTypes[] = $type->getName(); |
166
|
1 |
|
$this->processForbiddenTypes($endpoint, $forbiddenTypes); |
167
|
|
|
} |
168
|
|
|
} |
169
|
|
|
} |
170
|
|
|
|
171
|
1 |
|
foreach ($forbiddenTypes as $type) { |
172
|
|
|
$endpoint->removeType($type); |
173
|
|
|
} |
174
|
1 |
|
} |
175
|
|
|
|
176
|
|
|
/** |
177
|
|
|
* Remove |
178
|
|
|
* |
179
|
|
|
* @param Endpoint $endpoint |
180
|
|
|
* @param ExecutableDefinitionInterface $executableDefinition |
181
|
|
|
* @param array|string[] $forbiddenTypes |
182
|
|
|
*/ |
183
|
1 |
|
protected function secureOperations(Endpoint $endpoint, ExecutableDefinitionInterface $executableDefinition, $forbiddenTypes) |
184
|
|
|
{ |
185
|
1 |
|
$type = $endpoint->hasType($executableDefinition->getType()) ? $endpoint->getType($executableDefinition->getType()) : null; |
186
|
|
|
|
187
|
1 |
|
$node = null; |
188
|
|
|
//resolve the related node using interface |
189
|
1 |
|
if ($executableDefinition instanceof NodeAwareDefinitionInterface) { |
|
|
|
|
190
|
1 |
|
$node = $endpoint->hasType($executableDefinition->getNode()) ? $endpoint->getType($executableDefinition->getNode()) : null; |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
//resolve related node using metadata |
194
|
1 |
|
if (!$node) { |
195
|
|
|
$node = $endpoint->hasType($executableDefinition->getMeta('node')) ? $endpoint->getType($executableDefinition->getMeta('node')) : null; |
196
|
|
|
} |
197
|
|
|
|
198
|
1 |
|
$granted = true; |
199
|
1 |
|
if (($type && in_array($type->getName(), $forbiddenTypes)) |
200
|
1 |
|
|| ($node && in_array($node->getName(), $forbiddenTypes))) { |
201
|
|
|
$granted = false; |
202
|
1 |
|
} elseif (!$this->isGranted($endpoint, $executableDefinition)) { |
203
|
1 |
|
$granted = false; |
204
|
|
|
} |
205
|
|
|
|
206
|
1 |
|
if (!$granted) { |
207
|
1 |
|
if ($executableDefinition instanceof MutationDefinition) { |
208
|
|
|
$endpoint->removeMutation($executableDefinition->getName()); |
209
|
|
|
} else { |
210
|
1 |
|
$endpoint->removeQuery($executableDefinition->getName()); |
211
|
|
|
} |
212
|
|
|
} |
213
|
1 |
|
} |
214
|
|
|
|
215
|
1 |
|
protected function getForbiddenTypes(Endpoint $endpoint) |
216
|
|
|
{ |
217
|
1 |
|
$forbiddenTypes = []; |
218
|
1 |
|
foreach ($endpoint->allTypes() as $type) { |
219
|
1 |
|
if (!$this->isGranted($endpoint, $type)) { |
220
|
1 |
|
$forbiddenTypes[] = $type->getName(); |
221
|
|
|
} |
222
|
|
|
} |
223
|
|
|
|
224
|
1 |
|
return $forbiddenTypes; |
225
|
|
|
} |
226
|
|
|
|
227
|
1 |
|
protected function isGranted(Endpoint $endpoint, DefinitionInterface $definition) |
228
|
|
|
{ |
229
|
1 |
|
$endpoints = $this->normalizeConfig($definition, $definition->getMeta('endpoints', [])); |
230
|
1 |
|
if ($endpoints) { |
|
|
|
|
231
|
1 |
|
foreach ($endpoints as $index => $endpointName) { |
232
|
1 |
|
foreach ($this->endpointAlias as $alias => $targets) { |
233
|
|
|
if ($alias === $endpointName) { |
234
|
|
|
unset($endpoints[$index]); |
235
|
1 |
|
$endpoints = array_merge($endpoints, $targets); |
236
|
|
|
} |
237
|
|
|
} |
238
|
|
|
} |
239
|
|
|
} |
240
|
|
|
|
241
|
1 |
|
return empty($endpoints) || \in_array($endpoint->getName(), $endpoints); |
242
|
|
|
} |
243
|
|
|
} |
244
|
|
|
|
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.