Skip to content

Commit

Permalink
feature #9990 [SecurityBundle] added acl:set command (dunglas)
Browse files Browse the repository at this point in the history
This PR was merged into the 2.6-dev branch.

Discussion
----------

[SecurityBundle] added acl:set command

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | no
| License       | MIT
| Doc PR        | n/a

This new command allows to set ACL directly from the command line. This useful to quickly set up an environment and for debugging / maintenance purpose.

This PR also includes a functional test system for the ACL component. As an example, it is used to test the `acl:set` command.
The provided entity class is not mandatory (tests will still be green without it) but can be useful to test other ACL related things. I can remove it if necessary.

The instantiation of the `MaskBuilder` object is done in a separate method to be easily overridable to use a custom one (e.g. the SonataAdmin one).

Commits
-------

a702124 [SecurityBundle] added acl:set command
  • Loading branch information
fabpot committed Jun 3, 2014
2 parents e814681 + a702124 commit 4c12b7b
Show file tree
Hide file tree
Showing 8 changed files with 417 additions and 1 deletion.
1 change: 1 addition & 0 deletions composer.json
Expand Up @@ -70,6 +70,7 @@
"doctrine/data-fixtures": "1.0.*",
"doctrine/dbal": "~2.2",
"doctrine/orm": "~2.2,>=2.2.3",
"doctrine/doctrine-bundle": "~1.2",
"monolog/monolog": "~1.3",
"propel/propel1": "1.6.*",
"ircmaxell/password-compat": "1.0.*",
Expand Down
171 changes: 171 additions & 0 deletions src/Symfony/Bundle/SecurityBundle/Command/SetAclCommand.php
@@ -0,0 +1,171 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\SecurityBundle\Command;

use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Security\Acl\Domain\ObjectIdentity;
use Symfony\Component\Security\Acl\Domain\RoleSecurityIdentity;
use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity;
use Symfony\Component\Security\Acl\Exception\AclAlreadyExistsException;
use Symfony\Component\Security\Acl\Permission\MaskBuilder;
use Symfony\Component\Security\Acl\Model\MutableAclProviderInterface;

/**
* Sets ACL for objects
*
* @author Kévin Dunglas <kevin@les-tilleuls.coop>
*/
class SetAclCommand extends ContainerAwareCommand
{
/**
* {@inheritdoc}
*/
public function isEnabled()
{
if (!$this->getContainer()->has('security.acl.provider')) {
return false;
}

$provider = $this->getContainer()->get('security.acl.provider');
if (!$provider instanceof MutableAclProviderInterface) {
return false;
}

return parent::isEnabled();
}

/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setName('acl:set')
->setDescription('Sets ACL for objects')
->setHelp(<<<EOF
The <info>%command.name%</info> command sets ACL.
The ACL system must have been initialized with the <info>init:acl</info> command.
To set <comment>VIEW</comment> and <comment>EDIT</comment> permissions for the user <comment>kevin</comment> on the instance of <comment>Acme\MyClass</comment> having the identifier <comment>42</comment>:
<info>php %command.full_name% --user=Symfony/Component/Security/Core/User/User:kevin VIEW EDIT Acme/MyClass:42</info>
Note that you can use <comment>/</comment> instead of <comment>\\ </comment>for the namespace delimiter to avoid any
problem.
To set permissions for a role, use the <info>--role</info> option:
<info>php %command.full_name% --role=ROLE_USER VIEW Acme/MyClass:1936</info>
To set permissions at the class scope, use the <info>--class-scope</info> option:
<info>php %command.full_name% --class-scope --user=Symfony/Component/Security/Core/User/User:anne OWNER Acme/MyClass:42</info>
EOF
)
->addArgument('arguments', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'A list of permissions and object identities (class name and ID separated by a column)')
->addOption('user', null, InputOption::VALUE_REQUIRED, 'A list of security identities')
->addOption('role', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'A list of roles')
->addOption('class-scope', null, InputOption::VALUE_NONE, 'Use class-scope entries')
;
}

/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
// Parse arguments
$objectIdentities = array();
$maskBuilder = $this->getMaskBuilder();
foreach ($input->getArgument('arguments') as $argument) {
$data = explode(':', $argument, 2);

if (count($data) > 1) {
$objectIdentities[] = new ObjectIdentity($data[1], strtr($data[0], '/', '\\'));
} else {
$maskBuilder->add($data[0]);
}
}

// Build permissions mask
$mask = $maskBuilder->get();

