Обфускация данных

Для соблюдения требований по защите персональных данных при использовании Tengri может понадобиться техническое маскирование данных — обфускация.

В этом примере покажем, как произвести обфускацию персональных данных таким образом, чтобы обезличенные (обфусцированные) данные хранились в отдельной схеме (права на которую можно предоставить неограниченному кругу сотрудников), в то время как исходные данные хранились в схеме с ограниченным доступом. Прямой доступ к персональным данным таким образом можно будет полностью изолировать.

Защита персональных данных необходима для выполнения требований Федерального закона № 152-ФЗ «О персональных данных» в Российской Федерации и Общего регламента по защите данных (GDPR) в Европейском союзе.

Будем использовать три схемы:

  • demo_raw — схема с исходными данными (не обфусцированными)
    Доступ: ограниченный.
    Может быть удалена после обфускации.

  • demo_secret — схема с ключами соответствий
    Доступ: ограниченный.

  • demo_obfuscated — схема с обфусцированными данными
    Доступ: неограниченный.

Подготовка данных

В целях демонстрации создадим набор искусственных данных пользователей интернет-магазина со следующими полями:

  • User_ID — идентификатор пользователя

  • Full_Name — полное имя пользователя

  • Phone_Number — телефон пользователя

  • Address — адрес пользователя

  • Birth_Date — дата рождения пользователя

  • Orders_Count — количество заказов пользователя

Для этого воспользуемся библиотекой Python Faker:

import pandas as pd
from faker import Faker

# Инициализируем Faker для генерации русскоязычных данных
fake = Faker("ru_RU")

# Задаем количество пользователей
num_users = 1000

users_data = []

for _ in range(num_users):
    # Генерируем профили пользователей
    profile = fake.simple_profile()

    user = {
        "User_ID": fake.unique.random_int(min=10000, max=99999),
        "Full_Name": profile["name"],
        "Phone_Number": fake.phone_number(),
        "Address": fake.address().replace("\n", ", "),
        "Birth_Date": fake.date_of_birth(minimum_age=18, maximum_age=70).strftime(
            "%Y-%m-%d"
        ),
        "Orders_Count": fake.random_int(min=0, max=100),
    }
    users_data.append(user)

# Записываем в DataFrame
df = pd.DataFrame(users_data)
df.head()
+---+---------+----------------------------+-------------------+---------------------------------------------------+------------+--------------+
|   | User_ID |                  Full_Name |      Phone_Number |                                           Address | Birth_Date | Orders_Count |
+---+---------+----------------------------+-------------------+---------------------------------------------------+------------+--------------+
| 0 | 33165   | Ульяна Яковлевна Филиппова | 8 (222) 153-7935  | д. Курчатов, ул. Волкова, д. 4/1 стр. 212, 601586 | 1988-03-05 | 79           |
+---+---------+----------------------------+-------------------+---------------------------------------------------+------------+--------------+
| 1 | 62431   | Воронова Оксана Натановна  | 8 (390) 229-9364  | ст. Дербент, наб. Осипенко, д. 8 стр. 920, 010532 | 1962-04-02 | 75           |
+---+---------+----------------------------+-------------------+---------------------------------------------------+------------+--------------+
| 2 | 63202   | Яковлев Евсей Евстигнеевич | 8 (022) 393-80-71 | п. Сорочинск, ш. Ушакова, д. 92, 309643           | 1997-01-12 | 53           |
+---+---------+----------------------------+-------------------+---------------------------------------------------+------------+--------------+
| 3 | 59677   | Селиверст Власович Кулагин | +73939353959      | ст. Ессентуки, бул. Дорожный, д. 8/3, 828001      | 2004-07-09 | 14           |
+---+---------+----------------------------+-------------------+---------------------------------------------------+------------+--------------+
| 4 | 39532   | Баранов Ираклий Фролович   | 8 (987) 772-04-72 | к. Объячево, ул. Пролетарская, д. 3/7 к. 1/8, ... | 1992-08-24 | 83           |
+---+---------+----------------------------+-------------------+---------------------------------------------------+------------+--------------+

