docs: полный аудит проекта (AUDIT.md)

Статический аудит: архитектура, приложения/модели, сильные стороны,
покрытие тестами, безопасность, конфигурация, техдолг по приоритетам,
план действий.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 17:33:48 +03:00
parent 5e4528b23f
commit c42b1b533e
+168
View File
@@ -0,0 +1,168 @@
# Аудит проекта ACORE
**Система управления аптечной сетью** · корпоративный портал + внутренние интеграции
Дата аудита: **2026-07-22** · Ветка: `main` · Django 6.0.3 / Python 3.14 / PostgreSQL
---
## 1. Резюме
ACORE — внутренний портал аптечной сети: управление сотрудниками, аптеками, справочниками, ИТ-инвентарём, задачами, лентой публикаций, плюс живые интеграции с Active Directory и Mattermost. Проект в **хорошей инженерной форме**: архитектура последовательная, паттерны выдержаны, безопасность продумана на уровне выше среднего для внутренних систем. Растёт быстро — 13 приложений, ~14,4 тыс. строк Python (без миграций и тестов).
**Общая оценка: 7.5/10.** Фундамент добротный. Главные риски не архитектурные, а процессные: неравномерное покрытие тестами, большой объём незакоммиченной работы и хранение секретов интеграций открытым текстом.
| Показатель | Значение |
|---|---|
| Приложений | 13 |
| Моделей | 39 |
| Python-код (без миграций/тестов) | ~14 400 строк |
| Миграций | 56 |
| Тестов | 127 функций (~2000 строк) |
| Шаблонов | 39 HTML |
| JS-модулей (кастомных) | 12 |
| `manage.py check` | чисто |
| `check --deploy` (production) | 2 минорных предупреждения |
| Миграции консистентны | да (`No changes detected`) |
---
## 2. Архитектура
### Технологический стек
- **Backend:** Django 6.0.3, PostgreSQL (psycopg2), Django REST Framework (точечно, 5 endpoint-ов).
- **Async:** Celery 5.5 + Redis + django-celery-beat/results.
- **Frontend:** серверный рендеринг + **HTMX** (навигация без перезагрузки), **Tailwind CSS v4** (сборка через CLI), ванильный JS для виджетов. SPA-фреймворков нет — сознательно.
- **Админка:** django-unfold (кастомная тема) + django-import-export.
- **Интеграции:** ldap3 (Active Directory), mattermostautodriver + httpx (Mattermost).
- **Контент:** django-tinymce (WYSIWYG) + nh3 (санитизация HTML против XSS).
- **Справочники:** django-fast-treenode (иерархические деревья).
### Ключевые архитектурные решения
1. **Единый справочный слой** (`core.Directory` / `DirectoryType`) — иерархические справочники (организации, отделы, должности, регионы, типы/бренды оборудования, статусы). На него опирается почти всё. Реализовано аккуратно, с автоперестройкой дерева после миграций и импорта.
2. **Интеграции по единому паттерну:** сервис-клиент → sync-сервис → сигналы → Celery-задачи → management-команды. Благодаря этому AD лёг рядом с Mattermost без изобретения велосипеда.
3. **Единый шелл фронта** (`templates/index.html`) + страницы-фрагменты HTMX. Общие утилиты вынесены (`core/utils/htmx.py`, `permissions.py`, `templatetags/common_tags.py`).
4. **Роли через `auth.Group`**, а не поле-enum (миграция `27a917e`).
---
## 3. Приложения и модели
| Приложение | Назначение | Модели | Тесты |
|---|---|---|---|
| `core` | Справочники, конфиги коннекторов, портал-вью | Directory, DirectoryType, ConnectorConfig | 21 (смоук страниц, PII-доступ, дерево) |
| `employees` | Сотрудники (кастомный User), права | Employee, EmployeePermissionOverride, PermissionGroup | **0** |
| `locations` | Аптеки, склады, офисы (MTI) | Location, Pharmacy, PharmacyPhone, PharmacyHistory | **0** |
| `it_manager` | ИТ-инвентарь, заявки, интернет-договоры | Equipment, **EquipmentModel** (каталог), EquipmentAssignment, Consumable, Ticket, TicketComment, WorkLog, ServiceCall, InternetProvider, InternetContract | 10 |
| `feed` | Лента и публикации (TinyMCE) | Post, PostComment | 40 |
| `tasks` | Задачи (роли Битрикс-стиля) | Task, TaskComment | **0** ⚠ |
| `ad_connector` | Active Directory (провижининг) | ADSyncLog | 30 |
| `mattermost_connector` | Mattermost (аккаунты, каналы) | MattermostUser/Team/Channel + membership + sync-settings (7) | 19 |
| `visit_manager` | Учёт посещений, графики, смены | AttendanceLog, WorkShift, Shift, Holiday, Schedule* (9) | **0** |
| `hiring` | Заявки на найм | HiringRequest | 7 |
| `utilities` | Показания счётчиков | UtilityReading | 0 |
| `mail_connector`, `mailparser`, `portal_connector` | Заглушки / служебное | — | 0 |
### Заметные модели
- **`Employee`** — кастомный `AbstractBaseUser`, богатый: контакты, PII, локация (аптека/офис/склад), руководитель, интеграционные поля AD/Mattermost, гибкие права через `EmployeePermissionOverride` (grant/revoke с датами действия). Валидация в `clean()` разделяет аптечных/офисных сотрудников.
- **`EquipmentModel` + `Equipment`** (свежий рефакторинг) — каталог моделей отделён от экземпляров: тип+бренд+модель заводятся один раз, экземпляры несут только серийник/локацию. Массовый ввод и массовая выдача.
- **`Task`** — три роли (ответственный/соисполнители/наблюдатели), видимость через `for_user()`, наблюдатели не меняют статус.
---
## 4. Сильные стороны
✅ **Безопасность продумана.** Отдельный коммит hardening (`faa06b9`): исправлен open redirect в логине, добавлена аутентификация на API, MIME-валидация загрузок (magic bytes + Pillow verify), HttpOnly/SameSite куки, лимиты загрузки. Есть полноценный `production.py` (DEBUG=False, SSL-redirect, HSTS, secure cookies) — `check --deploy` даёт лишь 2 минорных предупреждения.
✅ **Разделение PII.** Личные данные (телефоны, ИНН, дата рождения, экстренный контакт) отделены от публичных полей — и в DRF-сериализаторах (`EmployeeSerializer` / `EmployeeDetailSerializer`), и на детальной странице сотрудника (`_can_view_employee_pii`: staff / сам / непосредственный руководитель). Покрыто тестами.
✅ **XSS-защита ленты.** Тело поста (HTML из TinyMCE) всегда прогоняется через `nh3` в `Post.save()` — чистка в модели, а не во view, поэтому защищён любой путь записи. Загрузка картинок отдельным endpoint-ом с проверкой содержимого; `data:`-URI вырезаются.
✅ **Оптимизация запросов.** Осознанная борьба с N+1: `Prefetch` с `select_related`, агрегаты вместо циклов `.count()`, `distinct=True` в `Count` при OR-джойнах по M2M.
✅ **Устойчивость к сбоям.** Сигналы обёрнуты в try/except — недоступный Celery-брокер или AD не роняют сохранение сотрудника. Reverse-миграции пишутся (где возможно).
✅ **Единый UX-язык.** HTMX + Tailwind + переиспользуемые виджеты (people-picker дерево, searchable-select, пагинация). Украинский интерфейс выдержан.
---
## 5. Тестирование
**127 тестовых функций, ~2000 строк.** Покрытие качественное там, где оно есть, но **неравномерное**:
**Покрыто хорошо:**
- `feed` (40) — публикация, права, санитизация, картинки, комментарии.
- `ad_connector` (30) — LDAP-клиент на мок-каталоге ldap3 (`MOCK_SYNC`), sync-сценарии, сигналы, страница сопоставления.
- `core` (21) — смоук всех страниц портала, PII-доступ, дерево справочников.
- `mattermost_connector` (19), `it_manager` (10), `hiring` (7).
**Не покрыто вообще (0 тестов):**
- ⚠ **`tasks`** — модуль с нетривиальной логикой видимости и ролей, но без тестов (задача осталась в бэклоге).
- ⚠ **`employees`** — кастомная модель User с валидацией `clean()`, правами-оверрайдами; критичный код без тестов.
- ⚠ **`visit_manager`** — 9 моделей (графики, смены, посещения) без единого теста.
- `locations`, `utilities`, mail/portal-заглушки.
**Инфраструктурный нюанс:** discovery `manage.py test apps.X` падает на структуре пакетов — тесты запускают явными метками модулей (`apps.feed.tests`). CI отсутствует — тесты гоняются вручную.
---
## 6. Безопасность — находки
| Приоритет | Находка | Детали |
|---|---|---|
| 🔴 Высокий | **Секреты интеграций открытым текстом** | `ConnectorConfig.password` / `token` — обычные `CharField` в БД. Пароль сервисной учётки AD и токен Mattermost лежат в базе как есть. Рекомендация: шифрование на уровне поля (`django-fernet-fields` или аналог) либо вынос в секрет-менеджер/переменные окружения. |
| 🟡 Средний | **Нет тестов на критичный код доступа** | `tasks.for_user()`, `employees` права, PII-гейтинг частично — регрессии в контроле доступа не отлавливаются автоматически. |
| 🟢 Низкий | `SECURE_HSTS_PRELOAD` не выставлен | `check --deploy` W021 — не даёт подать сайт в preload-список. Косметика. |
| 🟢 Низкий | Синхронные операции AD с паролем блокируют запрос | Сохранение формы ждёт LDAP-вызов (до 15 с таймаут при недоступном контроллере). Ошибка не роняет сохранение, но UX-задержка возможна. |
**Хорошо:** `.env` и `db.sqlite3` **не** в git; `.gitignore` покрывает env/sqlite/pycache/node_modules/шаблон-референс; SECRET_KEY/ALLOWED_HOSTS/DATABASE_URL — из окружения.
---
## 7. Конфигурация и деплой
- **Настройки разделены** на `base` / `local` / `production`. Production корректен.
- **БД:** PostgreSQL и в dev, и в prod (через `DATABASE_URL`). Лежащий в корне `db.sqlite3` — мёртвый артефакт, стоит удалить.
- **Celery:** приложение сконфигурировано (`acore/celery.py`), брокер из env, локально `CELERY_TASK_ALWAYS_EAGER=True` (без брокера задачи синхронно). Beat-задачи есть в mattermost/ad коннекторах.
- **Локализация:** `LANGUAGE_CODE='ru'`, `TIME_ZONE='Europe/Kiev'`, UI на украинском, `verbose_name` на русском, комментарии — смесь ru/uk. Работает, но единого языка нет.
- **Нет:** README, `docs/`, CI/CD (`.github`/`.gitlab-ci`), Makefile, зафиксированного процесса деплоя.
---
## 8. Технический долг — сводка по приоритетам
### 🔴 Высокий
1. **Незакоммиченная работа.** В рабочем дереве **116 изменённых/новых файлов** — целые модули (feed, tasks, employee-detail, equipment-каталог, виджеты) не в истории git. Последний коммит — `c3f56f8`. Риск потери работы; история не отражает реальное состояние. **Закоммитить логическими блоками.**
2. **Секреты открытым текстом** (см. §6).
### 🟡 Средний
3. **Тесты для `tasks`, `employees`, `visit_manager`** — критичная бизнес-логика без покрытия.
4. **Нет CI** — тесты не гоняются автоматически на push; регрессии всплывают вручную (в этой сессии дважды находились сломанные смоук-URL).
5. **Рост числа сигналов на `Employee.post_save`** (9 ресиверов между AD и Mattermost, у каждого свой pre_save-кеш). При третьей интеграции (Carbonio в планах) станет хрупким — напрашивается единый диспетчер изменений.
6. **Reverse-миграции частично ломаются** на восстановлении non-null полей (напр. `0018_equipment_model_catalog`) — forward безупречен, но откат в dev упадёт. Приемлемо, но стоит знать.
### 🟢 Низкий
7. **Дубль зависимости:** `import-export==0.3.1` (устаревший неофициальный) соседствует с `django-import-export==4.4.0`. Проверить и убрать лишнее.
8. **Единый язык** кода/комментариев/verbose_name.
9. **Мёртвые артефакты:** `db.sqlite3` в корне, `.env2`.
10. **Портал наполовину макет:** email, chat, документы, календарь, часть HR — статичные страницы без бэкенда. Стоит явно решить, что реализуем, а что убрать из меню.
---
## 9. Рекомендации (порядок действий)
1. **Закоммитить накопленное** логическими блоками — снять риск №1 немедленно.
2. **Зашифровать секреты `ConnectorConfig`** перед подключением живого AD в прод — именно эта логика трогает внешнюю систему, где ошибка видна всей компании.
3. **Покрыть тестами `tasks` и `employees`** — минимум контроль доступа и валидацию. Мок-сценарии для AD/Mattermost уже почти готовые тесты.
4. **Поднять минимальный CI** (GitHub Actions / GitLab CI): `manage.py check` + прогон тестов + `makemigrations --check`. Дёшево, ловит регрессии автоматически.
5. **Написать README** с процессом запуска (dev-сервер, Tailwind-сборка, Celery, тесты) и деплоя.
6. **Отрефакторить сигналы `Employee`** в единый диспетчер до третьей интеграции.
7. **Причесать:** удалить `db.sqlite3`/`.env2`, разобраться с дублем `import-export`, определиться с макет-страницами.
---
## 10. Вывод
Фундамент выбран правильно и выдержан — проект масштабируется **добавлением модулей, а не переписыванием**. Паттерны интеграций, справочный слой, безопасность, борьба с N+1 — всё на уровне выше среднего для внутренней системы. Основной долг — **процессный**, а не архитектурный: закоммитить работу, зашифровать секреты, дописать тесты на критичные модули и поднять CI. Закрыв эти пункты, проект будет готов к спокойной эксплуатации и дальнейшему росту.
---
*Аудит подготовлен на основе статического анализа кода, конфигурации, миграций, тестов и git-истории. Данные собраны инструментально (`manage.py check --deploy`, `makemigrations --check`, подсчёт моделей/тестов/строк).*