Skip to content

Commit 5deeb53

Browse files
author
Jules Pietri
committed
Improved documentation about access controls
1 parent 36b3ff4 commit 5deeb53

File tree

8 files changed

+89
-55
lines changed

8 files changed

+89
-55
lines changed

configuration/external_parameters.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ Symfony provides the following env var processors:
286286
# config/packages/security.yaml
287287
parameters:
288288
env(HEALTH_CHECK_METHOD): 'Symfony\Component\HttpFoundation\Request::METHOD_HEAD'
289+
289290
security:
290291
access_control:
291292
- { path: '^/health-check$', methods: '%env(const:HEALTH_CHECK_METHOD)%' }

security.rst

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ user to be logged in to access this URL:
226226
227227
access_control:
228228
# require ROLE_ADMIN for /admin*
229-
- { path: ^/admin, roles: ROLE_ADMIN }
229+
- { path: '^/admin', roles: ROLE_ADMIN }
230230
231231
.. code-block:: xml
232232
@@ -713,7 +713,7 @@ URL pattern. You saw this earlier, where anything matching the regular expressio
713713
714714
access_control:
715715
# require ROLE_ADMIN for /admin*
716-
- { path: ^/admin, roles: ROLE_ADMIN }
716+
- { path: '^/admin', roles: ROLE_ADMIN }
717717
718718
.. code-block:: xml
719719
@@ -751,7 +751,7 @@ URL pattern. You saw this earlier, where anything matching the regular expressio
751751
],
752752
'access_control' => [
753753
// require ROLE_ADMIN for /admin*
754-
['path' => '^/admin', 'role' => 'ROLE_ADMIN'],
754+
['path' => '^/admin', 'roles' => 'ROLE_ADMIN'],
755755
],
756756
]);
757757
@@ -773,8 +773,8 @@ matches the URL.
773773
# ...
774774
775775
access_control:
776-
- { path: ^/admin/users, roles: ROLE_SUPER_ADMIN }
777-
- { path: ^/admin, roles: ROLE_ADMIN }
776+
- { path: '^/admin/users', roles: ROLE_SUPER_ADMIN }
777+
- { path: '^/admin', roles: ROLE_ADMIN }
778778
779779
.. code-block:: xml
780780
@@ -801,8 +801,8 @@ matches the URL.
801801
// ...
802802
803803
'access_control' => [
804-
['path' => '^/admin/users', 'role' => 'ROLE_SUPER_ADMIN'],
805-
['path' => '^/admin', 'role' => 'ROLE_ADMIN'],
804+
['path' => '^/admin/users', 'roles' => 'ROLE_SUPER_ADMIN'],
805+
['path' => '^/admin', 'roles' => 'ROLE_ADMIN'],
806806
],
807807
]);
808808

security/access_control.rst

Lines changed: 59 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ for each ``access_control`` entry, which determines whether or not a given
2222
access control should be used on this request. The following ``access_control``
2323
options are used for matching:
2424

25-
* ``path``
26-
* ``ip`` or ``ips`` (netmasks are also supported)
27-
* ``host``
28-
* ``methods``
25+
* ``path``: a regular expression (without delimiters)
26+
* ``ip`` or ``ips``: netmasks are also supported
27+
* ``host``: a regular expression
28+
* ``methods``: one or many methods
2929

3030
Take the following ``access_control`` entries as an example:
3131

