Using Symfony's Security Annotation


In this video we are going to continue looking into the @Security annotation. We are going to use the Security Annotation to secure actions inside the members area controller.

The end result we are aiming for here is to stop a situation whereby a User is valid, and authenticated on our website, but may not be authorised to view certain parts of the system.

We might want to secure our members area entirely from people who have not yet logged in.

However, just because a User has logged in does not necessarily mean they should be able to access any part of our system.

Imagine we have a multi-user system whereby members of the site can create blog posts.

Once a member has created a blog post, they should be able to edit that post, maybe delete the post, and also create more posts.

Other members of the system should not be able to edit, or delete a different User's blog posts. That's the sort of set up we are aiming for here.

A standard access_control entry would not be sufficient here to restrict access down on such a granular basis. This is where the Security Annotation can come to the rescue.

Why wouldn't an access_control entry be sufficient?

Assuming we had something similar to this:

# app/config/security.yml
security:
    access_control:
        - { path: ^/members-area/, role: ROLE_USER }

As mentioned, this would ensure access to the members area was restricted to authenticated / logged in users. However, once a user had successfully logged in, if this was our only layer of protection, the User would be able to access any URL beginning with /members-area.

Restricting Individual Controller Actions

We can use the @Security annotations to apply more specific, or granular security requirements on a per controller action basis.

To be able to use the Security Annotation, we must tell Symfony that we want to use it, so we need the appropriate use statement:

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;

As soon as we have done this, we can start using the Security Annotation on the individual controller actions.

To make use of the Security Annotation we must supply our own expression.

An expression is Symfony specific concept. In fact, the ExpressionLanguage component was developed for use by the Symfony framework. Yes, another language to learn whilst using PHP...

Generally though, expressions are fairly simple, easy to read statements which most of the time will return a boolean value (true or false).

The @Security annotation will then use the result of this expression to determine whether or not we should be allowed access to the given resource.

There is plenty more to read up on the Expression Language Component, should you need to do so.

Without having to do anything further, the expression you are writing will have access to the following variables:

  • token: The current security token;
  • user: The current user object;
  • request: The request instance;
  • roles: The user roles;
  • and all request attributes.

(taken from the Symfony docs)

This is incredibly useful as immediately you can start writing logic against the currently logged in user.

To really make this more powerful, we can use another Symfony annotation to help us - the Param Convertor.

Param Convertors can take parts of a given URL (parameters) and convert them into the associated entity that they represent. For example, if we had a route configured as:

use AppBundle\Entity\Content;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;

/**
 * @Route("/edit/{id}", name="member_edit")
 * @ParamConverter("content", class="AppBundle:Content")
 */
public function editAction(Content $content)
{

}

The Param Convertor would be able to translate / convert the {id} part of our @Route directly into a Content entity, based on the id value.

If the id doesn't match a piece of Content in our database, then a 404 error will be thrown.

Also, we can even do away with the @ParamConverter("content", class="AppBundle:Content") line entirely, and rely on Symfony magic to understand that our type hinted Content directly maps to the id on the @Route. In other words, this is identical to the above:

use AppBundle\Entity\Content;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

/**
 * @Route("/edit/{id}", name="member_edit")
 */
public function editAction(Content $content)
{

}

Again, more to read here if unsure of how to use Param Convertors.

Did It Match?

I'm making the assumption here that our User entity has an associated collection of Content. At least, that's how it is set up in the video.

The reason for making this assumption is that this will give the User object access to the various methods available on a Doctrine Array Collection. We're going to use on of these in our security expression.

Thanks to the Param Convertor, we have a valid Content entity. This is perfect for passing in to the contains method of the User's Content collection.

Our @Security annotation can be then written:

use AppBundle\Entity\Content;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;

/**
 * @Route("/edit/{id}", name="member_edit")
 * @Security("user.getContent().contains(content)")
 */
public function editAction(Content $content)
{

}

Notice, no -> syntax, nor $ signs on our variables. It looks closer to JavaScript than PHP, but underneath it is still PHP.

This will check whether our User's collection of Content contains the request $content object, and if so, will allow us access. If not, it's 403 error time.

Security Annotation Reusability

There are two big drawbacks for me on Security annotations.

Firstly, I cannot write tests for my Security logic. Not good.

Secondly, I cannot easily re-use the individual Security annotation logic, without refactoring - as per the video (5 mins onwards).

Even if we do the refactoring, we move our 'validation' or guard logic inside our entity class, which I am not a fan of.

Instead, we can look at Security Voters which solve this problem in a nicer way (in my humble opinion), and are much easier to re-use.

That's exactly what we shall do in the next video.

Episodes

# Title Duration
1 The Application Setup and Introduction 06:17
2 Using Symfony's Security Annotation 06:43
3 Simple Security Voters 08:28
4 Customising your Access Decision Stategy 04:58