Розмежування прав доступу до даних, RBAC

1. Опис рішення

Доступ до даних реєстру мають декілька компонентів: Аналітика (Redash), та Портал, що забезпечує інтерфейс користувача (Lowcode). Розмежування рівнів доступу до даних з боку Аналітики більш детально описано на сторінці Побудова аналітичних звітів. Цей документ фокусується на розмежуванні прав доступу до операційних даних з боку користувачів реєстру з інтерфейсу порталу.

Розмежування прав доступу до даних
Зображення 1. Розмежування прав доступу до даних

Операційні дані будуть захищені картою ролей, в імплементації — службовою таблицею, створену окремим тегом Liquibase розширення. Створена таблиця задаватиме набір можливих операцій над кожною сутністю реєстру для ролей користувачів.

Структура тегу використовується для формування службової таблиці ddm_role_permission в операційній базі даних та наповненню її даними.

Запит та оновлення всіх колонок таблиці можливий через CRUD операції, часткове звернення або оновлення можливе через додавання до моделі тегів searchCondition та partialUpdate.

Роль для користувача визначається на певному реалмі Keycloak під час автентифікації й передається у JWT токені Lowcode’у. Кожен API запит Lowcode до Datafactory містить отриману з JWT токену роль та назву реалму.

Для передачі реалму в JWT токен слід додати до клієнта Keycloak мапер hardcoded claim

Звернення Datafactory до даних реєстру відбувається за наступною схемою:

  • Запити на будь-яку зміну даних (Insert/Update/Delete) відбуваються через відповідну службову процедуру реєстру. Окремим параметром передається масив ролей користувача. Перевірка прав відбувається до моменту зміни даних і виконуються у випадку якщо:

    1. дозвіл на операцію над необхідними колонками таблиці для переданих ролі й реалму присутні в ddm_role_permission явним чином

    2. існує дозвіл на операцію для внутрішньої ролі isAuthenticated

    3. карта доступів не містить інформації щодо таблиці, в яку вносяться зміни, взагалі

  • Перед запитом на отримання даних Datafactory робить виклик функції f_permission_check, що перевіряє наявність права доступу на читання всіх колонок необхідної таблиці для ролі користувача. Відсутність запису про таблицю, що містить запитувані дані у карті доступів ddm_role_permission, є ознакою того що RBAC не застосовується до відповідної таблиці. В такому випадку функція має давати дозвіл на запит даних.

Більш детально процес перевірки рівня доступу описано на діаграмі перевірка рівня доступу нижче.

Всі CRUD-операції та Search Conditions також враховують роль користувача, перевіряючи наявний доступ у ddm_role_permission згідно зазначеної схеми.

Liquibase-шаблон знаходиться в Gerrit репозитарії: data-model/role_permission.xml. Будь-які зміни прав доступу проходять через процес перевірки, після чого імплементуються через Jenkins pipeline:

  • перестворюється таблиця ddm_role_permission відповідно до шаблону

  • перестворюються API відповідно до заданих у шаблоні ролей.

Детальніше процес зміни прав доступу до таблиць реєстру описано на діаграмі зміна прав доступу

2. XML Шаблон

<changeSet id="roles" author="registry owner">
    <comment>SET PERMISSIONS</comment>
    <ext:rbac>

        <ext:role name="isAuthenticated">
            <ext:table name="person">
                <ext:column name="first_name" read="true"/>
                <ext:column name="last_name" read="true"/>
            </ext:table>
        </ext:role>

        <ext:role name="officer" realm="officer_realm">
            <ext:table name="person">
                <ext:column name="first_name" read="true" update="true"/>
                <ext:column name="last_name" read="true" update="true"/>
                <ext:column name="passport" read="true"/>
            </ext:table>
        </ext:role>

        <ext:role name="officer_realm.passport_officer">
            <ext:table name="person">
                <ext:column name="passport" update="true"/>
            </ext:table>
        </ext:role>

        <ext:role name="inn_officer" realm="officer_realm">
            <ext:table name="person">
                <ext:column name="inn" update="true"/>
            </ext:table>
        </ext:role>

        <ext:role name="officer_realm.birth_officer">
            <ext:table name="person" insert="true"/>
        </ext:role>

        <ext:role name="death_officer" realm="officer_realm">
            <ext:table name="person" delete="true"/>
        </ext:role>

    </ext:rbac>
