Skip to content

Commit

Permalink
Merge pull request #47 from adespresso/is-granted-for-user
Browse files Browse the repository at this point in the history
Add utility isGrantedForUser
  • Loading branch information
EmanueleMinotto authored Feb 25, 2019
2 parents 3408aee + 02365c5 commit 8f5960d
Show file tree
Hide file tree
Showing 12 changed files with 225 additions and 68 deletions.
1 change: 1 addition & 0 deletions DependencyInjection/AeFeatureExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
}
}
4 changes: 4 additions & 0 deletions DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions Resources/config/services.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
</service>
<service class="%ae_feature.security.class%" id="ae_feature.security" public="false">
<argument id="security.authorization_checker" type="service"/>
<argument id="security.token_storage" type="service"/>
<argument>%ae_feature.provider_key%</argument>
</service>
<service class="%ae_feature.feature.class%" id="ae_feature.feature">
<argument id="ae_feature.manager" type="service"/>
Expand Down
1 change: 1 addition & 0 deletions Resources/doc/configuration-reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 6 additions & 0 deletions Resources/doc/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
48 changes: 41 additions & 7 deletions Security/FeatureSecurity.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
}

/**
Expand All @@ -38,10 +58,6 @@ public function isGranted(Feature $feature)
return $feature->isEnabled();
}

if (null === $this->context) {
return false;
}

if (!$feature->isEnabled()) {
return false;
}
Expand All @@ -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;
}
}
13 changes: 13 additions & 0 deletions Service/Feature.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 <carlo@adespresso.com>
Expand Down Expand Up @@ -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);
}
}
4 changes: 4 additions & 0 deletions Service/FeatureInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Ae\FeatureBundle\Service;

use Symfony\Component\Security\Core\User\UserInterface;

/**
* @author Simone Di Maulo <simone@adespresso.com>
*/
Expand All @@ -16,4 +18,6 @@ interface FeatureInterface
* @return bool
*/
public function isGranted($name, $parent);

public function isGrantedForUser(string $name, string $parent, UserInterface $user): bool;
}
35 changes: 12 additions & 23 deletions Tests/DependencyInjection/AeFeatureExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -81,6 +58,7 @@ public function parametersProvider()
'ae_feature.twig.extension.feature.class',
FeatureExtension::class,
],
['ae_feature.provider_key', 'main'],
];
}

Expand Down Expand Up @@ -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);
}
}
113 changes: 104 additions & 9 deletions Tests/Security/FeatureSecurityTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,47 @@
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 <carlo@adespresso.com>
* @covers \Ae\FeatureBundle\Security\FeatureSecurity
*/
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');
}

/**
Expand All @@ -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
*/
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading

0 comments on commit 8f5960d

Please sign in to comment.