Skip to content

Commit 29b3461

Browse files
authored
Merge pull request api-platform#1240 from bartwesselink/feature/swagger-ui-api-key-implementation
feature: Implement SwaggerUI api key authentication functionality
2 parents 4d39fee + 77161f3 commit 29b3461

File tree

7 files changed

+221
-13
lines changed

7 files changed

+221
-13
lines changed

src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ public function load(array $configs, ContainerBuilder $container)
111111

112112
$this->registerMetadataConfiguration($container, $config, $loader);
113113
$this->registerOAuthConfiguration($container, $config, $loader);
114+
$this->registerApiKeysConfiguration($container, $config, $loader);
114115
$this->registerSwaggerConfiguration($container, $config, $loader);
115116
$this->registerJsonLdConfiguration($formats, $loader);
116117
$this->registerJsonHalConfiguration($formats, $loader);
@@ -269,6 +270,18 @@ private function registerOAuthConfiguration(ContainerBuilder $container, array $
269270
$container->setParameter('api_platform.oauth.scopes', $config['oauth']['scopes']);
270271
}
271272

273+
/**
274+
* Registers the api keys configuration.
275+
*
276+
* @param ContainerBuilder $container
277+
* @param array $config
278+
* @param XmlFileLoader $loader
279+
*/
280+
private function registerApiKeysConfiguration(ContainerBuilder $container, array $config, XmlFileLoader $loader)
281+
{
282+
$container->setParameter('api_platform.swagger.api_keys', $config['swagger']['api_keys']);
283+
}
284+
272285
/**
273286
* Registers the Swagger and Swagger UI configuration.
274287
*

src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,24 @@ public function getConfigTreeBuilder()
8686
->end()
8787
->end()
8888

89+
->arrayNode('swagger')
90+
->addDefaultsIfNotSet()
91+
->children()
92+
->arrayNode('api_keys')
93+
->addDefaultsIfNotSet()
94+
->children()
95+
->scalarNode('name')
96+
->info('The name of the header or query parameter containing the api key.')
97+
->end()
98+
->enumNode('type')
99+
->info('Whether the api key should be a query parameter or a header.')
100+
->values(['query', 'header'])
101+
->end()
102+
->end()
103+
->end()
104+
->end()
105+
->end()
106+
89107
->arrayNode('collection')
90108
->addDefaultsIfNotSet()
91109
->children()

src/Bridge/Symfony/Bundle/Resources/config/swagger.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
<argument>%api_platform.oauth.tokenUrl%</argument>
2323
<argument>%api_platform.oauth.authorizationUrl%</argument>
2424
<argument>%api_platform.oauth.scopes%</argument>
25-
<argument type="service" id="api_platform.subresource_operation_factory"></argument>
25+
<argument>%api_platform.swagger.api_keys%</argument>
26+
<argument type="service" id="api_platform.subresource_operation_factory" />
2627
<argument>%api_platform.collection.pagination.enabled%</argument>
2728
<argument>%api_platform.collection.pagination.page_parameter_name%</argument>
2829
<argument>%api_platform.collection.pagination.client_items_per_page%</argument>

src/Swagger/Serializer/DocumentationNormalizer.php

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ final class DocumentationNormalizer implements NormalizerInterface
5959
private $oauthTokenUrl;
6060
private $oauthAuthorizationUrl;
6161
private $oauthScopes;
62+
private $apiKeys;
6263
private $subresourceOperationFactory;
6364
private $paginationEnabled;
6465
private $paginationPageParameterName;
@@ -68,7 +69,7 @@ final class DocumentationNormalizer implements NormalizerInterface
6869
/**
6970
* @param ContainerInterface|FilterCollection|null $filterLocator The new filter locator or the deprecated filter collection
7071
*/
71-
public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, OperationMethodResolverInterface $operationMethodResolver, OperationPathResolverInterface $operationPathResolver, UrlGeneratorInterface $urlGenerator = null, $filterLocator = null, NameConverterInterface $nameConverter = null, $oauthEnabled = false, $oauthType = '', $oauthFlow = '', $oauthTokenUrl = '', $oauthAuthorizationUrl = '', $oauthScopes = [], SubresourceOperationFactoryInterface $subresourceOperationFactory = null, $paginationEnabled = true, $paginationPageParameterName = 'page', $clientItemsPerPage = false, $itemsPerPageParameterName = 'itemsPerPage')
72+
public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, OperationMethodResolverInterface $operationMethodResolver, OperationPathResolverInterface $operationPathResolver, UrlGeneratorInterface $urlGenerator = null, $filterLocator = null, NameConverterInterface $nameConverter = null, $oauthEnabled = false, $oauthType = '', $oauthFlow = '', $oauthTokenUrl = '', $oauthAuthorizationUrl = '', array $oauthScopes = [], array $apiKeys = [], SubresourceOperationFactoryInterface $subresourceOperationFactory = null, $paginationEnabled = true, $paginationPageParameterName = 'page', $clientItemsPerPage = false, $itemsPerPageParameterName = 'itemsPerPage')
7273
{
7374
if ($urlGenerator) {
7475
@trigger_error(sprintf('Passing an instance of %s to %s() is deprecated since version 2.1 and will be removed in 3.0.', UrlGeneratorInterface::class, __METHOD__), E_USER_DEPRECATED);
@@ -92,6 +93,8 @@ public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFa
9293
$this->subresourceOperationFactory = $subresourceOperationFactory;
9394
$this->paginationEnabled = $paginationEnabled;
9495
$this->paginationPageParameterName = $paginationPageParameterName;
96+
$this->apiKeys = $apiKeys;
97+
$this->subresourceOperationFactory = $subresourceOperationFactory;
9598
$this->clientItemsPerPage = $clientItemsPerPage;
9699
$this->itemsPerPageParameterName = $itemsPerPageParameterName;
97100
}
@@ -612,19 +615,41 @@ private function computeDoc(Documentation $documentation, \ArrayObject $definiti
612615
'paths' => $paths,
613616
];
614617

618+
$securityDefinitions = [];
619+
$security = [];
620+
615621
if ($this->oauthEnabled) {
616-
$doc['securityDefinitions'] = [
617-
'oauth' => [
618-
'type' => $this->oauthType,
619-
'description' => 'OAuth client_credentials Grant',
620-
'flow' => $this->oauthFlow,
621-
'tokenUrl' => $this->oauthTokenUrl,
622-
'authorizationUrl' => $this->oauthAuthorizationUrl,
623-
'scopes' => $this->oauthScopes,
624-
],
622+
$securityDefinitions['oauth'] = [
623+
'type' => $this->oauthType,
624+
'description' => 'OAuth client_credentials Grant',
625+
'flow' => $this->oauthFlow,
626+
'tokenUrl' => $this->oauthTokenUrl,
627+
'authorizationUrl' => $this->oauthAuthorizationUrl,
628+
'scopes' => $this->oauthScopes,
625629
];
626630

627-
$doc['security'] = [['oauth' => []]];
631+
$security[] = ['oauth' => []];
632+
}
633+
634+
if ($this->apiKeys) {
635+
foreach ($this->apiKeys as $key => $apiKey) {
636+
$name = $apiKey['name'];
637+
$type = $apiKey['type'];
638+
639+
$securityDefinitions[$key] = [
640+
'type' => 'apiKey',
641+
'in' => $type,
642+
'description' => sprintf('Value for the %s %s', $name, $type === 'query' ? sprintf('%s parameter', $type) : $type),
643+
'name' => $name,
644+
];
645+
646+
$security[] = [$key => []];
647+
}
648+
}
649+
650+
if ($securityDefinitions && $security) {
651+
$doc['securityDefinitions'] = $securityDefinitions;
652+
$doc['security'] = $security;
628653
}
629654

630655
if ('' !== $description = $documentation->getDescription()) {

tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,7 @@ private function getContainerBuilderProphecy()
471471
'api_platform.oauth.tokenUrl' => '/oauth/v2/token',
472472
'api_platform.oauth.authorizationUrl' => '/oauth/v2/auth',
473473
'api_platform.oauth.scopes' => [],
474+
'api_platform.swagger.api_keys' => [],
474475
'api_platform.enable_swagger' => true,
475476
'api_platform.enable_swagger_ui' => true,
476477
'api_platform.resource_class_directories' => Argument::type('array'),

tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ public function testDefaultConfig()
8484
'authorizationUrl' => '/oauth/v2/auth',
8585
'scopes' => [],
8686
],
87+
'swagger' => [
88+
'api_keys' => [],
89+
],
8790
'eager_loading' => [
8891
'enabled' => true,
8992
'max_joins' => 30,
@@ -208,4 +211,26 @@ public function testExceptionToStatusConfigWithInvalidHttpStatusCodeValue($inval
208211
],
209212
]);
210213
}
214+
215+
/**
216+
* Test config for api keys.
217+
*/
218+
public function testApiKeysConfig()
219+
{
220+
$exampleConfig = [
221+
'name' => 'Authorization',
222+
'type' => 'query',
223+
];
224+
225+
$config = $this->processor->processConfiguration($this->configuration, [
226+
'api_platform' => [
227+
'swagger' => [
228+
'api_keys' => $exampleConfig,
229+
],
230+
],
231+
]);
232+
233+
$this->assertTrue(isset($config['swagger']['api_keys']));
234+
$this->assertSame($exampleConfig, $config['swagger']['api_keys']);
235+
}
211236
}

