Saturday, February 18, 2012

Symfony2 and ACLs

EDIT: I have updated the post to reflect my latest findings.

First of all I suggest you read the Symfony2 documentation about ACLs in Symfony2, check it out here:

ACL
Advanced ACL

ClassScope

I was asking about ACL stuff on the Symfony2 Google Group, and I have received a short description from a user named "rmsint". His name is Roel, credit where credit is due :-)
The class-scope contains access rules (ACEs) that tell what a user can do with the class. The object-scope contains access rules that tell what a user can do with a specific object. Object-scope rules and class-scope rules can both be applied to the object when it is created.It's better to attach class-scope and object-scope rules to the ACL when the object is created. This way access rules can be inherited from a parent ACL. An example is that a Comment object should grant access if the user is allowed to edit the parent Post object.
This is how I setup the ACL respectively the (Class) ACE,  I did this via an app/console command. This is, as explained by Roel, by no means the "state of the art" how to do it. I just felt it was the easiest way for me, so I could easily create different ACL setups after dropping the database, since I am just doing this to figure out how all this stuff works.

ClassScopeCommand.php
namespace Liip\TestBundle\Command;

use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;

use Symfony\Component\Console\Input\InputInterface;
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\Permission\MaskBuilder;

class ClassScopeCommand extends ContainerAwareCommand
{

    protected function configure()
    {
        parent::configure();

        $this
            ->setName('acl:setup:class-scope')
            ->setDescription('Setup ACLs and ACEs for Class-Scope access demo');
    }

    /**
     * @param \Symfony\Component\Console\Input\InputInterface $input
     * @param \Symfony\Component\Console\Output\OutputInterface $output
     * @return int
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        /**
         * @var $aclProvider \Symfony\Component\Security\Acl\Dbal\MutableAclProvider
         * @var $acl \Symfony\Component\Security\Acl\Domain\Acl
         * @var $securityContext \Symfony\Component\Security\Core\SecurityContext
         * @var $user \Liip\TestBundle\Entity\User
         * @var $em \Doctrine\ORM\EntityManager
         */

        $aclProvider = $this->getContainer()->get('security.acl.provider');
        $oid = new ObjectIdentity('class', 'Liip\\TestBundle\\Entity\\Post');
        $acl = $aclProvider->createAcl($oid);

        $securityIdentity = new RoleSecurityIdentity("ROLE_POST_OWNER");

        // grant owner access
        $acl->insertClassAce($securityIdentity, MaskBuilder::MASK_OWNER);
        $aclProvider->updateAcl($acl);

        return 0;
    }
}


And this is how you can check the permissions in the controller:

ClassScopeController.php

namespace Liip\TestBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

use Symfony\Component\Security\Acl\Domain\ObjectIdentity;
use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity;
use Symfony\Component\Security\Acl\Permission\MaskBuilder;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;

use Liip\TestBundle\Entity\Post;