@@ -37,10 +37,11 @@ Take the following ``access_control`` entries as an example:
3737
security:
3838
# ...
3939
access_control:
40-
- { path: ^/admin, roles: ROLE_USER_IP, ip: 127.0.0.1 }
41-
- { path: ^/admin, roles: ROLE_USER_HOST, host: symfony\.com$ }
42-
- { path: ^/admin, roles: ROLE_USER_METHOD, methods: [POST, PUT] }
43-
- { path: ^/admin, roles: ROLE_USER }
40+
- { path: '^/admin', roles: ROLE_USER_IP, ip: 127.0.0.1 }
41+
- { path: '^/admin', roles: ROLE_USER_HOST, host: symfony\.com$ }
42+
- { path: '^/admin', roles: ROLE_USER_METHOD, methods: [POST, PUT] }
43+
# Defining many roles means we need one of them (like an OR condition)
44+
- { path: '^/admin', roles: [ROLE_MANAGER, ROLE_ADMIN] }
4445
4546
.. code-block:: xml
4647
@@ -57,7 +58,8 @@ Take the following ``access_control`` entries as an example:
5758
<rule path="^/admin" role="ROLE_USER_IP" ip="127.0.0.1" />
5859
<rule path="^/admin" role="ROLE_USER_HOST" host="symfony\.com$" />
5960
<rule path="^/admin" role="ROLE_USER_METHOD" methods="POST, PUT" />
60-
<rule path="^/admin" role="ROLE_USER" />
61+
<!-- Defining many roles, mean we need one of them (like an OR condition) -->
62+
<rule path="^/admin" roles="ROLE_ADMIN, ROLE_MANAGER" />
6163
</config>
6264
</srv:container>
6365
@@ -69,31 +71,32 @@ Take the following ``access_control`` entries as an example:
6971
'access_control' => [
7072
[
7173
'path' => '^/admin',
72-
'role' => 'ROLE_USER_IP',
73-
'ip' => '127.0.0.1',
74+
'roles' => 'ROLE_USER_IP',
75+
'ips' => '127.0.0.1',
7476
],
7577
[
7678
'path' => '^/admin',
77-
'role' => 'ROLE_USER_HOST',
79+
'roles' => 'ROLE_USER_HOST',
7880
'host' => 'symfony\.com$',
7981
],
8082
[
8183
'path' => '^/admin',
82-
'role' => 'ROLE_USER_METHOD',
84+
'roles' => 'ROLE_USER_METHOD',
8385
'methods' => 'POST, PUT',
8486
],
8587
[
8688
'path' => '^/admin',
87-
'role' => 'ROLE_USER',
89+
// Defining many roles, mean we need one of them (like an OR condition)
90+
'roles' => ['ROLE_MANAGER', 'ROLE_ADMIN'],
8891
],
8992
],
9093
]);
9194
9295
For each incoming request, Symfony will decide which ``access_control``
9396
to use based on the URI, the client's IP address, the incoming host name,
9497
and the request method. Remember, the first rule that matches is used, and
95-
if ``ip``, ``host`` or ``method`` are not specified for an entry, that ``access_control``
96-
will match any ``ip``, ``host`` or ``method``:
98+
if ``ips``, ``host`` or ``methods`` are not specified for an entry, that
99+
``access_control`` will match any ``ips``, ``host`` or ``methods``:
97100

98101
+-----------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+
99102
| URI | IP | HOST | METHOD | ``access_control`` | Why? |
@@ -114,9 +117,10 @@ will match any ``ip``, ``host`` or ``method``:
114117
| ``/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, |
115118
| | | | | | but the third - ``ROLE_USER_METHOD`` - matches and is used. |
116119
+-----------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+
117-
| ``/admin/user`` | 168.0.0.1 | example.com | GET | rule #4 (``ROLE_USER``) | The ``ip``, ``host`` and ``method`` prevent the first |
120+
| ``/admin/user`` | 168.0.0.1 | example.com | GET | rule #4 (``ROLE_MANAGER``) | The ``ip``, ``host`` and ``method`` prevent the first |
118121
| | | | | | three entries from matching. But since the URI matches the |
119-
| | | | | | ``path`` pattern of the ``ROLE_USER`` entry, it is used. |
122+
| | | | | | ``path`` pattern, then the ``ROLE_MANAGER`` (or the |
123+
| |    | | | | ``ROLE_ADMIN``) is used. |
120124
+-----------------+-------------+-------------+------------+--------------------------------+-------------------------------------------------------------+
121125
| ``/foo`` | 127.0.0.1 | symfony.com | POST | matches no entries | This doesn't match any ``access_control`` rules, since its |
122126
| | | | | | URI doesn't match any of the ``path`` values. |
@@ -144,6 +148,14 @@ options:
144148
does not match this value (e.g. ``https``), the user will be redirected
145149
(e.g. redirected from ``http`` to ``https``, or vice versa).
146150

151+
.. tip::
152+
153+
Behind the scenes, the array value of ``roles`` is passed as the
154+
``$attributes`` argument to each voter in the application with the
155+
:class:`Symfony\\Component\\HttpFoundation\\Request` as ``$subject``. You
156+
can learn how to use your custom attributes by reading
157+
:ref:`security/custom-voter`.
158+
147159
.. tip::
148160

