Symfony 2 - ACL check permission based on 'separate' roles
Asked Answered
S

3

8

Let's say we have 3 main roles that are directly bound to the database table user: ROLE_USER, ROLE_MODERATOR and ROLE_ADMIN.

BUT, we also got some other roles, which are used for the Crews component (see UML below). I use the following roles for actions peformed in a Crew: ROLE_CREW_BOSS, ROLE_CREW_LEFTHAND, ROLE_CREW_RIGHTHAND, ROLE_CREW_MEMBER.



      +----------------+                                     +------------------+
      | users          |                                     | crews            |
      |----------------|                                     |------------------|
      | id             |                                     | id               |
      | username       <---+                                 | name             |
      | password       |   |                             +---> cash             |
      | roles          |   |    +-------------------+    |   | ...              |
      | ...            |   |    | crew_members      |    |   |                  |
      |                |   |    |-------------------|    |   |                  |
      +----------------+   |    | crew_id +--------------+   |                  |
                           +----+ user_id           |        +--------^---------+
                                | roles             |                 |
                                | ...               |    +------------+
                                |                   |    |
                                |                   |    |   +------------------+
                                |                   |    |   | forum_topics     |
                                |                   |    |   |------------------|
                                |                   |    |   | id               |
                                +-------------------+    +---+ crew_id          |
                                                             | title            |
                                                             | description      |
                                                             | ...              |
                                                             |                  |
                                                             |                  |
                                                             |                  |
                                                             +------------------+

That is the base structure, I hope that part is clear. Now comes the problem...

The problem

Every user with the role ROLE_MODERATOR can create ForumTopic objects, but not the one where crew_id is set, because that one is private for a specific crew. Also, only crew members (which are also users) that have the role ROLE_CREW_BOSS, ROLE_CREW_LEFTHAND or ROLE_CREW_RIGHTHAND can edit the forum topics of their crew. How do I check those kind of complexity? With a Voter maybe?

UPDATE 1

I have solved the problem for 50%, but it's not solid. I've created a voter specific for the object Entity\\ForumTopic.

public function vote(TokenInterface $token, $object, array $attributes)
{
    if ($object instanceof ObjectIdentityInterface) {
        if ($object->getType() == 'Entity\\ForumTopic') {

            /**
             * @var Member $member
             */
            $member = $token->getUser();

            $userTable = new UserTable();
            $user = $userTable->getByMember($member);

            $userInCrewTable = new UserInCrewTable();
            $crewMember = $userInCrewTable->getByUser($user);

            if ($crewMember && in_array($crewMember->getRole(), array('boss', 'lefthand', 'righthand'))) {
                return self::ACCESS_GRANTED;
            }
        }
    }

    return self::ACCESS_ABSTAIN;
}

The only problem here is that I don't use the respective roles, so I can't use the role hierarchy functionality for example. Anyone got a better solution or a improvement on my current solution?

Thanks!

Steffen

Splenius answered 19/12, 2013 at 14:29 Comment(3)
+1 for the database diagram XDValse
please tell me, there is a tool to create such realtion-diagramms :)Adara
@Adara Now sure what I used, but just google "ASCII diagram", e.x. asciiflow.comSplenius
P
3

The default role system of Symfony is role bound to user. Having a role field in your manyToMany table crew_members does not make sense from this point of view.

What you want is authorization based on the user AND on the crew, so you should probably use ACL functionality, and use role only for global permission.

    $objectIdentity = ObjectIdentity::fromDomainObject($forumTopic);
    $acl = $aclProvider->createAcl($objectIdentity);

    $securityIdentity = UserSecurityIdentity::fromAccount($user);

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

