Як працює безпека access_control?

Дата оновлення перекладу 2024-05-29

Як працює безпека access_control?

Для кожного вхідного запиту, Symfony перевіряє кожний запис access_control, щоб знайти один, який співпадає з поточним запитом. Як тільки вона знаходить запис, що співпадає access_control, вона зупиняється - лише перший access_control, що співпадає, використовується для надання доступу.

Кожний access_control має декілька опцій, які конфігурують дві різних речі:

  1. чи має вхідний запит співпадати з цим записом контролю доступу
  2. при співпадінні, чи має застосовуватися якесь обмеження доступу :

1. Опції співпадіння

Symfony створює екземпляр класу RequestMatcher для кожного запису access_control, який визначає, чи має бути використано цей контроль доступу у цьому запиті. Наступні опції access_control використовуються для співставлення:

  • path: регулярний вираз (без розмежувачів)
  • ip або ips: маски мережі також підтримуються (може бути рядком, розділеним комою)
  • port: ціле число
  • host: регулярний вираз
  • methods: один або декілька методів
  • request_matcher: сервіс, що реалізує RequestMatcherInterface
  • attributes: масив, який може бути використано для вказання одного або більше атрибутів запиту , які мають точно співпасти
  • route: імʼя маршруту

Візьміть наступні записи access_control в якості прикладу:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# config/packages/security.yaml
parameters:
    env(TRUSTED_IPS): '10.0.0.1, 10.0.0.2'

security:
    # ...
    access_control:
        - { path: '^/admin', roles: ROLE_USER_PORT, ip: 127.0.0.1, port: 8080 }
        - { 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] }

        # IP можуть бути розділені комою, що особливо корисно при використанні змінних середовища
        - { path: '^/admin', roles: ROLE_USER_IP, ips: '%env(TRUSTED_IPS)%' }
        - { path: '^/admin', roles: ROLE_USER_IP, ips: [127.0.0.1, ::1, '%env(TRUSTED_IPS)%'] }
        
        # для користувацьких потреб співставлення, використайте сервіс співставлення запитів
        - { roles: ROLE_USER, request_matcher: App\Security\RequestMatcher\MyRequestMatcher }

Для кожного вхідного запиту Symfony буде вирішувати, який access_control використовувати використовувати на основі URI, IP-адреси клієнта, імені вхідного хоста та методу запиту. Пам'ятайте, що використовується перше правило, яке збігається, і якщо ip, port, host або method не вказано для запису, то access_control буде відповідати будь-якому ip, port, host або method. Дивіться наступні приклади:

Приклад #1:
  • URI /admin/user
  • IP: 127.0.0.1, Port: 80, Host: example.com, Method: GET
  • Rule applied: правило #2 (ROLE_USER_IP)
  • Why? URI співпадає з path, а IP співпадає з ip.
Приклад #2:
  • URI /admin/user
  • IP: 127.0.0.1, Port: 80, Host: symfony.com, Method: GET
  • Rule applied: правило #2 (ROLE_USER_IP)
  • Why? path та ip все ще співпадають. Це також співпадатиме із записом ROLE_USER_HOST, але лише перше співпадіння access_control буде використано.
Приклад #3:
  • URI /admin/user
  • IP: 127.0.0.1, Port: 8080, Host: symfony.com, Method: GET
  • Rule applied: правило #1 (ROLE_USER_PORT)
  • Why? path, ip та port співпадають.
Приклад #4:
  • URI /admin/user
  • IP: 168.0.0.1, Port: 80, Host: symfony.com, Method: GET
  • Rule applied: правило #3 (ROLE_USER_HOST)
  • Why? ip не співпадає ані з першим правилом, ані з другим.
  • Тому використовується третє правило (яке співпадає).
Приклад #5:
  • URI /admin/user
  • IP: 168.0.0.1, Port: 80, Host: symfony.com, Method: POST
  • Rule applied: правило #3 (ROLE_USER_HOST)
  • Why? Третє правило все ще співпадає. Це також співпадатиме з четвертим правилом
  • (ROLE_USER_METHOD), але використовується лише перший співпавший access_control.
Приклад #6:
  • URI /admin/user
  • IP: 168.0.0.1, Port: 80, Host: example.com, Method: POST
  • Rule applied: правило #4 (ROLE_USER_METHOD)
  • Why? ip та host не співпадають з першими трьома записами, але
  • четвертий - ROLE_USER_METHOD - співпадає та буде використано.
Приклад #7:
  • URI /foo
  • IP: 127.0.0.1, Port: 80, Host: symfony.com, Method: POST
  • Rule applied: не співпадає з жодним записом
  • Why? Не співпадає з жодним з правил access_control, так як його URI
  • не співпадає з жодним зі значень path.

Caution

Співставлення URI відбувається без параметрів $_GET. Відмовте у доступі в PHP-коді , якщо ви хочете заборонити доступ, засновуючись на значеннях параметра $_GET.

2. Форсування доступу

Після того, як Symfony вирішила, який запис access_control співпадає (якщо такий є), вона форсує обмеження доступу, засновані на опціях roles, allow_if і requires_channel:

  • roles Якщо користувач не має заданої ролі, то у доступі буде відмовлено (внутрішньо, викликаєтья AccessDeniedException);
  • allow_if Якщо вираз повертає "false", то у доступі буде відмовлено;
  • requires_channel Якщо канал вхідного запиту (наприклад, http) не співпадає з цим значенням (наприклад, https), користувач буде перенаправлений (наприклад, перенаправлений з http на https, або навпаки).

