I wish to limit the routes displayed by the API-Platform generated OpenAPI documentation based on each route's security attribute and the logged on user's roles (i.e. only ROLE_ADMIN can see the OpenApi documentation).
A similar question was earlier asked and this answer partially answered it but not completely:
This isn't supported out of the box (but it would be a nice contribution). What you can do is to decorate the DocumentationNormalizer to unset() the paths you don't want to appear in the OpenAPI documentation.
More information: https://api-platform.com/docs/core/swagger/#overriding-the-openapi-specification
It appears that DocumentationNormalizer
is depreciated and that one should decorate OpenApiFactoryInterface
instead.
Attempting to implement, I configured config/services.yaml
to decorate OpenApiFactory as shown below.
The first issue is I am unable to "unset() the paths you don't want to appear". The paths exist within the \ApiPlatform\Core\OpenApi\Model\Paths
property of \ApiPlatform\Core\OpenApi\OpenApi
, but there is only the ability to add additional paths to Paths
and not remove them. I've come up with a solution which is shown in the below code which creates new objects and only adds back properties if they do not require admin access, but I suspect that doing so is not the "right way" to do this. Also, I just realized while it removed the documentation from SwaggerUI's routes, it did not remove it from the Schema displayed below the routes.
The second issue is how to determine which paths to display, and I temporarily hardcoded them in my getRemovedPaths()
method. First, I will need to add a logon form to the SwaggerUi page so that we know the user's role which is fairly straightforward. Next, however, I will need to obtain the security attributes associated with each route so that I could determine whether a given route should be displayed, however, I have no idea how to do so. I expected the necessary data to be in each ApiPlatform\Core\OpenApi\Model\PathItem
, however, there does not appear to be any methods to retrieve it and the properties are private. I also attempted to access the information by using \App\Kernel::getContainer()->get('router')
, but was not successful locating the route security attributes.
In summary, how should one prevent routes from being displayed by the API-Platform generated OpenAPI documentation if the user does not have authority to access the route?
config/services.yaml
services:
App\OpenApi\OpenApiFactory:
decorates: 'api_platform.openapi.factory'
arguments: [ '@App\OpenApi\OpenApiFactory.inner' ]
autoconfigure: false
App/OpenApi/OpenApiFactory
<?php
namespace App\OpenApi;
use ApiPlatform\Core\OpenApi\Factory\OpenApiFactoryInterface;
use ApiPlatform\Core\OpenApi\OpenApi;
use ApiPlatform\Core\OpenApi\Model\Paths;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use App\Kernel;
class OpenApiFactory implements OpenApiFactoryInterface {
private $decorated, $tokenStorage, $kernel;
public function __construct(OpenApiFactoryInterface $decorated, TokenStorageInterface $tokenStorage, Kernel $kernel)
{
$this->decorated = $decorated;
$this->tokenStorage = $tokenStorage;
$this->kernel = $kernel;
}
public function __invoke(array $context = []): OpenApi
{
//$this->debug($context);
$openApi = $this->decorated->__invoke($context);
$removedPaths = $this->getRemovedPaths();
$paths = new Paths;
$pathArray = $openApi->getPaths()->getPaths();
foreach($openApi->getPaths()->getPaths() as $path=>$pathItem) {
if(!isset($removedPaths[$path])) {
// No restrictions
$paths->addPath($path, $pathItem);
}
elseif($removedPaths[$path]!=='*') {
// Remove one or more operation
foreach($removedPaths[$path] as $operation) {
$method = 'with'.ucFirst($operation);
$pathItem = $pathItem->$method(null);
}
$paths->addPath($path, $pathItem);
}
// else don't add this route to the documentation
}
$openApiTest = $openApi->withPaths($paths);
return $openApi->withPaths($paths);
}
private function getRemovedPaths():array
{
/*
Instead of hardcoding removed paths, remove all paths which $user does not have access to based on the route's security attributes and the user's credentials.
This hack returns an array with the path as the key, and either "*" to remove all operations or an array to remove specific operations.
*/
$user = $this->tokenStorage->getToken()->getUser();
return [
'/guids'=>'*', // Remove all operations
'/guids/{guid}'=>'*', // Remove all operations
'/accounts'=>['post'], // Remove only post operation
'/accounts/{uuid}'=>['delete'], // Remove only delete operation
];
}
private function debug(array $context = [])
{
$this->display($context, '$context');
$openApi = $this->decorated->__invoke($context);
$this->displayGetters($openApi);
$pathObject = $openApi->getPaths();
$this->displayGetters($pathObject, null, ['getPath', 'getPaths']);
$pathsArray = $pathObject->getPaths();
$this->display($pathsArray, '$openApi->getPaths()->getPaths()', true);
$pathItem = $pathsArray['/accounts'];
$this->displayGetters($pathItem);
$getGet = $pathItem->getGet();
$this->displayGetters($getGet, '$pathItem->getGet()', ['getResponses']);
$this->display($getGet->getTags(), '$getGet->getTags()');
$this->display($getGet->getParameters(), '$getGet->getParameters()');
$this->display($getGet->getSecurity(), '$getGet->getSecurity()');
$this->display($getGet->getExtensionProperties(), '$getGet->getExtensionProperties()');
$this->displayGetters($this->kernel, null, ['getBundles', 'getBundle']);
$container = $this->kernel->getContainer();
$this->displayGetters($container, null, ['getRemovedIds', 'getParameter', 'get', 'getServiceIds']);
$router = $container->get('router');
$this->displayGetters($router, null, ['getOption']);
$routeCollection = $router->getRouteCollection();
$this->displayGetters($routeCollection, null, ['get']);
$this->displayGetters($this, '$this');
$this->displayGetters($this->decorated, '$this->decorated');
$components = $openApi->getComponents ();
$this->displayGetters($components, null, []);
}
private function displayGetters($obj, ?string $notes=null, array $exclude=[])
{
echo('-----------------------------------------------------------'.PHP_EOL);
if($notes) {
echo($notes.PHP_EOL);
}
echo(get_class($obj).PHP_EOL);
echo('get_object_vars'.PHP_EOL);
print_r(array_keys(get_object_vars($obj)));
echo('get_class_methods'.PHP_EOL);
print_r(get_class_methods($obj));
foreach(get_class_methods($obj) as $method) {
if(substr($method, 0, 3)==='get') {
if(!in_array($method, $exclude)) {
$rs = $obj->$method();
$type = gettype($rs);
switch($type) {
case 'object':
printf('type: %s path: %s method: %s'.PHP_EOL, $type, $method, get_class($rs));
print_r(get_class_methods($rs));
break;
case 'array':
printf('type: %s method: %s'.PHP_EOL, $type, $method);
print_r($rs);
break;
default:
printf('type: %s method: %s, value: %s'.PHP_EOL, $type, $method, $rs);
}
}
else {
echo('Exclude method: '.$method.PHP_EOL);
}
}
}
}
private function display($rs, string $notes, bool $keysOnly = false)
{
echo('-----------------------------------------------------------'.PHP_EOL);
echo($notes.PHP_EOL);
print_r($keysOnly?array_keys($rs):$rs);
}
}