$userOption = $input->getOption('user');
$roleOption = $input->getOption('role');
$classScopeOption = $input->getOption('class-scope');

if (empty($userOption) && empty($roleOption)) {
throw new \InvalidArgumentException('A Role or a User must be specified.');
}

// Create security identities
$securityIdentities = array();

if ($userOption) {
foreach ($userOption as $user) {
$data = explode(':', $user, 2);

if (count($data) === 1) {
throw new \InvalidArgumentException('The user must follow the format "Acme/MyUser:username".');
}

$securityIdentities[] = new UserSecurityIdentity($data[1], strtr($data[0], '/', '\\'));
}
}

if ($roleOption) {
foreach ($roleOption as $role) {
$securityIdentities[] = new RoleSecurityIdentity($role);
}
}

/** @var $container \Symfony\Component\DependencyInjection\ContainerInterface */
$container = $this->getContainer();
/** @var $aclProvider MutableAclProviderInterface */
$aclProvider = $container->get('security.acl.provider');

// Sets ACL
foreach ($objectIdentities as $objectIdentity) {
// Creates a new ACL if it does not already exist
try {
$aclProvider->createAcl($objectIdentity);
} catch (AclAlreadyExistsException $e) {
}

$acl = $aclProvider->findAcl($objectIdentity, $securityIdentities);

foreach ($securityIdentities as $securityIdentity) {
if ($classScopeOption) {
$acl->insertClassAce($securityIdentity, $mask);
} else {
$acl->insertObjectAce($securityIdentity, $mask);
}
}

$aclProvider->updateAcl($acl);
}
}

/**
* Gets the mask builder
*
* @return MaskBuilder
*/
protected function getMaskBuilder()
{
return new MaskBuilder();
}
}
@@ -0,0 +1,21 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AclBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

/**
* @author Kévin Dunglas <kevin@les-tilleuls.coop>
*/
class AclBundle extends Bundle
{
}
@@ -0,0 +1,22 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AclBundle\Entity;

/**
* Car
*
* @author Kévin Dunglas <kevin@les-tilleuls.coop>
*/
class Car
{
public $id;
}
@@ -0,0 +1,168 @@
<?php

namespace Symfony\Bundle\SecurityBundle\Tests\Functional;

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\SecurityBundle\Command\InitAclCommand;
use Symfony\Bundle\SecurityBundle\Command\SetAclCommand;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\Security\Acl\Domain\ObjectIdentity;
use Symfony\Component\Security\Acl\Domain\RoleSecurityIdentity;
use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity;
use Symfony\Component\Security\Acl\Exception\NoAceFoundException;
use Symfony\Component\Security\Acl\Permission\BasicPermissionMap;