Tip

За лаштунками, значення масиву передається в якості аргументу $attributes кожному виборцю у додатку з Request як $subject. Ви можете дізнатися, як використовувати ваші користувацькі атрибути, прочитавши .

Caution

Якщо ви визначите і roles, і allow_if, і ви використовуєте Стратегію вирішення доступу за замовчуванням (affirmative), то користувачу буде надано доступ, якщо як мінімум одна умова валідна. Якщо ця поведінка не підходить під ваші потреби, змініть Стретегію вирішення доступу .

Tip

Якщо у доступі відмовлено, система спробує аутентифікувати користувача, якщо це ще не було зроблено (наприклад, перенаправити користувача на сторінку входу). Якщо користувач вже виконав виконав вхід, буде відображена сторінка помилка 403 "Доступ заборонено". Дивіться Як налаштувати сторінки помилок, щоб дізнатися більше.

Співставлення access_control за IP

Деякі ситуації можуть виникнути, коли вам потрібен запис access_control, який співпадає лише із запитами, що виходять з якоїсь IP-адреси або їх спектру. Наприклад, це мможе бути використано для відмови у доступі до URL-шаблону для всіх запитів, окрім тих, що виходять з внутрішнього довіреного сервера.

Caution

Як ви прочитаєте у поясненні під прикладом, опція ips не обмежується конкретною IP-адресою. Замість цього, викристання ключа ips означає, що запис access_control співпадатиме лише з цією IP-адресою, а користувачі, які отримують доступ до нього з інших IP-адрес, будуть йти далі за списком access_control.

Ось приклад того як ви можете сконфігурувати деякий шаблон URL /internal* так, щоб він був доступний лише за запитами з локального сервера:

1
2
3
4
5
6
7
8
# config/packages/security.yaml
security:
    # ...
    access_control:
        #
        # опція 'ips' підтримує IP-адреси та маски підмереж
        - { path: '^/internal', roles: IS_AUTHENTICATED_ANONYMOUSLY, ips: [127.0.0.1, ::1, 192.168.0.1/24] }
        - { path: '^/internal', roles: ROLE_NO_ACCESS }

Ось, як це працює, коли шлях /internal/something виходить від зовнішньої IP-адреси 10.0.0.1:

  • Перше правило контролю доступу ігнорується, так як path співпадає, але IP-адреси не співпадають з жодним з перерахованих IP;
  • Включається друге правило контролю доступу (єдине обмеження - path) і воно співпадає. Якщо ви переконаєтеся в тому, що жоден користувач не маєʼ ROLE_NO_ACCESS, то у доступі буде відмовлено (ROLE_NO_ACCESS може бути чим завгодно, що не співпадає з існуючою роллю, воно просто служить споссобом завжди відмовляти у доступі).

Але якщо той же запит поступить від 127.0.0.1 або ::1 (адреса зворотного звʼязку IPv6):

  • Тепер перше правило контролю доступу включається, так як співпадає і path, і ip: доступ дозволено, так як користувач завжди має роль IS_AUTHENTICATED_ANONYMOUSLY.
  • Друге правило контролю доступу не розгллядається, так як співпало перше.

Убезпечення за виразом

Коли запис access_control співпадає, ви можете відмовити у доступрі через ключ roles або використати складнішу логіку з виразом у ключі allow_if:

1
2
3
4
5
6
7
8
9
10
# config/packages/security.yaml
security:
    # ...
    access_control:
        -
            path: ^/_internal/secure
            # опції 'roles' і 'allow_if' працюють як вираз ОС, тому доступ надається,
            # якщо вираз - TRUE або якщо користувач має ROLE_ADMIN
            roles: 'ROLE_ADMIN'
            allow_if: "'127.0.0.1' == request.getClientIp() or request.headers.has('X-Secure-Access')"

У цьому випадку, коли користувач намагається отримати доступ до будь-якого URL, що починається з /_internal/secure, він його отримає лише якщо IP-адреса - 127.0.0.1 або якщо він має роль ROLE_ADMIN.

Note

Внутрішньо, allow_if запускає вбудований ExpressionVoter, ніби він є частиною атрибутів, визначених в опції roles.

Всередині виразу у вас є доступ до декількох різних змінних та функцій, включно з request, яка є обʼєктом Symfony Request (дивіться ).

Щоб побачити список інших функцій та змінних, дивіться функції та змінні .

Tip

Вирази allow_if можуть також містити користувацькі функції, зареєстровані за допомогою постачальників виразів .

Обмеження за портом

Додайте опцію port до будь-якого запису access_control, щоб вимагати від короистувачів отримання доступу до цих URL через конкретний порт. Це може бути корисно, наприклад, для localhost:8080.

1
2
3
4
5
# config/packages/security.yaml
security:
    # ...
    access_control:
        - { path: ^/cart/checkout, roles: PUBLIC_ACCESS, port: 8080 }

Форсування каналу (http, https)

Ви також можете зобовʼязати користувача отримувати доступ до URL через SSL; просто використайте аргумент requires_channel у будь-яких записах access_control. Якщо access_control співпаде, і запит використовує канал http, то користувач буде перенаправлений на https:

1
2
3
4
5
# config/packages/security.yaml
security:
    # ...
    access_control:
        - { path: ^/cart/checkout, roles: PUBLIC_ACCESS, requires_channel: https }