diff --git a/CHANGELOG.md b/CHANGELOG.md index 658640176..c2aa40f54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,7 @@ CHANGELOG in 3.0 * deprecated the following options: + * `fos_rest.access_denied_listener` * `fos_rest.exception.exception_controller` * `fos_rest.exception.exception_listener` * `fos_rest.exception.service` @@ -75,6 +76,7 @@ CHANGELOG * `FOS\RestBundle\Controller\ExceptionController` * `FOS\RestBundle\Controller\TemplatingExceptionController` * `FOS\RestBundle\Controller\TwigExceptionController` + * `FOS\RestBundle\EventListener\AccessDeniedListener` * `FOS\RestBundle\EventListener\ExceptionListener` * `FOS\RestBundle\Inflector\DoctrineInflector` * `FOS\RestBundle\Inflector\InflectorInterface` @@ -92,6 +94,7 @@ CHANGELOG * the following services and aliases are marked as `deprecated`, they will be removed in 3.0: + * `fos_rest.access_denied_listener` * `fos_rest.exception_listener` * `fos_rest.exception.controller` * `fos_rest.exception.twig_controller` diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index fce89cb41..099102eae 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -58,6 +58,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->scalarNode('disable_csrf_role')->defaultNull()->end() ->arrayNode('access_denied_listener') ->canBeEnabled() + ->setDeprecated('The "%path%.%node%" option is deprecated since FOSRestBundle 2.8.') ->beforeNormalization() ->ifArray()->then(function ($v) { if (!empty($v) && empty($v['formats'])) { diff --git a/EventListener/AccessDeniedListener.php b/EventListener/AccessDeniedListener.php index 7f21ec605..d8ab4b8f5 100644 --- a/EventListener/AccessDeniedListener.php +++ b/EventListener/AccessDeniedListener.php @@ -11,6 +11,8 @@ namespace FOS\RestBundle\EventListener; +@trigger_error(sprintf('The %s\AccessDeniedListener class is deprecated since FOSRestBundle 2.8.', __NAMESPACE__), E_USER_DEPRECATED); + use FOS\RestBundle\FOSRestBundle; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; diff --git a/Resources/config/access_denied_listener.xml b/Resources/config/access_denied_listener.xml index 32fa61826..ca370d7cf 100644 --- a/Resources/config/access_denied_listener.xml +++ b/Resources/config/access_denied_listener.xml @@ -11,6 +11,7 @@ + The "%service_id%" service is deprecated since FOSRestBundle 2.8. diff --git a/Tests/EventListener/AccessDeniedListenerTest.php b/Tests/EventListener/AccessDeniedListenerTest.php index 7f3068ca5..04f575609 100644 --- a/Tests/EventListener/AccessDeniedListenerTest.php +++ b/Tests/EventListener/AccessDeniedListenerTest.php @@ -29,6 +29,8 @@ /** * AccessDeniedListenerTest. * + * @group legacy + * * @author Boris Guéry */ class AccessDeniedListenerTest extends TestCase diff --git a/Tests/Functional/AbstractAuthenticatorTestCase.php b/Tests/Functional/AbstractAuthenticatorTestCase.php new file mode 100644 index 000000000..43c8fab95 --- /dev/null +++ b/Tests/Functional/AbstractAuthenticatorTestCase.php @@ -0,0 +1,82 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\RestBundle\Tests\Functional; + +use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRendererInterface; + +abstract class AbstractAuthenticatorTestCase extends WebTestCase +{ + protected static $client; + + public static function setUpBeforeClass() + { + if (!interface_exists(ErrorRendererInterface::class)) { + self::markTestSkipped(); + } + + parent::setUpBeforeClass(); + + self::$client = self::createClient(['test_case' => static::getTestCase()]); + } + + public static function tearDownAfterClass() + { + self::deleteTmpDir(static::getTestCase()); + + parent::tearDownAfterClass(); + } + + public function testNoCredentialsGives401() + { + self::$client->request('POST', '/api/login', [], [], ['CONTENT_TYPE' => 'application/json']); + $response = self::$client->getResponse(); + + $this->assertEquals(401, $response->getStatusCode()); + $this->assertEquals('application/json', $response->headers->get('Content-Type')); + } + + public function testWrongCredentialsGives401() + { + $this->sendRequestContainingInvalidCredentials('/api/login'); + + $response = self::$client->getResponse(); + + $this->assertEquals(401, $response->getStatusCode()); + $this->assertEquals('application/json', $response->headers->get('Content-Type')); + } + + public function testSuccessfulLogin() + { + $this->sendRequestContainingValidCredentials('/api/login'); + + $response = self::$client->getResponse(); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('application/json', $response->headers->get('Content-Type')); + } + + public function testAccessDeniedExceptionGives403() + { + $this->sendRequestContainingValidCredentials('/api/comments'); + + $response = self::$client->getResponse(); + + $this->assertEquals(403, $response->getStatusCode()); + $this->assertEquals('application/json', $response->headers->get('Content-Type')); + } + + abstract protected static function getTestCase(): string; + + abstract protected function sendRequestContainingInvalidCredentials(string $path): void; + + abstract protected function sendRequestContainingValidCredentials(string $path): void; +} diff --git a/Tests/Functional/AccessDeniedListenerTest.php b/Tests/Functional/AccessDeniedListenerTest.php index e0d35ed21..f907ae7f7 100644 --- a/Tests/Functional/AccessDeniedListenerTest.php +++ b/Tests/Functional/AccessDeniedListenerTest.php @@ -13,6 +13,9 @@ use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRendererInterface; +/** + * @group legacy + */ class AccessDeniedListenerTest extends WebTestCase { private static $client; diff --git a/Tests/Functional/BasicAuthTest.php b/Tests/Functional/BasicAuthTest.php new file mode 100644 index 000000000..f9aa6c9be --- /dev/null +++ b/Tests/Functional/BasicAuthTest.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\RestBundle\Tests\Functional; + +class BasicAuthTest extends AbstractAuthenticatorTestCase +{ + protected static function getTestCase(): string + { + return 'BasicAuth'; + } + + protected function sendRequestContainingInvalidCredentials(string $path): void + { + self::$client->request('POST', $path, [], [], [ + 'PHP_AUTH_USER' => 'restapi', + 'PHP_AUTH_PW' => 'wrongpw', + ]); + } + + protected function sendRequestContainingValidCredentials(string $path): void + { + self::$client->request('POST', $path, [], [], [ + 'PHP_AUTH_USER' => 'restapi', + 'PHP_AUTH_PW' => 'secretpw', + ]); + } +} diff --git a/Tests/Functional/Bundle/TestBundle/Security/ApiTokenAuthenticator.php b/Tests/Functional/Bundle/TestBundle/Security/ApiTokenAuthenticator.php index 1a1532f2b..8c6afe78d 100644 --- a/Tests/Functional/Bundle/TestBundle/Security/ApiTokenAuthenticator.php +++ b/Tests/Functional/Bundle/TestBundle/Security/ApiTokenAuthenticator.php @@ -55,7 +55,7 @@ public function onAuthenticationSuccess(Request $request, TokenInterface $token, public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response { - throw new AuthenticationException('Token not valid'); + return new JsonResponse(null, 401); } /** diff --git a/Tests/Functional/CustomGuardAuthenticatorTest.php b/Tests/Functional/CustomGuardAuthenticatorTest.php new file mode 100644 index 000000000..262f4f967 --- /dev/null +++ b/Tests/Functional/CustomGuardAuthenticatorTest.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\RestBundle\Tests\Functional; + +class CustomGuardAuthenticatorTest extends AbstractAuthenticatorTestCase +{ + protected static function getTestCase(): string + { + return 'CustomGuardAuthenticator'; + } + + protected function sendRequestContainingInvalidCredentials(string $path): void + { + self::$client->request('POST', $path, [], [], ['HTTP_X-FOO' => 'BAR', 'CONTENT_TYPE' => 'application/json']); + } + + protected function sendRequestContainingValidCredentials(string $path): void + { + self::$client->request('POST', $path, [], [], ['HTTP_X-FOO' => 'FOOBAR', 'CONTENT_TYPE' => 'application/json']); + } +} diff --git a/Tests/Functional/app/BasicAuth/bundles.php b/Tests/Functional/app/BasicAuth/bundles.php new file mode 100644 index 000000000..c507cc0e7 --- /dev/null +++ b/Tests/Functional/app/BasicAuth/bundles.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +return [ + new \Symfony\Bundle\FrameworkBundle\FrameworkBundle(), + new \Symfony\Bundle\SecurityBundle\SecurityBundle(), + new \FOS\RestBundle\FOSRestBundle(), + new \FOS\RestBundle\Tests\Functional\Bundle\TestBundle\TestBundle(), +]; diff --git a/Tests/Functional/app/BasicAuth/config.yml b/Tests/Functional/app/BasicAuth/config.yml new file mode 100644 index 000000000..742075488 --- /dev/null +++ b/Tests/Functional/app/BasicAuth/config.yml @@ -0,0 +1,17 @@ +imports: + - { resource: ../config/default.yml } + - { resource: security.php } + +framework: + serializer: + enabled: true + router: { resource: "%kernel.project_dir%/BasicAuth/routing.yml" } + +fos_rest: + body_listener: false + exception: + exception_listener: false + serialize_exceptions: false + routing_loader: false + zone: + - { path: ^/api/* } diff --git a/Tests/Functional/app/BasicAuth/routing.yml b/Tests/Functional/app/BasicAuth/routing.yml new file mode 100644 index 000000000..58aa480c5 --- /dev/null +++ b/Tests/Functional/app/BasicAuth/routing.yml @@ -0,0 +1,11 @@ +api: + path: /api/comments + defaults: + _controller: FOS\RestBundle\Tests\Functional\Bundle\TestBundle\Controller\Api\CommentController::getComments + _format: json + +api_login: + path: /api/login + defaults: + _controller: FOS\RestBundle\Tests\Functional\Bundle\TestBundle\Controller\Api\CommentController::loginAction + _format: json diff --git a/Tests/Functional/app/BasicAuth/security.php b/Tests/Functional/app/BasicAuth/security.php new file mode 100644 index 000000000..b38f2505e --- /dev/null +++ b/Tests/Functional/app/BasicAuth/security.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Component\Security\Core\Security; +use Symfony\Component\Security\Http\Controller\UserValueResolver; + +$defaultFirewall = []; +if (method_exists(Security::class, 'getUser') && !class_exists(UserValueResolver::class)) { + $defaultFirewall['logout_on_user_change'] = true; +} + +$container->loadFromExtension('security', [ + 'encoders' => ['Symfony\Component\Security\Core\User\User' => 'plaintext'], + 'providers' => [ + 'in_memory' => [ + 'memory' => [ + 'users' => [ + 'restapi' => ['password' => 'secretpw', 'roles' => ['ROLE_API']], + ], + ], + ], + ], + 'firewalls' => [ + 'default' => array_merge($defaultFirewall, [ + 'provider' => 'in_memory', + 'anonymous' => 'lazy', + 'stateless' => true, + 'http_basic' => null, + ]), + ], + 'access_control' => [ + ['path' => '^/api/comments', 'roles' => 'ROLE_ADMIN'], + ['path' => '^/api', 'roles' => 'ROLE_API'], + ], +]); diff --git a/Tests/Functional/app/CustomGuardAuthenticator/bundles.php b/Tests/Functional/app/CustomGuardAuthenticator/bundles.php new file mode 100644 index 000000000..c507cc0e7 --- /dev/null +++ b/Tests/Functional/app/CustomGuardAuthenticator/bundles.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +return [ + new \Symfony\Bundle\FrameworkBundle\FrameworkBundle(), + new \Symfony\Bundle\SecurityBundle\SecurityBundle(), + new \FOS\RestBundle\FOSRestBundle(), + new \FOS\RestBundle\Tests\Functional\Bundle\TestBundle\TestBundle(), +]; diff --git a/Tests/Functional/app/CustomGuardAuthenticator/config.yml b/Tests/Functional/app/CustomGuardAuthenticator/config.yml new file mode 100644 index 000000000..f9be773e4 --- /dev/null +++ b/Tests/Functional/app/CustomGuardAuthenticator/config.yml @@ -0,0 +1,21 @@ +imports: + - { resource: ../config/default.yml } + - { resource: security.php } + +framework: + serializer: + enabled: true + router: { resource: "%kernel.project_dir%/CustomGuardAuthenticator/routing.yml" } + +fos_rest: + body_listener: false + exception: + exception_listener: false + serialize_exceptions: false + routing_loader: false + zone: + - { path: ^/api/* } + +services: + api_token_authenticator: + class: FOS\RestBundle\Tests\Functional\Bundle\TestBundle\Security\ApiTokenAuthenticator diff --git a/Tests/Functional/app/CustomGuardAuthenticator/routing.yml b/Tests/Functional/app/CustomGuardAuthenticator/routing.yml new file mode 100644 index 000000000..58aa480c5 --- /dev/null +++ b/Tests/Functional/app/CustomGuardAuthenticator/routing.yml @@ -0,0 +1,11 @@ +api: + path: /api/comments + defaults: + _controller: FOS\RestBundle\Tests\Functional\Bundle\TestBundle\Controller\Api\CommentController::getComments + _format: json + +api_login: + path: /api/login + defaults: + _controller: FOS\RestBundle\Tests\Functional\Bundle\TestBundle\Controller\Api\CommentController::loginAction + _format: json diff --git a/Tests/Functional/app/CustomGuardAuthenticator/security.php b/Tests/Functional/app/CustomGuardAuthenticator/security.php new file mode 100644 index 000000000..7039766cc --- /dev/null +++ b/Tests/Functional/app/CustomGuardAuthenticator/security.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Component\Security\Core\Security; +use Symfony\Component\Security\Http\Controller\UserValueResolver; + +$defaultFirewall = []; +if (method_exists(Security::class, 'getUser') && !class_exists(UserValueResolver::class)) { + $defaultFirewall['logout_on_user_change'] = true; +} + +$container->loadFromExtension('security', [ + 'encoders' => ['Symfony\Component\Security\Core\User\User' => 'plaintext'], + 'providers' => [ + 'in_memory' => [ + 'memory' => [], + ], + ], + 'firewalls' => [ + 'default' => array_merge($defaultFirewall, [ + 'provider' => 'in_memory', + 'anonymous' => 'lazy', + 'stateless' => true, + 'guard' => [ + 'authenticators' => [ + 'api_token_authenticator', + ], + ], + ]), + ], + 'access_control' => [ + ['path' => '^/api/comments', 'roles' => 'ROLE_ADMIN'], + ['path' => '^/api', 'roles' => 'ROLE_API'], + ], +]); diff --git a/UPGRADING-2.8.md b/UPGRADING-2.8.md index e4ffd6e8c..654721a3e 100644 --- a/UPGRADING-2.8.md +++ b/UPGRADING-2.8.md @@ -47,6 +47,7 @@ Upgrading From 2.7 To 2.8 * The following options have been deprecated: + * `fos_rest.access_denied_listener` * `fos_rest.exception.exception_controller` * `fos_rest.exception.exception_listener` * `fos_rest.exception.service` @@ -67,6 +68,7 @@ Upgrading From 2.7 To 2.8 * `FOS\RestBundle\Controller\ExceptionController` * `FOS\RestBundle\Controller\TemplatingExceptionController` * `FOS\RestBundle\Controller\TwigExceptionController` + * `FOS\RestBundle\EventListener\AccessDeniedListener` * `FOS\RestBundle\EventListener\ExceptionListener` * `FOS\RestBundle\Inflector\DoctrineInflector` * `FOS\RestBundle\Inflector\InflectorInterface` @@ -84,6 +86,7 @@ Upgrading From 2.7 To 2.8 * The following services and aliases are marked as `deprecated`, they will be removed in 3.0: + * `fos_rest.access_denied_listener` * `fos_rest.exception_listener` * `fos_rest.exception.controller` * `fos_rest.exception.twig_controller` diff --git a/UPGRADING-3.0.md b/UPGRADING-3.0.md index da88cf979..6fe598732 100644 --- a/UPGRADING-3.0.md +++ b/UPGRADING-3.0.md @@ -43,6 +43,7 @@ Upgrading From 2.x To 3.0 * The following options have been removed: + * `fos_rest.access_denied_listener` * `fos_rest.exception.exception_controller` * `fos_rest.exception.exception_listener` * `fos_rest.exception.service` @@ -63,6 +64,7 @@ Upgrading From 2.x To 3.0 * `FOS\RestBundle\Controller\ExceptionController` * `FOS\RestBundle\Controller\TemplatingExceptionController` * `FOS\RestBundle\Controller\TwigExceptionController` + * `FOS\RestBundle\EventListener\AccessDeniedListener` * `FOS\RestBundle\EventListener\ExceptionListener` * `FOS\RestBundle\Inflector\DoctrineInflector` * `FOS\RestBundle\Inflector\InflectorInterface` @@ -80,6 +82,7 @@ Upgrading From 2.x To 3.0 * The following services and aliases have been removed: + * `fos_rest.access_denied_listener` * `fos_rest.exception_listener` * `fos_rest.exception.controller` * `fos_rest.exception.twig_controller`