Skip to content

Commit b86eede

Browse files
committed
feature #9990 [SecurityBundle] added acl:set command (dunglas)
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
2 parents 62ae756 + cac83a8 commit b86eede

File tree

7 files changed

+416
-1
lines changed

7 files changed

+416
-1
lines changed

Command/SetAclCommand.php

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\SecurityBundle\Command;
13+
14+
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
15+
use Symfony\Component\Console\Input\InputArgument;
16+
use Symfony\Component\Console\Input\InputInterface;
17+
use Symfony\Component\Console\Input\InputOption;
18+
use Symfony\Component\Console\Output\OutputInterface;
19+
use Symfony\Component\Security\Acl\Domain\ObjectIdentity;
20+
use Symfony\Component\Security\Acl\Domain\RoleSecurityIdentity;
21+
use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity;
22+
use Symfony\Component\Security\Acl\Exception\AclAlreadyExistsException;
23+
use Symfony\Component\Security\Acl\Permission\MaskBuilder;
24+
use Symfony\Component\Security\Acl\Model\MutableAclProviderInterface;
25+
26+
/**
27+
* Sets ACL for objects
28+
*
29+
* @author Kévin Dunglas <[email protected]>
30+
*/
31+
class SetAclCommand extends ContainerAwareCommand
32+
{
33+
/**
34+
* {@inheritdoc}
35+
*/
36+
public function isEnabled()
37+
{
38+
if (!$this->getContainer()->has('security.acl.provider')) {
39+
return false;
40+
}
41+
42+
$provider = $this->getContainer()->get('security.acl.provider');
43+
if (!$provider instanceof MutableAclProviderInterface) {
44+
return false;
45+
}
46+
47+
return parent::isEnabled();
48+
}
49+
50+
/**
51+
* {@inheritdoc}
52+
*/
53+
protected function configure()
54+
{
55+
$this
56+
->setName('acl:set')
57+
->setDescription('Sets ACL for objects')
58+
->setHelp(<<<EOF
59+
The <info>%command.name%</info> command sets ACL.
60+
The ACL system must have been initialized with the <info>init:acl</info> command.
61+
62+
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>:
63+
64+
<info>php %command.full_name% --user=Symfony/Component/Security/Core/User/User:kevin VIEW EDIT Acme/MyClass:42</info>
65+
66+
Note that you can use <comment>/</comment> instead of <comment>\\ </comment>for the namespace delimiter to avoid any
67+
problem.
68+
69+
To set permissions for a role, use the <info>--role</info> option:
70+
71+
<info>php %command.full_name% --role=ROLE_USER VIEW Acme/MyClass:1936</info>
72+
73+
To set permissions at the class scope, use the <info>--class-scope</info> option:
74+
75+
<info>php %command.full_name% --class-scope --user=Symfony/Component/Security/Core/User/User:anne OWNER Acme/MyClass:42</info>
76+
EOF
77+
)
78+
->addArgument('arguments', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'A list of permissions and object identities (class name and ID separated by a column)')
79+
->addOption('user', null, InputOption::VALUE_REQUIRED, 'A list of security identities')
80+
->addOption('role', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'A list of roles')
81+
->addOption('class-scope', null, InputOption::VALUE_NONE, 'Use class-scope entries')
82+
;
83+
}
84+
85+
/**
86+
* {@inheritdoc}
87+
*/
88+
protected function execute(InputInterface $input, OutputInterface $output)
89+
{
90+
// Parse arguments
91+
$objectIdentities = array();
92+
$maskBuilder = $this->getMaskBuilder();
93+
foreach ($input->getArgument('arguments') as $argument) {
94+
$data = explode(':', $argument, 2);
95+
96+
if (count($data) > 1) {
97+
$objectIdentities[] = new ObjectIdentity($data[1], strtr($data[0], '/', '\\'));
98+
} else {
99+
$maskBuilder->add($data[0]);
100+
}
101+
}
102+
103+
// Build permissions mask
104+
$mask = $maskBuilder->get();
105+
106+
$userOption = $input->getOption('user');
107+
$roleOption = $input->getOption('role');
108+
$classScopeOption = $input->getOption('class-scope');
109+
110+
if (empty($userOption) && empty($roleOption)) {
111+
throw new \InvalidArgumentException('A Role or a User must be specified.');
112+
}
113+
114+
// Create security identities
115+
$securityIdentities = array();
116+
117+
if ($userOption) {
118+
foreach ($userOption as $user) {
119+
$data = explode(':', $user, 2);
120+
121+
if (count($data) === 1) {
122+
throw new \InvalidArgumentException('The user must follow the format "Acme/MyUser:username".');
123+
}
124+
125+
$securityIdentities[] = new UserSecurityIdentity($data[1], strtr($data[0], '/', '\\'));
126+
}
127+
}
128+
129+
if ($roleOption) {
130+
foreach ($roleOption as $role) {
131+
$securityIdentities[] = new RoleSecurityIdentity($role);
132+
}
133+
}
134+
135+
/** @var $container \Symfony\Component\DependencyInjection\ContainerInterface */
136+
$container = $this->getContainer();
137+
/** @var $aclProvider MutableAclProviderInterface */
138+
$aclProvider = $container->get('security.acl.provider');
139+
140+
// Sets ACL
141+
foreach ($objectIdentities as $objectIdentity) {
142+
// Creates a new ACL if it does not already exist
143+
try {
144+
$aclProvider->createAcl($objectIdentity);
145+
} catch (AclAlreadyExistsException $e) {
146+
}
147+
148+
$acl = $aclProvider->findAcl($objectIdentity, $securityIdentities);
149+
150+
foreach ($securityIdentities as $securityIdentity) {
151+
if ($classScopeOption) {
152+
$acl->insertClassAce($securityIdentity, $mask);
153+
} else {
154+
$acl->insertObjectAce($securityIdentity, $mask);
155+
}
156+
}
157+
158+
$aclProvider->updateAcl($acl);
159+
}
160+
}
161+
162+
/**
163+
* Gets the mask builder
164+
*
165+
* @return MaskBuilder
166+
*/
167+
protected function getMaskBuilder()
168+
{
169+
return new MaskBuilder();
170+
}
171+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AclBundle;
13+
14+
use Symfony\Component\HttpKernel\Bundle\Bundle;
15+
16+
/**
17+
* @author Kévin Dunglas <[email protected]>
18+
*/
19+
class AclBundle extends Bundle
20+
{
21+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AclBundle\Entity;
13+
14+
/**
15+
* Car
16+
*
17+
* @author Kévin Dunglas <[email protected]>
18+
*/
19+
class Car
20+
{
21+
public $id;
22+
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
<?php
2+
3+
namespace Symfony\Bundle\SecurityBundle\Tests\Functional;
4+
5+
/*
6+
* This file is part of the Symfony package.
7+
*
8+
* (c) Fabien Potencier <[email protected]>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
use Symfony\Bundle\FrameworkBundle\Console\Application;
14+
use Symfony\Bundle\SecurityBundle\Command\InitAclCommand;
15+
use Symfony\Bundle\SecurityBundle\Command\SetAclCommand;
16+
use Symfony\Component\Console\Tester\CommandTester;
17+
use Symfony\Component\Security\Acl\Domain\ObjectIdentity;
18+
use Symfony\Component\Security\Acl\Domain\RoleSecurityIdentity;
19+
use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity;
20+
use Symfony\Component\Security\Acl\Exception\NoAceFoundException;
21+
use Symfony\Component\Security\Acl\Permission\BasicPermissionMap;
22+
23+
/**
24+
* Tests SetAclCommand
25+
*
26+
* @author Kévin Dunglas <[email protected]>
27+
*/
28+
class SetAclCommandTest extends WebTestCase
29+
{
30+
const OBJECT_CLASS = 'Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AclBundle\Entity\Car';
31+
const SECURITY_CLASS = 'Symfony\Component\Security\Core\User\User';
32+
33+
public function testSetAclUser()
34+
{
35+
$objectId = 1;
36+
$securityUsername1 = 'kevin';
37+
$securityUsername2 = 'anne';
38+
$grantedPermission1 = 'VIEW';
39+
$grantedPermission2 = 'EDIT';
40+
41+
$application = $this->getApplication();
42+
$application->add(new SetAclCommand());
43+
44+
$setAclCommand = $application->find('acl:set');
45+
$setAclCommandTester = new CommandTester($setAclCommand);
46+
$setAclCommandTester->execute(array(
47+
'command' => 'acl:set',
48+
'arguments' => array($grantedPermission1, $grantedPermission2, sprintf('%s:%s', self::OBJECT_CLASS, $objectId)),
49+
'--user' => array(sprintf('%s:%s', self::SECURITY_CLASS, $securityUsername1), sprintf('%s:%s', self::SECURITY_CLASS, $securityUsername2))
50+
));
51+
52+
$objectIdentity = new ObjectIdentity($objectId, self::OBJECT_CLASS);
53+
$securityIdentity1 = new UserSecurityIdentity($securityUsername1, self::SECURITY_CLASS);
54+
$securityIdentity2 = new UserSecurityIdentity($securityUsername2, self::SECURITY_CLASS);
55+
$permissionMap = new BasicPermissionMap();
56+
57+
/** @var \Symfony\Component\Security\Acl\Model\AclProviderInterface $aclProvider */
58+
$aclProvider = $application->getKernel()->getContainer()->get('security.acl.provider');
59+
$acl = $aclProvider->findAcl($objectIdentity, array($securityIdentity1));
60+
61+
$this->assertTrue($acl->isGranted($permissionMap->getMasks($grantedPermission1, null), array($securityIdentity1)));
62+
$this->assertTrue($acl->isGranted($permissionMap->getMasks($grantedPermission1, null), array($securityIdentity2)));
63+
$this->assertTrue($acl->isGranted($permissionMap->getMasks($grantedPermission2, null), array($securityIdentity2)));
64+
65+
try {
66+
$acl->isGranted($permissionMap->getMasks('OWNER', null), array($securityIdentity1));
67+
$this->fail('NoAceFoundException not throwed');
68+
} catch (NoAceFoundException $e) {
69+
}
70+
71+
try {
72+
$acl->isGranted($permissionMap->getMasks('OPERATOR', null), array($securityIdentity2));
73+
$this->fail('NoAceFoundException not throwed');
74+
} catch (NoAceFoundException $e) {
75+
}
76+
}
77+
78+
public function testSetAclRole()
79+
{
80+
$objectId = 1;
81+
$securityUsername = 'kevin';
82+
$grantedPermission = 'VIEW';
83+
$role = 'ROLE_ADMIN';
84+
85+
$application = $this->getApplication();
86+
$application->add(new SetAclCommand());
87+
88+
$setAclCommand = $application->find('acl:set');
89+
$setAclCommandTester = new CommandTester($setAclCommand);
90+
$setAclCommandTester->execute(array(
91+
'command' => 'acl:set',
92+
'arguments' => array($grantedPermission, sprintf('%s:%s', strtr(self::OBJECT_CLASS, '\\', '/'), $objectId)),
93+
'--role' => array($role)
94+
));
95+
96+
$objectIdentity = new ObjectIdentity($objectId, self::OBJECT_CLASS);
97+
$userSecurityIdentity = new UserSecurityIdentity($securityUsername, self::SECURITY_CLASS);
98+
$roleSecurityIdentity = new RoleSecurityIdentity($role);
99+
$permissionMap = new BasicPermissionMap();
100+
101+
/** @var \Symfony\Component\Security\Acl\Model\AclProviderInterface $aclProvider */
102+
$aclProvider = $application->getKernel()->getContainer()->get('security.acl.provider');
103+
$acl = $aclProvider->findAcl($objectIdentity, array($roleSecurityIdentity, $userSecurityIdentity));
104+
105+
$this->assertTrue($acl->isGranted($permissionMap->getMasks($grantedPermission, null), array($roleSecurityIdentity)));
106+
$this->assertTrue($acl->isGranted($permissionMap->getMasks($grantedPermission, null), array($roleSecurityIdentity)));
107+
108+
try {
109+
$acl->isGranted($permissionMap->getMasks('VIEW', null), array($userSecurityIdentity));
110+
$this->fail('NoAceFoundException not throwed');
111+
} catch (NoAceFoundException $e) {
112+
}
113+
114+
try {
115+
$acl->isGranted($permissionMap->getMasks('OPERATOR', null), array($userSecurityIdentity));
116+
$this->fail('NoAceFoundException not throwed');
117+
} catch (NoAceFoundException $e) {
118+
}
119+
}
120+
121+
public function testSetAclClassScope()
122+
{
123+
$objectId = 1;
124+
$grantedPermission = 'VIEW';
125+
$role = 'ROLE_USER';
126+
127+
$application = $this->getApplication();
128+
$application->add(new SetAclCommand());
129+
130+
$setAclCommand = $application->find('acl:set');
131+
$setAclCommandTester = new CommandTester($setAclCommand);
132+
$setAclCommandTester->execute(array(
133+
'command' => 'acl:set',
134+
'arguments' => array($grantedPermission, sprintf('%s:%s', self::OBJECT_CLASS, $objectId)),
135+
'--class-scope' => true,
136+
'--role' => array($role)
137+
));
138+
139+
$objectIdentity1 = new ObjectIdentity($objectId, self::OBJECT_CLASS);
140+
$objectIdentity2 = new ObjectIdentity(2, self::OBJECT_CLASS);
141+
$roleSecurityIdentity = new RoleSecurityIdentity($role);
142+
$permissionMap = new BasicPermissionMap();
143+
144+
/** @var \Symfony\Component\Security\Acl\Model\AclProviderInterface $aclProvider */
145+
$aclProvider = $application->getKernel()->getContainer()->get('security.acl.provider');
146+
147+
$acl1 = $aclProvider->findAcl($objectIdentity1, array($roleSecurityIdentity));
148+
$this->assertTrue($acl1->isGranted($permissionMap->getMasks($grantedPermission, null), array($roleSecurityIdentity)));
149+
150+
$acl2 = $aclProvider->createAcl($objectIdentity2);
151+
$this->assertTrue($acl2->isGranted($permissionMap->getMasks($grantedPermission, null), array($roleSecurityIdentity)));
152+
}
153+
154+
private function getApplication()
155+
{
156+
$kernel = $this->createKernel(array('test_case' => 'Acl'));
157+
$kernel->boot();
158+
159+
$application = new Application($kernel);
160+
$application->add(new InitAclCommand());
161+
162+
$initAclCommand = $application->find('init:acl');
163+
$initAclCommandTester = new CommandTester($initAclCommand);
164+
$initAclCommandTester->execute(array('command' => 'init:acl'));
165+
166+
return $application;
167+
}
168+
}

Tests/Functional/app/Acl/bundles.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
return array(
4+
new Doctrine\Bundle\DoctrineBundle\DoctrineBundle(),
5+
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
6+
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
7+
new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AclBundle\AclBundle()
8+
);

0 commit comments

Comments
 (0)