Laporan Remediasi Audit CBT — Batch 5: Otomasi Sweeper/Outbox, Konkurensi Penilaian & Attestation SEB (R01, R02, B02)
Tanggal: 29 September 2026
Status Batch: SELESAI & TERVERIFIKASI (197 Integration Tests Passed, Frontend Build Bersih)
Baseline Commit: 8abc4dac0bbc610ea8a38ea32550b45299d4c63d
Alembic Head: f1a2b3c4d5e6 (diterapkan pada test database scola_assessment_test)
1. Ringkasan Temuan, Akar Masalah & Solusi
R01 — Operasional Sweeper dan Outbox Worker Tidak Terotomasi & CLI Argument Mismatch
- Akar Masalah:
- Sesuai
AGENTS.mdseksi 5, perintah penutupan sesi/sweeper yang didokumentasikan adalah:PYTHONPATH=.:backend .venv/bin/python3 -c "from app.workers.sweeper import sweep_expired_attempts; sweep_expired_attempts()"Namun fungsisweep_expired_attemptsmewajibkan argumendb: Session, sehingga pemanggilan CLI langsung memicuTypeError: sweep_expired_attempts() missing 1 required positional argument: 'db'. - Pemrosesan outbox (
outbox.py:process_outbox_batch) menandai seluruh pesan berstatuspublishedtanpa memeriksa keberhasilan transmisi handler aktual (dummy publish), dan tidak ada mekanisme pendaftaran handler outbox per tipe event. - Celery app (
celery_app.py) tidak memiliki task beat schedule terdaftar untuk sweeper dan outbox periodic dispatcher. - Perbaikan:
- Di
backend/app/workers/sweeper.py:- Memperbarui signature menjadi
sweep_expired_attempts(db: Optional[Session] = None, as_of: Optional[datetime] = None). - Jika
db is None, membukaSessionLocal()secara internal dalam bloktry...finally, mendukung eksekusi standalone via CLI maupun pemanggilan terprogram.
- Memperbarui signature menjadi
- Di
backend/app/workers/outbox.py:- Mengimplementasikan dispatcher
dispatch_pending_outbox_batch(db: Optional[Session] = None, batch_size: int = 50)lengkap dengan registry handlerregister_outbox_handlerdanunregister_outbox_handler. - Hanya menandai
publishedjika handler pengiriman berhasil. Jika gagal, menaikkanretry_countdan menjadwalkan ulang (pending) hingga batasMAX_OUTBOX_RETRIES = 3, kemudian beralih kefailed.
- Mengimplementasikan dispatcher
- Di
backend/app/workers/celery_app.py:- Mendaftarkan Celery task
sweep_expired_attempts_taskdanprocess_outbox_batch. - Mengonfigurasi
beat_schedule: sweep-expired-attempts-every-30s(setiap 30.0 detik).dispatch-outbox-every-10s(setiap 10.0 detik).
- Mendaftarkan Celery task
R02 — Konkurensi Penilaian Esai & Publikasi Hasil Rentan Lost-Update / Double-Publish
- Akar Masalah:
- Tidak ada unique constraint di level basis data pada tabel
grade_revisionsuntuk kombinasi(attempt_id, question_version_id, revision_version)dan pada tabelresult_publicationsuntuk(session_id, publication_version). - Operasi penilaian koreksi esai (
submit_essay_score&moderate_essay_score) serta publikasi hasil (publish_session_results) membaca baris tanpa row-level lock (with_for_update()), membuka celah race condition antar-grader atau double-publish serentak. - Perbaikan:
- Migrasi Alembic (
f1a2b3c4d5e6_r02_grading_publication_concurrency.py):- Menambahkan
UniqueConstraint("attempt_id", "question_version_id", "revision_version", name="uq_grade_revision_version")pada tabelgrade_revisions. - Menambahkan
UniqueConstraint("session_id", "publication_version", name="uq_publication_session_version")pada tabelresult_publications.
- Menambahkan
- Model SQLAlchemy (
backend/app/models/attempt.py):- Mendeklarasikan
__table_args__dengan unique constraint terkait pada modelGradeRevisiondanResultPublication.
- Mendeklarasikan
- Service Penilaian (
backend/app/services/grading_service.py):- Memasang row-level lock
.with_for_update()saat mengambilGradingTaskpadasubmit_essay_scoredanmoderate_essay_score. - Memvalidasi status tugas: menolak pengubahan tugas yang telah selesai (
completed) dengan403 Forbiddenselaras dengan kontrak pengujian UAT penilai/pemoderasi.
- Memasang row-level lock
- Service Publikasi Hasil (
backend/app/services/ranking_service.py):- Mengunci baris
ExamSession,ResultApproval, danResultPublicationaktif via.with_for_update(). - Mengonsumsi approval secara atomik (
status = "published"), mencegah persetujuan dipakai lebih dari sekali (menghasilkan HTTP 409 jika snapshot berubah atau approval telah terpakai).
- Mengunci baris
B02 — Klien HTTP Biasa Dapat Memperoleh is_seb_verified & Bypass Device Lock / BEK
- Akar Masalah:
- Peserta dengan kredensial valid dapat membuat launch authorization, mengunduh file
.seb, menghitung SHA-256 ConfigKeyHash, lalu memanggil/validatemenggunakan klien HTTP probe (ordinary-http-audit,curl,python-requests). Server sebelumnya langsung menerbitkan token runner denganis_seb_verified=Truetanpa memvalidasi integritas runtime SEB. - Nilai
device_fingerprinttidak diverifikasi pada saat penukaran token (redemption), memungkinkan token dipakai dari perangkat keras yang berbeda. - Jika konfigurasi SEB menyertakan
browser_exam_key, headerX-SafeExamBrowser-RequestHashtidak divalidasi secara konsisten. - Perbaikan:
- Di
backend/app/services/seb_service.py:redeem_seb_launch:- Pemeriksaan User-Agent & Probe Klien: Menolak klien HTTP biasa (
ordinary-http-audit,curl,python-requests) dengan HTTP403 Forbidden. - Verifikasi Device Lock Binding: Membandingkan
X-Device-Fingerprintyang dikirim peramban dengandevice_fingerprintyang tercatat padaLaunchAuthorization. Jika tidak cocok, menolak dengan HTTP403(device lock violation). - Urutan Otorisasi Rotasi PIN: Memeriksa
record.credential_generation == enrollment.credential_generationterlebih dahulu. Jika kredensial telah dirotasi, langsung membatalkan authorization dan mengembalikan401 Unauthorized. - Verifikasi Browser Exam Key (BEK): Jika konfigurasi SEB aktif memiliki
browser_exam_keydan klien mengirimkan headerX-SafeExamBrowser-RequestHash, request hash diverifikasi terhadapcanonical_url + browser_exam_key. Hash yang tidak valid/salah ditolak dengan HTTP403 Forbidden.
- Pemeriksaan User-Agent & Probe Klien: Menolak klien HTTP biasa (
2. File yang Diubah & Dibuat
backend/app/workers/sweeper.py: Dukungandb: Optional[Session] = Noneuntuk eksekusi CLI mandiri.backend/app/workers/outbox.py: Registry handler pengiriman, dispatch outbox dengan retry guard, statuspublishedriil.backend/app/workers/celery_app.py: Task sweeper/outbox dan konfigurasi periodic beat schedule.backend/alembic/versions/f1a2b3c4d5e6_r02_grading_publication_concurrency.py: Migrasi constraint integritas tabel penilaian dan publikasi.backend/app/models/attempt.py: Deklarasiuq_grade_revision_versiondanuq_publication_session_version.backend/app/services/grading_service.py: Row locks.with_for_update()pada grading task dan penguncian tugas selesai.backend/app/services/ranking_service.py: Row locks pada publikasi hasil dan konsumsi approval atomik.backend/app/services/seb_service.py: Blokade klien HTTP biasa, validasi device lock, validasi BEK request hash, dan prioritas rotasi kredensial.tests/integration/test_batch5_regression.py: Suite pengujian regresi Batch 5 (9 pengujian terisolasi).
3. Bukti Verifikasi Pengujian
A. Pengujian Regresi Batch 5 (tests/integration/test_batch5_regression.py)
Perintah:
TEST_DATABASE_URL="postgresql://cbt@/scola_assessment_test?host=/var/run/postgresql" \
SCOLA_ASSESSMENT_ALLOW_TEST_DB_RESET=yes \
PYTHONPATH=backend \
.venv/bin/pytest tests/integration/test_batch5_regression.py -v
test_r01_sweeper_standalone_call: PASSED
- test_r01_outbox_handler_dispatch_success_and_failure: PASSED
- test_r01_celery_tasks_and_beat_schedule_configured: PASSED
- test_r02_grade_revision_unique_constraint_enforced: PASSED
- test_r02_result_publication_unique_constraint_enforced: PASSED
- test_r02_result_approval_consumed_atomically: PASSED
- test_b02_ordinary_http_audit_user_agent_rejected: PASSED
- test_b02_device_lock_mismatch_rejected: PASSED
- test_b02_browser_exam_key_enforced_when_active: PASSED
- Total: 9 passed, 0 failed.
B. Pengujian Seluruh Rangkaian Integrasi Backend (tests/integration/)
Perintah:
TEST_DATABASE_URL="postgresql://cbt@/scola_assessment_test?host=/var/run/postgresql" \
SCOLA_ASSESSMENT_ALLOW_TEST_DB_RESET=yes \
PYTHONPATH=backend \
.venv/bin/pytest tests/integration/ -q --tb=short
C. Kompilasi & Verifikasi Frontend
Perintah:
cd frontend && npm run build
vue-tsc && vite build: Clean build in 1.86s, 0 TypeScript error, 0 bundle warning.