tests/Swagger/Serializer/DocumentationNormalizerTest.php

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,131 @@ public function testNormalizeWithNameConverter()
424424
$this->assertEquals($expected, $normalizer->normalize($documentation));
425425
}
426426

427+
/**
428+
* @group legacy
429+
* @expectedDeprecation The use of ApiPlatform\Core\PathResolver\UnderscoreOperationPathResolver is deprecated since 2.1. Please use PathSegmentNameGenerator
430+
*/
431+
public function testNormalizeWithApiKeysEnabled()
432+
{
433+
$documentation = new Documentation(new ResourceNameCollection([Dummy::class]), 'Test API', 'This is a test API.', '1.2.3', ['jsonld' => ['application/ld+json']]);
434+
435+
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
436+
$propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->shouldBeCalled()->willReturn(new PropertyNameCollection(['name']));
437+
438+
$dummyMetadata = new ResourceMetadata('Dummy', 'This is a dummy.', null, ['get' => ['method' => 'GET']], [], []);
439+
440+
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
441+
$resourceMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn($dummyMetadata);
442+
443+
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
444+
$propertyMetadataFactoryProphecy->create(Dummy::class, 'name')->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), 'This is a name.', true, true, null, null, false));
445+
446+
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
447+
$resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true);
448+
449+
$operationMethodResolverProphecy = $this->prophesize(OperationMethodResolverInterface::class);
450+
$operationMethodResolverProphecy->getItemOperationMethod(Dummy::class, 'get')->shouldBeCalled()->willReturn('GET');
451+
452+
$operationPathResolver = new CustomOperationPathResolver(new UnderscoreOperationPathResolver());
453+
454+
$apiKeysConfiguration = [
455+
'header' => [
456+
'type' => 'header',
457+
'name' => 'Authorization',
458+
],
459+
'query' => [
460+
'type' => 'query',
461+
'name' => 'key',
462+
],
463+
];
464+
465+
$normalizer = new DocumentationNormalizer(
466+
$resourceMetadataFactoryProphecy->reveal(),
467+
$propertyNameCollectionFactoryProphecy->reveal(),
468+
$propertyMetadataFactoryProphecy->reveal(),
469+
$resourceClassResolverProphecy->reveal(),
470+
$operationMethodResolverProphecy->reveal(),
471+
$operationPathResolver,
472+
null,
473+
null,
474+
null,
475+
false,
476+
null,
477+
null,
478+
null,
479+
null,
480+
[],
481+
$apiKeysConfiguration
482+
);
483+
484+
$expected = [
485+
'swagger' => '2.0',
486+
'basePath' => '/app_dev.php/',
487+
'info' => [
488+
'title' => 'Test API',
489+
'description' => 'This is a test API.',
490+
'version' => '1.2.3',
491+
],
492+
'paths' => new \ArrayObject([
493+
'/dummies/{id}' => [
494+
'get' => new \ArrayObject([
495+
'tags' => ['Dummy'],
496+
'operationId' => 'getDummyItem',
497+
'produces' => ['application/ld+json'],
498+
'summary' => 'Retrieves a Dummy resource.',
499+
'parameters' => [
500+
[
501+
'name' => 'id',
502+
'in' => 'path',
503+
'type' => 'string',
504+
'required' => true,
505+
],
506+
],
507+
'responses' => [
508+
200 => [
509+
'description' => 'Dummy resource response',
510+
'schema' => ['$ref' => '#/definitions/Dummy'],
511+
],
512+
404 => ['description' => 'Resource not found'],
513+
],
514+
]),
515+
],
516+
]),
517+
'definitions' => new \ArrayObject([
518+
'Dummy' => new \ArrayObject([
519+
'type' => 'object',
520+
'description' => 'This is a dummy.',
521+
'properties' => [
522+
'name' => new \ArrayObject([
523+
'type' => 'string',
524+
'description' => 'This is a name.',
525+
]),
526+
],
527+
]),
528+
]),
529+
'securityDefinitions' => [
530+
'header' => [
531+
'type' => 'apiKey',
532+
'in' => 'header',
533+
'description' => 'Value for the Authorization header',
534+
'name' => 'Authorization',
535+
],
536+
'query' => [
537+
'type' => 'apiKey',
538+
'in' => 'query',
539+
'description' => 'Value for the key query parameter',
540+
'name' => 'key',
541+
],
542+
],
543+
'security' => [
544+
['header' => []],
545+
['query' => []],
546+
],
547+
];
548+
549+
$this->assertEquals($expected, $normalizer->normalize($documentation, DocumentationNormalizer::FORMAT, ['base_url' => '/app_dev.php/']));
550+
}
551+
427552
/**
428553
* @group legacy
429554
* @expectedDeprecation The use of ApiPlatform\Core\PathResolver\UnderscoreOperationPathResolver is deprecated since 2.1. Please use PathSegmentNameGenerator instead
@@ -1608,7 +1733,7 @@ public function testNormalizeWithSubResource()
16081733
$resourceClassResolverProphecy->reveal(),
16091734
$operationMethodResolverProphecy->reveal(),
16101735
$operationPathResolver,
1611-
null, null, null, false, '', '', '', '', [],
1736+
null, null, null, false, '', '', '', '', [], [],
16121737
$subresourceOperationFactory
16131738
);
16141739

0 commit comments

Comments
 (0)