/**
* Tests SetAclCommand
*
* @author Kévin Dunglas <kevin@les-tilleuls.coop>
*/
class SetAclCommandTest extends WebTestCase
{
const OBJECT_CLASS = 'Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AclBundle\Entity\Car';
const SECURITY_CLASS = 'Symfony\Component\Security\Core\User\User';

public function testSetAclUser()
{
$objectId = 1;
$securityUsername1 = 'kevin';
$securityUsername2 = 'anne';
$grantedPermission1 = 'VIEW';
$grantedPermission2 = 'EDIT';

$application = $this->getApplication();
$application->add(new SetAclCommand());

$setAclCommand = $application->find('acl:set');
$setAclCommandTester = new CommandTester($setAclCommand);
$setAclCommandTester->execute(array(
'command' => 'acl:set',
'arguments' => array($grantedPermission1, $grantedPermission2, sprintf('%s:%s', self::OBJECT_CLASS, $objectId)),
'--user' => array(sprintf('%s:%s', self::SECURITY_CLASS, $securityUsername1), sprintf('%s:%s', self::SECURITY_CLASS, $securityUsername2))
));

$objectIdentity = new ObjectIdentity($objectId, self::OBJECT_CLASS);
$securityIdentity1 = new UserSecurityIdentity($securityUsername1, self::SECURITY_CLASS);
$securityIdentity2 = new UserSecurityIdentity($securityUsername2, self::SECURITY_CLASS);
$permissionMap = new BasicPermissionMap();

/** @var \Symfony\Component\Security\Acl\Model\AclProviderInterface $aclProvider */
$aclProvider = $application->getKernel()->getContainer()->get('security.acl.provider');
$acl = $aclProvider->findAcl($objectIdentity, array($securityIdentity1));

$this->assertTrue($acl->isGranted($permissionMap->getMasks($grantedPermission1, null), array($securityIdentity1)));
$this->assertTrue($acl->isGranted($permissionMap->getMasks($grantedPermission1, null), array($securityIdentity2)));
$this->assertTrue($acl->isGranted($permissionMap->getMasks($grantedPermission2, null), array($securityIdentity2)));

try {
$acl->isGranted($permissionMap->getMasks('OWNER', null), array($securityIdentity1));
$this->fail('NoAceFoundException not throwed');
} catch (NoAceFoundException $e) {
}

try {
$acl->isGranted($permissionMap->getMasks('OPERATOR', null), array($securityIdentity2));
$this->fail('NoAceFoundException not throwed');
} catch (NoAceFoundException $e) {
}
}

public function testSetAclRole()
{
$objectId = 1;
$securityUsername = 'kevin';
$grantedPermission = 'VIEW';
$role = 'ROLE_ADMIN';

$application = $this->getApplication();
$application->add(new SetAclCommand());

$setAclCommand = $application->find('acl:set');
$setAclCommandTester = new CommandTester($setAclCommand);
$setAclCommandTester->execute(array(
'command' => 'acl:set',
'arguments' => array($grantedPermission, sprintf('%s:%s', strtr(self::OBJECT_CLASS, '\\', '/'), $objectId)),
'--role' => array($role)
));

$objectIdentity = new ObjectIdentity($objectId, self::OBJECT_CLASS);
$userSecurityIdentity = new UserSecurityIdentity($securityUsername, self::SECURITY_CLASS);
$roleSecurityIdentity = new RoleSecurityIdentity($role);
$permissionMap = new BasicPermissionMap();

/** @var \Symfony\Component\Security\Acl\Model\AclProviderInterface $aclProvider */
$aclProvider = $application->getKernel()->getContainer()->get('security.acl.provider');
$acl = $aclProvider->findAcl($objectIdentity, array($roleSecurityIdentity, $userSecurityIdentity));

$this->assertTrue($acl->isGranted($permissionMap->getMasks($grantedPermission, null), array($roleSecurityIdentity)));
$this->assertTrue($acl->isGranted($permissionMap->getMasks($grantedPermission, null), array($roleSecurityIdentity)));

try {
$acl->isGranted($permissionMap->getMasks('VIEW', null), array($userSecurityIdentity));
$this->fail('NoAceFoundException not throwed');
} catch (NoAceFoundException $e) {
}

try {
$acl->isGranted($permissionMap->getMasks('OPERATOR', null), array($userSecurityIdentity));
$this->fail('NoAceFoundException not throwed');
} catch (NoAceFoundException $e) {
}
}

public function testSetAclClassScope()
{
$objectId = 1;
$grantedPermission = 'VIEW';
$role = 'ROLE_USER';

$application = $this->getApplication();
$application->add(new SetAclCommand());

$setAclCommand = $application->find('acl:set');
$setAclCommandTester = new CommandTester($setAclCommand);
$setAclCommandTester->execute(array(
'command' => 'acl:set',
'arguments' => array($grantedPermission, sprintf('%s:%s', self::OBJECT_CLASS, $objectId)),
'--class-scope' => true,
'--role' => array($role)
));

$objectIdentity1 = new ObjectIdentity($objectId, self::OBJECT_CLASS);
$objectIdentity2 = new ObjectIdentity(2, self::OBJECT_CLASS);
$roleSecurityIdentity = new RoleSecurityIdentity($role);
$permissionMap = new BasicPermissionMap();

/** @var \Symfony\Component\Security\Acl\Model\AclProviderInterface $aclProvider */
$aclProvider = $application->getKernel()->getContainer()->get('security.acl.provider');

$acl1 = $aclProvider->findAcl($objectIdentity1, array($roleSecurityIdentity));
$this->assertTrue($acl1->isGranted($permissionMap->getMasks($grantedPermission, null), array($roleSecurityIdentity)));

$acl2 = $aclProvider->createAcl($objectIdentity2);
$this->assertTrue($acl2->isGranted($permissionMap->getMasks($grantedPermission, null), array($roleSecurityIdentity)));
}

private function getApplication()
{
$kernel = $this->createKernel(array('test_case' => 'Acl'));
$kernel->boot();

$application = new Application($kernel);
$application->add(new InitAclCommand());

$initAclCommand = $application->find('init:acl');
$initAclCommandTester = new CommandTester($initAclCommand);
$initAclCommandTester->execute(array('command' => 'init:acl'));

return $application;
}
}

0 comments on commit 4c12b7b

Please sign in to comment.