Symfony Access Decision Manager

Unamaous.

Unamouse.

Unamimous!

Yeah, I can’t spell.

I just hit upon a problem whereby I needed my Symfony security voter setup to always deny access even when one of my voters returned an Affirmative.

In this case I want to always deny.

By default Symfony will allow access if at least one voter returns GRANTED.

If you want to know more, check out this video I created about this very problem, and some potential solutions.

Gotcha: Upgrading To FOSRestBundle 2.0

This is not a huge issue, and likely quite easy to identify and fix. But I’m the kind of person who hits Google before engaging brain, and I didn’t find a direct answer (boo, wake up brain!).

I have, this very evening, decided to try FOSRestBundle 2.0. It’s currently still in development, so isn’t available without a little naughty composer tagging:

    "require": {
        "friendsofsymfony/rest-bundle": "^2.0@dev"
    },

A little tip there – if you use the @dev after your requested version number, you can force through development packages without globally changing your project’s minimum-stability  setting.

Anyway, being that I’ve been coding for the last 12.5 hours, I went with the lazy man’s option of copy / pasting my config from a FOSRestBundle 1.7 installation I happened to have laying around.

fos_rest:
    view:
        exception_wrapper_handler:  null
        mime_types:
            json: ['application/json', 'application/x-json']
            jpg: 'image/jpeg'
            png: 'image/png'

Firstly, I got this:

InvalidConfigurationException in ArrayNode.php line 317:
Unrecognized option "exception_wrapper_handler" under "fos_rest.view"

Which confuses me, as this seems to be the same as what’s in the configuration reference.

Anyway, simply removing that line makes the error go away.

The next error I got was this:

Warning: array_merge(): Argument #1 is not an array
500 Internal Server Error - ContextErrorException

And what this turns out to be is that the mime_types must now all be arrays:

fos_rest:
    view:
        mime_types:
            json: ['application/json', 'application/x-json']
            jpg: ['image/jpeg']
            png: ['image/png']

So there you go 🙂

Checking An Edge Case With Symfony Voters

I love Symfony Voters. The Access Control List is, in the vast majority of cases, totally overkill for my security needs. Symfony’s Security Voters are a very quick and easy alternative.

Yesterday I got caught out by an edge case I had not been testing for.

A little background on this – I have been creating a RESTful API using Symfony 3, Behat 3, PHPSpec, and a few other interesting technologies. If you are interested in knowing a little more about this then be sure to check out the video tutorial series here on CodeReviewVideos where I have been documenting as I go.

The video tutorial series covers three different entities – Users (FOSUserBundle), Accounts, and Files (with FlySystem).

I want to ensure the currently logged in User has access to the resources they are requesting. I’ve been doing this with Voters.

It’s likely easier to follow along with these words if I give them more context. Here’s the code for the File Voter:

<?php

namespace spec\AppBundle\Security\Authorization\Voter;

use AppBundle\Model\AccountInterface;
use AppBundle\Model\FileInterface;
use AppBundle\Model\UserInterface;
use AppBundle\Security\Authorization\Voter\FileVoter;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;

class FileVoterSpec extends ObjectBehavior
{
    function it_is_initializable()
    {
        $this->shouldHaveType('AppBundle\Security\Authorization\Voter\FileVoter');
        $this->shouldImplement('Symfony\Component\Security\Core\Authorization\Voter\VoterInterface');
    }

    function it_supports_expected_attributes()
    {
        $this->supportsAttribute('view')->shouldReturn(true);
    }

    function it_doesnt_support_unexpected_attributes()
    {
        $this->supportsAttribute('1')->shouldReturn(false);
    }

    function it_supports_expected_class(FileInterface $file)
    {
        $this->supportsClass($file)->shouldReturn(true);
    }

    function it_doesnt_support_unexpected_class()
    {
        $this->supportsClass(new \StdClass())->shouldReturn(false);
    }

    function it_abstains_if_doesnt_support_attribute(TokenInterface $token, FileInterface $file)
    {
        $this->vote($token, $file, ['unsupported'])->shouldReturn(VoterInterface::ACCESS_ABSTAIN);
    }

    function it_abstains_if_doesnt_support_this_class(TokenInterface $token)
    {
        $this->vote($token, new \StdClass(), [FileVoter::VIEW])->shouldReturn(VoterInterface::ACCESS_ABSTAIN);
    }

    function it_denies_if_not_matching(
        TokenInterface $token,
        UserInterface $user,
        FileInterface $file
    )
    {
        $token->getUser()->willReturn($user);

        $file->belongsToUser($user)->willReturn(false);

        $this->vote($token, $file, [FileVoter::VIEW])
            ->shouldReturn(VoterInterface::ACCESS_DENIED);
    }

