Skip to content

Добавить выдачу репозиториев через GitHub OAuth - #48

Open
nolick6667 wants to merge 1 commit into
mainfrom
feature/public-join-github-oauth
Open

Добавить выдачу репозиториев через GitHub OAuth#48
nolick6667 wants to merge 1 commit into
mainfrom
feature/public-join-github-oauth

Conversation

@nolick6667

@nolick6667 nolick6667 commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Выполнено задание #46

@markpolyak

Copy link
Copy Markdown
Owner

Код-ревью (автоматическое, high effort)

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. Удаление и пересоздание ещё не принятого приглашения

grading/repository_provisioner.py:341 (_ensure_access)

Если найдено 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 (нет явного правила, требующего это).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants