You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
8 поисковых угла × верификация каждого кандидата отдельным агентом. Ниже — 8 подтверждённых находок, от самой серьёзной к менее серьёзной.
1. FORWARDED_ALLOW_IPS — фиктивный контроль безопасности
docker-compose.example.yaml, .env.example
Переменная задокументирована как решение для корректного rate-limiting за reverse proxy, но нигде не используется в коде (grep -rn FORWARDED_ALLOW_IPS --include=*.py — 0 совпадений). Ни ProxyHeadersMiddleware, ни --forwarded-allow-ips не подключены ни в main.py, ни в backend.Dockerfile (CMD не менялся: uvicorn main:app --host 0.0.0.0 --port 8000).
Следствие:Limiter(key_func=get_remote_address) за Caddy будет читать IP самого прокси для всех запросов — все студенты попадут в один и тот же rate-limit bucket на /join/*/start (10/мин) и /join/callback (20/мин). Один активный студент исчерпает лимит для всей группы. ProxyHeadersMiddleware используется только в tests/test_join_rate_limit.py, оборачивая тестовое приложение, а не main.app.
2. lab_id разрешается по-разному в /join и /grade
main.py:340 (get_join_lab_config)
/join ищет лабу по точному строковому ключу YAML, а /grade (parse_lab_id) нормализует через int(...). На реальных конфигах courses/operating-systems-2025.yaml / 2026.yaml есть одновременно лабы с ключами "01" и "1". Тест test_join_info_uses_exact_lab_key подтверждает: /join/{course}/1 → 404, /join/{course}/01 → 200. Но для /grade оба значения lab_id нормализуются в одно и то же число — то есть parse_lab_id("01") и parse_lab_id("1") совпадают, и grade_lab не может отличить ЛР0.1 от ЛР1 в этих конфигах: запрос с lab_id="01" может быть тихо оценён по конфигу другой лабы (другой github-prefix, другие файлы/CI).
3. Race-recovery в provision() не проверяет происхождение репозитория
grading/repository_provisioner.py:300
При 409/422 от generate-from-template API код перепроверяет только repository_exists() (обычный GET, 200/404), но не то, что найденный репозиторий действительно создан из нужного шаблона. Имя репозитория — f"{github_prefix}-{join_key}", а github_prefix — свободный YAML-текст без проверки уникальности между курсами.
Следствие: если два курса/потока в одной GitHub-организации используют одинаковый github-prefix, либо студент заранее вручную создал репозиторий с таким именем, provision() посчитает это "гонкой", пропустит создание и выдаст доступ к чужому/несвязанному репозиторию. Такой сценарий не покрыт тестами.
4. start_github_oauth не перехватывает HTTPException
main.py:591
Вызов get_join_lab_config(course_id, lab_id) в start_github_oauth не обёрнут в try/except, в отличие от /join/callback, который для того же вызова перехватывает HTTPException и делает дружелюбный редирект на страницу ошибки.
Следствие: переход по /join/{course}/{lab}/start с несуществующим/опечатанным course_id/lab_id (например, старая закладка после переименования курса) вернёт голый JSON {"detail": "..."} вместо оформленной страницы ошибки JoinLab. Не покрыто тестами.
5. Дублирующийся GitHub-клиент без timeout и обработки rate-limit в старом коде
grading/github_client.py
PR добавляет новый GitHubRepositoryClient с явным timeout (REQUEST_TIMEOUT = (3.05, 15)) и детекцией rate-limit (X-RateLimit-Remaining, Retry-After, 429/403), но не переносит эти же исправления в существующий GitHubClient, который использует grade_lab.
Следствие:GitHubClient.get_job_logs/get_check_runs/etc. без timeout могут повиснуть на медленном ответе GitHub, а любой не-200 статус (включая 403/429 rate-limit) трактуется как "данных нет" — при исчерпании лимита GitHub grading может молча вернуть неверный результат вместо явной ошибки.
6. Удаление и пересоздание ещё не принятого приглашения
Если найдено pending-приглашение, код безусловно удаляет его и создаёт заново (подтверждено тестом test_pending_invitation_is_deleted_and_sent_again — DELETE+PUT), хотя permission во втором вызове всегда одинаковый (push) — то есть пересоздание не несёт функциональной пользы.
Следствие: двойной клик по "Присоединиться" или повторная загрузка страницы во время обработки первого запроса удалит и заново создаст приглашение — вероятно, повторное письмо от GitHub и инвалидация ссылки из первого письма, если студент уже открыл её в другой вкладке.
7. Join-flow не пишет ничего в Google Sheets
main.py:630
В отличие от register_student и grade_lab, которые читают/пишут в таблицу — систему истины проекта, — весь OAuth join-flow нигде не обращается к Sheets. Это осознанное ограничение объёма фичи (по документации — дополнительный путь, не замена регистрации), но teacher не увидит в таблице, кто присоединился через OAuth, и если финальный редирект после успешного provisioning потеряется (закрытая вкладка, сбой сети), нигде не останется записи об этом.
8. 404 от generate-from-template неоднозначен
grading/repository_provisioner.py:226
Любой 404 от GitHub при создании репозитория из шаблона трактуется как template_unavailable, хотя GitHub возвращает одинаковый generic 404 как при отсутствующем шаблоне, так и при нехватке прав токена на приватный шаблон/организацию — различить эти случаи по ответу API невозможно (ограничение самого GitHub API), но результат может ввести преподавателя в заблуждение при отладке (похоже на опечатку в конфиге, хотя на деле проблема в правах GITHUB_TOKEN).
Проверено и опровергнуто (не включено выше): несовпадение регистра username между join/grade-потоками (обе ветки одинаково не приводят к нижнему регистру), отсутствие catch-all в github_oauth_callback (все исключения перехватываются типизированными классами, подтверждено тестами), глобальные OAuth env vars вместо per-course конфига (OAuth используется только для идентификации через read:user, привилегированные действия уже используют per-course organization + существующий GITHUB_TOKEN), отсутствие обновления списка env vars в CLAUDE.md (нет явного правила, требующего это).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Выполнено задание #46