    function it_grants_if_matching(
        TokenInterface $token,
        UserInterface $user,
        FileInterface $file
    )
    {
        $token->getUser()->willReturn($user);

        $file->belongsToUser($user)->willReturn(true);

        $this->vote($token, $file, [FileVoter::VIEW])
            ->shouldReturn(VoterInterface::ACCESS_GRANTED);
    }
}

And the implementation:

<?php

namespace AppBundle\Security\Authorization\Voter;

use AppBundle\Model\AccountInterface;
use AppBundle\Model\FileInterface;
use AppBundle\Model\UserInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;

class FileVoter implements VoterInterface
{
    const VIEW = 'view';

    /**
     * @param string $attribute
     * @return bool
     */
    public function supportsAttribute($attribute)
    {
        return in_array($attribute, array(
            self::VIEW,
        ));
    }

    /**
     * @param string $class
     * @return bool
     */
    public function supportsClass($class)
    {
        $supportedClass = 'AppBundle\Model\FileInterface';

        return $supportedClass === $class || is_subclass_of($class, $supportedClass);
    }

    /**
     * @param TokenInterface            $token
     * @param null|FileInterface        $requestedFile
     * @param array                     $attributes
     * @return int
     */
    public function vote(TokenInterface $token, $requestedFile, array $attributes)
    {
        // check if class of this object is supported by this voter
        if (!$this->supportsClass(get_class($requestedFile))) {
            return VoterInterface::ACCESS_ABSTAIN;
        }

        // set the attribute to check against
        $attribute = $attributes[0];

        // check if the given attribute is covered by this voter
        if (!$this->supportsAttribute($attribute)) {
            return VoterInterface::ACCESS_ABSTAIN;
        }

        // get current logged in user
        $loggedInUser = $token->getUser(); /** @var $loggedInUser UserInterface */

        if ($requestedFile->belongsToUser($loggedInUser)) {
            return VoterInterface::ACCESS_GRANTED;
        }

        return VoterInterface::ACCESS_DENIED;
    }

}

This all works and is passing.

However, there is an edge case I hadn’t considered.

What happens if $requestedFile->belongsToUser($loggedInUser) fails, or returns null? Well, bad times as it happens.

Why might it fail? Well, let’s look at that interface, and the real method implementation:

<?php

namespace AppBundle\Security\Authorization;

use AppBundle\Model\UserInterface;

/**
 * Interface UserOwnableInterface
 * @package AppBundle\Security\Authorization
 */
interface UserOwnableInterface
{
    /**
     * @param UserInterface $user
     * @return bool
     */
    public function belongsToUser(UserInterface $user);
}

And the implementation:

    /**
     * @param UserInterface $user
     * @return bool
     */
    public function belongsToUser(UserInterface $user)
    {
        return $this
            ->getAccounts()
            ->filter(function ($account) use ($user) { /** @var $account AccountInterface */
                return $account->isManagedBy($user);
            })
            ->count() > 0
        ;
    }

Due to a mistake in my fixtures, the File wasn’t correctly being added to the Account I expected, so the call of getAccounts() was returning null.

In this case, I first fixed my fixtures as that situation should never really happen.

But what if it does? I don’t want to be throwing errors to the user. It’s easier to deny them access, log the fault (using monolog, and/or to ELK), and fix it in a slightly more controlled manner.

I added the following two specs to my FileVoterSpec test file:

    function it_denies_if_belongsToUser_throws(
        TokenInterface $token,
        UserInterface $user,
        FileInterface $file
    )
    {
        $token->getUser()->willReturn($user);

        $file->belongsToUser($user)->willThrow(new \Exception());

        $this->vote($token, $file, [FileVoter::VIEW])
            ->shouldReturn(VoterInterface::ACCESS_DENIED);
    }

    function it_denies_if_belongsToUser_returns_null(
        TokenInterface $token,
        UserInterface $user,
        FileInterface $file
    )
    {
        $token->getUser()->willReturn($user);

        $file->belongsToUser($user)->willReturn(null);

        $this->vote($token, $file, [FileVoter::VIEW])
            ->shouldReturn(VoterInterface::ACCESS_DENIED);
    }

And then changed up the implementation (uglify!):

    /**
     * @param TokenInterface            $token
     * @param null|FileInterface        $requestedFile
     * @param array                     $attributes
     * @return int
     */
    public function vote(TokenInterface $token, $requestedFile, array $attributes)
    {
        // check if class of this object is supported by this voter
        if (!$this->supportsClass(get_class($requestedFile))) {
            return VoterInterface::ACCESS_ABSTAIN;
        }

        // set the attribute to check against
        $attribute = $attributes[0];

        // check if the given attribute is covered by this voter
        if (!$this->supportsAttribute($attribute)) {
            return VoterInterface::ACCESS_ABSTAIN;
        }

        // get current logged in user
        $loggedInUser = $token->getUser(); /** @var $loggedInUser UserInterface */

        try {
            if ($requestedFile->belongsToUser($loggedInUser)) {

                return VoterInterface::ACCESS_GRANTED;
            }
        } catch (\Exception $e) {

            return VoterInterface::ACCESS_DENIED;
        }

        return VoterInterface::ACCESS_DENIED;
    }

