Недавно я реализовал Multi Tenancy систему на FastAPI + SQLAlchemy и пересобрал её в небольшой демо проект.
Ключевые особенности:
▫️ Основной всего является сущность Компании. Юзеры могут подключаться к разным компаниям, но в один момент времени могут находиться только в одной (по условиям ТЗ).
▫️ Разделение данных через FK-ссылку всех ключевых бизнес-сущностей на ID компании.
▫️ ID компании задаётся при логине и хранится в JWT. При смене компании потребуется новый токен.
▫️ Контекст активируется через зависимость.
▫️ Фильтр применяется автоматически для любого запроса.
Теперь подробней:
1️⃣ Логин
Когда юзер логинится, он указывает ID компании. Этот ID зашивается в JWT.
2️⃣ Запрос
Ключевое действие находится в функции get_current_user(...). Мы парсим JWT и достаем инфу о юзере и компании.
Для текущей и дочерних корутин этого запроса объявляется контекст конкретной компании. Объявление контекста происходит через
Никакого указания ID проекта\магазина\сайта со стороны юзера и проброса его по всем функциям. Это локальное состояние конкретного запроса и его цепочки корутин.
3️⃣ Активация фильтра для SQL
Реализация фильтра сделана штатными средствами SQLAlchemy - прослушивание ивента
Функция фильтрации add_tenant_filter(...) требует либо указать контекст, либо отключить фильтр. Если вдруг контекст не задан, то по умолчанию юзер просто получит ошибку
4️⃣ Админка
В
После логина админ тоже будет получать ошибки при обращении к юзерским сервисам, поэтому у него есть свои - админские сервисы через админские роуты.
На админских роутах стоит депенденси с функцией
Так же есть один юзерский запрос с
Если же админу нужно вызвать юзерский сервис с контекстом какой-либо компании, то есть функция
▫️Плюсы такого подхода:
- Контекст хранится в токене, нет лишнего запроса в БД
- При утечке токена одной компании другие не пострадают
- Фильтр автоматически применяется на любой запрос юзера
- Все данные в одной БД. Можно связывать данные разных клиентов через FK и хранить общие сущности без проблем.
▫️Минусы:
- Сломался один клиент - сломались все!
- Нужно продумывать индексы с учётом особенностей multi-tenancy
- Если не следить за чистотой кода и правильностью архитектуры, то можно серьезно накосячить. Правильные тесты - наше ВСЁ!
- Общие миграции
▫️Что еще можно добавить
- партиции таблиц с разделением по полю company_id
- автоматическое добавление текущей company_id для создаваемых юзером объектов. Моя версия в фукнции add_company_id(), но не факт, что это лучший способ.
▫️Как запустить
3. готово!
▫️Как протестить
1. клонируем проект
2. запускаем
3. готово!
И главное: мой пример не является эталоном и инструкцией к применению, скорей это эксперимент. Буду рад замечаниям и указаниям на проблемы такого подхода.
Другие статьи на почитать:
↗️один
↗️два
↗️три
#tricks #systemdesign
Ключевые особенности:
▫️ Основной всего является сущность Компании. Юзеры могут подключаться к разным компаниям, но в один момент времени могут находиться только в одной (по условиям ТЗ).
▫️ Разделение данных через FK-ссылку всех ключевых бизнес-сущностей на ID компании.
▫️ ID компании задаётся при логине и хранится в JWT. При смене компании потребуется новый токен.
▫️ Контекст активируется через зависимость.
▫️ Фильтр применяется автоматически для любого запроса.
Теперь подробней:
1️⃣ Логин
Когда юзер логинится, он указывает ID компании. Этот ID зашивается в JWT.
2️⃣ Запрос
Ключевое действие находится в функции get_current_user(...). Мы парсим JWT и достаем инфу о юзере и компании.
Для текущей и дочерних корутин этого запроса объявляется контекст конкретной компании. Объявление контекста происходит через
ContextVar (а вот и реальный пример их применения). Никакого указания ID проекта\магазина\сайта со стороны юзера и проброса его по всем функциям. Это локальное состояние конкретного запроса и его цепочки корутин.
3️⃣ Активация фильтра для SQL
Реализация фильтра сделана штатными средствами SQLAlchemy - прослушивание ивента
do_orm_execute и правка любого SQL запроса с with_loader_criteria. Фильтр реагирует только на модели с миксином CompanyMixin. Нужно только указать ID компании для филтра. Функция фильтрации add_tenant_filter(...) требует либо указать контекст, либо отключить фильтр. Если вдруг контекст не задан, то по умолчанию юзер просто получит ошибку
BadRequestError. 4️⃣ Админка
В
get_current_user() указание контекста необязательно. Это сделано для того, чтобы мог залогиниться суперадмин. Ведь должен же кто-то управлять компаниями "сверху". В моем случае я просто не указываю контекст, но можно явно проверять, является ли юзер суперадмином.
После логина админ тоже будет получать ошибки при обращении к юзерским сервисам, поэтому у него есть свои - админские сервисы через админские роуты.
На админских роутах стоит депенденси с функцией
bypass_company_filter(...). Это позволяет отключить фильтр и управлять любыми сущностями без ограничения. А так же депенденси пропускающий только админов.Так же есть один юзерский запрос с
bypass_company_filter() - это получение всех доступных юзеру компаний чтобы можно было на UI выбирать куда переключиться.Если же админу нужно вызвать юзерский сервис с контекстом какой-либо компании, то есть функция
set_company_context(...), которая временно включает фильтр и активирует ID указанной компании. Например получить все [имя сущности] юзера для конкретной компании.▫️Плюсы такого подхода:
- Контекст хранится в токене, нет лишнего запроса в БД
- При утечке токена одной компании другие не пострадают
- Фильтр автоматически применяется на любой запрос юзера
- Все данные в одной БД. Можно связывать данные разных клиентов через FK и хранить общие сущности без проблем.
▫️Минусы:
- Сломался один клиент - сломались все!
- Нужно продумывать индексы с учётом особенностей multi-tenancy
- Если не следить за чистотой кода и правильностью архитектуры, то можно серьезно накосячить. Правильные тесты - наше ВСЁ!
- Общие миграции
▫️Что еще можно добавить
- партиции таблиц с разделением по полю company_id
- автоматическое добавление текущей company_id для создаваемых юзером объектов. Моя версия в фукнции add_company_id(), но не факт, что это лучший способ.
▫️Как запустить
1. клонируем проект
2. запускаем just run3. готово!
▫️Как протестить
1. клонируем проект
2. запускаем
just test3. готово!
- Почему just?
- Патамушта!
И главное: мой пример не является эталоном и инструкцией к применению, скорей это эксперимент. Буду рад замечаниям и указаниям на проблемы такого подхода.
Другие статьи на почитать:
↗️один
↗️два
↗️три
#tricks #systemdesign
👍2🔥1
Модуль pwd может использоваться для считывания информации о пользователях из базы данных паролей Unix (обычно
Получить объект текущего юзера можно так:
Теперь нам доступны основные данные из файла
Когда это может быть полезно?
▫️ Проверка наличия юзера в системе по имени
▫️ Преобразование имени юзера в ID
Пример с понижением привелегий процесса когда он запущен от root
▫️Преобразование ID юзера в имя
▫️Определение домашней директории не текущего юзера
#libs
/etc/passwd ).Получить объект текущего юзера можно так:
import pwd
user_pwd = pwd.getpwnam(username)
Теперь нам доступны основные данные из файла
passwdprint('ID:', f'{user_pwd.pw_uid}:{user_pwd.pw_gid}')
print('HOME:', user_pwd.pw_dir)
print('Shell:', user_pwd.pw_shell)Когда это может быть полезно?
▫️ Проверка наличия юзера в системе по имени
def user_exists(username):
try:
pwd.getpwnam(username)
return True
except KeyError:
return False
▫️ Преобразование имени юзера в ID
Пример с понижением привелегий процесса когда он запущен от root
import os
import pwd
safe_user = 'nobody'
# находим UID для безопасного пользователя
target_uid = pwd.getpwnam(safe_user).pw_uid
target_gid = pwd.getpwnam(safe_user).pw_gid
# меняем права процесса
os.setgid(target_gid)
os.setuid(target_uid)
# Выполняем какие-то действия
os.chdir('/tmp')
with open('nobody-file.txt', 'w') as f:
pass
# теперь проверьте, файл будет создан от имени юзера nobody
▫️Преобразование ID юзера в имя
user_name = pwd.getpwuid(user_id).pw_name
▫️Определение домашней директории не текущего юзера
pwd.getpwnam(username).pw_dir
Работает только в Unix-системах
#libs
Python documentation
pwd — The password database
This module provides access to the Unix user account and password database. It is available on all Unix versions. Availability: Unix, not WASI, not iOS. Password database entries are reported as a ...
👍4
В прошлый раз был пример с понижением привелегий процесса. Проблема в том, что обратно поднять привилегии нельзя.
Чтобы не потерять уровень доступа текущего процесса, нужно выполнять понижение в дочернем процессе. Для этого можно использовать os.fork().
Теперь запустите скрипт от root. У файлов будут разные владельцы.
PS. В дочернем процессе os.fork() вернёт 0, чтобы можно было понять в каком процессе продолжает исполняться код. В родительском это будет реальный PID. Это написано в документации.
# tricks
Чтобы не потерять уровень доступа текущего процесса, нужно выполнять понижение в дочернем процессе. Для этого можно использовать os.fork().
# example.py
from pathlib import Path
import os
import sys
import pwd
def example(username):
# получаем pwd юзера по имени
try:
pw_record = pwd.getpwnam(username)
except KeyError:
raise Exception(f"User {username} not found")
return
# создаем форк процесса
pid = os.fork()
if pid == 0:
# если мы в дочернем процессе - сбрасываем привилегии
os.setgid(pw_record.pw_gid)
os.setuid(pw_record.pw_uid)
# создаем файл от имени другого юзера
Path('user-file.txt').touch()
sys.exit(0)
else:
# родительский процесс, создаем файл от имени исходного юзера
Path('root-file.txt').touch()
os.waitpid(pid, 0)
example('paul')
# проверяем владельца файлов
print('user-file.txt', Path('user-file.txt').owner())
print('root-file.txt', Path('root-file.txt').owner())
Теперь запустите скрипт от root. У файлов будут разные владельцы.
~$ sudo python example.py
user-file.txt paul
root-file.txt root
PS. В дочернем процессе os.fork() вернёт 0, чтобы можно было понять в каком процессе продолжает исполняться код. В родительском это будет реальный PID. Это написано в документации.
# tricks
Python documentation
os — Miscellaneous operating system interfaces
Source code: Lib/os.py This module provides a portable way of using operating system dependent functionality. If you just want to read or write a file see open(), if you want to manipulate paths, s...
👍1
У меня основная ОС это Linux. Но нередко требуется делать сборку клиента под Windows. Собираю обычно через Pynstaller/Nuitka.
И вот этапыборьбы поиска удобного решения.
▫️ Dual Boot
Пока был дуалбут, я перезагружался в Windows, копировал исходники и запускал сборку. Рабочий вариант, но времени занимает слишком уж много. К тому же дуалбута давно нет.
▫️ VirtualBox
Если нет дуалбута, то выручает VirtualBox. Тут стало проще, я расшарил папку с проектом в Windows как сетевой диск. Просто запускаю готовый скрипт сборки и готово. Файл сохраняется сразу на место. Но горький опыт заставил делать шару ReadOnly, что требует сначала копировать проект на локальный диск в виртуалку и только потом запускать сборку. После чего перебрасывать результат. В общем, тоже так себе вариант 😖
Ясное дело, что эти два способа достаточно наивны и никакой автоматики. Поэтому пошли далее...
▫️ Wine
Выглядит всё просто. Выполняем обычные виндовые команды в терминале, просто в начале нужно дописывать
Попробуйте выполнить команду
Главное - установить все необходимые зависимости.
В целом запускается, но дебажить не удобно. Проверить запуск на "чистой винде" тоже не получится, только через тот же Wine. Поэтому в итоге я остановился на следующем варианте.
▫️ VirtualBox CLI
Это тоже самое что пункт 2 но полностью на автомате. Используя команду
-
-
-
В результате, мы можем кодом запустить виртуалку, выполнить необходимые действия, забрать результат и погасить виртуалку!
В скрипте я запускаю VM, копирую в неë батник сборки и исходники и запускаю. После сборки файл копируется обратно на хост в
А если надо, запускаем виртуалку в обычном режиме и визуально дебажим скрипты сборки и само приложение.
Из минусов только 20 занятых гигов на винду.
Итого, получился такой демо-проект↗️ со сборкой простого диалога на PySide6. В проекте примеры с wine и VirtualBox CLI
Какие еще есть варианты
▫️В Docker контейнере. Например есть такой cdrx/pyinstaller-windows
▫️Говорят PyOxidizer в будущем сможет делать это без танцев с бубном, но пока не реализовано. Только вот проект 2 года не обновляется, может и не реализуют уже 😢
#pyside #linux
И вот этапы
▫️ Dual Boot
Пока был дуалбут, я перезагружался в Windows, копировал исходники и запускал сборку. Рабочий вариант, но времени занимает слишком уж много. К тому же дуалбута давно нет.
▫️ VirtualBox
Если нет дуалбута, то выручает VirtualBox. Тут стало проще, я расшарил папку с проектом в Windows как сетевой диск. Просто запускаю готовый скрипт сборки и готово. Файл сохраняется сразу на место. Но горький опыт заставил делать шару ReadOnly, что требует сначала копировать проект на локальный диск в виртуалку и только потом запускать сборку. После чего перебрасывать результат. В общем, тоже так себе вариант 😖
Ясное дело, что эти два способа достаточно наивны и никакой автоматики. Поэтому пошли далее...
▫️ Wine
Выглядит всё просто. Выполняем обычные виндовые команды в терминале, просто в начале нужно дописывать
wine.Попробуйте выполнить команду
wine cmd и вы окажетесь в "обычной" виндовой консоли. Ну а там просто выполняем команды сборки.Главное - установить все необходимые зависимости.
В целом запускается, но дебажить не удобно. Проверить запуск на "чистой винде" тоже не получится, только через тот же Wine. Поэтому в итоге я остановился на следующем варианте.
▫️ VirtualBox CLI
Это тоже самое что пункт 2 но полностью на автомате. Используя команду
VBoxManage можно манипулировать виртуалкой. -
VBoxManage startvm - запускает виртуалку по имени, в том числе headless-
VBoxManage guestcontrol "VM_NAME" "CMD" позволяет запускать шел-команды-
copyto и copyfrom - позволяет копировать туда-сюда файлыВ результате, мы можем кодом запустить виртуалку, выполнить необходимые действия, забрать результат и погасить виртуалку!
В скрипте я запускаю VM, копирую в неë батник сборки и исходники и запускаю. После сборки файл копируется обратно на хост в
dest.А если надо, запускаем виртуалку в обычном режиме и визуально дебажим скрипты сборки и само приложение.
Из минусов только 20 занятых гигов на винду.
Итого, получился такой демо-проект↗️ со сборкой простого диалога на PySide6. В проекте примеры с wine и VirtualBox CLI
Не обещаю, что у вас запустится сразу, но у скрипты точно рабочие!
Какие еще есть варианты
▫️В Docker контейнере. Например есть такой cdrx/pyinstaller-windows
▫️Говорят PyOxidizer в будущем сможет делать это без танцев с бубном, но пока не реализовано. Только вот проект 2 года не обновляется, может и не реализуют уже 😢
#pyside #linux
GitHub
GitHub - paulwinex/build-win-app
Contribute to paulwinex/build-win-app development by creating an account on GitHub.
👍6❤2🔥1🤔1
Не так давно столкнулся с API который в ответе с ошибкой присылал только статус-код. Без подробностей и текста ошибки.
Неудобно когда нет явного описания что произошло, но вот такой сервис достался 😐
Чтобы сообщение совсем не было пустым хотелось бы показывать хоть что-то, например стандартный текст ошибок соответствующего кода.
В стандартных библиотеках текст статус-кодов можно достать из модуля http.
#libs 🫖
Неудобно когда нет явного описания что произошло, но вот такой сервис достался 😐
Чтобы сообщение совсем не было пустым хотелось бы показывать хоть что-то, например стандартный текст ошибок соответствующего кода.
В стандартных библиотеках текст статус-кодов можно достать из модуля http.
from http import HTTPStatus
HTTPStatus(404).phrase
# 'Not Found'
HTTPStatus(201).phrase
# 'Created'
HTTPStatus(418).phrase
# "I'm a Teapot"
#libs 🫖
Python documentation
http — HTTP modules
Source code: Lib/http/__init__.py http is a package that collects several modules for working with the HyperText Transfer Protocol: http.client is a low-level HTTP protocol client; for high-level U...
👍6🔥1