Symfony2: Implementing ACL rules in your Data Fixtures

Warning: This blogpost has been posted over two years ago. That is a long time in development-world! The story here may not be relevant, complete or secure. Code might not be complete or obsoleted, and even my current vision might have (completely) changed on the subject. So please do read further, but use it with caution.
Posted on 04 Jul 2012
Tagged with: [ doctrine ]  [ PHP ]  [ proxy ]  [ symfony2

Doctrine’s DataFixtures are a great way to add test data to your application. It’s fairly easy to get this going: Create a fixureLoader that extends Doctrine\Common\DataFixtures\AbstractFixture, had a load() method and off you go. However, sometimes you want your data also to be protected by Symfony 2’s ACL layer. Since there isn’t a common way to do this, here is one way on how I implemented this

One of the drawbacks of fixtures is that by default haven’t got the container so it’s not possible to get other stuff easily. However, it’s really easy to fix this. All you need to do is implement the ContainerAware interface, and create a setter:

class EntityFixtureLoader extends AbstractFixture 
   implements OrderedFixtureInterface, ContainerAwareInterface
{
  /**
   * @var ContainerInterface
   */
  private $container;

  public function setContainer(ContainerInterface $container = null) 
  {
    $this->container = $container;
  }

  public function load(ObjectManager $manager) {
    ...
  }
}

If your fixtureLoader implements the ContainerAwareInterface, it will automatically inject the container through the setter method (setContainer()). There is no need to change any DI configuration for this. So from this point, we have a container we can easily use.

Next up, we can define some fixtures and add some ACL to them in the normal manner:

public function load(ObjectManager $manager) {
   $user = new User();
   $user->setName("John Doe");
   $user->setEmail("johndoe@example.org");
   $manager->persist($user);

   $blog = new Blog();
   $blog->setTitle("My title");
   $blog->setOwner($user);
   $manager->persist($blog);

   $manager->flush();
   
   // create the ACL
   $aclProvider = $this->container->get('security.acl.provider');
   $objectIdentity = ObjectIdentity::fromDomainObject($blog);
   $acl = $aclProvider->createAcl($objectIdentity);

   $securityIdentity = UserSecurityIdentity::fromAccount($user);
   $acl->insertObjectAce($securityIdentity, MaskBuilder::MASK_OPERATOR);

   $roleSecurityIdentity = new RoleSecurityIdentity('ROLE_ADMIN');
   $acl->insertObjectAce($roleSecurityIdentity, MaskBuilder::MASK_MASTER);
   $aclProvider->updateAcl($acl);
}

And this pretty much works. We have defined  a user entity and a blogpost entity. In the ACL  we have set the user to be an operator of the object (can view,edit,delete etc). Users with a  ROLE_ADMIN role are master of the blogpost (can do everything an operator can, but also grant rights to others).

In order to get things running you use the following commands:

# php app/console doctrine:database:create
# php app/console doctrine:schema:create
# php app/console init:acl
# php app/console doctrine:fixtures:load

which will create the database, the schema, the acl schema’s and load the fixtures.

There is a big catch though when you are working with fixtures loaded from multiple files. This is quite a normal way to setup your fixtures, so the userFixtureLoader creates users, while your blogFixtureLoader will create blogs. Just like we did in our fixture example above, we sometimes need some information from a fixtureloader inside another fixtureloader. In our case: we need to have the user class inside the blogfixtureloader in order to set the owner (and the ACL). Again, doctrine provides an easy way for this with the help of the addReference() and getReference() methods.

// UserFixtureLoader.php:
public function load(ObjectManager $manager) {
   $user = new User();
   $user->setName("John Doe");
   $user->setEmail("johndoe@example.org");
   $manager->persist($user);

   $manager->flush();

   $this->addReference("user-1", $user);
}
   
// BlogFixtureLoader.php:
public function load(ObjectManager $manager)
{
   $blog = new Blog();
   $blog->setTitle("My title");
   $blog->setOwner($this->getReference("user-1"));
   $manager->persist($blog);

   $manager->flush();
}

This works perfectly, but it’s not going to work with the ACL’s. The problem lies in the fact that our User objects gets “converted” to a Doctrine User Proxy class. The getReference() in the blogFixtureLoader does not get the actual User object back, but the proxy class. This is ok for doctrine itself, since it knows how to convert it so we still can use it safely as our user class, but the ACL will go wrong. This is because inside the ACL tables, it will store the classname of the actual object that needs the permission. The userSecurityIdentify::fromAccount($user) will actually do a get_class($user) inside its function, which off course returns the name of the proxy class, not the name of the actual user class. You can see this inside the “acl_security_identities” table inside your database. As soon as we check the permissions in our blogpost controller for instance, the ACL layer will check the User that has logged in against the ACL, but it will not find it (obviously, since we are checking a USER class against a USERPROXY class, which aren’t the same).

Again, this is an easy fix: we override the actual class that gets saved into the ACL:

$securityIdentity = new UserSecurityIdentity($user, 'Acme\DefaultBundle\Entity\User');
$acl->insertObjectAce($securityIdentity, MaskBuilder::MASK_OPERATOR);

Now, we force the system to save the user as a Acme\DefaultBundle\Entity\User class, even though it’s a Proxy class. I haven’t checked if this is a problem with symfony2 version 2.1, but if so, it’s still an easy fix by not using the fromAccount() method from the UserSecurityIdentity class.