Note the try/catch block. It’s definitely made the code uglier, but it ‘fixes’ a potential problem I hadn’t considered.

I say ‘fixes’ as this should never happen. But if I have learned anything over the last 15-odd years, it’s that those things that should never happen… they happen.

Custom Route Name in FOSRESTBundle

I recently had an issue where I wanted to have a controller called SocialMediaProfilesController, but whatever I tried, the automatically generated route names kept coming out as e.g. get_socialmedia_profiles.

I haven’t dived deep into the code to determine as to why this weird spacing issue is occurring, but my guess would be that if you created a controller called SomeEvenLongerStringController, your action might be get_someevenlonger_string. But as I say, I haven’t tried this, so it’s just a guess as to what might be happening.

Anyway, the fix is pretty simple. It’s probably out there on Google, or in the docs, or something, but I couldn’t find it. Instead, I stumbled upon this and it works, so here we go:

<?php

namespace AppBundle\Controller;

use FOS\RestBundle\View\View;
use FOS\RestBundle\Controller\Annotations;
use FOS\RestBundle\Controller\FOSRestController;
use FOS\RestBundle\Routing\ClassResourceInterface;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
// some other use statements here, but these are the interesting ones 

/**
 * Class SocialMediaProfilesController
 * @package AppBundle\Controller
 * @Annotations\RouteResource("socialmediaprofiles")
 */
class SocialMediaProfilesController extends FOSRestController implements ClassResourceInterface
{

By using the RouteResource annotation, you can control how the route names will be generated.

Without the annotation:

  cget_accounts_socialmedia_profiles     GET      ANY      ANY    /accounts/{accountId}/social-media-profiles
  get_accounts_socialmedia_profiles      GET      ANY      ANY    /accounts/{accountId}/social-media-profiles/{socialMediaProfileId}

And then with:

  cget_accounts_socialmediaprofiles     GET      ANY      ANY    /accounts/{accountId}/social-media-profiles
  get_accounts_socialmediaprofiles      GET      ANY      ANY    /accounts/{accountId}/social-media-profiles/{socialMediaProfileId}

So that’s pretty handy.

Debugging By Dumping

By now, most of us will likely have used the dump($var); function made available for us in Symfony 2.6.

If you haven’t, or you think you can’t because you a) don’t have the latest and greatest version of Symfony, or b) don’t use Symfony, then never fear, you can include the VarDumper component in any project. No excuses.

Whilst the  dump($var); function is incredibly useful, in one instance so far, it has caused me more headaches than it has solved.

The Situation

I’m working on a new RESTful API using Symfony 2 as my back end.

Along the way, I want to restrict Users from being able to access other peoples data, for obvious reasons.

Without getting too deep into the code – as it’s really not that important to this problem / solution – I hit upon an issue where I wasn’t getting the expected output. Such is life of a developer.

However, I can tell you are getting twitchy from having not yet seen any code, so to placate your code cravings, here is the method:

    /**
     * @return array
     */
    public function findAll()
    {
        $accounts = $this->repository->findAll();

        foreach ($accounts as $account) {
            $this->denyAccessUnlessGranted('view', $account);
        }

        return $accounts;
    }

See, I told you, not much to look at.

At some point along the way (and in truth, I haven’t figured out exactly where as of yet), I know that $this->denyAccessUnlessGranted(‘view’, $account);  is having a bad time.

To begin with, I wanted to see exactly what the contents of $accounts was before it even hit the foreach loop.

To the dumper!

    /**
     * @return array
     */
    public function findAll()
    {
        $accounts = $this->repository->findAll();

        exit(dump($accounts));

        foreach ($accounts as $account) {
            $this->denyAccessUnlessGranted('view', $account);
        }

        return $accounts;
    }

Alas, no:

the-output-of-symfony-vardumper-dump

The next thing you might think is – ok, just use print_r($var); and be done with it.

Unfortunately, that’s not an option when dealing with most Doctrine entities. If you try, you may get lucky, you may hit funky maximum nesting level errors, or worse, you may crash your DHC client. Guess which I encountered?

My Solution

Never the less, there is a solution. Quite an old school solution. Something I used to use all the time before the VarDumper component was a thing:

exit(\Doctrine\Common\Util\Debug::dump($yourVariableHere));

This will give you an equivalent of print_r($var); but without the nasty blow ups:

the-output-of-doctrine-dump

It’s an old technique sir, but it checks out.

Now just to figure out why the test is failing, and everything will be right with the world once more.