ice
ice

Reputation: 7

Symfony restrict page access to a group of people

I have pages like 'localhost/articles/show/id', representing the article details with the corresponding id. I'd like to restrict the page access to a group of people.

In my database, each User belongs to a Family and each Article belongs to a Family as well. And I want the users to be able to access article informations only if the article has been created by the family that the user is member of.

I could just verify manually by comparing the article's family to the current user family with some request in the Controller before rendering but I would to duplicated this code for every page like '/show/id', '/edit/id', ... Yet I'd like to know if there is a more beautiful way of doing it with symfony, something like 'every page that refers to a specific Article (/edit/id, /show/id and so on so forth) use a specific class to verify if the user is a member of the Family that created the article.

Upvotes: 0

Views: 768

Answers (1)

Julien D.
Julien D.

Reputation: 68

I think the thing you're looking for is Voter.

Security voters are the most granular way of checking permissions. All voters are called each time you use the isGranted() method on Symfony’s authorization checker or call denyAccessUnlessGranted() in a controller.

see: https://symfony.com/doc/current/security/voters.html

// src/Security/PostVoter.php

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

class ArticleVoter extends Voter
{
    // these strings are just invented: you can use anything
    const VIEW = 'view';
    const EDIT = 'edit';

    /**
     * return true if the voter support your entity ($subject) type
     */
    protected function supports(string $attribute, $subject)
    {
        // if the attribute isn't one we support, return false
        if (!in_array($attribute, [self::VIEW, self::EDIT])) {
            return false;
        }

        // only vote on `Article` objects
        if (!$subject instanceof Article) {
            return false;
        }

        return true;
    }

    protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token)
    {
        $user = $token->getUser();

        if (!$user instanceof User) {
            // the user must be logged in; if not, deny access
            return false;
        }

        // you know $subject is a Article object, thanks to `supports()`
        /** @var Article $post */
        $article = $subject;

        switch ($attribute) {
            case self::VIEW:
                return $this->canView($article, $user);
            case self::EDIT:
                return $this->canEdit($article, $user);
        }

        throw new \LogicException('This code should not be reached!');
    }

    private function canView(Article $article, User $user)
    {
        //Return true if user can view article, false otherwise 
    }

    private function canEdit(Article $article, User $user)
        //Return true if user can edit article, false otherwise
    }
}

Voters are used when you call $this->denyAccessUnlessGranted(String $actionName, $entity) from your controllers. this method will throws an exception if a voter that support your $entity type and your $actionName return false.

// src/Controller/ArticleController.php
// ...

class ArticleController extends AbstractController
{
    /**
     * @Route("/article/{id}", name="article_show")
     */
    public function show($id)
    {
        
        $article = ...;

        // check for "view" access: calls all voters
        $this->denyAccessUnlessGranted('view', $article);

        // ...do your stuff
    }

    /**
     * @Route("/article/{id}/edit", name="article_edit")
     */
    public function edit($id)
    {
        $article = ...;

        // check for "edit" access: calls all voters
        $this->denyAccessUnlessGranted('edit', $article);

        // ... do your stuff
    }
}

Upvotes: 4

Related Questions