Closed MrGreenStuff closed 10 years ago
Hi @MrGreenStuff and thanks for your feedback.
If I understand the use case, you can already achieve your need with the current code:
OPERATOR
permissions on categoriescreateObjectSecurity()
method (https://github.com/sonata-project/SonataAdminBundle/blob/master/Admin/Admin.php#L2431) of your Product
's Admin
class to add permissions of the parent category to the created productIs this what you want?
If you create objects outside of the admin scope (e.g. in a frontend controller), you cannot use the createObjectSecurity()
method and you need to deal directly with the Symfony Security Component to copy ACL from the parent object.
If it's interesting you, at @coopTilleuls we should have a code sample doing that.
@dunglas @MrGreenStuff Yep, we've that sample code here: http://www.sroze.io/2014/01/08/sonata-admin-create-acl-on-object-created-outside-of-admin/
Ok it's want i want to do. Thanks very much.
After some test it's not exactly what i search. So i modify the code. I reexplain the behavior i expect example :
3 Tables : Shop, Product and Country
3 Users :
Behavior expected :
So i made a major modification to the Admin/AclAdminExtension.php ( configureQuery method ) :
<?php
/*
* (c) La Coopérative des Tilleuls <contact@les-tilleuls.coop>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*
*
* Modified By JUILLARD Yoann
*/
namespace CoopTilleuls\Bundle\AclSonataAdminExtensionBundle\Admin;
use Sonata\AdminBundle\Admin\AdminExtension;
use Sonata\AdminBundle\Admin\AdminInterface;
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
use Doctrine\DBAL\Connection;
use Symfony\Component\Security\Acl\Permission\MaskBuilder;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity;
/**
* Admin extension filtering the list
*
* @author Kévin Dunglas <kevin@les-tilleuls.coop>
*/
class AclAdminExtension extends AdminExtension
{
/**
* @var SecurityContextInterface
*/
protected $securityContext;
/**
* @var Connection
*/
protected $databaseConnection;
/**
* @param SecurityContextInterface $securityContext
* @param Connection $databaseConnection
*/
public function __construct(SecurityContextInterface $securityContext, Connection $databaseConnection)
{
$this->securityContext = $securityContext;
$this->databaseConnection = $databaseConnection;
}
/**
* Filters with ACL
*
* @param AdminInterface $admin
* @param ProxyQueryInterface $query
* @param string $context
* @throws \RuntimeException
*/
public function configureQuery(AdminInterface $admin, ProxyQueryInterface $query, $context = 'list')
{
// Don't filter for admins and for not ACL enabled classes and for command cli
if (!$admin->isAclEnabled() || !$this->securityContext->getToken() || $admin->isGranted(sprintf($admin->getSecurityHandler()->getBaseRole($admin), 'ADMIN'))) {
return;
}
// Retrieve current logged user SecurityIdentity
$user = $this->securityContext->getToken()->getUser();
$securityIdentity = UserSecurityIdentity::fromAccount($user);
// Get identity ACL identifier
$identifier = sprintf('%s-%s', $securityIdentity->getClass(), $securityIdentity->getUsername());
$identityStmt = $this->databaseConnection->prepare('SELECT id FROM acl_security_identities WHERE identifier = :identifier');
$identityStmt->bindValue('identifier', $identifier);
$identityStmt->execute();
$identityId = $identityStmt->fetchColumn();
// Get class ACL identifier
$classType = $admin->getClass();
$classStmt = $this->databaseConnection->prepare('SELECT id FROM acl_classes WHERE class_type = :classType');
$classStmt->bindValue('classType', $classType);
$classStmt->execute();
$classId = $classStmt->fetchColumn();
if ($identityId && $classId) {
$entriesStmt = $this->databaseConnection->prepare('SELECT object_identifier FROM acl_entries AS ae JOIN acl_object_identities AS aoi ON ae.object_identity_id = aoi.id WHERE ae.class_id = :classId AND ae.security_identity_id = :identityId AND (:view = ae.mask & :view OR :operator = ae.mask & :operator OR :master = ae.mask & :master OR :owner = ae.mask & :owner)');
$entriesStmt->bindValue('classId', $classId);
$entriesStmt->bindValue('identityId', $identityId);
$entriesStmt->bindValue('view', MaskBuilder::MASK_VIEW);
$entriesStmt->bindValue('operator', MaskBuilder::MASK_OPERATOR);
$entriesStmt->bindValue('master', MaskBuilder::MASK_MASTER);
$entriesStmt->bindValue('owner', MaskBuilder::MASK_OWNER);
$entriesStmt->execute();
$ids = array();
foreach ($entriesStmt->fetchAll() as $row) {
$ids[] = $row['object_identifier'];
}
//IF THERE IS NOT DIRECT RESULT WE MADE A QUERY ON THE MASTER ACL CLASS
//Test if method getMasterACLclass and getPathToMasterACL exist on the admin CLASS -> SEE THE DOC
if (count($ids)==0 && method_exists($admin,'getMasterACLclass') && method_exists($admin,'getPathToMasterACL')) {
$classStmt = $this->databaseConnection->prepare('SELECT id FROM acl_classes WHERE class_type = :classType');
//QUERY ON MASTER ACL CLASS (method $admin->getMasterACLclass() return a string like 'Acme\Bundle\Entity\MasterACLEntity');
$classStmt->bindValue('classType', $admin->getMasterACLclass());
$classStmt->execute();
$classId = $classStmt->fetchColumn();
$entriesStmt = $this->databaseConnection->prepare('SELECT object_identifier FROM acl_entries AS ae JOIN acl_object_identities AS aoi ON ae.object_identity_id = aoi.id WHERE ae.class_id = :classId AND ae.security_identity_id = :identityId AND (:view = ae.mask & :view OR :operator = ae.mask & :operator OR :master = ae.mask & :master OR :owner = ae.mask & :owner)');
$entriesStmt->bindValue('classId', $classId);
$entriesStmt->bindValue('identityId', $identityId);
$entriesStmt->bindValue('view', MaskBuilder::MASK_VIEW);
$entriesStmt->bindValue('operator', MaskBuilder::MASK_OPERATOR);
$entriesStmt->bindValue('master', MaskBuilder::MASK_MASTER);
$entriesStmt->bindValue('owner', MaskBuilder::MASK_OWNER);
$entriesStmt->execute();
//ARRAY OF IDS
$ids = array();
foreach ($entriesStmt->fetchAll() as $row) {
$ids[] = $row['object_identifier'];
}
/*The method $admin->getPathToMasterACL() have to return an array (BE CAREFULL OF ORDER) like :
array(
array('parent','p1'),
array('grandParent','p2'),
array('grandGrandParent','p3')
...
)
TODO MADE SHORTCUT AUTOMATIC
*/
$parents=$admin->getPathToMasterACL();
//HERE UPDATE THE QUERY
foreach($parents as $key=>$parent){
//FIRST shorcut is 'o'
if($key==0){
$query->leftJoin('o.'.$parent[0],$parent[1]);
}else{
//Shortcut is precedent shortcut
$query->leftJoin($parents[$key-1][1].'.'.$parent[0],$parent[1]);
}
//HERE WE ARE AFTER THE LEFT JOIN ON MASTER ACL CLASS WE PASS ids array param
if(($key+1)==count($parents)){
$query->andWhere($parent[1].'.id IN (:ids'.$key.')')
->setParameter('ids'.$key, $ids);
}
}
return;
}elseif(count($ids)){
//NORMAL BEHAVIOR
$query
->andWhere('o.id IN (:ids)')
->setParameter('ids', $ids)
;
return;
}
}
// Display an empty list
$query->andWhere('1 = 2');
}
}
And here the two method to add to your admin class for ACTIVATE (it's optionnal) and PARAM the new behavior
class AcmeAdmin extends Admin
{
public function getMasterACLclass()
{
return 'Acme\AcmeBundle\Entity\MasterEntity';
}
public function getPathToMasterACL()
{
//CAREFULL WITH ORDER
return array(
array('parent','p1'),
array('grandParent','p2'),
...
...etc
);
/*where the first value is the name of the property wich make the relation with the parent and second a unique selector*/
}
Every thing works fine BUT only when the creation of ACL on concerned table is disabled and i don't know how to make that :( . I try this but it don't disble just put view ACCESS :
public function createObjectSecurity($object)
{
return;
}
If you have an idea for disable all ACL creation (even owner !) on an admin class ?
Thanks for reading
It's OK i find a solution without disabled ACL creation (and i prefer).
The solution is a OR (build via $query->expr to keep filter working) condition in certain cases.
With this version you don't have to override or touch at the default createObjectSecurity method !
Here the final version of my AclAdminExtension class :
<?php
/*
* (c) La Coopérative des Tilleuls <contact@les-tilleuls.coop>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace CoopTilleuls\Bundle\AclSonataAdminExtensionBundle\Admin;
use Sonata\AdminBundle\Admin\AdminExtension;
use Sonata\AdminBundle\Admin\AdminInterface;
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
use Doctrine\DBAL\Connection;
use Symfony\Component\Security\Acl\Permission\MaskBuilder;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity;
/**
* Admin extension filtering the list
*
* @author Kévin Dunglas <kevin@les-tilleuls.coop>
*/
class AclAdminExtension extends AdminExtension
{
/**
* @var SecurityContextInterface
*/
protected $securityContext;
/**
* @var Connection
*/
protected $databaseConnection;
/**
* @param SecurityContextInterface $securityContext
* @param Connection $databaseConnection
*/
public function __construct(SecurityContextInterface $securityContext, Connection $databaseConnection)
{
$this->securityContext = $securityContext;
$this->databaseConnection = $databaseConnection;
}
/**
* Filters with ACL
*
* @param AdminInterface $admin
* @param ProxyQueryInterface $query
* @param string $context
* @throws \RuntimeException
*/
public function configureQuery(AdminInterface $admin, ProxyQueryInterface $query, $context = 'list')
{
// Don't filter for admins and for not ACL enabled classes and for command cli
if (!$admin->isAclEnabled() || !$this->securityContext->getToken() || $admin->isGranted(sprintf($admin->getSecurityHandler()->getBaseRole($admin), 'ADMIN'))) {
return;
}
// Retrieve current logged user SecurityIdentity
$user = $this->securityContext->getToken()->getUser();
$securityIdentity = UserSecurityIdentity::fromAccount($user);
// Get identity ACL identifier
$identifier = sprintf('%s-%s', $securityIdentity->getClass(), $securityIdentity->getUsername());
$identityStmt = $this->databaseConnection->prepare('SELECT id FROM acl_security_identities WHERE identifier = :identifier');
$identityStmt->bindValue('identifier', $identifier);
$identityStmt->execute();
$identityId = $identityStmt->fetchColumn();
// Get class ACL identifier
$classType = $admin->getClass();
$classStmt = $this->databaseConnection->prepare('SELECT id FROM acl_classes WHERE class_type = :classType');
$classStmt->bindValue('classType', $classType);
$classStmt->execute();
$classId = $classStmt->fetchColumn();
if ($identityId && $classId) {
$entriesStmt = $this->databaseConnection->prepare('SELECT object_identifier FROM acl_entries AS ae JOIN acl_object_identities AS aoi ON ae.object_identity_id = aoi.id WHERE ae.class_id = :classId AND ae.security_identity_id = :identityId AND (:view = ae.mask & :view OR :operator = ae.mask & :operator OR :master = ae.mask & :master OR :owner = ae.mask & :owner)');
$entriesStmt->bindValue('classId', $classId);
$entriesStmt->bindValue('identityId', $identityId);
$entriesStmt->bindValue('view', MaskBuilder::MASK_VIEW);
$entriesStmt->bindValue('operator', MaskBuilder::MASK_OPERATOR);
$entriesStmt->bindValue('master', MaskBuilder::MASK_MASTER);
$entriesStmt->bindValue('owner', MaskBuilder::MASK_OWNER);
$entriesStmt->execute();
$ids = array();
foreach ($entriesStmt->fetchAll() as $row) {
$ids[] = $row['object_identifier'];
}
//IF THERE IS NOT DIRECT RESULT WE MADE A QUERY ON THE MASTER ACL CLASS
//Test if method getMasterACLclass and getPathToMasterACL exist on the admin CLASS -> SEE THE DOC
if (method_exists($admin,'getMasterACLclass') && method_exists($admin,'getPathToMasterACL')) {
$classStmt = $this->databaseConnection->prepare('SELECT id FROM acl_classes WHERE class_type = :classType');
//QUERY ON MASTER ACL CLASS (method $admin->getMasterACLclass() return a string like 'Acme\Bundle\Entity\MasterACLEntity');
$classStmt->bindValue('classType', $admin->getMasterACLclass());
$classStmt->execute();
$classId = $classStmt->fetchColumn();
$entriesStmt = $this->databaseConnection->prepare('SELECT object_identifier FROM acl_entries AS ae JOIN acl_object_identities AS aoi ON ae.object_identity_id = aoi.id WHERE ae.class_id = :classId AND ae.security_identity_id = :identityId AND (:view = ae.mask & :view OR :operator = ae.mask & :operator OR :master = ae.mask & :master OR :owner = ae.mask & :owner)');
$entriesStmt->bindValue('classId', $classId);
$entriesStmt->bindValue('identityId', $identityId);
$entriesStmt->bindValue('view', MaskBuilder::MASK_VIEW);
$entriesStmt->bindValue('operator', MaskBuilder::MASK_OPERATOR);
$entriesStmt->bindValue('master', MaskBuilder::MASK_MASTER);
$entriesStmt->bindValue('owner', MaskBuilder::MASK_OWNER);
$entriesStmt->execute();
//ARRAY OF idsMaster
$idsMaster = array();
foreach ($entriesStmt->fetchAll() as $row) {
$idsMaster[] = $row['object_identifier'];
}
/*The method $admin->getPathToMasterACL() have to return an array (BE CAREFULL OF ORDER) like :
array(
array('firstChild','c1'),
array('secondChild','c2'),
array('thirdChild','c3')
...
)
where the first argument is the name of the property relation and second a unique selector (TODO MADE NAME SELECTOR AUTOMATIC)
*/
$parents=$admin->getPathToMasterACL();
//HERE UPDATE THE QUERY
foreach($parents as $key=>$parent){
//FIRST shorcut is 'o'
if($key==0){
$query->leftJoin('o.'.$parent[0],$parent[1]);
}else{
//Shortcut is precedent shortcut
$query->leftJoin($parents[$key-1][1].'.'.$parent[0],$parent[1]);
}
//HERE WE ARE AFTER THE LEFT JOIN ON MASTER ACL CLASS WE PASS idsMaster array param
if(($key+1)==count($parents)){
//HERE FOR OBJECT CREATED BY CURRENT USER
if(count($ids)){
//création de l'expression OR EXPRESSION
$orCondition = $query->expr()->orx();
$orCondition->add($query->expr()->in('o.id', ':ids'));
$orCondition->add($query->expr()->in($parent[1].'.id',':idsMaster'));
$query->andWhere($orCondition)->setParameter('ids', $ids)->setParameter('idsMaster', $idsMaster);
}else{
$query->andWhere($parent[1].'.id IN (:idsMaster'.$key.')')->setParameter('idsMaster'.$key, $idsMaster);
}
}
}
return;
}elseif(count($ids)){
//NORMAL BEHAVIOR
$query
->andWhere('o.id IN (:ids)')
->setParameter('ids', $ids)
;
return;
}
}
// Display an empty list
$query->andWhere('1 = 2');
}
}
I made a fork with this enhancements here : MrGreenStuffAclSonataAdminExtensionBundle.
On packagist : mrgreenstuff/acl-sonata-admin-extension-bundle.
I will write a clear DOC soon
I every one,
First thanks for sharing this awesome bundle !
I have and idea for enhance the code : filter child entity from parent ACL (optionnal).
Exemple an application with 2 tables (Category and Product) a user who have OPERATOR ACL access to a specific category can view only the products of this category.
I will try to do this but if somebody have some good ideas ?
My problem is i don't know how i can make it optionnal (how i can param it) and how i can specify wich parent entity to choose for filtering.
If i success to code it i will send it tou you for merge.
Bye