From 808a83f819fc787a9352ee2f5904229ca2782fc8 Mon Sep 17 00:00:00 2001 From: Marc Espiard Date: Fri, 9 Jun 2023 14:17:37 +1200 Subject: [PATCH 1/2] Add ability to specify root security and securitySchemes components --- README.md | 31 ++++++++++ config/config.php | 75 +++++++++++++++++++++++++ src/Builders/SecurityBuilder.php | 16 ++++++ src/Builders/SecuritySchemesBuilder.php | 16 ++++++ src/Commands/GenerateCommand.php | 10 ++-- src/Descriptors/Server.php | 67 +++++++++++++++++++--- src/Generator.php | 30 ++++++---- tests/Feature/OpenApiSchemaTest.php | 14 +++++ tests/TestCase.php | 13 +++++ 9 files changed, 247 insertions(+), 25 deletions(-) create mode 100644 src/Builders/SecurityBuilder.php create mode 100644 src/Builders/SecuritySchemesBuilder.php diff --git a/README.md b/README.md index 279c325..000af0b 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,37 @@ class Post extends Schema implements DescribesEndpoints } ``` +### Security schemes & requirements + +It is possible to declare security schemes and requirements for each server using our config file. +Examples of each security scheme can be found in the config file. + +`config/openapi.php`: +``` php +return [ + 'servers' => [ + 'v1' => [ + //... + + 'securitySchemes' => [ + 'MyBearerScheme' => [ + 'type' => 'http', + 'description' => 'Example scheme instructions, can be done in Markdown for long / formatted descriptions', + 'scheme' => 'bearer', + 'bearerFormat' => 'JWT', + ], + ], + + 'security' => [ + 'MyBearerScheme', + ], + + //... + ], + ], + //... +``` + ## Generating Documentation ### [Speccy](https://github.com/wework/speccy) diff --git a/config/config.php b/config/config.php index fac19ff..6981d01 100644 --- a/config/config.php +++ b/config/config.php @@ -11,6 +11,81 @@ 'description' => 'JSON:API built using Laravel', 'version' => '1.0.0', ], + + /* + * Available security schemes, each scheme needs a unique name as key of the entry + * This unique name can be used in the security array below to enable the scheme at the root level + * Examples commented below + */ + 'securitySchemes' => [ +// 'Bearer' => [ +// 'type' => 'http', +// 'description' => 'My http Scheme description', +// 'scheme' => 'bearer', +// 'bearerFormat' => 'JWT', +// ], +// 'ApiKey' => [ +// 'type' => 'apiKey', +// 'description' => 'My apiKey Scheme description', +// 'name' => 'X-API-KEY', +// 'in' => 'header', +// ], +// 'OAuth2' => [ +// 'type' => 'oauth2', +// 'description' => 'My oauth2 Scheme description', +// 'flows' => [ +// 'implicit' => [ +// 'authorizationUrl' => 'https://example.com/api/oauth/dialog', +// 'scopes' => [ +// 'write:posts' => 'modify posts in your account', +// 'read:posts' => 'read your posts', +// ], +// ], +// 'password' => [ +// 'tokenUrl' => 'https://example.com/api/oauth/token', +// 'scopes' => [ +// 'write:posts' => 'modify posts in your account', +// 'read:posts' => 'read your posts', +// ], +// ], +// 'clientCredentials' => [ +// 'tokenUrl' => 'https://example.com/api/oauth/token', +// 'scopes' => [ +// 'write:posts' => 'modify posts in your account', +// 'read:posts' => 'read your posts', +// ], +// ], +// 'authorizationCode' => [ +// 'authorizationUrl' => 'https://example.com/api/oauth/dialog', +// 'tokenUrl' => 'https://example.com/api/oauth/token', +// 'refreshUrl' => 'https://example.com/api/oauth/refresh', +// 'scopes' => [ +// 'write:posts' => 'modify posts in your account', +// 'read:posts' => 'read your posts', +// ], +// ], +// ], +// ], +// 'OpenId' => [ +// 'type' => 'openIdConnect', +// 'description' => 'My openIdConnect Scheme description', +// 'openIdConnectUrl' => 'https://example.com/api/oauth/openid', +// ], + ], + + /* + * Root level security array, each entry should be a reference to a security scheme declared above + * Examples commented below + */ + 'security' => [ +// 'Bearer', +// 'ApiKey', +// 'OAuth2' => [ +// 'write:posts', +// 'read:posts', +// ], +// 'OpenId', + ], ], ], diff --git a/src/Builders/SecurityBuilder.php b/src/Builders/SecurityBuilder.php new file mode 100644 index 0000000..e5d4279 --- /dev/null +++ b/src/Builders/SecurityBuilder.php @@ -0,0 +1,16 @@ +generator))->security(); + } +} diff --git a/src/Builders/SecuritySchemesBuilder.php b/src/Builders/SecuritySchemesBuilder.php new file mode 100644 index 0000000..a6e4ae8 --- /dev/null +++ b/src/Builders/SecuritySchemesBuilder.php @@ -0,0 +1,16 @@ +generator))->securitySchemes(); + } +} diff --git a/src/Commands/GenerateCommand.php b/src/Commands/GenerateCommand.php index 4df432f..86515e6 100644 --- a/src/Commands/GenerateCommand.php +++ b/src/Commands/GenerateCommand.php @@ -42,7 +42,7 @@ public function handle() collect($exception->getErrors()) ->map(function ($val) { return collect($val)->map(function ($val, $key) { - return sprintf('%s: %s', ucfirst($key), $val); + return sprintf('%s: %s', ucfirst($key), is_string($val) ? $val : json_encode($val)); })->join("\n"); })->each(function ($string) { $this->line($string); @@ -55,13 +55,13 @@ public function handle() /** @var \Illuminate\Filesystem\FilesystemAdapter $storageDisk */ $storageDisk = Storage::disk(config('openapi.filesystem_disk')); - $fileName = $serverKey.'_openapi.'.$format; - $filePath = str_replace(base_path().'/', '', $storageDisk->path($fileName)); + $fileName = $serverKey . '_openapi.' . $format; + $filePath = str_replace(base_path() . '/', '', $storageDisk->path($fileName)); - $this->line('Complete! '.$filePath); + $this->line('Complete! ' . $filePath); $this->newLine(); $this->line('Run the following to see your API docs'); - $this->info('speccy serve '.$filePath); + $this->info('speccy serve ' . $filePath); $this->newLine(); return 0; diff --git a/src/Descriptors/Server.php b/src/Descriptors/Server.php index d934311..dd6d123 100644 --- a/src/Descriptors/Server.php +++ b/src/Descriptors/Server.php @@ -17,9 +17,9 @@ class Server extends BaseDescriptor public function info(): Objects\Info { return Objects\Info::create() - ->title(config("openapi.servers.{$this->generator->key()}.info.title")) - ->description(config("openapi.servers.{$this->generator->key()}.info.description")) - ->version(config("openapi.servers.{$this->generator->key()}.info.version")); + ->title(config("openapi.servers.{$this->generator->key()}.info.title")) + ->description(config("openapi.servers.{$this->generator->key()}.info.description")) + ->version(config("openapi.servers.{$this->generator->key()}.info.version")); } /** @@ -32,11 +32,62 @@ public function info(): Objects\Info public function servers(): array { return [ - Objects\Server::create() - ->url('{serverUrl}') - ->variables(Objects\ServerVariable::create('serverUrl') - ->default($this->generator->server()->url()) - ), + Objects\Server::create() + ->url('{serverUrl}') + ->variables( + Objects\ServerVariable::create('serverUrl') + ->default($this->generator->server()->url()) + ), ]; } + + /** + * @return \GoldSpecDigital\ObjectOrientedOAS\Objects\SecurityScheme[] + */ + public function securitySchemes(): array + { + return collect(config("openapi.servers.{$this->generator->key()}.securitySchemes") ?? []) + ->map(function ($scheme, $key) { + return Objects\SecurityScheme::create() + ->objectId($key) + ->type($scheme['type']) + ->scheme($scheme['scheme'] ?? null) + ->bearerFormat($scheme['bearerFormat'] ?? null) + ->description($scheme['description'] ?? null) + ->name($scheme['name'] ?? null) + ->in($scheme['in'] ?? null) + ->openIdConnectUrl($scheme['openIdConnectUrl'] ?? null) + ->flows( + ...collect($scheme['flows'] ?? []) + ->map(function ($flow, $key) { + return Objects\OAuthFlow::create() + ->flow($key) + ->authorizationUrl($flow['authorizationUrl'] ?? null) + ->tokenUrl($flow['tokenUrl'] ?? null) + ->refreshUrl($flow['refreshUrl'] ?? null) + ->scopes($flow['scopes'] ?? []); + }) + ->toArray() + ); + }) + ->toArray(); + } + + /** + * @return \GoldSpecDigital\ObjectOrientedOAS\Objects\SecurityRequirement[] + */ + public function security(): array + { + return collect(config("openapi.servers.{$this->generator->key()}.security") ?? []) + ->map(function ($security, $key) { + return Objects\SecurityRequirement::create() + ->securityScheme( + is_string($key) ? $key : $security + ) + ->scopes( + ...(is_array($security) ? $security : []) + ); + }) + ->toArray(); + } } diff --git a/src/Generator.php b/src/Generator.php index 1713b1c..4f1634a 100644 --- a/src/Generator.php +++ b/src/Generator.php @@ -7,22 +7,20 @@ use LaravelJsonApi\Core\Support\AppResolver; use LaravelJsonApi\OpenApiSpec\Builders\InfoBuilder; use LaravelJsonApi\OpenApiSpec\Builders\PathsBuilder; +use LaravelJsonApi\OpenApiSpec\Builders\SecurityBuilder; +use LaravelJsonApi\OpenApiSpec\Builders\SecuritySchemesBuilder; use LaravelJsonApi\OpenApiSpec\Builders\ServerBuilder; class Generator { protected string $key; - protected Server $server; - protected InfoBuilder $infoBuilder; - protected ServerBuilder $serverBuilder; - protected PathsBuilder $pathsBuilder; - + protected SecuritySchemesBuilder $securitySchemesBuilder; + protected SecurityBuilder $securityBuilder; protected ComponentsContainer $components; - protected ResourceContainer $resources; /** @@ -44,6 +42,8 @@ public function __construct($key) $this->components = new ComponentsContainer(); $this->resources = new ResourceContainer($this->server); $this->pathsBuilder = new PathsBuilder($this, $this->components); + $this->securitySchemesBuilder = new SecuritySchemesBuilder($this); + $this->securityBuilder = new SecurityBuilder($this); } /** @@ -52,13 +52,19 @@ public function __construct($key) public function generate(): OpenApi { return OpenApi::create() - ->openapi(OpenApi::OPENAPI_3_0_2) - ->info($this->infoBuilder->build()) - ->servers(...$this->serverBuilder->build()) - ->paths(...array_values($this->pathsBuilder->build())) - ->components($this->components()->components()); + ->openapi(OpenApi::OPENAPI_3_0_2) + ->info($this->infoBuilder->build()) + ->servers(...$this->serverBuilder->build()) + ->paths(...array_values($this->pathsBuilder->build())) + ->components( + $this + ->components() + ->components() + ->securitySchemes(...array_values($this->securitySchemesBuilder->build())) + ) + ->security(...array_values($this->securityBuilder->build())); } - + /** * @return string */ diff --git a/tests/Feature/OpenApiSchemaTest.php b/tests/Feature/OpenApiSchemaTest.php index 69c269c..1e4b2f2 100644 --- a/tests/Feature/OpenApiSchemaTest.php +++ b/tests/Feature/OpenApiSchemaTest.php @@ -41,4 +41,18 @@ public function testItCreatesAnEmptyDescriptionIfASchemaDoesNotImplementTheDescr { $this->assertEquals('', $this->spec['paths']['/videos']['get']['description']); } + + public function testItCreatesSecuritySchemes() + { + $this->assertEquals('http', $this->spec['components']['securitySchemes']['Bearer']['type']); + $this->assertEquals('bearer', $this->spec['components']['securitySchemes']['Bearer']['scheme']); + $this->assertEquals('JWT', $this->spec['components']['securitySchemes']['Bearer']['bearerFormat']); + $this->assertEquals('Test Bearer description', $this->spec['components']['securitySchemes']['Bearer']['description']); + } + + public function testItCreatesSecurityEntries() + { + $this->assertArrayHasKey('Bearer', $this->spec['security'][0]); + $this->assertIsArray($this->spec['security'][0]['Bearer']); + } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 69892da..1abfa4c 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -25,6 +25,19 @@ protected function defineEnvironment($app) ], ], ]); + + $app['config']->set('openapi.servers.v1.securitySchemes', [ + 'Bearer' => [ + 'type' => 'http', + 'scheme' => 'bearer', + 'bearerFormat' => 'JWT', + 'description' => 'Test Bearer description', + ], + ]); + + $app['config']->set('openapi.servers.v1.security', [ + 'Bearer', + ]); } protected function defineRoutes($router) From cc57a976ccbfde0a5928c1bf08e89b86a36a1361 Mon Sep 17 00:00:00 2001 From: Marc Espiard Date: Fri, 9 Jun 2023 14:56:35 +1200 Subject: [PATCH 2/2] FIX security yaml output not compatible with OpenAPI spec --- src/OpenApiGenerator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenApiGenerator.php b/src/OpenApiGenerator.php index 58b0adb..737b7a2 100644 --- a/src/OpenApiGenerator.php +++ b/src/OpenApiGenerator.php @@ -22,7 +22,7 @@ public function generate(string $serverKey, string $format = 'yaml'): string $fileName = $serverKey.'_openapi.'.$format; if ($format === 'yaml') { - $output = Yaml::dump($openapi->toArray()); + $output = Yaml::dump($openapi->toArray(), 2, 4, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE); } elseif ($format === 'json') { $output = json_encode($openapi->toArray(), JSON_PRETTY_PRINT); }