Запишем полученную таблицу в схему demo_raw. Используем для этого функции tngri.sql и tngri.create_table:

import tngri

table_name = 'demo_raw.users'

tngri.sql(f'drop table if exists {table_name}')
tngri.create_table(df, table_name)
done at 2026-05-20 12:26:44.803990

Теперь в ячейке типа SQL выведем записанную таблицу с не обфусцированными данными:

SELECT *
FROM demo_raw.users
LIMIT 5;
+---------+----------------------------+-------------------+------------------------------------------------------+------------+--------------+
| User_ID | Full_Name                  | Phone_Number      | Address                                              | Birth_Date | Orders_Count |
+---------+----------------------------+-------------------+------------------------------------------------------+------------+--------------+
| 33165   | Ульяна Яковлевна Филиппова | 8 (222) 153-7935  | д. Курчатов, ул. Волкова, д. 4/1 стр. 212, 601586    | 1988-03-05 | 79           |
+---------+----------------------------+-------------------+------------------------------------------------------+------------+--------------+
| 62431   | Воронова Оксана Натановна  | 8 (390) 229-9364  | ст. Дербент, наб. Осипенко, д. 8 стр. 920, 010532    | 1962-04-02 | 75           |
+---------+----------------------------+-------------------+------------------------------------------------------+------------+--------------+
| 63202   | Яковлев Евсей Евстигнеевич | 8 (022) 393-80-71 | п. Сорочинск, ш. Ушакова, д. 92, 309643              | 1997-01-12 | 53           |
+---------+----------------------------+-------------------+------------------------------------------------------+------------+--------------+
| 59677   | Селиверст Власович Кулагин | +73939353959      | ст. Ессентуки, бул. Дорожный, д. 8/3, 828001         | 2004-07-09 | 14           |
+---------+----------------------------+-------------------+------------------------------------------------------+------------+--------------+
| 39532   | Баранов Ираклий Фролович   | 8 (987) 772-04-72 | к. Объячево, ул. Пролетарская, д. 3/7 к. 1/8, 027088 | 1992-08-24 | 83           |
+---------+----------------------------+-------------------+------------------------------------------------------+------------+--------------+

Обфускация данных

Предположим, что нам нужно обфусцировать три колонки из таблицы demo_raw.users — Full_Name, Phone_Number и Address. Для каждой из этих колонок в схеме demo_secret создадим таблицу соответствий реальных данных и случайного идентификатора UUID v4. При создании таблиц мы будем генерировать идентификатор с помощью функции uuid. Для идентификации данных в этих таблицах будем использовать исходный идентификатор пользователя — User_ID.

CREATE OR REPLACE TABLE demo_secret.user_full_name_resolver AS
SELECT
    User_ID,
    Full_Name,
    uuid() AS name_uuid
FROM demo_raw.users;

CREATE OR REPLACE TABLE demo_secret.user_address_resolver AS
SELECT
    User_ID,
    Address,
    uuid() AS address_uuid
FROM demo_raw.users;

CREATE OR REPLACE TABLE demo_secret.user_phone_number_resolver AS
SELECT
    User_ID,
    Phone_Number,
    uuid() AS phone_uuid
FROM demo_raw.users;
+--------+
| status |
+--------+
| CREATE |
+--------+

То же самое можно сделать через цикл в ячейке типа Python с помощью функции tngri.sql. Зададим список колонок для обфускации и имена таблиц и будем итерироваться по списку колонок. Это позволит написать код SQL один раз и не переписывать его для всех колонок:

import tngri

source_schema = 'demo_raw'
source_table = 'users'
columns_to_obfuscate = ['Full_Name', 'Address', 'Phone_Number']
secret_schema = 'demo_secret'