149161
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:
180192
access_control:
181193
#
182194
# the 'ips' option supports IP addresses and subnet masks
183-
- { path: ^/internal, roles: IS_AUTHENTICATED_ANONYMOUSLY, ips: [127.0.0.1, ::1, 192.168.0.1/24] }
184-
- { path: ^/internal, roles: ROLE_NO_ACCESS }
195+
- { path: '^/internal', roles: IS_AUTHENTICATED_ANONYMOUSLY, ips: [127.0.0.1, ::1, 192.168.0.1/24] }
196+
- { path: '^/internal', roles: ROLE_NO_ACCESS }
185197
186198
.. code-block:: xml
187199
@@ -214,13 +226,13 @@ pattern so that it is only accessible by requests from the local server itself:
214226
'access_control' => [
215227
[
216228
'path' => '^/internal',
217-
'role' => 'IS_AUTHENTICATED_ANONYMOUSLY',
229+
'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY',
218230
// the 'ips' option supports IP addresses and subnet masks
219231
'ips' => ['127.0.0.1', '::1'],
220232
],
221233
[
222234
'path' => '^/internal',
223-
'role' => 'ROLE_NO_ACCESS',
235+
'roles' => 'ROLE_NO_ACCESS',
224236
],
225237
],
226238
]);
@@ -265,7 +277,9 @@ key:
265277
access_control:
266278
-
267279
path: ^/_internal/secure
268-
allow_if: "'127.0.0.1' == request.getClientIp() or has_role('ROLE_ADMIN')"
280+
roles: 'ROLE_ADMIN'
281+
// with this expression, we need it to be true OR a ROLE_ADMIN
282+
allow_if: "'127.0.0.1' == request.getClientIp() or request.header.has('X-Secure-Access')"
269283
270284
.. code-block:: xml
271285
@@ -279,7 +293,9 @@ key:
279293
280294
<config>
281295
<rule path="^/_internal/secure"
282-
allow-if="'127.0.0.1' == request.getClientIp() or has_role('ROLE_ADMIN')" />
296+
role="ROLE_ADMIN"
297+
<!-- with this expression, we need it to be true OR a ROLE_ADMIN -->
298+
allow-if="'127.0.0.1' == request.getClientIp() or request.header.has('X-Secure-Access')" />
283299
</config>
284300
</srv:container>
285301
@@ -288,13 +304,27 @@ key:
288304
'access_control' => [
289305
[
290306
'path' => '^/_internal/secure',
291-
'allow_if' => '"127.0.0.1" == request.getClientIp() or has_role("ROLE_ADMIN")',
307+
'roles' => 'ROLE_ADMIN',
308+
// with this expression, we need it to be true OR a ROLE_ADMIN
309+
'allow_if' => '"127.0.0.1" == request.getClientIp() or request.header.has('X-Secure-Access')',
292310
],
293311
],
294312
295-
In this case, when the user tries to access any URL starting with ``/_internal/secure``,
296-
they will only be granted access if the IP address is ``127.0.0.1`` or if
297-
the user has the ``ROLE_ADMIN`` role.
313+
In this case, when the user tries to access any URL starting with
314+
``/_internal/secure``, they will only be granted access if the IP address is
315+
``127.0.0.1`` or a secure header, or if the user has the ``ROLE_ADMIN`` role.
316+
317+
.. note::
318+
319+
Internally ``allow_if`` triggers the built-in
320+
:class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\ExpressionVoter`
321+
as like it was part of the attributes defined in the ``roles`` option.
322+
By default, access is granted if one of the attribute is granted. In other
323+
words, it means ``allow_if`` can grant access even if the
324+
:class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\RoleVoter` or
325+
any other voter denied it.
326+
To change this behavior, you may need to learn more about
327+
:ref:`access decision strategy <security-voters-change-strategy>`.
298328

299329
Inside the expression, you have access to a number of different variables
300330
and functions including ``request``, which is the Symfony
@@ -345,7 +375,7 @@ the user will be redirected to ``https``:
345375
'access_control' => [
346376
[
347377
'path' => '^/cart/checkout',
348-
'role' => 'IS_AUTHENTICATED_ANONYMOUSLY',
378+
'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY',
349379
'requires_channel' => 'https',
350380
],
351381
],

security/api_key_authentication.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ If you have defined ``access_control``, make sure to add a new entry:
369369
# ...
370370
371371
access_control:
372-
- { path: ^/api, roles: ROLE_API }
372+
- { path: '^/api', roles: ROLE_API }
373373
374374
.. code-block:: xml
375375
@@ -392,7 +392,7 @@ If you have defined ``access_control``, make sure to add a new entry:
392392
'access_control' => [
393393
[
394394
'path' => '^/api',
395-
'role' => 'ROLE_API',
395+
'roles' => 'ROLE_API',
396396
],
397397
],
398398
]);

