Тестовое задание: backend-приложение с собственной аутентификацией и системой
разграничения прав доступа. Возможности фреймворка «из коробки» намеренно не
используются — django.contrib.auth, django.contrib.sessions и
authentication/permission-классы DRF отключены, вместо них написан свой слой.
Стек: Python 3.12, Django 5.1, Django REST Framework, PostgreSQL (SQLite как запасной вариант для быстрого запуска), bcrypt, PyJWT.
# вариант 1 — docker (PostgreSQL внутри)
docker compose up --build
# приложение на http://127.0.0.1:8000
# вариант 2 — локально, на SQLite
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
python manage.py migrate
python manage.py seed # роли, объекты, правила доступа, тестовые пользователи
python manage.py runserverТесты:
python manage.py test testsТестовые пользователи создаются командой seed:
| пароль | роль | |
|---|---|---|
| admin@example.com | AdminPass123 | admin |
| manager@example.com | ManagerPass123 | manager |
| user@example.com | UserPass123 | user |
| guest@example.com | GuestPass123 | guest |
Пароль никогда не хранится в открытом виде: при регистрации он превращается в
bcrypt-хеш с индивидуальной солью (accounts/hashing.py).
После успешного login создаются две сущности:
- Серверная сессия — запись в таблице
sessionsсо случайным ключом, сроком жизни и полемrevoked_at. - JWT-токен доступа, внутри которого лежат
sub(id пользователя) иsid(ключ сессии).
Токен возвращается в теле ответа, а ключ сессии дополнительно ставится в
httponly-cookie sessionid — клиент может работать любым из двух способов:
Authorization: Bearer <jwt>
Cookie: sessionid=<ключ>
Связка «JWT + серверная сессия» выбрана намеренно. Чистый JWT нельзя отозвать
до истечения срока: после logout или удаления аккаунта старый токен
продолжал бы работать. Здесь же каждый запрос сверяется с сессией в базе,
поэтому выход из системы и мягкое удаление обрывают доступ мгновенно.
Пользователя определяет собственный middleware
(accounts/middleware.py): до обработки запроса он достаёт ключ сессии,
проверяет, что сессия не истекла и не отозвана, а владелец активен, и кладёт
пользователя в request.auth_user. Отдельный атрибут вместо request.user
нужен потому, что DRF подменяет request.user собственным объектом.
Мягкое удаление (DELETE /api/auth/profile/delete) выставляет
is_active=False и deleted_at, отзывает все сессии пользователя, но саму
запись в базе сохраняет — войти под ней больше нельзя.
Три таблицы:
roles — роли: admin, manager, user, guest.
business_elements — объекты приложения, доступ к которым разграничивается:
users, products, shops, orders, access_rules.
access_roles_rules — правило на пару «роль × объект», семь булевых флагов:
| поле | смысл |
|---|---|
read_permission |
читать свои объекты |
read_all_permission |
читать любые объекты |
create_permission |
создавать объекты |
update_permission |
изменять свои объекты |
update_all_permission |
изменять любые объекты |
delete_permission |
удалять свои объекты |
delete_all_permission |
удалять любые объекты |
Парность флагов — ключевая идея схемы. «Свой» объект определяется полем
owner, ссылающимся на пользователя. Менеджеру можно выдать
read_all_permission на заказы, а обычному пользователю — только
read_permission, и тот же самый эндпоинт вернёт ему лишь его собственные
записи. Так одно правило описывает и факт доступа, и его область видимости,
без отдельных ролей вида «менеджер-который-видит-всё».
Проверка живёт в access/permissions.py. Функция check_access(request, element, action) возвращает решение и область видимости:
- пользователь не определён → 401 Unauthorized;
- правило отсутствует или флаг выключен → 403 Forbidden;
- разрешение есть → выдаётся ресурс, отфильтрованный по владельцу, если разрешена только своя область.
Порядок проверки: сначала *_all_permission (даёт полную область), затем
обычный флаг (ограничивает своими объектами). Для create понятие владельца
неприменимо, поэтому область не сужается.
Матрица прав по умолчанию задаётся командой seed и дальше правится через
API администратора — правила лежат в базе, а не в коде, поэтому меняются без
перезапуска приложения.
| метод | путь | описание |
|---|---|---|
| POST | /api/auth/register |
регистрация: имя, фамилия, отчество, email, пароль + повтор |
| POST | /api/auth/login |
вход по email и паролю, выдаёт токен и cookie |
| POST | /api/auth/logout |
выход, сессия отзывается |
| GET | /api/auth/profile |
текущий профиль |
| PATCH | /api/auth/profile |
изменение имени, фамилии, отчества |
| DELETE | /api/auth/profile/delete |
мягкое удаление аккаунта |
| метод | путь | описание |
|---|---|---|
| GET, POST | /api/admin/roles |
список и создание ролей |
| GET, POST | /api/admin/business-elements |
список и создание объектов |
| GET, POST | /api/admin/access-rules |
список и создание правил |
| GET, PATCH, DELETE | /api/admin/access-rules/<id> |
чтение, изменение, удаление правила |
| метод | путь |
|---|---|
| GET, POST, PATCH, DELETE | /api/products, /api/shops, /api/orders |
Таблицы для них по условию задания не создаются: объекты лежат в
business/storage.py и содержат поле owner_id, на котором и проверяется
разграничение «свои против всех». Ответ на чтение включает поле scope
(own или all) — по нему видно, какая область прав сработала.
# регистрация
curl -X POST http://127.0.0.1:8000/api/auth/register \
-H 'Content-Type: application/json' \
-d '{"email":"ivan@example.com","password":"StrongPass123",
"password_repeat":"StrongPass123","first_name":"Иван","last_name":"Иванов"}'
# вход
TOKEN=$(curl -s -X POST http://127.0.0.1:8000/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"user@example.com","password":"UserPass123"}' | jq -r .access_token)
curl http://127.0.0.1:8000/api/orders # 401
curl http://127.0.0.1:8000/api/orders -H "Authorization: Bearer $TOKEN"
# 200, "scope":"own" — только заказы этого пользователя
curl http://127.0.0.1:8000/api/admin/access-rules -H "Authorization: Bearer $TOKEN"
# 403 — правилами управляет только администратор16 тестов в tests/test_api.py покрывают: хеширование пароля и отказ при
несовпадении паролей, повторный email, выдачу токена и cookie, вход с неверным
паролем, доступ по Bearer-токену, отзыв сессии при выходе, мягкое удаление,
а также разграничение доступа — 401 без пользователя, 403 без правила,
сужение выдачи до своих объектов, полную выдачу по *_all_permission,
закрытость административного API и применение изменённого правила на лету.
accounts/ пользователи, сессии, bcrypt, JWT, middleware, эндпоинты профиля
access/ роли, объекты, правила доступа, движок проверки, админское API
business/ mock-объекты бизнес-приложения
config/ настройки, маршруты, единый формат ошибок
tests/ тесты