for column in columns_to_obfuscate:
    cur_table = f'{secret_schema}.{source_table}_{column}_resolver'

    tngri.sql(f'''
    CREATE OR REPLACE TABLE {cur_table} AS
    SELECT
        User_ID,
        {column},
        uuidv4() AS {column}_uuid
    FROM {source_schema}.{source_table};
    ''')

    print(f'Column "{column}" obfuscated')
    print(f'Created table: {cur_table}')
Column "Full_Name" obfuscated
Created table: demo_secret.users_Full_Name_resolver
Column "Address" obfuscated
Created table: demo_secret.users_Address_resolver
Column "Phone_Number" obfuscated
Created table: demo_secret.users_Phone_Number_resolver

Проверим полученные таблицы.

SELECT *
FROM demo_secret.user_full_name_resolver
LIMIT 5;
+---------+----------------------------+--------------------------------------+
| User_ID | Full_Name                  | name_uuid                            |
+---------+----------------------------+--------------------------------------+
| 33165   | Ульяна Яковлевна Филиппова | c6ead1c1-7135-4efc-9b8f-cc84ae43422d |
+---------+----------------------------+--------------------------------------+
| 62431   | Воронова Оксана Натановна  | a3bcd3f5-38b9-4b75-925d-23ee248355f8 |
+---------+----------------------------+--------------------------------------+
| 63202   | Яковлев Евсей Евстигнеевич | 82c201e0-2b05-4ea9-8a31-dfcb932efe50 |
+---------+----------------------------+--------------------------------------+
| 59677   | Селиверст Власович Кулагин | 398fc6fd-c040-4049-8cbd-6db877fdf9f9 |
+---------+----------------------------+--------------------------------------+
| 39532   | Баранов Ираклий Фролович   | e6a226d6-294e-4ca2-8392-c31c56ef9a38 |
+---------+----------------------------+--------------------------------------+
SELECT *
FROM demo_secret.user_phone_number_resolver
LIMIT 5;
+---------+-------------------+--------------------------------------+
| User_ID | Phone_Number      | phone_uuid                           |
+---------+-------------------+--------------------------------------+
| 33165   | 8 (222) 153-7935  | c66fe50a-d9c5-4537-9a71-c6684ea6f72b |
+---------+-------------------+--------------------------------------+
| 62431   | 8 (390) 229-9364  | 4a8e6c45-75fa-4ad6-9da5-dca1433b778d |
+---------+-------------------+--------------------------------------+
| 63202   | 8 (022) 393-80-71 | 1873bd3d-9788-41f4-a7bf-7a7826e8af7a |
+---------+-------------------+--------------------------------------+
| 59677   | +73939353959      | ee464308-54df-4634-89ee-24053b738651 |
+---------+-------------------+--------------------------------------+
| 39532   | 8 (987) 772-04-72 | 2dc785b8-ddeb-49e2-ae3a-387540edb40d |
+---------+-------------------+--------------------------------------+
SELECT *
FROM demo_secret.user_address_resolver
LIMIT 5;
+---------+------------------------------------------------------+--------------------------------------+
| User_ID | Address                                              | address_uuid                         |
+---------+------------------------------------------------------+--------------------------------------+
| 33165   | д. Курчатов, ул. Волкова, д. 4/1 стр. 212, 601586    | 0c5ff6f9-d55e-4cbb-80d3-452e40134475 |
+---------+------------------------------------------------------+--------------------------------------+
| 62431   | ст. Дербент, наб. Осипенко, д. 8 стр. 920, 010532    | 20704512-0e5e-41ec-bd5c-3383c53a2f53 |
+---------+------------------------------------------------------+--------------------------------------+
| 63202   | п. Сорочинск, ш. Ушакова, д. 92, 309643              | 8108f398-dc2c-41f2-b8b3-26c584fd69f8 |
+---------+------------------------------------------------------+--------------------------------------+
| 59677   | ст. Ессентуки, бул. Дорожный, д. 8/3, 828001         | 3db054ee-4aa4-4fe8-bf39-4aae9e1f5637 |
+---------+------------------------------------------------------+--------------------------------------+
| 39532   | к. Объячево, ул. Пролетарская, д. 3/7 к. 1/8, 027088 | c19d2a24-7ebf-44af-a2d3-e4df2f3b4f84 |
+---------+------------------------------------------------------+--------------------------------------+

