access_control Authorization & Roles

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Everything that we've done so far has been about authentication: how your user logs in. But now, our space-traveling users can log in! We're loading users from the database, checking their password and even protecting ourselves from the Borg Collective... with CSRF tokens.

So let's start to look at the second part of security: authorization. Authorization is all about deciding whether or not a user should have access to something. This is where, for example, you can require a user to log in before they see some page - or restrict some sections to admin users only.

There are two main ways to handle authorization: first, access_control and second, denying access in your controller. We'll see both, but I want to talk about access_control first, it's pretty cool.

access_control in security.yaml

At the bottom of your security.yaml file, you'll find a key called, well, access_control:

security:
... lines 2 - 38
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }

Uncomment the first access control:

security:
... lines 2 - 40
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
... lines 43 - 44

The path is a regular expression. So, this access control says that any URL that starts with /admin should require a role called ROLE_ADMIN. We'll talk about roles in a minute.

Go to your terminal and run

php bin/console debug:router

Ah, yes, we do already have a few URLs that start with /admin, like /admin/comment. Well... let's see what happens when we try to go there!

Access denied! Cool! We get kicked out!

Roles!

Let's talk about how roles work in Symfony: it's simple and it's beautiful. Down on the web debug toolbar, click on the user icon. Cool: we're logged in as spacebar1@example.com and we have one role: ROLE_USER. Here's the idea: when a user logs in, you give them whatever "roles" you want - like ROLE_USER. Then, you run around your code and make different URLs require different roles. Because our user does not have ROLE_ADMIN, we are denied access.

But... why does our user have ROLE_USER? I don't remember doing anything with roles during the login code. Open the User class. When we ran the make:user command, one of the methods that it generated was getRoles():

... lines 1 - 10
class User implements UserInterface
{
... lines 13 - 66
/**
* @see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
... lines 78 - 128
}

Look at it carefully: it reads a roles property, which is an array that's stored in the database:

... lines 1 - 10
class User implements UserInterface
{
... lines 13 - 24
/**
* @ORM\Column(type="json")
*/
private $roles = [];
... lines 29 - 128
}

Right now, this property is empty for every user in the database: we have not set this to any value in the fixtures.

But, inside getRoles(), there's a little extra logic that guarantees that every user at least has this one role: ROLE_USER:

... lines 1 - 10
class User implements UserInterface
{
... lines 13 - 69
public function getRoles(): array
{
... line 72
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
... lines 75 - 76
}
... lines 78 - 128
}

This is nice because we now know that, if you are logged in, you definitely have this one role. Also... you need to make sure that getRoles() always returns at least one role... otherwise weird stuff happens: the user becomes an undead zombie that is "sort of" logged in.

To prove that this roles system works like we expect, change ROLE_ADMIN to ROLE_USER in the access control:

security:
    # ...
    access_control:
        - { path: ^/admin, roles: ROLE_USER }

Then, click back to the admin page and... access granted!

Change that back to ROLE_ADMIN.

Only One access_control Matches per Page

As you can see in the examples down here, you're allowed to have as many access_control lines as you want: each has their own regular expression path. But, there is one super important thing to understand. Access controls work like routes: Symfony checks them one-by-one from top to bottom. And as soon as it finds one access control that matches the URL, it uses that and stops. Yep, a maximum of one access control is used on each page load.

Actually... this fact allows you to do some cool things if you want most of your pages to require login. We'll talk about that later.

Now that we can deny access... something interesting happens if you try to access a protected page as an anonymous user. Let's see that next.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-time-bundle": "^1.8", // 1.8.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.2.0
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.1.4
        "symfony/console": "^4.0", // v4.1.4
        "symfony/flex": "^1.0", // v1.9.10
        "symfony/framework-bundle": "^4.0", // v4.1.4
        "symfony/lts": "^4@dev", // dev-master
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.1.4
        "symfony/serializer-pack": "^1.0", // v1.0.1
        "symfony/twig-bundle": "^4.0", // v4.1.4
        "symfony/web-server-bundle": "^4.0", // v4.1.4
        "symfony/yaml": "^4.0", // v4.1.4
        "twig/extensions": "^1.5" // v1.5.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.7
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/debug-bundle": "^3.3|^4.0", // v4.1.4
        "symfony/dotenv": "^4.0", // v4.1.4
        "symfony/maker-bundle": "^1.0", // v1.7.0
        "symfony/monolog-bundle": "^3.0", // v3.3.0
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.1.4
        "symfony/profiler-pack": "^1.0", // v1.0.3
        "symfony/var-dumper": "^3.3|^4.0" // v4.1.4
    }
}