Skip to content

Improved documentation about access controls #11118

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions configuration/external_parameters.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)%' }
Expand Down
14 changes: 7 additions & 7 deletions security.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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'],
],
]);

Expand All @@ -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

Expand All @@ -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'],
],
]);

Expand Down
88 changes: 59 additions & 29 deletions security/access_control.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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

Expand All @@ -57,7 +58,8 @@ Take the following ``access_control`` entries as an example:
<rule path="^/admin" role="ROLE_USER_IP" ip="127.0.0.1" />
<rule path="^/admin" role="ROLE_USER_HOST" host="symfony\.com$" />
<rule path="^/admin" role="ROLE_USER_METHOD" methods="POST, PUT" />
<rule path="^/admin" role="ROLE_USER" />
<!-- Defining many roles, mean we need one of them (like an OR condition) -->
<rule path="^/admin" roles="ROLE_ADMIN, ROLE_MANAGER" />
</config>
</srv:container>

Expand All @@ -69,31 +71,32 @@ 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'],
],
],
]);

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? |
Expand All @@ -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 |
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ROLE_MANAGER or ROLE_ADMIN

right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the amount of confusion that are provided by the array roles, should we even document them? I mean, the role hierarchy is complex enough to manage everything with single roles instead of array roles. Sometimes it's better to hide confusing things 😃

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This question will especially, if roles is used in plural everywhere. Let's just be clear about it to avoid any confusion anymore. I've added a comment here too.

| | | | | | 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. |
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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',
],
],
]);
Expand Down Expand Up @@ -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')"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should document with a comment here that the logic is or. You explain it more below, but a comment is all most people will read ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, thank you!


.. code-block:: xml

Expand All @@ -279,7 +293,9 @@ key:

<config>
<rule path="^/_internal/secure"
allow-if="'127.0.0.1' == request.getClientIp() or has_role('ROLE_ADMIN')" />
role="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')" />
</config>
</srv:container>

Expand All @@ -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 <security-voters-change-strategy>`.

Inside the expression, you have access to a number of different variables
and functions including ``request``, which is the Symfony
Expand Down Expand Up @@ -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',
],
],
Expand Down
4 changes: 2 additions & 2 deletions security/api_key_authentication.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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',
],
],
]);
Expand Down
8 changes: 4 additions & 4 deletions security/force_https.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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',
],
],
Expand All @@ -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

Expand Down Expand Up @@ -97,7 +97,7 @@ role:
'access_control' => [
[
'path' => '^/login',
'role' => 'IS_AUTHENTICATED_ANONYMOUSLY',
'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY',
'requires_channel' => 'https',
],
],
Expand Down
12 changes: 6 additions & 6 deletions security/form_login_setup.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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

Expand All @@ -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
Expand Down
12 changes: 6 additions & 6 deletions security/multiple_guard_authenticators.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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'],
],
]);
5 changes: 4 additions & 1 deletion security/voters.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
.. index::
single: Security; Data Permission Voters

.. _security/custom-voter:

How to Use Voters to Check User Permissions
===========================================

Expand All @@ -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 <security-access-control-enforcement-options>`.

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
Expand Down