# SECURITY_MODEL.md — Speech2Text MBIP ## Prinsip Asas Keselamatan Sistem ini direka untuk menyimpan data sensitif kerajaan/PBT. Prinsip berikut mesti dipatuhi di setiap lapisan: 1. **Privacy by Design** — Admin ialah pentadbir akaun, bukan pentadbir kandungan. 2. **Defense in Depth** — Authorization dikuatkuasakan di Policy, Controller, Query, dan UI. 3. **Least Privilege** — Setiap role hanya mendapat akses minimum yang diperlukan. 4. **Audit Everything** — Semua tindakan sensitif direkodkan dengan lengkap. 5. **No Cloud Egress** — Data tidak keluar dari Docker network. --- ## Model Kebenaran (Authorization) ### Admin — APA yang BOLEH dan TIDAK BOLEH | Tindakan | Admin | |---|---| | Daftar pengguna baru | ✓ | | Lihat senarai pengguna | ✓ | | Activate/deactivate pengguna | ✓ | | Delete pengguna (belum guna) | ✓ | | Tukar emel pengguna | ✓ | | Lihat statistik penggunaan (tanpa kandungan) | ✓ | | Lihat metadata projek (tajuk, owner, status, saiz, tarikh) | ✓ | | Transfer ownership projek (dengan justifikasi) | ✓ | | Lihat audit log pentadbiran | ✓ | | **Dengar / download audio** | ✗ | | **Baca transcript** | ✗ | | **Lihat komen dalam projek** | ✗ | | **Masuk halaman detail projek (kandungan)** | ✗ | | **Edit transcript** | ✗ | ### Owner (Pengguna Jabatan — pemilik projek) | Tindakan | Owner | |---|---| | Cipta projek | ✓ | | Upload audio | ✓ | | Lihat / dengar audio sendiri | ✓ | | Lihat / edit transcript sendiri | ✓ | | Tambah / buang collaborator | ✓ | | Delete projek / audio / teks | ✓ | | Retry transcription | ✓ | | Lihat versi history | ✓ | | Restore versi lama | ✓ | | Buat komen | ✓ | ### Collaborator (Pengguna yang dikongsi projek) | Tindakan | Collaborator | |---|---| | Lihat projek yang dikongsi | ✓ | | Dengar audio asal | ✓ | | Lihat / edit transcript | ✓ | | Buat komen | ✓ | | Lihat versi history | ✓ | | Restore versi lama | ✓ | | **Delete projek / audio / teks** | ✗ | | **Transfer ownership** | ✗ | | **Urus collaborator lain** | ✗ | --- ## Laravel Policies ### `TranscriptionProjectPolicy` ```php // Setiap method semak sama ada user adalah owner ATAU collaborator // Admin sengaja TIDAK diberikan akses ke content view() → owner ATAU collaborator (bukan admin) create() → user aktif (bukan admin) update() → owner ATAU collaborator delete() → owner sahaja viewAudio() → owner ATAU collaborator downloadAudio()→ owner ATAU collaborator retryTranscription() → owner sahaja manageCollaborators() → owner sahaja transferOwner() → admin sahaja (metadata action, bukan content) viewMetadata() → admin ATAU owner (metadata minimum sahaja untuk admin) ``` ### `CommentPolicy` ```php view() → owner ATAU collaborator (bukan admin) create() → owner ATAU collaborator delete() → owner komen sendiri ATAU owner projek ``` ### `TranscriptVersionPolicy` ```php view() → owner ATAU collaborator restore() → owner ATAU collaborator ``` ### `UserPolicy` ```php create() → admin sahaja update() → admin sahaja delete() → admin sahaja (dengan semak tiada usage) activate() → admin sahaja deactivate() → admin sahaja changeEmail()→ admin sahaja ``` --- ## Lapisan Keselamatan ### L1: Authentication - Laravel Breeze/custom auth dengan session - `is_active` check di middleware `EnsureUserIsActive` - Rate limit login: 5 percubaan per minit per IP - Session expire: dikonfig dalam `.env` - CSRF protection aktif untuk semua form ### L2: Authorization (Policy) - Setiap controller action wajib panggil `$this->authorize()` - Jangan rely pada UI hide/show sahaja - Gunakan `abort(403)` jika policy gagal ### L3: Input Validation - Semua input melalui Form Request class - File upload: validate MIME type dari magic bytes (bukan extension sahaja) - Allowed audio types: `audio/mpeg`, `audio/wav`, `audio/mp4`, `audio/x-m4a`, `video/mp4` - Max file size: dikonfig `PRIVATE_AUDIO_MAX_MB` dalam `.env` - Rate limit upload: 10 upload per jam per user ### L4: Storage Security - Fail audio **tidak** disimpan dalam `public/` atau `storage/app/public/` - Path simpan: `storage/app/private/transcriptions/{project_uuid}/audio/{filename}` - Semua akses audio melalui controller yang semak policy - Guna `Storage::disk('private')->get()` untuk stream audio - Response header `Content-Disposition: inline` untuk audio player - Response header `X-Content-Type-Options: nosniff` ### L5: IDOR Prevention - Gunakan UUID dalam URL projek, bukan integer ID - Route: `/projects/{project:uuid}` bukan `/projects/{id}` - Policy semak ownership/collaborator, bukan hanya URL - Semua query pakai `where('owner_user_id', auth()->id())` atau policy scope ### L6: Output Sanitization - Escape semua output Blade dengan `{{ }}` (bukan `{!! !!}`) - Sanitize comment sebelum display dengan `htmlspecialchars()` - Transcript display menggunakan `{{ }}` escape - Content-Security-Policy header via middleware ### L7: Audit Trail - Semua tindakan sensitif log ke `audit_logs` - Log IP address dan user agent - Log nilai lama dan nilai baru (kecuali transcript content) - Audit log adalah append-only (tiada update/delete pada audit_logs) ### L8: Admin Content Isolation ```php // Middleware atau scope yang enforce admin tidak dapat content class AdminProjectScope { public function apply($builder, $model) { if (auth()->user()->role === 'admin') { // Admin hanya dapat metadata sahaja $builder->select([ 'id', 'uuid', 'title', 'owner_user_id', 'transcription_status', 'created_at', 'file_size', 'duration_seconds', 'deleted_at' // TIADA transcript_text, stored_audio_path ]); } } } ``` --- ## Keselamatan Fail Audio ### Upload Flow ``` 1. Validate MIME type (magic bytes, bukan extension) 2. Generate nama fail baru yang random (jangan guna nama asal) 3. Simpan ke path private: transcriptions/{uuid}/audio/{random_name}.{ext} 4. Simpan nama asal dalam database (original_filename) 5. Jangan simpan path dalam session atau cookie ``` ### Serve/Stream Audio ```php // AudioController@stream public function stream(TranscriptionProject $project) { $this->authorize('viewAudio', $project); $path = $project->stored_audio_path; // Verify file exists abort_unless(Storage::disk('private')->exists($path), 404); return response()->stream(function () use ($path) { $stream = Storage::disk('private')->readStream($path); fpassthru($stream); }, 200, [ 'Content-Type' => $project->mime_type, 'Content-Length' => Storage::disk('private')->size($path), 'Content-Disposition' => 'inline', 'Cache-Control' => 'no-store, no-cache, private', 'X-Content-Type-Options' => 'nosniff', ]); } ``` --- ## Konfigurasi Security Header ```php // middleware/SecurityHeaders.php 'X-Frame-Options' => 'DENY', 'X-XSS-Protection' => '1; mode=block', 'X-Content-Type-Options' => 'nosniff', 'Referrer-Policy' => 'strict-origin-when-cross-origin', 'Content-Security-Policy' => "default-src 'self'; ..." ``` --- ## Enkripsi Data Sensitif ### Transcript Text Encryption Gunakan Laravel `encrypted` cast untuk `transcript_text`: ```php // Model TranscriptionProject protected $casts = [ 'transcript_text' => 'encrypted', ]; ``` - Data dienkripsi menggunakan `APP_KEY` sebelum simpan dalam database. - Decrypt berlaku secara automatik apabila diakses melalui Eloquent. - Tanpa `APP_KEY`, data tidak boleh dibaca walaupun database dicuri. - **Penting:** Simpan `APP_KEY` dalam `.env` dan jangan commit ke git. --- ## Semakan Keselamatan Deployment Sebelum production, semak: - [ ] `APP_DEBUG=false` - [ ] `APP_ENV=production` - [ ] `.env` tidak dalam git repository (ada dalam `.gitignore`) - [ ] `APP_KEY` telah dijanakan (`php artisan key:generate`) - [ ] Database password kuat dan berbeza dari default - [ ] `storage/app/private/` tidak accessible dari web - [ ] Nginx tidak serve `storage/` secara direct - [ ] HTTPS/TLS aktif - [ ] Rate limiting dikonfig - [ ] Fail upload di luar `public_html` - [ ] `storage:link` hanya untuk assets (bukan audio/transcript) - [ ] Queue worker berjalan dengan user yang restricted - [ ] Transcription worker tidak accessible dari internet (internal only) - [ ] Ollama tidak accessible dari internet (internal only) --- ## Nota untuk Pentadbir Sistem 1. **Backup `APP_KEY`** — Jika `APP_KEY` hilang, semua `transcript_text` yang dienkripsi tidak boleh dibaca. 2. **Backup database secara berkala** — Termasuk `transcript_versions` yang menyimpan history. 3. **Log rotation** — Pastikan `storage/logs/` dirotate supaya tidak penuh. 4. **Retention policy** — Tentukan berapa lama fail audio disimpan selepas projek deleted. 5. **Monitor disk space** — Audio files boleh membesar. Tetapkan alert.