Using an expression for disabling the security of a Symfony administration in the dev environment

Published on 2020-10-15 • Modified on 2020-10-15

In this post, we will see how to use an expression for disabling the security of a Symfony administration in the dev environment. We won't use an IP based test like the documentation explains, but we will use the application's environment instead. Let's go! 😎

» Published in "A week of Symfony 720" (12-18 October 2020).

Prerequisite

I will assume you have a basic knowledge of Symfony and the security component.

Configuration

I already migrated to Symfony 5.2, but there is no difference at all with 5.1 for this blog post.

  • PHP 7.4
  • Symfony 5.2.0-BETA2

Introduction

On this project and several other "small" projects, I use the Symfony binary to develop and the EasyAdmin bundle to have a basic administration interface for my databases. This administration is, of course, secured so no one except me can access it. For this kind of projects, I often use the http_basic authentication, which is fast to set up and avoid creating user tables. So when you want to access the administration you get the following browser popup:


The basic HTTP authentication Firefox popup

This browser popup is practical because you don't have to create a login form in your application. But it's also annoying because it's modal and blocks the browser until you validate your login/password or you cancel. You can't even switch to another tab. That's why I wanted to avoid it when I develop, to enhance, once again, the developer experience (DX).

What about the documentation?

First, let's have a look at the documentation; the examples show that we can use an "IP test" to achieve this goal. In this first example, the access is allowed if the request matches one the IP or IP range in the ips option:

# config/packages/security.yaml
security:
    # ...
    access_control:
        #
        # the 'ips' option supports IP addresses and subnet masks
        - { path: '^/internal', roles: IS_AUTHENTICATED_ANONYMOUSLY, ips: [127.0.0.1, ::1, 192.168.0.1/24] }
        - { path: '^/internal', roles: ROLE_NO_ACCESS }

In the second example, an expression is used. This expression checks if the request IP is the local one or if the headers contain a given key:

# config/packages/security.yaml
security:
    # ...
    access_control:
        -
            path: ^/_internal/secure
            # the 'role' and 'allow-if' options work like an OR expression, so
            # access is granted if the expression is TRUE or the user has ROLE_ADMIN
            roles: 'ROLE_ADMIN'
            allow_if: "'127.0.0.1' == request.getClientIp() or request.headers.has('X-Secure-Access')"

Using the application's environment

Now, let's have a look at a different approach. We want the admin to be accessible without authentication only if we use the dev environment. As we saw previously in the documentation, inside the security.yml file, the expression language has access to the request object. We can access the active environment through the server parameter bag :

    access_control:
        - { path: '^/admin', roles: ROLE_ADMIN, allow_if: "'dev' == request.server.get('APP_ENV')" } # role OR allow if...

Now open your browser and access the wanted URL. You shouldn't have to log in to access it like before. You can modify your .env or .env.local files and change the APP_ENV parameter to prod to check that the login popup is back as soon as we switch to the production environment. But better, let's add a functional test for this. In the tests, the environment is "test" so the access should be secured too like the production. The good thing here is that the test will work locally and on the CI, we don't have to "fake" an IP.

<?php declare(strict_types=1);

namespace App\Tests\Controller\Admin;

use App\Tests\WebTestCase;
use Symfony\Component\HttpFoundation\Response;

/**
 * @see DashboardController
 */
final class DashboardControllerTest extends WebTestCase
{
    /**
     * @covers DashboardController::index
     */
    public function testIndex(): void
    {
        $client = static::createClient();
        $client->request('GET', '/admin');
        self::assertResponseStatusCodeSame(Response::HTTP_UNAUTHORIZED);
    }
}

Eventually, you can add a link to access the admin in your layout. We could use the same test (with the app prefix) app.request.server.get('APP_ENV') as the one used by the expression, but here we have a shortcut provided by the app Twig global variable "app.environment" which is much more straightforward:

{% if app.environment == 'dev' %} {# request.server.get('APP_ENV') // works too like in the expression #}
    <a class="dropdown-item" href="{{ path('app_admin_dashboard_index') }}">{{ 'menu_admin'|trans }}</a>
{% endif %}

Conclusion

It's a small test I wanted to do. I prefer using the environment instead of the IP. It's more robust in this case as it's a "server" information that doesn't change: In production, it will always be "prod".

That's it! I hope you like it. Check out the links below to have additional information related to the post. As always, feedback, likes and retweets are welcome. (see the box below) See you! COil. 😊

  Read the doc  More on Stackoverflow


Call to action

Did you like this post? You can help me back in several ways: (use the Tweet on the right to comment or to contact me )

Thank you for reading! And see you soon on Strangebuzz! 😉

COil