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.

7 comments:

  1. hi,
    with the OBJECT-SCOPE ACL the MASK_VIEW denied to modify object (change fields and update record) to the related user? minds1 at libero.it

    ReplyDelete
    Replies
    1. Hi

      I'm not sure if I understand what you are asking.

      If you setup the ACL with the MASK_VIEW, then the user won't be able to modify the object, that's true yes.

      Incase that does not answer your question let me know.

      Delete
  2. This is weird.. why isn't there a ClassIdentity? Isn't this going to cause problems if one of my object's id is 'class'?

    ReplyDelete
    Replies
    1. Hmmmm, I'm not sure what you mean, can you give me more detail about your question?

      Delete
  3. Hi,

    I want to add new user and at the same time I also want to set some pages that can access by this user.

    That means:
    Add new User A, allow to View, Create and Edit users
    Add new User B, allow to View and Edit users

    How can I add these requirements to ACL table and User table
    How can I check their permissions?

    Please help.

    Thanks.

    ReplyDelete