Мы получили таблицы соответствия ключей в отдельной схеме demo_secret. Теперь можно ограничить привилегии на эту схему так, чтобы просматривать реальные данные мог только ограниченный круг пользователей. При необходимости можно предоставить привилегии и на отдельные таблицы этой схемы.

Теперь создадим полностью обфусцированную таблицу в схеме demo_obfuscated. В этой таблице будут все данные из исходной таблицы demo_raw.users, но выбранные нами колонки будут обфусцированными.

CREATE OR REPLACE TABLE demo_obfuscated.users AS
SELECT
    u.User_ID,
    n.name_uuid AS Full_Name,
    p.phone_uuid AS Phone_Number,
    a.address_uuid AS Address,
    u.Birth_Date,
    u.Orders_Count
FROM demo_raw.users u
JOIN demo_secret.user_full_name_resolver n ON u.User_ID = n.User_ID
JOIN demo_secret.user_phone_number_resolver p ON u.User_ID = p.User_ID
JOIN demo_secret.user_address_resolver a ON u.User_ID = a.User_ID;
+--------+
| status |
+--------+
| CREATE |
+--------+

Проверим созданную таблицу:

SELECT *
FROM demo_obfuscated.users
LIMIT 5;
+---------+--------------------------------------+--------------------------------------+--------------------------------------+------------+--------------+
| User_ID | Full_Name                            | Phone_Number                         | Address                              | Birth_Date | Orders_Count |
+---------+--------------------------------------+--------------------------------------+--------------------------------------+------------+--------------+
| 33165   | c6ead1c1-7135-4efc-9b8f-cc84ae43422d | c66fe50a-d9c5-4537-9a71-c6684ea6f72b | 0c5ff6f9-d55e-4cbb-80d3-452e40134475 | 1988-03-05 | 79           |
+---------+--------------------------------------+--------------------------------------+--------------------------------------+------------+--------------+
| 62431   | a3bcd3f5-38b9-4b75-925d-23ee248355f8 | 4a8e6c45-75fa-4ad6-9da5-dca1433b778d | 20704512-0e5e-41ec-bd5c-3383c53a2f53 | 1962-04-02 | 75           |
+---------+--------------------------------------+--------------------------------------+--------------------------------------+------------+--------------+
| 63202   | 82c201e0-2b05-4ea9-8a31-dfcb932efe50 | 1873bd3d-9788-41f4-a7bf-7a7826e8af7a | 8108f398-dc2c-41f2-b8b3-26c584fd69f8 | 1997-01-12 | 53           |
+---------+--------------------------------------+--------------------------------------+--------------------------------------+------------+--------------+
| 59677   | 398fc6fd-c040-4049-8cbd-6db877fdf9f9 | ee464308-54df-4634-89ee-24053b738651 | 3db054ee-4aa4-4fe8-bf39-4aae9e1f5637 | 2004-07-09 | 14           |
+---------+--------------------------------------+--------------------------------------+--------------------------------------+------------+--------------+
| 39532   | e6a226d6-294e-4ca2-8392-c31c56ef9a38 | 2dc785b8-ddeb-49e2-ae3a-387540edb40d | c19d2a24-7ebf-44af-a2d3-e4df2f3b4f84 | 1992-08-24 | 83           |
+---------+--------------------------------------+--------------------------------------+--------------------------------------+------------+--------------+
Строго говоря, наличие в этой таблице обфусцированных колонок необязательно, так как эти данные уже сохранены в таблицах соответствий схемы demo_secret. В этом примере мы сохраняем здесь все колонки из исходной таблицы для наглядности.

Доступ к схеме demo_obfuscated может быть самым широким, так как в ней уже нет персональных данных в открытом виде.