(You can check further docs on http://symfony.com/doc/current/cookbook/security/acl.html. You can also use the excellent https://github.com/Problematic/ProblematicAclManagerBundle)

You combine it with a voter :

function vote(TokenInterface $token, $object, array $attributes)
{
    if ($object instanceof ObjectIdentityInterface) {
        if ($object->getType() == 'Entity\\ForumTopic') {

            /**
             * @var Member $member
             */
            $member = $token->getUser();

            if(in_array('ROLE_MODERATOR', $member->getRoles() && empty($object->getCrew()) {
                return self::ACCESS_GRANTED;
            }

            // inject security component via dependecy injection
            // delegate further check to ACL
            if ($this->container['security']->isGranted('EDIT', $object)) {
                return self::ACCESS_GRANTED;
            }
        }
    }
Phane answered 28/12, 2013 at 13:50 Comment(0)
B
0

I would use Symfony acl:

// creating the ACL
$aclProvider = $this->get('security.acl.provider');
$objectIdentity = ObjectIdentity::fromDomainObject($comment);
$acl = $aclProvider->createAcl($objectIdentity);

$roleSecurityIdentity = new RoleSecurityIdentity('ROLE_CREW');
$securityIdentity = $roleSecurityIdentity;

// grant owner access
$acl->insertObjectAce($securityIdentity, MaskBuilder::MASK_OWNER);
$aclProvider->updateAcl($acl);
Bamboo answered 23/12, 2013 at 21:20 Comment(0)
F
0

You can touch the solution ! You just have to do some few things if you want to check the roles. First, register your Voter as a service in order to construct it with the security context :

Add this in your services.yml file :

services:
    your_app.security.voter.forum_topic_owner:
        class: Your\AppBundle\Security\Authorization\Voter\ForumTopicOwnerVoter
        arguments: ["@security.context"]
        tags:
            - { name: security.vote

Now, you will have to define a constructor to get the securityContext and to use it in the vote method :

<?php

namespace Your\AppBundle\Security\Authorization\Voter;

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

class ForumTopicOwnerVoter implements VoterInterface
{
    /** @var SecurityContext */
    protected $securityContext;

    /**
     * @param SecurityContext     $securityContext SecurityContext is the main entry point of the Security component.

     */
    public function __construct(SecurityContext $securityContext)
    {
        $this->securityContext = $securityContext;
    }

    /**
     * {@inheritDoc}
     */
    public function supportsAttribute($attribute)
    {
        return 'FORUM_TOPIC_OWNER' === $attribute;
    }

    /**
     * {@inheritDoc}
     */
    public function supportsClass($class)
    {
        return $class->getType() == 'Entity\\ForumTopic';
    }

    /**
     * {@inheritDoc}
     */
    public function vote(TokenInterface $token, $forumTopic, array $attributes)
    {
        foreach ($attributes as $attribute) {
            if ($this->supportsAttribute($attribute) && $this->supportsClass($forumTopic)) {
                $user = $token->getUser();
                if ($user->hasRole('ROLE_CREW_BOSS')
                    or $this->securityContext->isGranted('ROLE_LEFTHAND')
                    ) {
                        return VoterInterface::ACCESS_GRANTED;
                }
            }
        }

        return VoterInterface::ACCESS_DENIED;
    }
}

Now, you have a Voter, you have to call it on a ForumTopic object, apparently you know how to do this and it was not your problem, may I suggest you anyway to look around the SecureParam annotation of the well known Jms/SecurityExtraBundle. Here is a way to use it in your controller action :

namespace Your\AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use JMS\SecurityExtraBundle\Annotation\Secure;
use JMS\SecurityExtraBundle\Annotation\SecureParam;

/**
 * ForumTopic controller.
 *
 */
class ForumTopicController extends Controller

/**
 * Edit an existing forum topic entity.
 *
 * @param Request    $request    An HTTP request.
 * @param ForumTopic $forumTopic A forumTopic entity.
 *
 * @Secure(roles="ROLE_CREW")
 * @SecureParam(name="forumTopic", permissions="FORUM_TOPIC_OWNER")
 * @ParamConverter("forumTopic", class="YourAppBundle:ForumTopic")
 */
public function editAction(Request $request, ForumTopic $forumTopic)
{
    //Add here your logic
}

I hope it has been helpful to you!

Good luck !

Cheers.

Famous answered 28/12, 2013 at 13:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.