Lewati ke isi

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.md seksi 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 fungsi sweep_expired_attempts mewajibkan argumen db: Session, sehingga pemanggilan CLI langsung memicu TypeError: sweep_expired_attempts() missing 1 required positional argument: 'db'.
  • Pemrosesan outbox (outbox.py:process_outbox_batch) menandai seluruh pesan berstatus published tanpa 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, membuka SessionLocal() secara internal dalam blok try...finally, mendukung eksekusi standalone via CLI maupun pemanggilan terprogram.
  • Di backend/app/workers/outbox.py:
    • Mengimplementasikan dispatcher dispatch_pending_outbox_batch(db: Optional[Session] = None, batch_size: int = 50) lengkap dengan registry handler register_outbox_handler dan unregister_outbox_handler.
    • Hanya menandai published jika handler pengiriman berhasil. Jika gagal, menaikkan retry_count dan menjadwalkan ulang (pending) hingga batas MAX_OUTBOX_RETRIES = 3, kemudian beralih ke failed.
  • Di backend/app/workers/celery_app.py:
    • Mendaftarkan Celery task sweep_expired_attempts_task dan process_outbox_batch.
    • Mengonfigurasi beat_schedule:
    • sweep-expired-attempts-every-30s (setiap 30.0 detik).
    • dispatch-outbox-every-10s (setiap 10.0 detik).

R02 — Konkurensi Penilaian Esai & Publikasi Hasil Rentan Lost-Update / Double-Publish

  • Akar Masalah:
  • Tidak ada unique constraint di level basis data pada tabel grade_revisions untuk kombinasi (attempt_id, question_version_id, revision_version) dan pada tabel result_publications untuk (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 tabel grade_revisions.
    • Menambahkan UniqueConstraint("session_id", "publication_version", name="uq_publication_session_version") pada tabel result_publications.
  • Model SQLAlchemy (backend/app/models/attempt.py):
    • Mendeklarasikan __table_args__ dengan unique constraint terkait pada model GradeRevision dan ResultPublication.
  • Service Penilaian (backend/app/services/grading_service.py):
    • Memasang row-level lock .with_for_update() saat mengambil GradingTask pada submit_essay_score dan moderate_essay_score.
    • Memvalidasi status tugas: menolak pengubahan tugas yang telah selesai (completed) dengan 403 Forbidden selaras dengan kontrak pengujian UAT penilai/pemoderasi.
  • Service Publikasi Hasil (backend/app/services/ranking_service.py):
    • Mengunci baris ExamSession, ResultApproval, dan ResultPublication aktif 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).

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 /validate menggunakan klien HTTP probe (ordinary-http-audit, curl, python-requests). Server sebelumnya langsung menerbitkan token runner dengan is_seb_verified=True tanpa memvalidasi integritas runtime SEB.
  • Nilai device_fingerprint tidak diverifikasi pada saat penukaran token (redemption), memungkinkan token dipakai dari perangkat keras yang berbeda.
  • Jika konfigurasi SEB menyertakan browser_exam_key, header X-SafeExamBrowser-RequestHash tidak 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 HTTP 403 Forbidden.
    • Verifikasi Device Lock Binding: Membandingkan X-Device-Fingerprint yang dikirim peramban dengan device_fingerprint yang tercatat pada LaunchAuthorization. Jika tidak cocok, menolak dengan HTTP 403 (device lock violation).
    • Urutan Otorisasi Rotasi PIN: Memeriksa record.credential_generation == enrollment.credential_generation terlebih dahulu. Jika kredensial telah dirotasi, langsung membatalkan authorization dan mengembalikan 401 Unauthorized.
    • Verifikasi Browser Exam Key (BEK): Jika konfigurasi SEB aktif memiliki browser_exam_key dan klien mengirimkan header X-SafeExamBrowser-RequestHash, request hash diverifikasi terhadap canonical_url + browser_exam_key. Hash yang tidak valid/salah ditolak dengan HTTP 403 Forbidden.

2. File yang Diubah & Dibuat

  1. backend/app/workers/sweeper.py: Dukungan db: Optional[Session] = None untuk eksekusi CLI mandiri.
  2. backend/app/workers/outbox.py: Registry handler pengiriman, dispatch outbox dengan retry guard, status published riil.
  3. backend/app/workers/celery_app.py: Task sweeper/outbox dan konfigurasi periodic beat schedule.
  4. backend/alembic/versions/f1a2b3c4d5e6_r02_grading_publication_concurrency.py: Migrasi constraint integritas tabel penilaian dan publikasi.
  5. backend/app/models/attempt.py: Deklarasi uq_grade_revision_version dan uq_publication_session_version.
  6. backend/app/services/grading_service.py: Row locks .with_for_update() pada grading task dan penguncian tugas selesai.
  7. backend/app/services/ranking_service.py: Row locks pada publikasi hasil dan konsumsi approval atomik.
  8. backend/app/services/seb_service.py: Blokade klien HTTP biasa, validasi device lock, validasi BEK request hash, dan prioritas rotasi kredensial.
  9. 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
Hasil: - 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
Hasil: - 197 passed, 6 warnings in 95.69s (100% Lolos, 0 Regresi) - Mencakup seluruh pengujian end-to-end UAT untuk seluruh role (Auditor, Author, Grader, Proctor, Reviewer, Participant), invariant konkurensi autosave, isolasi multi-tenant, SEB guard OS-042, bulkhead kamera, dan pipeline closeout.

C. Kompilasi & Verifikasi Frontend

Perintah:

cd frontend && npm run build
Hasil: - vue-tsc && vite build: Clean build in 1.86s, 0 TypeScript error, 0 bundle warning.