Обфускация данных
Для соблюдения требований по защите персональных данных при использовании 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 |
+---------+---------------------------------+--------------------+----------------------------------------------------+--------------+------------+
Таким образом, работа с данными для разных сотрудников ограничена разными правами доступа, а персональные данные остаются надежно защищенными.