From 5deeb53bb20c1c9e7288bbfe8f44503f1bbd1f8f Mon Sep 17 00:00:00 2001 From: Jules Pietri Date: Fri, 8 Mar 2019 18:01:31 +0100 Subject: [PATCH] Improved documentation about access controls --- configuration/external_parameters.rst | 1 + security.rst | 14 ++-- security/access_control.rst | 88 +++++++++++++++------- security/api_key_authentication.rst | 4 +- security/force_https.rst | 8 +- security/form_login_setup.rst | 12 +-- security/multiple_guard_authenticators.rst | 12 +-- security/voters.rst | 5 +- 8 files changed, 89 insertions(+), 55 deletions(-) diff --git a/configuration/external_parameters.rst b/configuration/external_parameters.rst index 579bc168a75..f3cf284a784 100644 --- a/configuration/external_parameters.rst +++ b/configuration/external_parameters.rst @@ -286,6 +286,7 @@ Symfony provides the following env var processors: # config/packages/security.yaml parameters: env(HEALTH_CHECK_METHOD): 'Symfony\Component\HttpFoundation\Request::METHOD_HEAD' + security: access_control: - { path: '^/health-check$', methods: '%env(const:HEALTH_CHECK_METHOD)%' } diff --git a/security.rst b/security.rst index 4443a7ef33a..034390b00d3 100644 --- a/security.rst +++ b/security.rst @@ -226,7 +226,7 @@ user to be logged in to access this URL: access_control: # require ROLE_ADMIN for /admin* - - { path: ^/admin, roles: ROLE_ADMIN } + - { path: '^/admin', roles: ROLE_ADMIN } .. code-block:: xml @@ -713,7 +713,7 @@ URL pattern. You saw this earlier, where anything matching the regular expressio access_control: # require ROLE_ADMIN for /admin* - - { path: ^/admin, roles: ROLE_ADMIN } + - { path: '^/admin', roles: ROLE_ADMIN } .. code-block:: xml @@ -751,7 +751,7 @@ URL pattern. You saw this earlier, where anything matching the regular expressio ], 'access_control' => [ // require ROLE_ADMIN for /admin* - ['path' => '^/admin', 'role' => 'ROLE_ADMIN'], + ['path' => '^/admin', 'roles' => 'ROLE_ADMIN'], ], ]); @@ -773,8 +773,8 @@ matches the URL. # ... access_control: - - { path: ^/admin/users, roles: ROLE_SUPER_ADMIN } - - { path: ^/admin, roles: ROLE_ADMIN } + - { path: '^/admin/users', roles: ROLE_SUPER_ADMIN } + - { path: '^/admin', roles: ROLE_ADMIN } .. code-block:: xml @@ -801,8 +801,8 @@ matches the URL. // ... 'access_control' => [ - ['path' => '^/admin/users', 'role' => 'ROLE_SUPER_ADMIN'], - ['path' => '^/admin', 'role' => 'ROLE_ADMIN'], + ['path' => '^/admin/users', 'roles' => 'ROLE_SUPER_ADMIN'], + ['path' => '^/admin', 'roles' => 'ROLE_ADMIN'], ], ]); diff --git a/security/access_control.rst b/security/access_control.rst index d639e56b611..a3c3e57e3a6 100644 --- a/security/access_control.rst +++ b/security/access_control.rst @@ -22,10 +22,10 @@ for each ``access_control`` entry, which determines whether or not a given access control should be used on this request. The following ``access_control`` options are used for matching: -* ``path`` -* ``ip`` or ``ips`` (netmasks are also supported) -* ``host`` -* ``methods`` +* ``path``: a regular expression (without delimiters) +* ``ip`` or ``ips``: netmasks are also supported +* ``host``: a regular expression +* ``methods``: one or many methods Take the following ``access_control`` entries as an example: @@ -37,10 +37,11 @@ Take the following ``access_control`` entries as an example: security: # ... access_control: - - { path: ^/admin, roles: ROLE_USER_IP, ip: 127.0.0.1 } - - { path: ^/admin, roles: ROLE_USER_HOST, host: symfony\.com$ } - - { path: ^/admin, roles: ROLE_USER_METHOD, methods: [POST, PUT] } - - { path: ^/admin, roles: ROLE_USER } + - { path: '^/admin', roles: ROLE_USER_IP, ip: 127.0.0.1 } + - { path: '^/admin', roles: ROLE_USER_HOST, host: symfony\.com$ } + - { path: '^/admin', roles: ROLE_USER_METHOD, methods: [POST, PUT] } + # Defining many roles means we need one of them (like an OR condition) + - { path: '^/admin', roles: [ROLE_MANAGER, ROLE_ADMIN] } .. code-block:: xml @@ -57,7 +58,8 @@ Take the following ``access_control`` entries as an example: - + + @@ -69,22 +71,23 @@ Take the following ``access_control`` entries as an example: 'access_control' => [ [ 'path' => '^/admin', - 'role' => 'ROLE_USER_IP', - 'ip' => '127.0.0.1', + 'roles' => 'ROLE_USER_IP', + 'ips' => '127.0.0.1', ], [ 'path' => '^/admin', - 'role' => 'ROLE_USER_HOST', + 'roles' => 'ROLE_USER_HOST', 'host' => 'symfony\.com$', ], [ 'path' => '^/admin', - 'role' => 'ROLE_USER_METHOD', + 'roles' => 'ROLE_USER_METHOD', 'methods' => 'POST, PUT', ], [ 'path' => '^/admin', - 'role' => 'ROLE_USER', + // Defining many roles, mean we need one of them (like an OR condition) + 'roles' => ['ROLE_MANAGER', 'ROLE_ADMIN'], ], ], ]); @@ -92,8 +95,8 @@ Take the following ``access_control`` entries as an example: For each incoming request, Symfony will decide which ``access_control`` to use based on the URI, the client's IP address, the incoming host name, and the request method. Remember, the first rule that matches is used, and -if ``ip``, ``host`` or ``method`` are not specified for an entry, that ``access_control`` -will match any ``ip``, ``host`` or ``method``: +if ``ips``, ``host`` or ``methods`` are not specified for an entry, that +``access_control`` will match any ``ips``, ``host`` or ``methods``: +-----------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ | URI | IP | HOST | METHOD | ``access_control`` | Why? | @@ -114,9 +117,10 @@ will match any ``ip``, ``host`` or ``method``: | ``/admin/user`` | 168.0.0.1 | example.com | POST | rule #3 (``ROLE_USER_METHOD``) | The ``ip`` and ``host`` don't match the first two entries, | | | | | | | but the third - ``ROLE_USER_METHOD`` - matches and is used. | +-----------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ -| ``/admin/user`` | 168.0.0.1 | example.com | GET | rule #4 (``ROLE_USER``) | The ``ip``, ``host`` and ``method`` prevent the first | +| ``/admin/user`` | 168.0.0.1 | example.com | GET | rule #4 (``ROLE_MANAGER``) | The ``ip``, ``host`` and ``method`` prevent the first | | | | | | | three entries from matching. But since the URI matches the | -| | | | | | ``path`` pattern of the ``ROLE_USER`` entry, it is used. | +| | | | | | ``path`` pattern, then the ``ROLE_MANAGER`` (or the | +| |    | | | | ``ROLE_ADMIN``) is used. | +-----------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+ | ``/foo`` | 127.0.0.1 | symfony.com | POST | matches no entries | This doesn't match any ``access_control`` rules, since its | | | | | | | URI doesn't match any of the ``path`` values. | @@ -144,6 +148,14 @@ options: does not match this value (e.g. ``https``), the user will be redirected (e.g. redirected from ``http`` to ``https``, or vice versa). +.. tip:: + + Behind the scenes, the array value of ``roles`` is passed as the + ``$attributes`` argument to each voter in the application with the + :class:`Symfony\\Component\\HttpFoundation\\Request` as ``$subject``. You + can learn how to use your custom attributes by reading + :ref:`security/custom-voter`. + .. tip:: If access is denied, the system will try to authenticate the user if not @@ -180,8 +192,8 @@ pattern so that it is only accessible by requests from the local server itself: 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 } + - { path: '^/internal', roles: IS_AUTHENTICATED_ANONYMOUSLY, ips: [127.0.0.1, ::1, 192.168.0.1/24] } + - { path: '^/internal', roles: ROLE_NO_ACCESS } .. code-block:: xml @@ -214,13 +226,13 @@ pattern so that it is only accessible by requests from the local server itself: 'access_control' => [ [ 'path' => '^/internal', - 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY', + 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY', // the 'ips' option supports IP addresses and subnet masks 'ips' => ['127.0.0.1', '::1'], ], [ 'path' => '^/internal', - 'role' => 'ROLE_NO_ACCESS', + 'roles' => 'ROLE_NO_ACCESS', ], ], ]); @@ -265,7 +277,9 @@ key: access_control: - path: ^/_internal/secure - allow_if: "'127.0.0.1' == request.getClientIp() or has_role('ROLE_ADMIN')" + roles: 'ROLE_ADMIN' + // with this expression, we need it to be true OR a ROLE_ADMIN + allow_if: "'127.0.0.1' == request.getClientIp() or request.header.has('X-Secure-Access')" .. code-block:: xml @@ -279,7 +293,9 @@ key: + role="ROLE_ADMIN" + + allow-if="'127.0.0.1' == request.getClientIp() or request.header.has('X-Secure-Access')" /> @@ -288,13 +304,27 @@ key: 'access_control' => [ [ 'path' => '^/_internal/secure', - 'allow_if' => '"127.0.0.1" == request.getClientIp() or has_role("ROLE_ADMIN")', + 'roles' => 'ROLE_ADMIN', + // with this expression, we need it to be true OR a ROLE_ADMIN + 'allow_if' => '"127.0.0.1" == request.getClientIp() or request.header.has('X-Secure-Access')', ], ], -In this case, when the user tries to access any URL starting with ``/_internal/secure``, -they will only be granted access if the IP address is ``127.0.0.1`` or if -the user has the ``ROLE_ADMIN`` role. +In this case, when the user tries to access any URL starting with +``/_internal/secure``, they will only be granted access if the IP address is +``127.0.0.1`` or a secure header, or if the user has the ``ROLE_ADMIN`` role. + +.. note:: + + Internally ``allow_if`` triggers the built-in + :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\ExpressionVoter` + as like it was part of the attributes defined in the ``roles`` option. + By default, access is granted if one of the attribute is granted. In other + words, it means ``allow_if`` can grant access even if the + :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\RoleVoter` or + any other voter denied it. + To change this behavior, you may need to learn more about + :ref:`access decision strategy `. Inside the expression, you have access to a number of different variables and functions including ``request``, which is the Symfony @@ -345,7 +375,7 @@ the user will be redirected to ``https``: 'access_control' => [ [ 'path' => '^/cart/checkout', - 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY', + 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY', 'requires_channel' => 'https', ], ], diff --git a/security/api_key_authentication.rst b/security/api_key_authentication.rst index b0761402e68..dc8394caa55 100644 --- a/security/api_key_authentication.rst +++ b/security/api_key_authentication.rst @@ -369,7 +369,7 @@ If you have defined ``access_control``, make sure to add a new entry: # ... access_control: - - { path: ^/api, roles: ROLE_API } + - { path: '^/api', roles: ROLE_API } .. code-block:: xml @@ -392,7 +392,7 @@ If you have defined ``access_control``, make sure to add a new entry: 'access_control' => [ [ 'path' => '^/api', - 'role' => 'ROLE_API', + 'roles' => 'ROLE_API', ], ], ]); diff --git a/security/force_https.rst b/security/force_https.rst index dd8e4fc1b3b..5876c6a5f18 100644 --- a/security/force_https.rst +++ b/security/force_https.rst @@ -18,7 +18,7 @@ to use HTTPS then you could use the following configuration: # ... access_control: - - { path: ^/secure, roles: ROLE_ADMIN, requires_channel: https } + - { path: '^/secure', roles: ROLE_ADMIN, requires_channel: https } .. code-block:: xml @@ -46,7 +46,7 @@ to use HTTPS then you could use the following configuration: 'access_control' => [ [ 'path' => '^/secure', - 'role' => 'ROLE_ADMIN', + 'roles' => 'ROLE_ADMIN', 'requires_channel' => 'https', ], ], @@ -66,7 +66,7 @@ role: # ... access_control: - - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https } + - { path: '^/login', roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https } .. code-block:: xml @@ -97,7 +97,7 @@ role: 'access_control' => [ [ 'path' => '^/login', - 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY', + 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY', 'requires_channel' => 'https', ], ], diff --git a/security/form_login_setup.rst b/security/form_login_setup.rst index 527ffa120f2..0f5f02c261c 100644 --- a/security/form_login_setup.rst +++ b/security/form_login_setup.rst @@ -283,7 +283,7 @@ all URLs (including the ``/login`` URL), will cause a redirect loop: # ... access_control: - - { path: ^/, roles: ROLE_ADMIN } + - { path: '^/', roles: ROLE_ADMIN } .. code-block:: xml @@ -307,7 +307,7 @@ all URLs (including the ``/login`` URL), will cause a redirect loop: // ... 'access_control' => [ - ['path' => '^/', 'role' => 'ROLE_ADMIN'], + ['path' => '^/', 'roles' => 'ROLE_ADMIN'], ], Adding an access control that matches ``/login/*`` and requires *no* authentication @@ -321,8 +321,8 @@ fixes the problem: # ... access_control: - - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: ^/, roles: ROLE_ADMIN } + - { path: '^/login', roles: IS_AUTHENTICATED_ANONYMOUSLY } + - { path: '^/', roles: ROLE_ADMIN } .. code-block:: xml @@ -347,8 +347,8 @@ fixes the problem: // ... 'access_control' => [ - ['path' => '^/login', 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY'], - ['path' => '^/', 'role' => 'ROLE_ADMIN'], + ['path' => '^/login', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'], + ['path' => '^/', 'roles' => 'ROLE_ADMIN'], ], 3. Be Sure check_path Is Behind a Firewall diff --git a/security/multiple_guard_authenticators.rst b/security/multiple_guard_authenticators.rst index 7816f6030d3..185dedebc51 100644 --- a/security/multiple_guard_authenticators.rst +++ b/security/multiple_guard_authenticators.rst @@ -108,9 +108,9 @@ the solution is to split the configuration into two separate firewalls: authenticators: - AppBundle\Security\LoginFormAuthenticator access_control: - - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: ^/api, roles: ROLE_API_USER } - - { path: ^/, roles: ROLE_USER } + - { path: '^/login', roles: IS_AUTHENTICATED_ANONYMOUSLY } + - { path: '^/api', roles: ROLE_API_USER } + - { path: '^/', roles: ROLE_USER } .. code-block:: xml @@ -168,8 +168,8 @@ the solution is to split the configuration into two separate firewalls: ], ], 'access_control' => [ - ['path' => '^/login', 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY'], - ['path' => '^/api', 'role' => 'ROLE_API_USER'], - ['path' => '^/', 'role' => 'ROLE_USER'], + ['path' => '^/login', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'], + ['path' => '^/api', 'roles' => 'ROLE_API_USER'], + ['path' => '^/', 'roles' => 'ROLE_USER'], ], ]); diff --git a/security/voters.rst b/security/voters.rst index f557ef07bd4..532a6eccb8b 100644 --- a/security/voters.rst +++ b/security/voters.rst @@ -1,6 +1,8 @@ .. index:: single: Security; Data Permission Voters +.. _security/custom-voter: + How to Use Voters to Check User Permissions =========================================== @@ -19,7 +21,8 @@ How Symfony Uses Voters In order to use voters, you have to understand how Symfony works with them. All voters are called each time you use the ``isGranted()`` method on Symfony's authorization checker or call ``denyAccessUnlessGranted`` in a controller (which -uses the authorization checker). +uses the authorization checker), or by +:ref:`access controls `. Ultimately, Symfony takes the responses from all voters and makes the final decision (to allow or deny access to the resource) according to the strategy defined