Symfony 4: ROLE_USER doesn’t inherit IS_AUTHENTICATED_FULLY and Voter throws AccessDeniedException

Tags: , , ,



I’m working on a legacy project with a lot of ancient staffs. There’re huge numbers of actions which are used really rare. Half a year ago we upgraded from Symfony 2.8 to Symfony 4.4. All worked pretty well until a manager tried to use one of the old action that now returns AccessDeniedException: Access Denied.

I’ve checked Symfony documentation and all seems pretty straightforward for me.

The documentation says:

Checking to see if a User is Logged In (IS_AUTHENTICATED_FULLY)

If you only want to check if a user is logged in (you don’t care about roles), you have two options. First, if you’ve given every user ROLE_USER, you can check for that role.

There is app/config/security.yml with the next configuration:

security:
    access_decision_manager:
        strategy: unanimous
        allow_if_all_abstain: true

    encoders:
        FOSUserBundleModelUserInterface:
            algorithm: sha512
            encode_as_base64: false
            iterations: 1

    role_hierarchy:
        ROLE_CUSTOMER_ADMIN: ROLE_USER
        ROLE_ADMIN: ROLE_USER
        ROLE_ADMIN_CAN_EDIT_PERMISSIONS: ROLE_ADMIN
        ROLE_SUPER_ADMIN: ROLE_ADMIN_CAN_EDIT_PERMISSIONS

    providers:
          fos_user:
              id: fos_user.user_provider.username

    firewalls:
        main:
            pattern: ^/
            form_login:
                provider: fos_user
                default_target_path: /user-post-login
                always_use_default_target_path: true
                login_path: user_security_login
                check_path: fos_user_security_check
            logout:
                path: /logout
                target: /login
                handlers: [mp.logout_handler]
                invalidate_session: false
            anonymous: ~
            remember_me:
                secret:      "%secret%"
                lifetime: 31536000 # 365 days in seconds
                path:     /
                domain:   ~ # Defaults to the current domain from $_SERVER

    access_control:
        - { path: ^/login, role: IS_AUTHENTICATED_ANONYMOUSLY }
        ...
        - { path: ^/api, role: IS_AUTHENTICATED_ANONYMOUSLY }
        ...
        - { path: ^/admin/select-customer-status, role: [ROLE_CUSTOMER_ADMIN, ROLE_SUPER_ADMIN] }
        ...
        - { path: ^/admin, role: ROLE_USER }

My current user has the role ROLE_SUPER_ADMIN, according to role_hierarchy this role has ROLE_USER in ancestors, but when I try to open http://localhost:8080/admin/select-customer-status/1 I get this Access Denied exception.

I tried to debug and figured out this exception arose in SymfonyComponentSecurityHttpFirewallAccessListener

enter image description here

But the real problem is that Voter is checking for IS_AUTHENTICATED_FULLY under the hood, but this one isn’t present in $attributes.

Another interesting thing is when I add this configuration directly to Action it works as expected and no exception is thrown:

/**
 * @Route("/admin/update-user-permissions/{id}", name="update_user_permissions")
 * @Method({"POST"})
 * 
 * @IsGranted("ROLE_SUPER_ADMIN")
 * @IsGranted("ROLE_CUSTOMER_ADMIN")
 */

Can anybody help with this strange behaviour?

P.S. There is a similar question, but it’s for Symfony 2 and not reliable for me.

Answer

what apparently happens, is, that all of the roles mentioned are required, i.e. both ROLE_SUPER_ADMIN as well as ROLE_CUSTOMER_ADMIN. As far as I understand your problem description, your user is either, but not both.

The screenshot you provided shows the access manager handling the request at the bottom: enter image description here

which clearly shows, that the AuthenticatedVoter is NOT the problem, because it abstains (since it only votes on IS_AUTHENTICATED_* roles (for example IS_AUTHENTICATED_FULLY), which is also easily seen in its code.

However, as can be easily seen as well, both roles are checked separately(!). The RoleHierarchyVoter now tries to see, if the ROLE_CUSTOMER_ADMIN is granted, and I assume your user has the ROLE_SUPER_ADMIN, which the RoleHierarchyVoter will expand by using the hierarchy to ROLE_SUPER_ADMIN,ROLE_ADMIN_CAN_EDIT_PERMISSIONS, ROLE_ADMIN and ROLE_USER. None of which is ROLE_CUSTOMER_ADMIN.

As I have suggested in a comment, you might want to choose to add a new ROLE that both extended admin roles hold. if ROLE_ADMIN is not appropriate, maybe a ROLE_EXTENDED_ADMIN or something.

ROLE_CUSTOMER_ADMIN: [ROLE_ADMIN, ROLE_EXTENDED_ADMIN]
ROLE_SUPER_ADMIN: [ROLE_ADMIN_CAN_EDIT_PERMISSIONS, ROLE_EXTENDED_ADMIN]

and then in access control:

 - { path: ^/admin/select-customer-status, roles: ROLE_EXTENDED_ADMIN }


Source: stackoverflow