После этого исходная таблица в схеме demo_raw может быть удалена, так как все данные из нее теперь сохранены и распределены по двум другим схемам с разными уровнями доступа.

Использование обфусцированных данных

Предположим, что аналитик с ограниченным доступом к персональным данным работает в схеме demo_obfuscated и ищет топ-5 пользователей по количеству покупок в возрасте 18-25.

SELECT
    User_ID,
    Orders_Count,
    date_diff('year', Birth_Date::DATE, current_date) AS Age
FROM demo_obfuscated.users
WHERE date_diff('year', Birth_Date::DATE, current_date) BETWEEN 18 AND 25
ORDER BY Orders_Count DESC
LIMIT 5;
+---------+--------------+-----+
| User_ID | Orders_Count | Age |
+---------+--------------+-----+
| 43184   | 100          | 23  |
+---------+--------------+-----+
| 20635   | 100          | 25  |
+---------+--------------+-----+
| 37231   | 99           | 20  |
+---------+--------------+-----+
| 71821   | 99           | 23  |
+---------+--------------+-----+
| 38810   | 96           | 20  |
+---------+--------------+-----+

В качестве результата он получает обезличенный список из 5 User_ID.

Предположим, что он передает этот список другому сотруднику (например, менеджеру), у которого есть доступ к персональным данным. Менеджер в свою очередь может выполнить запрос c использованием переданного ему списка идентификаторов пользователей и таблиц из схемы demo_secret, но без использования исходной таблицы из схемы demo_raw (!), чтобы получить реальные данные выбранных аналитиком пользователей:

SELECT
    u.User_ID,
    n.Full_Name AS Real_Name,
    p.Phone_Number AS Real_Phone,
    a.Address AS Real_Address,
    u.Orders_Count,
    u.Birth_Date
FROM demo_obfuscated.users u
JOIN demo_secret.user_full_name_resolver n ON u.User_ID = n.User_ID
JOIN demo_secret.user_phone_number_resolver p ON u.User_ID = p.User_ID
JOIN demo_secret.user_address_resolver a ON u.User_ID = a.User_ID
WHERE u.User_ID IN (43184,
                    20635,
                    37231,
                    71821,
                    38810);
+---------+---------------------------------+--------------------+----------------------------------------------------+--------------+------------+
| User_ID | Real_Name                       | Real_Phone         | Real_Address                                       | Orders_Count | Birth_Date |
+---------+---------------------------------+--------------------+----------------------------------------------------+--------------+------------+
| 43184   | Павлова Ульяна Дмитриевна       | +7 (606) 457-73-87 | к. Троицк (Моск.), пер. Ручейный, д. 738, 509804   | 100          | 2003-05-02 |
+---------+---------------------------------+--------------------+----------------------------------------------------+--------------+------------+
| 20635   | Федосеева Александра Наумовна   | 8 (937) 491-6658   | п. Каменск-Шахтинский, ул. Большая, д. 362, 653554 | 100          | 2001-11-15 |
+---------+---------------------------------+--------------------+----------------------------------------------------+--------------+------------+
| 38810   | Лыткин Пахом Демидович          | +78903266054       | к. Чегем, пер. Дорожников, д. 7/6 стр. 2, 408961   | 96           | 2006-05-28 |
+---------+---------------------------------+--------------------+----------------------------------------------------+--------------+------------+
| 37231   | Ангелина Оскаровна Лобанова     | 8 (012) 843-50-16  | к. Мелеуз, ш. Заовражное, д. 518 стр. 6, 512707    | 99           | 2006-08-26 |
+---------+---------------------------------+--------------------+----------------------------------------------------+--------------+------------+
| 71821   | Аксенов Максимильян Викентьевич | 8 (802) 245-72-10  | с. Яшалта, бул. Щербакова, д. 46 к. 847, 306033    | 99           | 2003-01-02 |
+---------+---------------------------------+--------------------+----------------------------------------------------+--------------+------------+

Таким образом, работа с данными для разных сотрудников ограничена разными правами доступа, а персональные данные остаются надежно защищенными.