Gabriel Theron
Gabriel Theron

Reputation: 1376

Symfony2: creating a new SecurityIdentity

I'm using ACL in Symfony 2.1, and I need to create a new SecurityIdentity, so that my ACL can be set in function of some sort of groups.

Picture the following situation: there are groups with users (with different roles) that each have user information. In group 1, users with the ROLE_ADMIN can't edit other users from the same group's information, but in group 2, users with ROLE_ADMIN can edit others information.

So basically my ACL will vary in function of what group the user is in.

I thought I'd start solving this problem with the creation of a new "GroupSecurityIdentity". However the class itself doesn't suffice, as I get this exception when I use it:

$sid must either be an instance of UserSecurityIdentity, or RoleSecurityIdentity.

My question is: how do I "register" my new SecurityIdentity so I can use it as RoleSecurityIdentity and UserSecurityIdentity?

What better ways are there to implement a system similar to this I want to do?

Upvotes: 2

Views: 825

Answers (2)

Chopchop
Chopchop

Reputation: 2949

I write my answer here to keep a track of this error message.

I implemented group support with ACL and i had to hack a bit the symfony core "MutableAclProvider.php"

protected function getSelectSecurityIdentityIdSql(SecurityIdentityInterface $sid)
{
    if ($sid instanceof UserSecurityIdentity) {
        $identifier = $sid->getClass().'-'.$sid->getUsername();
        $username = true;
    } elseif ($sid instanceof RoleSecurityIdentity) {
        $identifier = $sid->getRole();
        $username = false;
    }else {
        //throw new \InvalidArgumentException('$sid must either be an instance of UserSecurityIdentity, or RoleSecurityIdentity.');
        $identifier = $sid->getClass().'-'.$sid->getGroupname();
        $username = true;
    }

    return sprintf(
        'SELECT id FROM %s WHERE identifier = %s AND username = %s',
        $this->options['sid_table_name'],
        $this->connection->quote($identifier),
        $this->connection->getDatabasePlatform()->convertBooleans($username)
    );
}

Even if the provided object is not an instance of UserSecurityIdentity or RoleSecurityIdentity it return a value. So now i can use a custom "GroupSecurityIdentity"

It's not easy to put in place but was much adapted to my system.

Upvotes: 0

kgilden
kgilden

Reputation: 10356

2 years ago I went down that path, it turned out to be a bad decision. Modifying the ACL system is difficult and might cause problems when updating Symfony. There are at least 2 better solutions. I'll list them all so you can decide which best suits your needs.

New security identity

I'm using the GroupInterface from FOSUserBundle, but I guess you could use your own too. The following files need to be added:

Next up: rewire the dependency injection container by providing the following parameters:

<parameter key="security.acl.dbal.provider.class">
    Acme\Bundle\DemoBundle\Security\Acl\Dbal\MutableAclProvider
</parameter>
<parameter key="security.acl.security_identity_retrieval_strategy.class">
    Acme\Bundle\DemoBundle\Security\Acl\Domain\SecurityIdentityRetrievalStrategy
</parameter>

Time to cross fingers and see whether it works. Since this is old code I might have forgotten something.

Use roles for groups

The idea is to have group names correspond to roles.

A simple way is to have your User entity re-implement UserInterface::getRoles:

public function getRoles()
{
    $roles = parent::getRoles();

    // This can be cached should there be any performance issues
    // which I highly doubt there would be.
    foreach ($this->getGroups() as $group) {
        // GroupInterface::getRole() would probably have to use its
        // canonical name to get something like `ROLE_GROUP_NAME_OF_GROUP`
        $roles[] = $group->getRole();
    }

    return $roles;
}

A possible implementation of GroupInterface::getRole():

public function getRole()
{
    $name = $this->getNameCanonical();

    return 'ROLE_GROUP_'.mb_convert_case($name, MB_CASE_UPPER, 'UTF-8');
}

It's now just a matter of creating the required ACE-s as written in the cookbook article.

Create a voter

Finally, you could use custom voters that check for the presence of specific groups and whether the user has access to said object. A possible implementation:

<?php

namespace Acme\Bundle\DemoBundle\Authorization\Voter;

use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;

class MySecureObjectVoter implements VoterInterface
{
    /**
     * {@inheritDoc}
     */
    public function supportsAttribute($attribute)
    {
        $supported = array('VIEW');

        return in_array($attribute, $supported);
    }

    /**
     * {@inheritDoc}
     */
    public function supportsClass($class)
    {
        return $class instanceof GroupableInterface;
    }

    /**
     * {@inheritDoc}
     */
    public function vote(TokenInterface $token, $object, array $attributes)
    {
        $result = VoterInterface::ACCESS_ABSTAIN;

        if (!$object instanceof MySecureObject) {
            return VoterInterface::ACCESS_ABSTAIN;
        }

        foreach ($attributes as $attribute) {
            if (!$this->supportsAttribute($attribute)) {
                continue;
            }

            // Access is granted, if the user and object have at least 1
            // group in common.
            if ('VIEW' === $attribute) {
                $objGroups = $object->getGroups();
                $userGroups = $token->getUser()->getGroups();

                foreach ($userGroups as $userGroup) {
                    foreach ($objGroups as $objGroup) {
                        if ($userGroup->equals($objGroup)) {
                            return VoterInterface::ACCESS_GRANTED;
                        }
                    }
                }

                return voterInterface::ACCESS_DENIED;
            }
        }
    }
}

For more details on voters please refer to the cookbook example.


I would avoid creating a custom security identity. Use the two other methods provided. The second solution works best, if you will be having lots of records and each of them must have different access settings. Voters could be used for setting up simple access granting logic (which most smaller systems seem to fall under) or when flexibility is necessary.

Upvotes: 2

Related Questions