Skip to content

Commit

Permalink
security implementation according OpenAPI 3.1.0 specification (#717)
Browse files Browse the repository at this point in the history
* make security implementation follow the specification

* make security the same for openapi and operation objects

* Fix styling

* handle missing security in secure  method

---------

Co-authored-by: romalytvynenko <[email protected]>
  • Loading branch information
romalytvynenko and romalytvynenko authored Feb 9, 2025
1 parent 544fa34 commit 88314d2
Show file tree
Hide file tree
Showing 7 changed files with 85 additions and 45 deletions.
25 changes: 10 additions & 15 deletions src/Support/Generator/OpenApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ class OpenApi
/** @var Path[] */
public array $paths = [];

private ?Security $defaultSecurity = null;
/** @var SecurityRequirement[]|null */
public ?array $security = [];

public function __construct(string $version)
{
Expand All @@ -38,12 +39,10 @@ public function setComponents(Components $components)

public function secure(SecurityScheme $securityScheme)
{
$securityScheme->default();

$this->components->addSecurityScheme($securityScheme->schemeName, $securityScheme);
if ($securityScheme->default) {
$this->defaultSecurity(new Security($securityScheme->schemeName));
}

$this->security ??= [];
$this->security[] = new SecurityRequirement([$securityScheme->schemeName => []]);

return $this;
}
Expand Down Expand Up @@ -79,13 +78,6 @@ public function addServer(Server $server)
return $this;
}

public function defaultSecurity(Security $security)
{
$this->defaultSecurity = $security;

return $this;
}

public function toArray()
{
$result = [
Expand All @@ -100,8 +92,11 @@ public function toArray()
);
}

if ($this->defaultSecurity) {
$result['security'] = [$this->defaultSecurity->toArray()];
if ($this->security) {
$result['security'] = array_map(
fn (SecurityRequirement $sr) => $sr->toArray(),
$this->security,
);
}

if (count($this->paths)) {
Expand Down
20 changes: 12 additions & 8 deletions src/Support/Generator/Operation.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ class Operation

public bool $deprecated = false;

/** @var array<Security|array> */
public array $security = [];
/** @var array<SecurityRequirement>|null */
public ?array $security = null;

public array $tags = [];

Expand Down Expand Up @@ -74,6 +74,11 @@ public function addResponse($response)

public function addSecurity($security)
{
if ($security === []) {
$security = new SecurityRequirement([]);
}

$this->security ??= [];
$this->security[] = $security;

return $this;
Expand Down Expand Up @@ -181,12 +186,11 @@ public function toArray()
$result['responses'] = $responses;
}

if (count($this->security)) {
$securities = [];
foreach ($this->security as $security) {
$securities[] = (object) (is_array($security) ? $security : $security->toArray());
}
$result['security'] = $securities;
if ($this->security !== null) {
$result['security'] = array_map(
fn ($s) => $s->toArray(),
$this->security,
);
}

if (count($this->servers)) {
Expand Down
20 changes: 0 additions & 20 deletions src/Support/Generator/Security.php

This file was deleted.

28 changes: 28 additions & 0 deletions src/Support/Generator/SecurityRequirement.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Dedoc\Scramble\Support\Generator;

class SecurityRequirement
{
/**
* @var array<string, string[]>
*/
private array $items = [];

public function __construct(array|string $items)
{
if (is_string($items)) { // keeping backward compatibility with synthetic Security object
$this->items[$items] = [];
} else {
$this->items = $items;
}
}

public function toArray()
{
return count($this->items) ? $this->items : (object) [];
}
}

// To keep backward compatibility
class_alias(SecurityRequirement::class, 'Dedoc\Scramble\Support\Generator\Security');
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public function handle(Operation $operation, RouteInfo $routeInfo)
->addParameters($pathParams);

if (count($routeInfo->phpDoc()->getTagsByName('@unauthenticated'))) {
$operation->addSecurity([]);
$operation->security = [];
}

$operation->setAttribute('operationId', $this->getOperationId($routeInfo));
Expand Down
33 changes: 33 additions & 0 deletions tests/OpenApiBuildersTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use Dedoc\Scramble\Support\Generator\InfoObject;
use Dedoc\Scramble\Support\Generator\OpenApi;
use Dedoc\Scramble\Support\Generator\SecurityRequirement;
use Dedoc\Scramble\Support\Generator\SecurityScheme;
use Dedoc\Scramble\Support\Generator\SecuritySchemes\OAuthFlow;

Expand Down Expand Up @@ -58,3 +59,35 @@
expect($document['components']['securitySchemes']['oauth2']['flows']['implicit']['scopes'])
->toBeObject();
});

it('allows securing with complex security rules', function () {
$openApi = (new OpenApi('3.1.0'))
->setInfo(InfoObject::make('API')->setVersion('0.0.1'));

$openApi->components->securitySchemes['tenant'] = SecurityScheme::apiKey('header', 'X-Tenant');
$openApi->components->securitySchemes['bearer'] = SecurityScheme::http('bearer');

$openApi->security[] = new SecurityRequirement([
'tenant' => [],
'bearer' => [],
]);

$serialized = $openApi->toArray();

expect($serialized['security'])->toBe([[
'tenant' => [],
'bearer' => [],
]])
->and($serialized['components']['securitySchemes'])
->toBe([
'tenant' => [
'type' => 'apiKey',
'in' => 'header',
'name' => 'X-Tenant',
],
'bearer' => [
'type' => 'http',
'scheme' => 'bearer',
],
]);
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ servers:
security:
- { apiKey: { } }
paths:
/test: { get: { operationId: controllerWithoutSecurity.index, tags: [WithoutSecurity], responses: { 200: { description: '' } }, security: [{ }] } }
/test: { get: { operationId: controllerWithoutSecurity.index, tags: [WithoutSecurity], responses: { 200: { description: '' } }, security: { } } }
components:
securitySchemes: { apiKey: { type: apiKey, in: query, name: api_token } }

0 comments on commit 88314d2

Please sign in to comment.