From 25a3dbd0fbdc50940a583ec4c4bed7e9c5743370 Mon Sep 17 00:00:00 2001 From: Vladislav Kopnin Date: Tue, 27 Oct 2026 20:39:37 +0300 Subject: [PATCH] =?UTF-8?q?docs:=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B0=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B5=D0=BF=D0=BE=D0=B4=D0=B0=D0=B2=D0=B0=D1=82=D0=B5?= =?UTF-8?q?=D0=BB=D1=8F,=20=D0=B0=D1=83=D0=B4=D0=B8=D1=82,=20CI=20=D0=B8?= =?UTF-8?q?=20=D0=B8=D1=82=D0=BE=D0=B3=D0=BE=D0=B2=D1=8B=D0=B5=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 12 +++ deploy/DEPLOY.md | 11 ++- deploy/IMPORT.md | 4 +- docs/02-deployment.md | 2 +- docs/07-groups-courses.md | 40 +++++++- docs/08-groups-courses-plan.md | 58 ++++++------ docs/09-teacher-student-profile.md | 5 +- docs/10-stage0-report.md | 5 +- docs/13-stage3-report.md | 23 ++++- docs/15-stage5-course-materials.md | 10 ++ docs/16-stage6-report.md | 141 +++++++++++++++-------------- docs/README.md | 2 +- 12 files changed, 204 insertions(+), 109 deletions(-) diff --git a/README.md b/README.md index 433ea26..92815fb 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,8 @@ progress `:8083`, web `:8090`. Интерфейс: `http://localhost:8090`. - Gradle daemon фиксируется на JDK 21 (`gradle/gradle-daemon-jvm.properties`): detekt 1.23 не работает на JDK 25. - Тестовая инфраструктура — `libs:test-support` (JUnit 5, MockK, Testcontainers `postgres:18`). +- CI merge gate — `.gitlab-ci.yml`: `./gradlew --no-daemon test ktlintCheck detekt` + на push/MR. Runner должен разрешать privileged `docker:dind` для Testcontainers. ## Запуск в Docker (для сервера) @@ -119,6 +121,16 @@ cp .env.prod.example .env.prod # заполнить LMS_DB_PASSWORD маршрут показывают сводку, оценки, комментарии с авторами, защиты и все попытки сдачи со скачиванием ZIP; студенту API отдаёт только его профиль и опубликованные данные. +- **Работа преподавателя**: ведомость группы переиспользует + `/admin/groups/{groupId}?courseId=...` без админских контролов — оценки и + комментарии, публикация, уровни/варианты и раздача, защиты (код/reset), + скачивание попыток и ZIP группы; лабораторные/уровни/варианты своего курса, + привязка/отвязка групп на `/teacher/courses/{id}`, авторские материалы на + `/teacher/courses/{id}/content`. Чужой курс отклоняется на сервисах. +- **Аудит**: чувствительные действия (профили, скачивания, оценки, защиты, + курсы, группы, материалы, преподаватели, жизненный цикл) пишутся в + `identity.audit_events`; админ смотрит `/admin/audit` с фильтром по действию + и временем МСК. - **Повышение и жизненный цикл**: `/admin/promotion` — dry-run, применение и откат повышения (автоматический dry-run 31 августа МСК); на странице группы — академотпуск, возврат, отчисление, архив и восстановление; diff --git a/deploy/DEPLOY.md b/deploy/DEPLOY.md index 50242c7..f3ad534 100644 --- a/deploy/DEPLOY.md +++ b/deploy/DEPLOY.md @@ -109,7 +109,16 @@ git pull `deploy.sh up` **обязательно** делает backup перед обновлением. Если backup завершился ошибкой, деплой прерывается. Миграции БД (Flyway) применяются -автоматически при старте сервисов. +автоматически при старте сервисов: identity `V4–V10` (`V10` создаёт +`identity.audit_events`), academic `V6–V12`, progress `V13`. + +После обновления проверьте, что админ открывает `/admin/audit` и видит события +(например, после входа в профиль студента — `PROFILE_VIEW`); преподаватель и +студент на эту страницу не попадают. + +CI (`.gitlab-ci.yml`) на каждом push/MR выполняет +`./gradlew --no-daemon test ktlintCheck detekt`; для Testcontainers runner +должен разрешать privileged `docker:dind`. ## 8. Резервное копирование diff --git a/deploy/IMPORT.md b/deploy/IMPORT.md index 61967dd..38f951d 100644 --- a/deploy/IMPORT.md +++ b/deploy/IMPORT.md @@ -101,7 +101,9 @@ chapters/01-глава/01-раздел/01-статья.md # front-matter: titl assets/ # картинки для md ``` -Материалы видны всем; для редактирования — «Материалы» с правами администратора. +Материалы видны всем; глобальные материалы редактирует только администратор +(«Материалы»). Преподаватель редактирует материалы своего курса на +`/teacher/courses/{id}/content`. Импорт бывает в двух режимах: с галочкой «Удалять разделы и статьи, которых нет в архиве» (синхронизация — архив считается источником истины) и без неё (merge — только добавить и diff --git a/docs/02-deployment.md b/docs/02-deployment.md index 78f85ea..8d975de 100644 --- a/docs/02-deployment.md +++ b/docs/02-deployment.md @@ -101,7 +101,7 @@ sequenceDiagram DC->>DC: docker build (5 стадий) DC->>PG: старт postgres (volume lms_pgdata) DC->>DC: старт identity → academic → progress → gateway → web - Note over DC: Flyway применяет миграции автоматически
при старте сервисов (identity V1..V3, academic V1..V5, progress V1..V9; у web миграций нет) + Note over DC: Flyway применяет миграции автоматически
при старте сервисов (identity V1..V10, academic V1..V12, progress V1..V9 и V11..V13; у web миграций нет) ``` - Локально (`docker-compose.yml`) поднимается только `postgres`; прод-стек — `docker-compose.prod.yml`. diff --git a/docs/07-groups-courses.md b/docs/07-groups-courses.md index a867efd..99f6ff0 100644 --- a/docs/07-groups-courses.md +++ b/docs/07-groups-courses.md @@ -205,6 +205,30 @@ INSERT INTO membership_history(student_id, to_group_id, action, at) SELECT student_id, group_id, 'ENROLL', enrolled_at FROM enrollments; ``` +**V8–V9 — история членства и статусы аккаунтов** реализованы на этапе 1 +(`membership_history`, `accounts.status`). + +**V10 — журнал аудита (добавлен на этапе 6, `V10__audit_events.sql`)** +```sql +CREATE TABLE identity.audit_events ( + id uuid PRIMARY KEY, + occurred_at timestamptz NOT NULL DEFAULT now(), + actor_account_id uuid, + actor_login varchar(100), + action varchar(64) NOT NULL, + entity_type varchar(64) NOT NULL, + entity_id varchar(128), + course_id uuid, + student_id uuid, + details text NOT NULL DEFAULT '' +); +CREATE INDEX audit_events_occurred_idx ON identity.audit_events (occurred_at DESC); +CREATE INDEX audit_events_action_idx ON identity.audit_events (action); +``` +Внешних ключей нет: событие должно переживать удаление субъекта. Запись идёт +из сервисов через `POST /api/v1/internal/audit` (`X-Internal-Token`), чтение — +`GET /api/v1/audit` и `/admin/audit` только для админа. + ### academic **V6 — курс (пересоздание)** @@ -365,7 +389,17 @@ V13 идемпотентна: если старая feature-миграция V10 - скачивание файлов не даёт доступа к чужим сдачам; - профиль не заменяет ведомость: оценки по-прежнему выставляются в ведомости; - старые комментарии без автора отображаются как «автор не указан»; -- аудит скачиваний — этап 6. +- аудит скачиваний реализован на этапе 6 (см. §5.2). + +### 5.2. Аудит (реализовано на этапе 6) + +Чувствительные действия пишутся в `identity.audit_events` (`V10`): создание и +сброс преподавателя, жизненный цикл и архив групп, повышение и удаление, +действия с курсами/группами/материалами/лабораториями, просмотр профиля, +скачивания, оценки/публикация, раздача вариантов и защиты. Админ смотрит +журнал на `/admin/audit` с фильтром по действию; время отображается в МСК. +`membership_history` и `promotion_runs/items` остаются доменной историей и +дополняют общий журнал. --- @@ -465,7 +499,9 @@ V13 идемпотентна: если старая feature-миграция V10 старые ссылки на текущие материалы не поддерживаются. 7. Миграция существующих лаб/материалов в курс C; «общих» лаб нет. 8. Немедленный отзыв доступа (преподаватель/группа/публикация материала). -9. Аудит-лог: переводы, академ, повышение, назначения преподавателей, публикации, архив/удаление. +9. Аудит-лог: переводы, академ, повышение, назначения преподавателей, публикации, + архив/удаление — реализован в `identity.audit_events` (`V10`), просмотр + `/admin/audit`. 10. Производительность: индексы `course_id`/`group_id`, пагинация материалов и групп. 11. Бэкфилл `enrollment_year` для существующих групп — приблизительный, требует подтверждения админом. 12. Лабы: уникальность номера — в рамках курса (`(number, course_id)`), прежний ключ по семестру снимается. diff --git a/docs/08-groups-courses-plan.md b/docs/08-groups-courses-plan.md index a8c5ee5..9768a79 100644 --- a/docs/08-groups-courses-plan.md +++ b/docs/08-groups-courses-plan.md @@ -1,9 +1,8 @@ # 08. План реализации: группы, курсы, преподаватели -**Статус:** план согласован, этапы 0–5 выполнены. Проверка этапа 6 выполнена -(тесты, upgrade миграций, security, deploy-runbook), но этап 6 закрыт не -полностью: остаются права преподавателя на запись и CI — -см. § 9.1 и [16-stage6-report.md](16-stage6-report.md). +**Статус:** план согласован, этапы 0–6 выполнены. Этап 6 закрыт: права +преподавателя на запись, аудит и CI добавлены; остаточные риски — § 9.1 и +[16-stage6-report.md](16-stage6-report.md). **Основа:** [07-groups-courses.md](07-groups-courses.md). **Дата решений:** 2026-10-23. @@ -117,7 +116,7 @@ отдельная таблица не нужна. - Для производительности: один агрегирующий запрос/эндпоинт, пакетная выборка комментариев, попыток, защит и публикаций по списку `lab_id`; без N+1. -- Аудит скачиваний: этап 6 (журнал `audit_events` или отдельная таблица). +- Аудит скачиваний реализован на этапе 6 в `identity.audit_events`. ### 2.6. Ограничения @@ -192,10 +191,14 @@ ### Этап 6. Hardening -- Полная security-матрица. +- Права преподавателя на запись: лабораторные/уровни/варианты, ведомость, + привязка групп, авторские материалы курса. +- Полная security-матрица и negative-тесты «свой/чужой курс». - Upgrade-тест миграций. -- Индексы и пагинация. -- Аудит скачиваний и чувствительных действий. +- Индексы; пагинация и N+1 — follow-up (§ 9.1). +- Аудит скачиваний и чувствительных действий (`identity.audit_events`, + `/admin/audit`). +- CI merge gate: `test ktlintCheck detekt` на каждом push/MR. - Проверка backup: создание, retention, `verify`, restore на staging. - Документация, deploy/runbook, smoke. @@ -205,13 +208,13 @@ Номера уточняются при реализации, чтобы не конфликтовать с максимумами master (identity V3, **academic V5**, progress V12). Максимумы этой ветки: -identity V9, **academic V12**, progress V13 (+V14 при необходимости аудита). +identity V10, **academic V12**, progress V13. | Сервис | Миграции | Содержание | |---|---|---| -| identity | V4–V9 | роли, поля групп, статусы студентов, `membership_history`, `promotion_runs/items`, `accounts.status` | +| identity | V4–V10 | роли, поля групп, статусы студентов, `membership_history`, `promotion_runs/items`, `accounts.status`; `V10` — `audit_events` без внешних ключей и индексы по `occurred_at`/`action` | | academic | V6–V12 + post-bootstrap step | курсы, преподаватели, группы курса, `labs.course_id` (nullable), материалы курса/аудитории, частичные индексы, активы; `V10` — уникальный индекс `lower(title)` по курсам; `V11` — backfill `audience='ALL'` для глобальных глав; `V12` — backfill связей `article_assets` из markdown-ссылок; `SET NOT NULL` — post-bootstrap шаг вне Flyway | -| progress | V13 (+ V14 при необходимости аудита) | `grade_comments.author_account_id`, `lab_grades.updated_by_account_id`, индекс `lab_grades(lab_id)`; аудит скачиваний — этап 6 | +| progress | V13 | `grade_comments.author_account_id`, `lab_grades.updated_by_account_id`, индекс `lab_grades(lab_id)`; аудит реализован в identity `V10` | ### Переход со старой feature-миграции V10 на V13 @@ -320,12 +323,12 @@ identity V9, **academic V12**, progress V13 (+V14 при необходимос `/learn/course/{course_no}/...` и аудитории, публичные материалы `ALL`, скрытые материалы для преподавателя, перепроверка статуса web-сессии. Отчёт: [15-stage5-course-materials.md](15-stage5-course-materials.md). -- **Этап 6 — проверка выполнена, закрыт не полностью.** Проверены полный - прогон тестов/линтеров, upgrade миграций с master-схемы, security-матрица, - gateway, видимость активов и deploy-runbook. Отчёт: [16-stage6-report.md](16-stage6-report.md). - Не закрыты: права преподавателя на запись, аудит скачиваний и CI. Остальные - пункты hardening (индексы, backup/restore, документация, smoke) описаны в - отчёте и чек-листе. +- **Этап 6 — выполнен.** Права преподавателя на запись (лабораторные, + ведомость, группы, материалы), центральный аудит `identity.audit_events` + с `/admin/audit`, CI merge gate и полный hardening. Проверены прогон + тестов/линтеров (**267**), upgrade миграций с master-схемы (identity + `V4–V10`, academic `V6–V12`, progress `V13`), security-матрица, gateway, + видимость активов и deploy-runbook. Отчёт: [16-stage6-report.md](16-stage6-report.md). ### 9.1. Известные пробелы после этапа 6 @@ -333,27 +336,26 @@ identity V9, **academic V12**, progress V13 (+V14 при необходимос проверяет `article_assets → article → section → chapter`, а `V12` восстанавливает связи из markdown. Файлы без ссылок в статьях остаются публичными; нужна инвентаризация и привязка или удаление. -- **Преподаватель — в основном read-only.** Авторство лабораторных/материалов, - распределение, оценки, комментарии и защита остаются admin-only в UI/API; - ТЗ ожидает преподавательский self-service. -- **Имена групп в кабинете преподавателя.** На странице курса показывается - UUID группы, batch-разрешения имён нет. -- **Аудит.** Есть история членства и журнал повышения, но нет audit-log - скачиваний, просмотров профилей, назначений и публикаций. - **Импорт.** Старый формат не поддерживается; ведущие нули номера группы нормализуются (`01` → `1`). - **JWT 30 дней.** Web перепроверяет статус каждые 45 секунд, но прямой API - с уже выданным токеном живёт до истечения. -- **CI отсутствует.** Гейт — локальный `./gradlew test ktlintCheck detekt`; - автоматического pipeline и upgrade-теста в CI нет. + с уже выданным токеном живёт до истечения; нужны короткий TTL + refresh + или revocation. +- **Пагинация и N+1.** Журнал аудита ограничен `limit` без offset/keyset; + часть списков групп, материалов и ведомости отдаётся целиком; на больших + объёмах нужны пагинация и пакетные выборки. ## 10. Definition of Done - Все решения раздела 1 реализованы. - Текущие лабы и материалы работают в курсе C. -- Преподаватель видит только свои курсы и профили своих студентов. +- Преподаватель видит только свои курсы и профили своих студентов, пишет + в лабы/ведомость/группы/материалы только своего курса. - Профиль показывает статистику, комментарии и все попытки; файлы скачиваются. - Повышение, академотпуск, выпуск и архив работают по ТЗ. +- Аудит чувствительных действий пишется в `identity.audit_events` и доступен + админу на `/admin/audit`. - Security-матрица и миграционный upgrade-тест зелёные. +- CI на каждом push/MR прогоняет `test ktlintCheck detekt`. - Backup перед деплоем обязателен; restore проверен на staging. - Документация, deploy/runbook и smoke обновлены. diff --git a/docs/09-teacher-student-profile.md b/docs/09-teacher-student-profile.md index 0c08e2d..87422b8 100644 --- a/docs/09-teacher-student-profile.md +++ b/docs/09-teacher-student-profile.md @@ -281,5 +281,6 @@ GET /api/v1/internal/courses/{courseId}/labs - Показывать ли профиль для архивных/отвязанных студентов (история или `404`). - Нужен ли студенческий просмотр своего профиля в первом релизе. -- Формат аудита скачиваний: общий `audit_events` или отдельная таблица. -- Ограничение количества попыток в ответе и пагинация. +- ~~Формат аудита скачиваний: общий `audit_events` или отдельная таблица.~~ + Решено на этапе 6: общий `identity.audit_events` (`V10`). +- Ограничение количества попыток в ответе и пагинация — follow-up. diff --git a/docs/10-stage0-report.md b/docs/10-stage0-report.md index e63bc7f..77528af 100644 --- a/docs/10-stage0-report.md +++ b/docs/10-stage0-report.md @@ -9,8 +9,9 @@ - ADR с ключевыми решениями: [adr/0001-groups-courses-foundation.md](adr/0001-groups-courses-foundation.md). - Контракт API профиля студента: [09-teacher-student-profile.md](09-teacher-student-profile.md). - Миграции: - - identity `V4–V9`: роли, группы-когорты, статусы студентов, повышение, - история членства, статусы аккаунтов; + - identity `V4–V10`: роли, группы-когорты, статусы студентов, повышение, + история членства, статусы аккаунтов; `V10` (`audit_events`) добавлена + позже, на этапе 6; - academic `V6–V12`: курсы, преподаватели, группы курса, `labs.course_id`, материалы курса/аудитории, индексы активов; `V10` (уникальный индекс `lower(title)` по курсам) добавлена на этапе 2, после этого отчёта; diff --git a/docs/13-stage3-report.md b/docs/13-stage3-report.md index e29a203..e1b3e24 100644 --- a/docs/13-stage3-report.md +++ b/docs/13-stage3-report.md @@ -154,7 +154,10 @@ constant-time. Gateway не маршрутизирует `/api/v1/internal/**`, По результатам последнего прогона (2026-10-27) перечисленные наборы прошли с `0 failures / 0 errors`. -## 6. Известные пробелы +## 6. Известные пробелы (исторические, на момент этапа 3) + +> Список ниже сохранён как история; актуальный статус — в разделе 7 и +> [16-stage6-report.md](16-stage6-report.md). - **Контент курса.** URL материалов вида `/learn/course/{course_no}/...`, проверка `chapters.course_id/audience`, наследуемая видимость активов @@ -170,3 +173,21 @@ constant-time. Gateway не маршрутизирует `/api/v1/internal/**`, страницы профиля — преподавательская и админская. - **Аудит скачиваний.** Скачивание попыток пока не пишется в аудит; это этап 6. + +## 7. Обновление после этапа 6 + +- **Права преподавателя на запись** закрыты: преподаватель своего курса + управляет лабораторными/уровнями/вариантами, выставляет оценки и + комментарии, публикует их, раздаёт варианты, назначает/запускает защиты, + привязывает группы и авторит материалы курса. Ведомость переиспользует + `/admin/groups/{groupId}?courseId=...` без админских контролов; сервисы + проверяют курс и отклоняют чужие лабы/студентов/группы. +- **Имена групп в кабинете преподавателя** разрешены: страница курса + показывает название, при недоступности — id. +- **Контент курса и активы** закрыты на этапе 5: URL + `/learn/course/{course_no}/...`, проверка `chapters.course_id/audience`, + наследуемая видимость активов; на этапе 6 добавлено авторство материалов + преподавателем (`/teacher/courses/{id}/content`), глобальные материалы + остаются admin-only. +- **Аудит скачиваний** реализован: `identity.audit_events`, внутренний + `POST /api/v1/internal/audit` и админский `/admin/audit`. diff --git a/docs/15-stage5-course-materials.md b/docs/15-stage5-course-materials.md index ae590ad..4eb6e8f 100644 --- a/docs/15-stage5-course-materials.md +++ b/docs/15-stage5-course-materials.md @@ -53,3 +53,13 @@ публичным. Нужна инвентаризация таких файлов и привязка к статье либо удаление; до этого поведение сохраняется осознанно, чтобы не ломать старые публичные ссылки. + +## Авторство материалов преподавателем (добавлено на этапе 6) + +Преподаватель своего курса редактирует главы, разделы и статьи на +`/teacher/courses/{id}/content`: создание/переименование/удаление, markdown- +редактор с предпросмотром, публикация/снятие с публикации. API — +`/api/v1/courses/{courseId}/content/**`; любая изменяемая сущность проверяется +на принадлежность курсу. Глобальные материалы (`course_id IS NULL`, +аудитории `ALL/STUDENTS/TEACHERS`) по-прежнему создаёт и публикует только +админ. diff --git a/docs/16-stage6-report.md b/docs/16-stage6-report.md index 0c79db8..b6ae022 100644 --- a/docs/16-stage6-report.md +++ b/docs/16-stage6-report.md @@ -2,23 +2,22 @@ **Дата:** 2026-10-27. **Ветка:** `feature/groups-courses`. -**Статус:** проверка этапа 6 выполнена; этап 6 закрыт **не полностью**. -Миграции, security-матрица, upgrade-тест, видимость активов и deploy-runbook -проверены. Открытыми остаются права преподавателя на запись, аудит и CI — см. -раздел 4. +**Статус:** этап 6 закрыт. Остаточные риски перечислены в разделе 4 и не +блокируют передачу. -## 1. Итоговый объём этапов 0–5 +## 1. Итоговый объём этапов 0–6 | Этап | Что реализовано | Отчёт | |---|---|---| -| 0 | Схема и контракты: identity `V4–V9`, academic `V6–V12`, progress `V13`; preflight-скрипты; ADR; контракт профиля студента курса | [10](10-stage0-report.md) | +| 0 | Схема и контракты: identity `V4–V10` (`V10` — аудит, добавлена на этапе 6), academic `V6–V12`, progress `V13`; preflight-скрипты; ADR; контракт профиля студента курса | [10](10-stage0-report.md) | | 1 | Роли `ADMIN/TEACHER/STUDENT`, группы-когорты и парсер имени, статусы студентов и аккаунтов, `membership_history`, импорт v2 | [11](11-stage1-report.md) | | 2 | Курсы, `course_no` для браузерных URL, история преподавателей и групп, bootstrap курса C, привязка лабораторных к курсу, копирование лабораторных, админ-UI | [12](12-stage2-report.md) | | 3 | Аккаунты преподавателей, курсовая область видимости, внутренние API identity с `LMS_INTERNAL_TOKEN`, кабинет преподавателя, профиль студента курса и скачивание всех попыток | [13](13-stage3-report.md) | | 4 | Повышение (dry-run/apply/rollback, планировщик 31 августа МСК), академотпуск, отчисление, выпуск, архив/восстановление, усиленное удаление, блокировка входа | [14](14-stage4-report.md) | | 5 | Курсовые материалы `/learn/course/{course_no}/...`, публичные глобальные материалы `ALL`, скрытые материалы для преподавателя, перепроверка статуса web-сессии | [15](15-stage5-course-materials.md) | +| 6 | Права преподавателя на запись (лабораторные, ведомость, группы, материалы), центральный аудит `identity.audit_events`, CI merge gate, hardening и deploy-runbook | этот отчёт | -Ключевые инварианты, действующие после этапов 0–5: +Ключевые инварианты, действующие после этапов 0–6: - все лабораторные принадлежат курсу; существующие лабораторные, главы и активные группы перенесены в курс C; @@ -29,9 +28,15 @@ - активы наследуют видимость статьи → раздела → главы: курсовые требуют доступа к курсу, глобальные — аудитории, а старые файлы без привязки остаются публичными (см. раздел 4); +- преподаватель своего курса может писать: лабораторные/уровни/варианты, + ведомость, привязка групп и авторские материалы курса; чужие курс, лаба, + студент и группа отклоняются на сервисах; - вход закрыт для отчисленных, выпущенных и архивных студентов, а также для архивных аккаунтов; web дополнительно перепроверяет статус; -- повышение и жизненный цикл пишут историю; применение повышения идемпотентно. +- повышение и жизненный цикл пишут историю; применение повышения идемпотентно; +- чувствительные действия пишутся в центральный `identity.audit_events`, + админ смотрит журнал на `/admin/audit`; +- CI на каждый push/MR прогоняет `test ktlintCheck detekt`. ## 2. Проверка @@ -40,15 +45,11 @@ Выполнено 2026-10-27: ```bash -./gradlew test ktlintCheck detekt +./gradlew --no-daemon test ktlintCheck detekt ``` Результат: `BUILD SUCCESSFUL`, код возврата `0`, все модули зелёные. -Счётчики включают новые тесты видимости активов: `academic-service` +9 -(`ContentAssetAccessIntegrationTest`, импорт активов), `web` +2 -(`ApiClientContentAssetTest`). Итог — **183 теста, 0 падений**. - Для получения актуальных счётчиков тесты запущены принудительно: ```bash @@ -57,16 +58,17 @@ | Модуль | Тестов | Failures | Errors | Skipped | |---|---:|---:|---:|---:| -| identity-service | 62 | 0 | 0 | 0 | -| academic-service | 62 | 0 | 0 | 0 | -| web | 42 | 0 | 0 | 0 | -| progress-service | 12 | 0 | 0 | 0 | +| identity-service | 72 | 0 | 0 | 0 | +| academic-service | 88 | 0 | 0 | 0 | +| progress-service | 33 | 0 | 0 | 0 | +| web | 69 | 0 | 0 | 0 | | markdown | 5 | 0 | 0 | 0 | -| **Итого** | **183** | **0** | **0** | **0** | +| **Итого** | **267** | **0** | **0** | **0** | `ktlintCheck` и `detekt` прошли без замечаний. Отдельных unit-тестов у gateway и `common-http` в текущем дереве нет; маршрутизация gateway проверена -конфигурацией и security-матрицей ниже. +конфигурацией и security-матрицей ниже. Тот же набор команд выполняет +`.gitlab-ci.yml`. ### 2.2. Upgrade-тест миграций @@ -79,7 +81,7 @@ (`ПИ-11БО`, `ПИ-21БО`), зачисления, семестр, лабораторная, глава/раздел/ статья/актив, оценка, комментарий и попытка сдачи. 3. Поверх master применены миграции ветки: - - identity `V4–V9`; + - identity `V4–V10`; - academic `V6–V12`; - progress `V13`. 4. Выполнены postflight-запросы и повторное применение `V13`. @@ -90,7 +92,8 @@ (`ПИ-11БО` → поток `ПИ`, курс 1, номер 1, `БО`, год набора 2026; `ПИ-21БО` → курс 2, год 2025); статус `ACTIVE`; созданы `promotion_runs`, `promotion_run_items`; зачисления перенесены в `membership_history` - действием `ENROLL`; добавлен `accounts.status`. + действием `ENROLL`; добавлен `accounts.status`; `V10` создаёт + `identity.audit_events` без внешних ключей и индексы по `occurred_at`/`action`. - **academic:** добавлены `labs.course_id`, `chapters.course_id/audience`, индексы `labs_number_course_uidx`, `chapters_slug_global_uidx`, `chapters_slug_course_uidx`, `article_assets_article_idx`, @@ -116,8 +119,9 @@ ### 2.3. Security-проверки -Проверено автоматическими тестами (полный прогон 183 теста) и разбором -конфигурации gateway. +Проверено автоматическими тестами (полный прогон 267 тестов) и разбором +конфигурации gateway. Запись преподавателя дополнительно покрыта +negative-тестами «свой/чужой курс». | Сценарий | Ожидание | Факт | |---|---|---| @@ -131,8 +135,11 @@ | Глобальный `STUDENTS`-актив → преподаватель | `403` | `403`; студент получает `200` | | Legacy-актив без связи со статьёй | публично | `200`; остаточный риск, раздел 4 | | Админ → курс, профиль, скачивание | полный доступ | доступ к любому курсу и студенту, скачивание любой попытки курса | -| Преподаватель своего курса | курс, группы, профиль, материалы | видит опубликованные и скрытые материалы, всех студентов активной группы курса, неопубликованные оценки и все попытки | -| Преподаватель чужого курса | `403` | `403` на материалы, курс по `course_no`, состав группы и профиль | +| Преподаватель своего курса → чтение | курс, группы, профиль, материалы | видит опубликованные и скрытые материалы, всех студентов активной группы курса, неопубликованные оценки и все попытки | +| Преподаватель своего курса → запись | лабы/уровни/варианты, ведомость, группы, материалы | оценки/комментарии, публикация, уровни/варианты и раздача, защиты (код/reset), привязка групп, авторство материалов; проверки в progress `CourseScopeService` и academic course/content authoring | +| Преподаватель чужого курса | `403` | `403` на чтение и запись: материалы, курс по `course_no`, состав группы, профиль, лабы, оценки, группы | +| Аудит → админ | чтение журнала | `GET /api/v1/audit?limit=&action=` и `/admin/audit` доступны только `ADMIN` | +| Аудит → запись из сервисов | только по токену | `POST /api/v1/internal/audit` с `X-Internal-Token`; без/неверный токен — `403` | | Студент → свой профиль | только своё и опубликованное | скрытые оценки/комментарии отсутствуют; чужой `studentId` — `403` | | Студент → чужой курс/группа | `403` | доступ только к курсам активной группы; архивный курс — `403` | | Чужая сдача / лаба другого курса | `403` | скачивание отклоняется; своя попытка курса скачивается ZIP | @@ -142,7 +149,8 @@ | Web-сессия | быстрый отзыв доступа | статус перепроверяется не реже чем раз в 45 секунд; `active=false` или `401/403` инвалидируют сессию; недоступность identity не разлогинивает всех | Остаточные риски перечислены в разделе 4: legacy-активы без привязки, -права преподавателя на запись и 30-дневный JWT для прямого API. +30-дневный JWT для прямого API, ведущие нули номера группы и отсутствие +пагинации/N+1 на больших объёмах. ## 3. Чек-лист деплоя @@ -158,14 +166,18 @@ Там же задан `LMS_DB_PASSWORD`; `.env.prod` не коммитится. - [ ] Проверить, что на сервере нет незакоммиченных изменений и выполнен `git pull` нужной ветки/тега. +- [ ] **CI зелёный.** `.gitlab-ci.yml` выполняет + `./gradlew --no-daemon test ktlintCheck detekt`. Runner должен разрешать + privileged-сервисы `docker:dind`: Testcontainers поднимает PostgreSQL 18 + через Docker-in-Docker. ### 3.2. Порядок выкладки 1. **Flyway.** `./deploy/deploy.sh up` собирает образы и запускает `postgres → identity → academic → progress → gateway → web`. Миграции применяются автоматически при старте сервисов: - identity `V4–V9`, academic `V6–V12`, progress `V13`. - Отдельный ручной запуск Flyway не нужен. + identity `V4–V10` (`V10` — `identity.audit_events`), academic `V6–V12`, + progress `V13`. Отдельный ручной запуск Flyway не нужен. 2. **Bootstrap курса C.** После входа админа открыть `/admin/courses`. Пока курсов нет, показан блок первичной настройки. Bootstrap идемпотентно создаёт/находит курс «Промышленное программирование на языке C», @@ -181,7 +193,8 @@ - `chapters_without_course = 0` (если все текущие материалы курсовые); - ровно один курс C; - `comments_without_author` соответствует старым комментариям; - - группы без полного разбора отсутствуют. + - группы без полного разбора отсутствуют; + - таблица `identity.audit_events` существует (`V10`). 4. **V10 → V13 — только dev/staging.** На production/master записи `progress V10__profile_authors.sql` нет, ничего удалять не нужно. Не путать с academic `V10__course_title_unique.sql` — он относится к другой схеме и @@ -204,8 +217,12 @@ отдаёт `UP`. - [ ] **Админ:** вход, `/admin/courses` (курс C и счётчики), `/admin/groups`, `/admin/labs`, `/admin/teachers`, `/admin/promotion`. -- [ ] **Преподаватель:** вход, `/teacher`, карточка своего курса, состав группы, - профиль студента, скачивание попытки, курсовые материалы. +- [ ] **Преподаватель:** вход, `/teacher`, карточка своего курса (имена групп, + привязка/отвязка группы по `available-groups`), ведомость + `/admin/groups/{groupId}?courseId=...` без админских контролов, оценка и + публикация, уровень/вариант и раздача, защита (код/reset), профиль студента, + скачивание попытки и ZIP группы, авторство материалов + `/teacher/courses/{id}/content`. - [ ] **Студент:** вход, дашборд, лабораторная, сдача/история, курсовые материалы; видны только опубликованные оценки и комментарии. - [ ] **Материалы курса:** админ и преподаватель видят скрытые статьи, студент — @@ -221,6 +238,10 @@ - [ ] **Повышение:** `/admin/promotion` строит dry-run за год, данные не меняются; применение — только после проверки плана; откат доступен для завершённого запуска. На production не применять план «на всякий случай». +- [ ] **Аудит:** `/admin/audit` открывается у админа, показывает события + (например, `PROFILE_VIEW`, `SUBMISSION_DOWNLOAD`, `GRADE_SET`, + `CONTENT_*`, `TEACHER_*`) с фильтром по действию и временем МСК; + преподаватель/студент получают `403`. - [ ] Проверить логи `web`, `gateway`, `identity`, `academic`, `progress` на `ERROR`/`WARN` после smoke. @@ -233,59 +254,39 @@ остаются публичными. Нужна инвентаризация и привязка либо удаление таких файлов; до этого fallback сохраняется, чтобы не ломать старые ссылки. Детали — в [15](15-stage5-course-materials.md). -2. **Права преподавателя в основном read-only.** Кабинет преподавателя даёт - обзор курса, групп, лабораторных и профилей. Авторство лабораторных и - материалов, распределение вариантов, выставление оценок, комментарии, - коды защиты и действия защиты остаются admin-only в UI/API. ТЗ - ([07](07-groups-courses.md), раздел ролей) ожидает преподавательский - self-service; это главный функциональный разрыв. -3. **Названия групп в кабинете преподавателя.** На странице курса группа - показывается UUID: academic отдаёт `groupId`, а имя группы не входит в - контракт карточки курса. Нужен batch-запрос имён в identity или расширение - DTO. Тест `TeacherGroupTemplateTest` фиксирует fallback на id. -4. **Аудит чувствительных действий.** Есть `membership_history` (переводы, - академ, отчисление, архив, повышение) и журнал `promotion_runs/items`. - Отдельного audit-log для скачиваний файлов, просмотра профилей, - назначений преподавателей и публикаций нет. Нужна таблица `audit_events` - и запись из сервисов/BFF. -5. **Старый формат импорта не поддерживается.** Импорт принимает только +2. **Старый формат импорта не поддерживается.** Импорт принимает только шаблон v2 (поток/курс/номер/степень/год/ФИО/№/база). Ведущие нули номера группы нормализуются к числу (`01` → `1`), поэтому `ПИ-01БО` и `ПИ-1БО` неразличимы; для разных когорт нужен строковый номер. Ограничение зафиксировано в [11](11-stage1-report.md). -6. **JWT живёт до 30 дней.** Web-сессия перепроверяет статус каждые 45 секунд +3. **JWT живёт до 30 дней.** Web-сессия перепроверяет статус каждые 45 секунд и закрывает UI при отчислении/архиве, но уже выданный JWT для прямого вызова API действует до истечения. Нужны короткий TTL + refresh, denylist/revocation или проверка статуса на каждом API-вызове. -7. **Нет CI.** В репозитории нет pipeline (`.github` и аналогов); гейтом - служит локальный `./gradlew test ktlintCheck detekt`. Это не защищает - ветку от merge без прогона и не проверяет upgrade-тест автоматически. +4. **Пагинация и N+1.** Журнал аудита ограничен `limit` (максимум 500), но + без offset/keyset; часть списков групп, материалов и ведомости отдаётся + целиком. На больших курсах нужны пагинация и пакетные выборки вместо + запросов в цикле. ## 5. Рекомендуемые следующие шаги Приоритет — сверху вниз: -1. **Права преподавателя на запись.** Дать преподавателю своего курса - authoring лабораторных/материалов, распределение вариантов, выставление - оценок и комментариев, коды и действия защиты; закрыть negative-тестами - «свой/чужой курс». Это закрывает основной разрыв с ТЗ. -2. **Инвентаризация legacy-активов.** Найти файлы без строк в `article_assets`, +1. **Инвентаризация legacy-активов.** Найти файлы без строк в `article_assets`, привязать их к статьям или удалить; после этого можно убрать публичный fallback для несвязанных файлов. -3. **UI авторства контента.** Выбор курса и аудитории при создании глав/ - статей, чтобы преподаватель и админ не работали только через импорт; - сейчас глобальные главы создаются с `ALL`, курсовые — без аудитории. -4. **Аудит.** Ввести `audit_events` для скачиваний, профилей, назначений, - публикаций, повышения и жизненного цикла; экран просмотра для админа. -5. **CI.** Пайплайн на каждый push/MR: `test ktlintCheck detekt`, отдельный - шаг upgrade-теста миграций на PostgreSQL 18, сборка `bootJar`; запрет - merge при красном статусе. +2. **JWT/refresh.** Короткий TTL access-токена + refresh, denylist/revocation + или проверка статуса на каждом API-вызове; закрыть остаточный риск + отчисленного/архивного пользователя с уже выданным токеном. +3. **Пагинация и устранение N+1.** Keyset/offset для аудита, групп, материалов + и ведомости; пакетные выборки профилей, комментариев и сдач. +4. **Внешняя интеграция «Контингент»/next.uniyar.ac.ru.** Отдельный + anti-corruption слой, идемпотентная синхронизация, журнал и отчёт дрифта; + внешние идентификаторы добавляются миграцией. ## 6. Итог -Этапы 0–5 реализованы и проверены. Security-матрица, upgrade-тест миграций, -видимость активов и deploy-runbook этапа 6 зелёные; отчёт и чек-лист передачи -готовы. Этап 6 **нельзя считать полностью завершённым** до закрытия прав -преподавателя и CI. Видимость связанных активов реализована; публичными -остаются только старые файлы без привязки к статье (см. раздел 4), а -преподаватели пока работают в режиме просмотра. +Этап 6 закрыт: права преподавателя на запись, аудит, CI, security-матрица, +upgrade-тест миграций, видимость активов и deploy-runbook проверены. +Остаточные риски (legacy-активы, 30-дневный JWT, ведущие нули номера группы, +пагинация/N+1) задокументированы в разделе 4 и не блокируют передачу. diff --git a/docs/README.md b/docs/README.md index faf06c4..846cc7f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -23,7 +23,7 @@ Preview Mermaid Support) и в IntelliJ IDEA (плагин Mermaid). | [13-stage3-report.md](13-stage3-report.md) | Отчёт этапа 3: преподаватели, доступ к курсам, внутренние API, кабинет и профиль студента курса | | [14-stage4-report.md](14-stage4-report.md) | Отчёт этапа 4: повышение, академотпуск, отчисление, архив, восстановление и удаление | | [15-stage5-course-materials.md](15-stage5-course-materials.md) | Отчёт этапа 5: курсовые материалы, публичные `ALL`-главы, видимость активов и legacy-файлы без привязки | -| [16-stage6-report.md](16-stage6-report.md) | Отчёт этапа 6: проверка, upgrade-тест миграций, security-матрица, чек-лист деплоя и оставшиеся пробелы | +| [16-stage6-report.md](16-stage6-report.md) | Отчёт этапа 6: права преподавателя на запись, аудит, CI, upgrade-тест миграций, security-матрица, чек-лист деплоя и остаточные риски | | [ADR 0001](adr/0001-groups-courses-foundation.md) | Ключевые архитектурные решения: курс C, миграции, доступ, статусы, повышение | ## Кратко о системе