diff --git a/DependencyInjection/AeFeatureExtension.php b/DependencyInjection/AeFeatureExtension.php index d6fd3ff..a5992c9 100644 --- a/DependencyInjection/AeFeatureExtension.php +++ b/DependencyInjection/AeFeatureExtension.php @@ -30,5 +30,6 @@ public function load(array $configs, ContainerBuilder $container) $loader->load('services.xml'); $container->setAlias('ae_feature.cache', $config['cache']); + $container->setParameter('ae_feature.provider_key', $config['provider_key']); } } diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 99cc2a5..c9a5fe7 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -20,6 +20,10 @@ public function getConfigTreeBuilder() ->scalarNode('cache') ->defaultValue('ae_feature.default_cache') ->end() + ->scalarNode('provider_key') + ->defaultValue('main') + ->cannotBeEmpty() + ->end() ->end(); return $treeBuilder; diff --git a/Resources/config/services.xml b/Resources/config/services.xml index e5dc713..ea9e5a3 100644 --- a/Resources/config/services.xml +++ b/Resources/config/services.xml @@ -18,6 +18,8 @@ + + %ae_feature.provider_key% diff --git a/Resources/doc/configuration-reference.rst b/Resources/doc/configuration-reference.rst index 7811bc2..d0e1b43 100644 --- a/Resources/doc/configuration-reference.rst +++ b/Resources/doc/configuration-reference.rst @@ -6,3 +6,4 @@ Default Bundle Configuration # app/config/config.yml ae_feature: cache: ae_feature.default_cache # default cache is of type ArrayCache + provider_key: main diff --git a/Resources/doc/usage.rst b/Resources/doc/usage.rst index ed38fdf..2d00b76 100644 --- a/Resources/doc/usage.rst +++ b/Resources/doc/usage.rst @@ -21,5 +21,11 @@ or from the service directly: throw new Exception(); } + // $user instanceof Symfony\Component\Security\Core\User\UserInterface + // after the check the previous token will be restored + if (!$featureService->isGrantedForUser('feature', 'group', $user)) { + throw new Exception(); + } + If the feature or the parent feature has at least a role, the `ae_feature.feature` service will require a token defined in the `security.token_storage`. diff --git a/Security/FeatureSecurity.php b/Security/FeatureSecurity.php index 70988c1..193d4be 100644 --- a/Security/FeatureSecurity.php +++ b/Security/FeatureSecurity.php @@ -3,7 +3,10 @@ namespace Ae\FeatureBundle\Security; use Ae\FeatureBundle\Entity\Feature; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; +use Symfony\Component\Security\Core\User\UserInterface; /** * Controls access to a Feature. @@ -13,16 +16,33 @@ class FeatureSecurity { /** - * @param AuthorizationCheckerInterface|null + * @param AuthorizationCheckerInterface */ protected $context; + /** + * @param TokenStorageInterface + */ + private $storage; + + /** + * @param string + */ + private $providerKey; + /** * @param AuthorizationCheckerInterface $context + * @param TokenStorageInterface $storage + * @param string $providerKey */ - public function __construct(AuthorizationCheckerInterface $context = null) - { + public function __construct( + AuthorizationCheckerInterface $context, + TokenStorageInterface $storage, + string $providerKey + ) { $this->context = $context; + $this->storage = $storage; + $this->providerKey = $providerKey; } /** @@ -38,10 +58,6 @@ public function isGranted(Feature $feature) return $feature->isEnabled(); } - if (null === $this->context) { - return false; - } - if (!$feature->isEnabled()) { return false; } @@ -60,4 +76,22 @@ public function isGranted(Feature $feature) return true; } + + public function isGrantedForUser(Feature $feature, UserInterface $user): bool + { + $oldToken = $this->storage->getToken(); + + $this->storage->setToken(new UsernamePasswordToken( + $user, + null, + $this->providerKey, + $user->getRoles() + )); + + $granted = $this->isGranted($feature); + + $this->storage->setToken($oldToken); + + return $granted; + } } diff --git a/Service/Feature.php b/Service/Feature.php index e492c3f..49f4bc1 100644 --- a/Service/Feature.php +++ b/Service/Feature.php @@ -2,9 +2,11 @@ namespace Ae\FeatureBundle\Service; +use Ae\FeatureBundle\Entity\Feature as FeatureEntity; use Ae\FeatureBundle\Entity\FeatureManager; use Ae\FeatureBundle\Security\FeatureSecurity; use Exception; +use Symfony\Component\Security\Core\User\UserInterface; /** * @author Carlo Forghieri @@ -51,4 +53,15 @@ public function isGranted($name, $parent) return false; } } + + public function isGrantedForUser(string $name, string $parent, UserInterface $user): bool + { + $feature = $this->manager->find($name, $parent); + + if (!$feature instanceof FeatureEntity) { + return false; + } + + return $this->security->isGrantedForUser($feature, $user); + } } diff --git a/Service/FeatureInterface.php b/Service/FeatureInterface.php index eca2d68..f0ab644 100644 --- a/Service/FeatureInterface.php +++ b/Service/FeatureInterface.php @@ -2,6 +2,8 @@ namespace Ae\FeatureBundle\Service; +use Symfony\Component\Security\Core\User\UserInterface; + /** * @author Simone Di Maulo */ @@ -16,4 +18,6 @@ interface FeatureInterface * @return bool */ public function isGranted($name, $parent); + + public function isGrantedForUser(string $name, string $parent, UserInterface $user): bool; } diff --git a/Tests/DependencyInjection/AeFeatureExtensionTest.php b/Tests/DependencyInjection/AeFeatureExtensionTest.php index 34dce35..b1f045e 100644 --- a/Tests/DependencyInjection/AeFeatureExtensionTest.php +++ b/Tests/DependencyInjection/AeFeatureExtensionTest.php @@ -26,29 +26,6 @@ protected function getContainerExtensions() ]; } - /** - * Test parameters "alias" to migrate from CreativeWeb to AdEspresso. - * - * @param string $parameterName - * @param string $expectedParameterValue - * - * @dataProvider parametersProvider - * @group legacy - */ - public function testLegacyParameters( - $parameterName, - $expectedParameterValue - ) { - $this->load(); - $this->compile(); - - $parameterName = 'cw'.substr($parameterName, 2); - $this->assertContainerBuilderHasParameter( - $parameterName, - $expectedParameterValue - ); - } - /** * Test parameters. * @@ -81,6 +58,7 @@ public function parametersProvider() 'ae_feature.twig.extension.feature.class', FeatureExtension::class, ], + ['ae_feature.provider_key', 'main'], ]; } @@ -164,4 +142,15 @@ public function testCacheProviderHasDefinedAlias() $this->assertContainerBuilderHasAlias('ae_feature.cache', 'service'); } + + public function testKeyProviderCanBeDefined() + { + $providerKey = 'test_'.sha1(mt_rand()); + + $this->load([ + 'provider_key' => $providerKey, + ]); + + $this->assertContainerBuilderHasParameter('ae_feature.provider_key', $providerKey); + } } diff --git a/Tests/Security/FeatureSecurityTest.php b/Tests/Security/FeatureSecurityTest.php index 98af58c..75205d8 100644 --- a/Tests/Security/FeatureSecurityTest.php +++ b/Tests/Security/FeatureSecurityTest.php @@ -5,7 +5,11 @@ use Ae\FeatureBundle\Entity\Feature; use Ae\FeatureBundle\Security\FeatureSecurity; use PHPUnit_Framework_TestCase; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; +use Symfony\Component\Security\Core\User\UserInterface; /** * @author Carlo Forghieri @@ -13,22 +17,35 @@ */ class FeatureSecurityTest extends PHPUnit_Framework_TestCase { - protected $security; + /** + * @var AuthorizationCheckerInterface + */ + private $context; + + /** + * @var TokenStorageInterface + */ + private $storage; + + /** + * @var FeatureSecurity + */ + private $security; protected function setUp() { - $context = $this - ->getMockBuilder(AuthorizationCheckerInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $context + $this->context = $this->createMock(AuthorizationCheckerInterface::class); + $this->storage = $this->createMock(TokenStorageInterface::class); + + $this->context ->expects($this->any()) ->method('isGranted') ->will($this->returnValueMap([ ['ROLE_USER', null, true], ['ROLE_ADMIN', null, false], ])); - $this->security = new FeatureSecurity($context); + + $this->security = new FeatureSecurity($this->context, $this->storage, 'test'); } /** @@ -39,6 +56,78 @@ public function testIsGranted($feature, $expected) $this->assertEquals($expected, $this->security->isGranted($feature)); } + /** + * @dataProvider getTokenDataProvider + */ + public function testIsGrantedForUser($token) + { + $expected = (bool) mt_rand(0, 1); + $roles = [ + 'ROLE_USER', + ]; + $providerKey = 'test'; + + $feature = $this->createMock(Feature::class); + + $security = $this + ->getMockBuilder(FeatureSecurity::class) + ->setConstructorArgs([ + $this->context, + $this->storage, + $providerKey, + ]) + ->setMethods(['isGranted']) + ->getMock(); + + $this->storage + ->expects($this->at(0)) + ->method('getToken') + ->willReturn($token); + + $user = $this->createMock(UserInterface::class); + $user + ->expects($this->once()) + ->method('getRoles') + ->willReturn($roles); + + $security + ->expects($this->once()) + ->method('isGranted') + ->with($feature) + ->willReturn($expected); + + $this->storage + ->expects($this->at(1)) + ->method('setToken') + ->with($this->callback(function ($argument) use ($user, $roles, $providerKey) { + return $argument instanceof UsernamePasswordToken + && $argument->getUser() === $user + && $argument->getProviderKey() === $providerKey; + })); + + $this->storage + ->expects($this->at(2)) + ->method('setToken') + ->with($token); + + $this->assertEquals($expected, $security->isGrantedForUser($feature, $user)); + } + + /** + * @return array + */ + public function getTokenDataProvider() + { + return [ + 'web request' => [ + $this->createMock(TokenInterface::class), + ], + 'server process' => [ + null, + ], + ]; + } + /** * @return array */ @@ -116,10 +205,13 @@ public function getFeatures() public function testFeaturesWithoutRolesEnabled() { $context = $this->createMock(AuthorizationCheckerInterface::class); + $storage = $this->createMock(TokenStorageInterface::class); + $providerKey = 'test'; + $context ->expects($this->never()) ->method('isGranted'); - $security = new FeatureSecurity($context); + $security = new FeatureSecurity($context, $storage, $providerKey); $feature = $this->createMock(Feature::class); $feature @@ -138,10 +230,13 @@ public function testFeaturesWithoutRolesEnabled() public function testFeaturesWithoutRolesDisabled() { $context = $this->createMock(AuthorizationCheckerInterface::class); + $storage = $this->createMock(TokenStorageInterface::class); + $providerKey = 'test'; + $context ->expects($this->never()) ->method('isGranted'); - $security = new FeatureSecurity($context); + $security = new FeatureSecurity($context, $storage, $providerKey); $feature = $this->createMock(Feature::class); $feature diff --git a/Tests/Security/LegacyFeatureSecurityTest.php b/Tests/Security/LegacyFeatureSecurityTest.php deleted file mode 100644 index 441541c..0000000 --- a/Tests/Security/LegacyFeatureSecurityTest.php +++ /dev/null @@ -1,29 +0,0 @@ - - * @covers \Ae\FeatureBundle\Security\FeatureSecurity - */ -class LegacyFeatureSecurityTest extends FeatureSecurityTest -{ - protected function setUp() - { - $context = $this - ->getMockBuilder(SecurityContextInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $context - ->expects($this->any()) - ->method('isGranted') - ->will($this->returnValueMap([ - ['ROLE_USER', null, true], - ['ROLE_ADMIN', null, false], - ])); - $this->security = new FeatureSecurity($context); - } -} diff --git a/Tests/Service/FeatureTest.php b/Tests/Service/FeatureTest.php index 7be5e6c..33c4f38 100644 --- a/Tests/Service/FeatureTest.php +++ b/Tests/Service/FeatureTest.php @@ -7,6 +7,7 @@ use Ae\FeatureBundle\Security\FeatureSecurity; use Ae\FeatureBundle\Service\Feature as FeatureService; use PHPUnit_Framework_TestCase; +use Symfony\Component\Security\Core\User\UserInterface; /** * @author Carlo Forghieri @@ -72,4 +73,40 @@ public function testIsGrantedFalse() $this->assertFalse($this->service->isGranted('featureB', 'group')); } + + /** + * @dataProvider isGrantedForUserDataProvider + */ + public function testIsGrantedForUser($expected, $feature) + { + $name = sha1(mt_rand()); + $parent = sha1(mt_rand()); + $user = $this->createMock(UserInterface::class); + + $this->manager + ->expects($this->once()) + ->method('find') + ->with($name, $parent) + ->willReturn($feature); + + $this->security + ->expects($expected ? $this->once() : $this->any()) + ->method('isGrantedForUser') + ->with($feature, $user) + ->willReturn($expected); + + $this->assertSame($expected, $this->service->isGrantedForUser($name, $parent, $user)); + } + + /** + * @return array + */ + public function isGrantedForUserDataProvider() + { + return [ + 'granted' => [true, $this->createMock(Feature::class)], + 'not granted' => [false, $this->createMock(Feature::class)], + 'no feature' => [false, null], + ]; + } }