</changeSet>

3. Службова таблиця

Таблиця ролей створюється під час розгортання регламенту реєстру. Приклад значень карти доступів для зазначеного вище шаблону

rbac table
Зображення 2. Приклад заповненої ddm_role_permission

4. Процес перевірки рівня доступу

перевірка рівня доступу
Зображення 3. перевірка рівня доступу

5. API

Для кожної точки інтеграції встановлюються ролі які є сумою всіх необхідних ролей для доступу до полів цієї точки інтеграції. У випадку коли одна і та сама операція над полем дозволена для декількох ролей то застосовується логічне АБО для таких ролей.

5.1. CRUD

hasRole('officer_realm.birth_officer')

POST /person
{
   "first_name": "...",
   "last_name": "...",
   "passport": "...",
   "inn": "..."
}

hasRole('officer_realm.death_officer')

DELETE /person/{id}

denyAll

GET /person/{id}

hasRole('officer_realm.officer') && hasRole('officer_realm.passport_officer') && hasRole('officer_realm.inn_officer')

PUT /person/{id}
{
   "first_name": "...",
   "last_name": "...",
   "passport": "...",
   "inn": "..."
}

5.2. Додаткові ендпоінти

Для забезпечення більшої стабільності на прогнозованості контракту Дата Фабрики додаткові точки інтеграції не створюються. При необхідності реалізації часткового читання сутностей слід явно використовувати searchCondition, а для зміни частин сутностей - partialUpdate.

isAuthenticated()

GET /person/public/{id}

{
   "first_name": "...",
   "last_name": "...",
}

hasRole('officer_realm.officer')

GET /person/officer/{id}

{
   "first_name": "...",
   "last_name": "...",
   "passport": "..."
}

hasRole('officer_realm.officer')

PATCH /person/officer/{id}

{
   "first_name": "...",
   "last_name": "...",
}

hasRole('officer_realm.passport_officer')

PATCH /person/passport-officer/{id}

{
   "passport": "..."
}

hasRole('officer_realm.inn_officer')

PATCH /person/inn-officer/{id}

{
   "inn": "..."
}

5.3. Search Conditions

У зазначеному прикладі такий SearchConditions буде мати права доступу denayAll оскільки в жодної ролі нема права читання поля inn

Але у разі додавання такого права inn_officer права доступу будуть виглядати - так hasRole('officer_realm.inn_officer') AND (hasAnyRole('officer_realm.officer') OR isAuthenticated())

GET /name-and-inn-by-inn/{inn}

{
    "inn": "...",
    "first_name": "..."
}

isAuthenticated()

GET /name-by-inn/{inn}

{
    "first_name": "..."
}

5.4. Partial update

hasRole('officer_realm.passport_officer')

PATH /partial/person-passport/{id}
{
    "passport": "..."
}

hasRole('officer_realm.passport_officer') AND hasRole('officer_realm.officer')

PATH /partial/change-identity/{id}
{
    "first_name": "...",
    "last_name": "...",
    "passport": "..."
}

6. Процес зміни прав доступу

зміна прав доступу
Зображення 4. зміна прав доступу

7. Kafka API

7.1. Читання даних

Для перевірки можливості того чи іншого запиту перед його виконанням викликається процедура в яку передається список полів які будуть викликані та перелік ролей користувача. Процедура повертає true або false. У разі true відбувається запит, а у разі false користувачу буде відправлена 403 помилка.

7.2. Модифікація даних

Для операцій модифікації механізм перевірки RBAC вбудований в процедури, і вимагає лише передачі переліку ролей користувача і відповідного реалму Keycloak