security/force_https.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ to use HTTPS then you could use the following configuration:
1818
# ...
1919
2020
access_control:
21-
- { path: ^/secure, roles: ROLE_ADMIN, requires_channel: https }
21+
- { path: '^/secure', roles: ROLE_ADMIN, requires_channel: https }
2222
2323
.. code-block:: xml
2424
@@ -46,7 +46,7 @@ to use HTTPS then you could use the following configuration:
4646
'access_control' => [
4747
[
4848
'path' => '^/secure',
49-
'role' => 'ROLE_ADMIN',
49+
'roles' => 'ROLE_ADMIN',
5050
'requires_channel' => 'https',
5151
],
5252
],
@@ -66,7 +66,7 @@ role:
6666
# ...
6767
6868
access_control:
69-
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https }
69+
- { path: '^/login', roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https }
7070
7171
.. code-block:: xml
7272
@@ -97,7 +97,7 @@ role:
9797
'access_control' => [
9898
[
9999
'path' => '^/login',
100-
'role' => 'IS_AUTHENTICATED_ANONYMOUSLY',
100+
'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY',
101101
'requires_channel' => 'https',
102102
],
103103
],

security/form_login_setup.rst

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ all URLs (including the ``/login`` URL), will cause a redirect loop:
283283
284284
# ...
285285
access_control:
286-
- { path: ^/, roles: ROLE_ADMIN }
286+
- { path: '^/', roles: ROLE_ADMIN }
287287
288288
.. code-block:: xml
289289
@@ -307,7 +307,7 @@ all URLs (including the ``/login`` URL), will cause a redirect loop:
307307
308308
// ...
309309
'access_control' => [
310-
['path' => '^/', 'role' => 'ROLE_ADMIN'],
310+
['path' => '^/', 'roles' => 'ROLE_ADMIN'],
311311
],
312312
313313
Adding an access control that matches ``/login/*`` and requires *no* authentication
@@ -321,8 +321,8 @@ fixes the problem:
321321
322322
# ...
323323
access_control:
324-
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
325-
- { path: ^/, roles: ROLE_ADMIN }
324+
- { path: '^/login', roles: IS_AUTHENTICATED_ANONYMOUSLY }
325+
- { path: '^/', roles: ROLE_ADMIN }
326326
327327
.. code-block:: xml
328328
@@ -347,8 +347,8 @@ fixes the problem:
347347
348348
// ...
349349
'access_control' => [
350-
['path' => '^/login', 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY'],
351-
['path' => '^/', 'role' => 'ROLE_ADMIN'],
350+
['path' => '^/login', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'],
351+
['path' => '^/', 'roles' => 'ROLE_ADMIN'],
352352
],
353353
354354
3. Be Sure check_path Is Behind a Firewall

security/multiple_guard_authenticators.rst

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,9 @@ the solution is to split the configuration into two separate firewalls:
108108
authenticators:
109109
- AppBundle\Security\LoginFormAuthenticator
110110
access_control:
111-
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
112-
- { path: ^/api, roles: ROLE_API_USER }
113-
- { path: ^/, roles: ROLE_USER }
111+
- { path: '^/login', roles: IS_AUTHENTICATED_ANONYMOUSLY }
112+
- { path: '^/api', roles: ROLE_API_USER }
113+
- { path: '^/', roles: ROLE_USER }
114114
115115
.. code-block:: xml
116116
@@ -168,8 +168,8 @@ the solution is to split the configuration into two separate firewalls:
168168
],
169169
],
170170
'access_control' => [
171-
['path' => '^/login', 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY'],
172-
['path' => '^/api', 'role' => 'ROLE_API_USER'],
173-
['path' => '^/', 'role' => 'ROLE_USER'],
171+
['path' => '^/login', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'],
172+
['path' => '^/api', 'roles' => 'ROLE_API_USER'],
173+
['path' => '^/', 'roles' => 'ROLE_USER'],
174174
],
175175
]);

security/voters.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
.. index::
22
single: Security; Data Permission Voters
33

4+
.. _security/custom-voter:
5+
46
How to Use Voters to Check User Permissions
57
===========================================
68

@@ -19,7 +21,8 @@ How Symfony Uses Voters
1921
In order to use voters, you have to understand how Symfony works with them.
2022
All voters are called each time you use the ``isGranted()`` method on Symfony's
2123
authorization checker or call ``denyAccessUnlessGranted`` in a controller (which
22-
uses the authorization checker).
24+
uses the authorization checker), or by
25+
:ref:`access controls <security-access-control-enforcement-options>`.
2326

2427
Ultimately, Symfony takes the responses from all voters and makes the final
2528
decision (to allow or deny access to the resource) according to the strategy defined

0 commit comments

Comments
 (0)