class ClassScopeController extends Controller
{
    /**
     * @return \Symfony\Component\HttpFoundation\Response
     * @throws \Symfony\Component\Security\Core\Exception\AccessDeniedException
     */
    public function indexAction()
    {
        /**
         * @var $postRepository  \Liip\TestBundle\Entity\PostRepository
         * @var $em              \Doctrine\ORM\EntityManager
         * @var $securityContext \Symfony\Component\Security\Core\SecurityContext;
         */
        $postRepository = $this->getDoctrine()->getRepository('Liip\TestBundle\Entity\Post');

        $postExists = count($postRepository->findOneBy(array('id' => 1))) === 1;

        // if there is no Post in the database yet, create one
        if (!$postExists) {
            $post = new Post();
            $post->setPost('This post is protected by an ACL and should only be visible to users with the role "ROLE_POST_OWNER"');

            $em = $this->getDoctrine()->getEntityManager();
            $em->persist($post);
            $em->flush();

            unset($post);
        }

        $post = $postRepository->findOneBy(array('id' => 1));

        $securityContext = $this->get('security.context');
        $objectIdentity = new ObjectIdentity('class', 'Liip\\TestBundle\\Entity\\Post');

        // check for edit access
        if (true === $securityContext->isGranted('EDIT', $objectIdentity)) {
            echo "Edit Access granted to: 

"; print_r("
");
            print_r($post);
            print_r("
"); } else { throw new AccessDeniedException(); } return $this->render('LiipTestBundle:Default:index.html.twig'); } }


Class-Field-Scope
The ACL/ACE setup is a bit different, that's what I came up with:

ClassFieldScopeCommand.php
namespace Liip\TestBundle\Command;

use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
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\Permission\MaskBuilder;

class ClassFieldScopeCommand extends ContainerAwareCommand
{
    protected function configure()
    {
        parent::configure();

        $this
            ->setName('acl:setup:class-field-scope')
            ->setDescription('Setup ACLs and ACEs for Class-Field-Scope access demo');
    }

    /**
     * @param \Symfony\Component\Console\Input\InputInterface $input
     * @param \Symfony\Component\Console\Output\OutputInterface $output
     * @return int
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        /**
         * @var $aclProvider \Symfony\Component\Security\Acl\Dbal\MutableAclProvider
         * @var $acl \Symfony\Component\Security\Acl\Domain\Acl
         * @var $securityContext \Symfony\Component\Security\Core\SecurityContext
         * @var $user \Liip\TestBundle\Entity\User
         * @var $em \Doctrine\ORM\EntityManager
         */

        $aclProvider = $this->getContainer()->get('security.acl.provider');
        // Object Identity for a whole class respectively entity
        $oid = new ObjectIdentity('class', 'Liip\\TestBundle\\Entity\\Post');
        // Object Identity for specific domainObject
        // $oid = new ObjectIdentity::fromDomainObject($post);
        $acl = $aclProvider->createAcl($oid);

        // Role based securityIdentity
        $securityIdentity = new RoleSecurityIdentity("ROLE_POST_OWNER");

        // User based securityIdentity
        // $securityContext =$this->getContainer()->get('security.context');
        // $user = $securityContext->getToken()->getUser();
        // $securityIdentity = UserSecurityIdentity::fromAccount($user);

        // grant access
        //$acl->insertClassAce($securityIdentity, MaskBuilder::MASK_OWNER);
        $acl->insertClassFieldAce('post', $securityIdentity, MaskBuilder::MASK_EDIT);
        $acl->insertClassFieldAce('id', $securityIdentity, MaskBuilder::MASK_EDIT);

        $aclProvider->updateAcl($acl);

        return 0;
    }
}


And this is how you can check the permissions in the controller:

ClassFieldScopeController.php

namespace Liip\TestBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

use Symfony\Component\Security\Acl\Domain\ObjectIdentity;
use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity;
use Symfony\Component\Security\Acl\Permission\MaskBuilder;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\SecurityContext;
use Symfony\Component\Security\Acl\Voter\FieldVote;
use Symfony\Component\Security\Acl\Domain\RoleSecurityIdentity;

use Liip\TestBundle\Entity\Post;

class ClassFieldScopeController extends Controller
{
    public function indexAction()
    {
        /**
         * @var $postRepo \Liip\TestBundle\Entity\PostRepository
         * @var $em \Doctrine\ORM\EntityManager
         * @var $securityContext SecurityContext;
         */
        $postRepo = $this->getDoctrine()->getRepository('Liip\TestBundle\Entity\Post');

        $postExists = count ($postRepo->findOneById(1)) === 1;
        if($postExists){
            $post = $postRepo->findOneById(1);

            $securityContext = $this->get('security.context');
            $oid = new ObjectIdentity('class', 'Liip\\TestBundle\\Entity\\Post');

            $object = new FieldVote($oid, 'id');
            if (true === $securityContext->isGranted('EDIT', $object)){
                echo "Access to 'id' field granted";
            }else{
                echo "Access denied";
            }

            $object = new FieldVote($oid, 'post');
            if (true === $securityContext->isGranted('EDIT', $object))
            {
                echo "Access to 'post' field granted";
            }else{
                echo "Access denied";
            }

        }else{
            $post = new Post();
            $post->setPost('This post is protected by an ACL and should only be visible to users with the role "ROLE_POST_OWNER"');

            $em = $this->getDoctrine()->getEntityManager();
            $em->persist($post);
            $em->flush();
        }
        return $this->render('LiipTestBundle:Default:index.html.twig');
    }
}



Based on this it's also possible to do this on an "Object-Scope" or "Object-Field-Scope". You simply need to adjust the objectIdentity accordingly with ObjectIdentity::fromDomainObject() when setting up the ACL.