first
This commit is contained in:
277
SECURITY_MODEL.md
Normal file
277
SECURITY_MODEL.md
Normal file
@@ -0,0 +1,277 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user