Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Role Hierarchy

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

So far, our site has two types of users. First, for some pages, like the account page, we only care that you are logged in - a "normal" user. And second, there are a few admin pages. Open up ArticleAdminController and CommentAdminController. Both of these are protected by ROLE_ADMIN:

... lines 1 - 11
/**
* @IsGranted("ROLE_ADMIN")
*/
class ArticleAdminController extends AbstractController
{
... lines 17 - 29
}

... lines 1 - 11
/**
* @IsGranted("ROLE_ADMIN")
*/
class CommentAdminController extends Controller
{
... lines 17 - 35
}

A lot of sites are just this simple: you have normal users and admin users, who have access to all of the admin sections. But, if you have a more complex setup - like a bigger company where different groups of people need access to different things, this isn't good enough. The question is: what's the best way to organize that with roles?

Role Naming

Well, there are only two possibilities. First, you could use roles that are named by the type of user that will have them - like ROLE_EDITOR, ROLE_HUMAN_RESOURCES or ROLE_THE_PERSON_THAT_OWNS_THE_COMPANY... or something like that. But, I don't love this option. It's just not super clear what having ROLE_EDITOR will give me access to.

Instead, I like to use role names that specifically describe what you're protecting - like ROLE_ADMIN_ARTICLE for ArticleAdminController:

... lines 1 - 11
/**
* @IsGranted("ROLE_ADMIN_ARTICLE")
*/
class ArticleAdminController extends AbstractController
{
... lines 17 - 29
}

And, for CommentAdminController: ROLE_ADMIN_COMMENT:

... lines 1 - 11
/**
* @IsGranted("ROLE_ADMIN_COMMENT")
*/
class CommentAdminController extends Controller
{
... lines 17 - 35
}

Oh, and also open base.html.twig. There's one other spot here where we use ROLE_ADMIN. There it is: to hide or show the "Create Post" link. Now that should be ROLE_ADMIN_ARTICLE:

... line 1
<html lang="en">
... lines 3 - 15
<body>
<nav class="navbar navbar-expand-lg navbar-dark navbar-bg mb-5">
... lines 18 - 21
<div class="collapse navbar-collapse" id="navbarNavDropdown">
... lines 23 - 34
<ul class="navbar-nav ml-auto">
{% if is_granted('ROLE_USER') %}
<li class="nav-item dropdown" style="margin-right: 75px;">
... lines 38 - 40
<div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
... line 42
{% if is_granted('ROLE_ADMIN_ARTICLE') %}
<a class="dropdown-item" href="{{ path('admin_article_new') }}">Create Post</a>
{% endif %}
... line 46
</div>
</li>
... lines 49 - 52
{% endif %}
</ul>
</div>
</nav>
... lines 57 - 74
</body>
</html>

role_hierarchy

I love it! Except... for one problem. Go to /admin/comment. Access denied! Well, I'm not even logged in as an admin user. But even if I were, I would still not have access! Admin users do not have these two new roles!

And, yea, we could go back to UserFixture, add ROLE_ADMIN_COMMENT and ROLE_ADMIN_ARTICLE and then reload the fixtures. But, this highlights an annoying problem. Each time we add a new admin section to the site and introduce a new role, we will need to go into the database, find all the users who need access to that new section, and give them that new role. That's a bummer!

But... don't worry! Symfony has our backs with a sweet feature called role_hierarchy. Open config/packages/security.yaml. Anywhere inside, I'll do it above firewalls, add role_hierarchy. Below, put ROLE_ADMIN set to an array with ROLE_ADMIN_COMMENT and ROLE_ADMIN_ARTICLE:

security:
... lines 2 - 13
role_hierarchy:
ROLE_ADMIN: [ROLE_ADMIN_COMMENT, ROLE_ADMIN_ARTICLE]
... lines 16 - 55

It's just that simple. Now, anybody that has ROLE_ADMIN also has these two roles, automatically. To prove it, go log out so that we can log in as one of our admin users: admin2@thespacebar.com, password engage.

Go back to /admin/comment and... access granted!

This is even cooler than you might think! It allows us to organize our roles into different groups of people in our company. For example, ROLE_EDITOR could be given access to all the sections that "editors" need. Then, the only role that you need to assign to an editor user is this one role: ROLE_EDITOR. And if all editors need access to a new section in the future, just add that new role to role_hierarchy.

We can use this new super-power to try out a really cool feature that allows you to impersonate users... and become the international spy you always knew you would.

Leave a comment!

4
Login or Register to join the conversation
Krzysztof-K Avatar
Krzysztof-K Avatar Krzysztof-K | posted 3 years ago | edited

Sorry about comment in Symfony 2 Track.

What about Roles Hierarchy inside Voter?

ROLE_ADMIN: [ROLE_USER, ROLE_EVENT_CREATE]``

if (in_array('ROLE_EVENT_CREATE', $user->getRoles())) {
return true;
}
`
Above code will fail about that when user has ROLE_ADMIN - it will check only for ROLE_EVENT_CREATE in roles, but ignore whole hierarchy.
How to deal with that?

Is
`
foreach ($user->getRoles() as $role) {

                if (in_array('ROLE_EVENT_CREATE',$this->roleHierarchy->getReachableRoleNames([$role]))) {
                    return true;
                }
            }

`
propper solution?

Reply

Hey Krzysztof K.!

Excellent question :). You're 100% correct that checking for a role in $user->getRoles() is not the correct approach - and that's why I recommend not doing this. There are people that use the roleHierarchy->getReachableRoleNames([$role]) approach also. While this will work, it's not the proper solution - and I think you have already guessed that :). The main problem is that there is already code in the security system which handles all of this. So checking the role hierarchy manually is just re-inventing that wheel. Plus, the proper solution is easier!

The proper solution is hiding way at the end of the tutorial - inject the Security object (no different than any other part of your code that needs to check security) and use it: https://symfonycasts.com/screencast/symfony-security/custom-voter#checking-for-roles-inside-a-voter

Let me know if that helps!

Cheers!

Reply
David B. Avatar
David B. Avatar David B. | posted 3 years ago

What's up!
Why not to use the naming like "ROLE_{RESOURCE}_ADMIN" instead of "ROLE_ADMIN_{RESOURCE}"? =)

Reply

Hey Bagar,

Just a matter of taste? :p Really, there's no any reason behind of it. The only requirement is that your role should start with "ROLE_" and that's it. Well, it comes form the Symfony docs that we advise ROLE_SUPER_ADMIN instead of ROLE_ADMIN_SUPER - probably because the 2nd sounds weird? :) So, it looks like the 1st template sounds better, but techincally it does not matter you can do whatever you want ;)

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

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.17.6
        "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
    }
}
userVoice