Skip to content

Commit 4c12b7b

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 e814681 + a702124 commit 4c12b7b

File tree

8 files changed

+417
-1
lines changed

8 files changed

+417
-1
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
"doctrine/data-fixtures": "1.0.*",
7171
"doctrine/dbal": "~2.2",
7272
"doctrine/orm": "~2.2,>=2.2.3",
73+
"doctrine/doctrine-bundle": "~1.2",
7374
"monolog/monolog": "~1.3",
7475
"propel/propel1": "1.6.*",
7576
"ircmaxell/password-compat": "1.0.*",
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 <fabien@symfony.com>
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 <kevin@les-tilleuls.coop>
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 <fabien@symfony.com>
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 <kevin@les-tilleuls.coop>
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 <fabien@symfony.com>
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 <kevin@les-tilleuls.coop>
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 <fabien@symfony.com>
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 <kevin@les-tilleuls.coop>
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+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy