Konfigurasi Bagian 2 mendefinisikan aturan mainnya, tetapi aturan tidak menjalankan dirinya sendiri: valid users = @keluarga baru berarti sesuatu setelah ada user yang benar-benar tercatat di grup keluarga, dan directory mask = 2770 baru bekerja setelah kita memahami bit setgid yang dibawanya. Bagian ini mengisi lapisan penghuni dari file server — user, grup, dan permission — lalu membuktikan setiap klaimnya: setgid mewariskan grup ke berkas baru, tamu ditolak saat menulis share yang salah, dan seluruh aktivitas bisa diaudit dari log. Semua demo dijalankan pada fixture lokal dengan permission sungguhan dari chmod, chgrp, dan kernel.

Grup dan User #

Skrip buat-struktur.sh dari Bagian 1 sudah membuat grup keluarga, tamu, dan backup saat dijalankan sebagai root. Yang tersisa adalah mengisi keanggotaannya. Menambahkan anggota adalah pekerjaan usermod -aG — user budi dan siti masuk grup keluarga, akun tamu masuk grup tamu, dan hanya budi yang masuk grup backup. Untuk akses Samba, tiap user juga perlu password Samba sendiri, karena akun Samba selalu berdiri di atas akun Linux:

bash
sudo usermod -aG keluarga budi
sudo usermod -aG keluarga siti
sudo usermod -aG tamu tamu1
sudo usermod -aG backup budi
sudo smbpasswd -a budi
sudo smbpasswd -a siti

Keanggotaan baru berlaku pada login berikutnya — sesi yang sedang berjalan tidak ikut berubah, kecuali kita membuka shell baru dengan newgrp keluarga. Mekanisme dua lapis akun Linux dan Samba ini sudah dibedah di Chapter Samba .

Di lingkungan latihan kita tidak bisa membuat user sungguhan, tetapi bisa merasakan aturan kernel yang paling menentukan: user biasa hanya boleh memindahkan grup sebuah berkas ke grup tempatnya menjadi anggota. Blok berikut membuat fixture lalu mengalihkan grupnya — di komputer pembaca, ganti z dengan grup bersama seperti keluarga:

bash
mkdir -p latihan/share/dokumen
printf 'catatan belanja bulan ini\n' > latihan/share/dokumen/belanja.txt
chgrp z latihan/share/dokumen/belanja.txt
stat -c '%U:%G %n' latihan/share/dokumen/belanja.txt

Output yang Dihasilkan:

text
z:z latihan/share/dokumen/belanja.txt

Perintah chgrp di atas berhasil karena kita pemilik berkas sekaligus anggota grup z — dan berkas hasilnya terbaca z:z. Persis di sinilah letak bedanya di server: chgrp keluarga membutuhkan sudo karena keluarga bukan grup utama kita, itulah sebabnya pemindahan grup pada struktur share dikerjakan buat-struktur.sh yang memang berjalan sebagai root.

setgid: Grup yang Diwariskan #

Inilah masalah yang membuat share tim berantakan dalam hitungan minggu: tanpa pengaman, berkas baru membawa grup utama pembuatnya. Siti membuat berkas di dokumen, grupnya grup utama Siti; Budi membuat berkas lain, grupnya grup Budi — dan tiba-tiba anggota yang satu tidak bisa membaca berkas anggota yang lain, meski foldernya sama. Solusinya sudah tertanam di mode 2770 sejak Bagian 1: digit 2 memasang bit setgid pada direktori, sehingga berkas dan folder baru di dalamnya mewarisi grup foldernya. Blok berikut membuktikan mesin pewarisannya bekerja:

bash
umask 002
mkdir -p latihan/share/dokumen
chmod 2770 latihan/share/dokumen
touch latihan/share/dokumen/catatan.txt
mkdir latihan/share/dokumen/laporan
echo "== folder share =="
stat -c '%A %G' latihan/share/dokumen
echo "== file baru di dalamnya =="
stat -c '%A %G' latihan/share/dokumen/catatan.txt
echo "== folder baru di dalamnya =="
stat -c '%A %G' latihan/share/dokumen/laporan

Output yang Dihasilkan:

text
== folder share ==
drwxrws--- z
== file baru di dalamnya ==
-rw-rw-r-- z
== folder baru di dalamnya ==
drwxrwsr-x z

Mari kita bedah hasilnya:

  1. drwxrws---: huruf s di posisi execute kelas grup adalah wajah setgid — mode oktal 2770 tercetak penuh sebagai 2 (setgid) + 770.
  2. Berkas baru -rw-rw-r--: grupnya z, grup folder kita di lingkungan latihan ini. Di lingkungan ini grup utama kita memang kebetulan sama, sehingga perubahan namanya tidak terlihat; di server, folder ini sudah dipindahkan ke grup keluarga, dan berkas yang dibuat budi akan membawa keluarga alih-alih grup utama budi — perubahan inilah inti pewarisan.
  3. Folder baru drwxrwsr-x: perhatikan s-nya — folder laporan mewarisi bit setgid tanpa perintah chmod apa pun. Ini bukti paling terlihat bahwa mesin pewarisan hidup: setiap subfolder yang dibuat siapa pun di dalam share otomatis meneruskan aturan grup ke generasi berikutnya.
  4. umask 002: berkas baru lahir dengan write untuk kelas grup — tanpa ini, anggota lain bisa membaca tetapi tidak bisa mengubah. Pada share Samba, peran serupa dimainkan create mask dari Bagian 2; NFS memakai umask proses di klien.

Perilaku ini adalah alasan directory mask = 2770 kita tulis di smb.conf: folder yang dibuat klien Windows lewat share juga membawa setgid, sehingga skema grup tidak bocor oleh klien yang tidak tahu apa-apa soal permission Linux. Teori lengkap bit khusus ini ada di Chapter Special Permissions .

Pengecualian per User dengan ACL #

Model grup menutup 95% kebutuhan, tetapi selalu ada pengecualian: tamu1 ikut mengerjakan satu folder proyek selama sepekan, atau siti butuh akses penuh ke satu subfolder dokumen tanpa menjadi anggota penuh keluarga. Untuk pengecualian seperti ini, ACL jauh lebih tepat daripada melubangi aturan grup:

bash
sudo setfacl -m u:tamu1:rwX /srv/share/dokumen/proyek-bersama
getfacl /srv/share/dokumen/proyek-bersama

Entri ACL u:tamu1:rwX memberi tamu1 baca-tulis pada folder itu saja — X besar berarti execute hanya untuk direktori, bukan berkas — tanpa mengubah keanggotaan grup siapa pun. Tanda + yang muncul di akhir mode (drwxrws---+) adalah penanda bahwa berkas membawa ACL tambahan. Panduan lengkapnya ada di Chapter Access Control Lists dan setfacl(1) .

Menguji Akses dari Klien #

Dengan user dan grup beres, saatnya berpindah kursi ke sisi klien. Alat uji pertama adalah smbclient bergaya FTP: opsi -L meminta daftar share yang ditawarkan server, dan daftar itu sendiri sudah satu hasil uji — share backup yang browseable = no tidak boleh muncul. Berikut contoh tampilan sesi dari laptop budi:

console
$ smbclient -L //server-rumah -U budi
Password for [RUMAH\budi]:

    Sharename       Type      Comment
    ---------       ----      -------
    dokumen         Disk      Dokumen kerja dan catatan keluarga
    media           Disk      Foto dan video keluarga
    IPC$            IPC       IPC Service (Server File Rumah)

Dua share kita tampil beserta comment-nya, entri IPC$ adalah saluran internal Samba yang selalu ada, dan backup memang tidak terlihat — meski tetap bisa dibuka bila namanya diketahui, valid users tetap berjaga. Dari klien Linux, share NFS dipasang seperti biasa:

bash
sudo mkdir -p /mnt/media
sudo mount -t nfs server-rumah:/srv/share/media /mnt/media
ls /mnt/media

Perintah terakhir yang paling penting: coba tulis ke dalamnya. ls berhasil, touch /mnt/media/coba.txt ditolak — share media diekspor ro, dan kernel klien menolak membuka berkas untuk ditulis. Persis perilaku itulah yang kita simulasikan di bawah ini.

Simulasi ro vs rw #

Tiga pintu berujung pada penolakan yang sama: mode 444 di disk, read list di Samba, dan ro di ekspor NFS. Semuanya berakhir pada satu pemeriksaan kernel ketika proses mencoba membuka berkas untuk ditulis. Blok berikut membuat berkas read-only lalu mencoba menambah isinya dari klien — 2>&1 membawa pesan errornya ikut tampil:

bash
mkdir -p latihan/share/media
printf 'video liburan 2026\n' > latihan/share/media/indeks.txt
chmod 444 latihan/share/media/indeks.txt
( echo 'entri baru dari klien' >> latihan/share/media/indeks.txt ) 2>&1

Output yang Dihasilkan:

text
bash: latihan/share/media/indeks.txt: Permission denied

Kernel menolak sebelum satu byte pun tersentuh: mode 444 berarti kelas owner, grup, dan other sama-sama tanpa write, dan redirect >> membutuhkan write. Kurung mengelompokkan echo menjadi satu perintah sehingga 2>&1 di luarnya membawa pesan error shell ikut tampil — tanpa itu, pesan seperti ini hanya pergi ke stderr dan tak terlihat di log. Blok kedua memperlihatkan sisi sebaliknya — kelas yang berhak menulis:

bash
mkdir -p latihan/share/media
printf 'video liburan 2026\n' > latihan/share/media/indeks.txt
chmod 664 latihan/share/media/indeks.txt
echo 'entri baru dari klien' >> latihan/share/media/indeks.txt
cat latihan/share/media/indeks.txt

Output yang Dihasilkan:

text
video liburan 2026
entri baru dari klien

Mode 664 mengembalikan write untuk owner dan grup — pemetaan persis seperti keadaan server: anggota keluarga menulis media dengan lancar, sementara tamu menabrak Permission denied yang sama seperti blok pertama, baik datang lewat Samba (read list) maupun NFS (ro).

Audit: Siapa Mengakses Apa #

File server yang dipakai banyak orang butuh jawaban atas dua pertanyaan rutin: siapa sedang mengakses apa, dan adakah akses yang tidak seharusnya. Jawaban real-time diberikan sudo smbstatus — tabel koneksi aktif, user, dan share-nya; riwayatnya tersimpan di log per klien /var/log/samba/log.%m yang kita konfigurasi di Bagian 2. Berikut cuplikan dua berkas log yang formatnya disederhanakan untuk latihan, lalu dirangkum awk:

bash
mkdir -p latihan/log
cat > latihan/log/log.hape-siti <<'LOG'
2026/01/15 09:12:01 hape-siti connect service=media user=siti
2026/01/15 10:02:11 hape-siti connect service=dokumen user=siti
LOG
cat > latihan/log/log.laptop-tamu <<'LOG'
2026/01/15 11:30:45 laptop-tamu connect service=media user=tamu1
2026/01/15 11:31:02 laptop-tamu connect service=dokumen user=tamu1
LOG
echo "== akses per user =="
awk '{ sub(/service=/, "", $5); sub(/user=/, "", $6); print $6, $5 }' \
    latihan/log/log.* | sort -u
echo "== akses tamu di luar media (perlu ditinjau) =="
awk '{ sub(/service=/, "", $5); sub(/user=/, "", $6)
    if ($6 ~ /^tamu/ && $5 != "media") print $1, $2, $3, $6 " -> " $5 }' \
    latihan/log/log.*

Output yang Dihasilkan:

text
== akses per user ==
siti dokumen
siti media
tamu1 dokumen
tamu1 media
== akses tamu di luar media (perlu ditinjau) ==
2026/01/15 11:31:02 laptop-tamu tamu1 -> dokumen

Bagian awal output adalah jawaban pertanyaan pertama: pasangan user dan share yang unik, hasil sort -u dari seluruh berkas log. Bagian kedua adalah alarm buatan kita sendiri: tamu yang tercatat mengakses share di luar media. Baris tamu1 -> dokumen seharusnya mustahil — valid users = @keluarga harus menolaknya — sehingga kemunculannya berarti konfigurasi berubah tanpa sengaja atau ada yang memakai akun tamu lebih jauh dari haknya; dua-duanya layak ditelusuri hari itu juga. Audit isi berkasnya sendiri bisa memakai find /srv/share -type f -not -group keluarga di server untuk memburu berkas yang grupnya melenceng dari skema. Rincian smbstatus ada di smbstatus(8) .

Seluruh klaim bagian ini — struktur, konfigurasi, pewarisan setgid, penolakan tulis — dirangkum menjadi dua puluh assertion otomatis pada tests/run-tests.sh: skrip menjalankan buat-struktur.sh di folder terisolasi, memanggil verifikasi-share.sh pada config/ bawaan dan pada konfigurasi rusak, memeriksa entri fstab-share, lalu membuktikan setgid terwarisi dan tulis ke berkas read-only ditolak. Jalankan bash tests/run-tests.sh dari folder proyek; keluaran PASSED: 20/20 test lulus berarti file server Anda lulus seluruh uji yang sama dengan milik kami.

File server kini utuh: penyimpanan terencana, dua protokol berbagi dengan batasan yang sejajar, dan lapisan user-grup-permission yang terbukti bekerja. Pada proyek berikutnya, Layanan Home Server , mesin ini kita hidupkan lebih jauh — layanan inti, monitoring sederhana, dan backup terjadwal dibangun di atas share yang barusan kita amankan.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

deploy/buat-struktur.sh

Unduh berkas
bash
#!/usr/bin/env bash
# buat-struktur.sh — bangun struktur penyimpanan file server.
#
# Membuat tiga share (dokumen, media, backup), mengatur group dan
# mode permission-nya, lalu memverifikasi hasil akhirnya dengan stat.
#
# Dua mode eksekusi:
#   - sebagai root di server  : membuat group sungguhan (groupadd bila
#     belum ada), folder di bawah TARGET, dan chgrp ke group aslinya.
#   - sebagai user biasa      : simulasi — folder tetap dibuat, chgrp
#     digantikan ke group utama kita, dan rencana group dicatat ke
#     TARGET/etc/group sebagai fixture yang bisa dibaca ulang.
#
# Pemakaian:
#   sudo bash buat-struktur.sh /srv      # di server sungguhan
#   bash buat-struktur.sh latihan        # latihan lokal tanpa root
#
# Kode keluar:
#   0 sukses                             1 argumen tidak valid
#   2 struktur gagal dibuat/tidak sesuai

set -u

TARGET="${1:-latihan}"

# Rencana share: nama:group:mode (mode 4 digit oktal, setgid aktif).
RENCANA="dokumen:keluarga:2770 media:keluarga:2775 backup:backup:2770"

if [ -z "$TARGET" ] || [ "$TARGET" = "/" ]; then
    printf 'buat-struktur.sh: target tidak valid: %s\n' "$TARGET" >&2
    exit 1
fi
if [ "$TARGET" = "/srv" ] && [ "$(id -u)" -ne 0 ]; then
    printf 'buat-struktur.sh: /srv milik root; jalankan dengan sudo\n' >&2
    exit 1
fi

mkdir -p "$TARGET" || exit 1
SHARE_DIR="$TARGET/share"

# --- 1. Siapkan group ----------------------------------------------------
if [ "$(id -u)" -eq 0 ]; then
    printf 'mode    : server (root)\n'
    for grup in keluarga tamu backup; do
        if ! getent group "$grup" > /dev/null; then
            groupadd "$grup" || exit 2
        fi
    done
else
    printf 'mode    : simulasi tanpa root\n'
    mkdir -p "$TARGET/etc" || exit 2
    cat > "$TARGET/etc/group" <<'GRUP'
keluarga:x:1100:budi,siti
tamu:x:1101:tamu1
backup:x:1102:budi
GRUP
    printf 'group   : keluarga, tamu, backup dicatat di %s/etc/group\n' "$TARGET"
fi

# --- 2. Buat folder share beserta permission-nya -------------------------
printf 'struktur: %s/{dokumen,media,backup}\n' "$SHARE_DIR"
gagal=0
for entri in $RENCANA; do
    nama=${entri%%:*}
    sisa=${entri#*:}
    grup=${sisa%%:*}
    mode=${sisa##*:}
    dir="$SHARE_DIR/$nama"

    mkdir -p "$dir" || { gagal=$((gagal + 1)); continue; }
    if [ "$(id -u)" -eq 0 ]; then
        chgrp "$grup" "$dir" || { gagal=$((gagal + 1)); continue; }
    else
        # Tanpa root kita hanya boleh memindahkan group ke group sendiri;
        # di server, baris ini menjadi: chgrp "$grup" "$dir"
        chgrp "$(id -gn)" "$dir" || { gagal=$((gagal + 1)); continue; }
    fi
    chmod "$mode" "$dir"

    # --- 3. Verifikasi hasil dengan stat -------------------------------
    nyata=$(stat -c '%a' "$dir")
    if [ "$nyata" != "$mode" ]; then
        printf 'GAGAL  %s mode %s, harapan %s\n' "$dir" "$nyata" "$mode"
        gagal=$((gagal + 1))
    else
        printf 'OK     %s %s\n' "$mode" "$dir"
    fi
done

if [ "$gagal" -gt 0 ]; then
    printf 'GAGAL: %d pemeriksaan tidak sesuai\n' "$gagal"
    exit 2
fi
printf 'selesai tanpa error\n'

deploy/verifikasi-share.sh

Unduh berkas
bash
#!/usr/bin/env bash
# verifikasi-share.sh — periksa directive wajib smb.conf dan format exports.
#
# Pengganti testparm/exportfs di lingkungan tanpa Samba dan NFS: menerima
# folder yang memuat smb.conf dan exports, lalu memeriksa directive
# keamanan yang kita rancang satu per satu dengan grep dan awk.
#
# Pemakaian:
#   bash verifikasi-share.sh <folder-config>
#
# Kode keluar = jumlah pemeriksaan yang gagal.

set -u

if [ $# -ne 1 ]; then
    printf 'pemakaian: verifikasi-share.sh <folder-config>\n' >&2
    exit 1
fi

SMB="$1/smb.conf"
EXP="$1/exports"
gagal=0

for f in "$SMB" "$EXP"; do
    if [ ! -f "$f" ]; then
        printf 'verifikasi-share.sh: berkas tidak ditemukan: %s\n' "$f" >&2
        exit 1
    fi
done

ok()     { printf 'OK     %s\n' "$1"; }
hilang() { printf 'HILANG %s\n' "$1"; gagal=$((gagal + 1)); }

# --- smb.conf: section dan directive wajib --------------------------------
for pola in \
    '[global]' \
    '[dokumen]' \
    '[media]' \
    '[backup]' \
    'path = /srv/share/dokumen' \
    'path = /srv/share/media' \
    'path = /srv/share/backup' \
    'valid users = @keluarga' \
    'read only = no' \
    'create mask = 0660' \
    'directory mask = 2770' \
    'browseable = no' \
    'bind interfaces only = yes'
do
    if grep -qF -- "$pola" "$SMB"; then
        ok "smb.conf: $pola"
    else
        hilang "smb.conf: $pola"
    fi
done

# --- exports: format dan opsi keamanan -----------------------------------

# 1. Setiap entri aktif memuat kurung opsi di kolom klien.
n=$(awk '!/^[[:space:]]*#/ && NF && $2 !~ /\(/ { c++ } END { print c + 0 }' "$EXP")
if [ "$n" -eq 0 ]; then
    ok 'exports: semua entri memuat opsi klien'
else
    hilang 'exports: ada entri tanpa opsi klien'
fi

# 2. Setiap entri memuat sync dan no_subtree_check.
n=$(awk '
    !/^[[:space:]]*#/ && NF && ($2 !~ /sync/ || $2 !~ /no_subtree_check/) { c++ }
    END { print c + 0 }' "$EXP")
if [ "$n" -eq 0 ]; then
    ok 'exports: semua entri memuat sync + no_subtree_check'
else
    hilang 'exports: ada entri tanpa sync / no_subtree_check'
fi

# 3. root_squash tidak boleh dimatikan.
if grep -q 'no_root_squash' "$EXP"; then
    hilang 'exports: no_root_squash terdeteksi'
else
    ok 'exports: root_squash tetap aktif'
fi

# 4. dokumen rw, media ro.
mode_dokumen=$(awk '$1 == "/srv/share/dokumen" {
    print ($2 ~ /\(rw/) ? "rw" : "ro" }' "$EXP")
if [ "$mode_dokumen" = "rw" ]; then
    ok 'exports: dokumen dibagikan rw'
else
    hilang 'exports: dokumen bukan rw'
fi
mode_media=$(awk '$1 == "/srv/share/media" {
    print ($2 ~ /\(ro/) ? "ro" : "rw" }' "$EXP")
if [ "$mode_media" = "ro" ]; then
    ok 'exports: media dibagikan ro'
else
    hilang 'exports: media bukan ro'
fi

# 5. backup hanya untuk satu klien tepercaya.
klien=$(awk '$1 == "/srv/share/backup" {
    k = $2; sub(/\(.*/, "", k); print k }' "$EXP")
if [ "$klien" = "192.0.2.10" ]; then
    ok 'exports: backup terbatas ke 192.0.2.10'
else
    hilang 'exports: klien backup bukan 192.0.2.10'
fi

exit "$gagal"

tests/run-tests.sh

Unduh berkas
bash
#!/usr/bin/env bash
# run-tests.sh — test suite untuk proyek server file lokal.
# Suite menyiapkan folder kerja terisolasi (mktemp), lalu menguji:
# struktur share hasil deploy/buat-struktur.sh (folder, mode, fixture
# group), directive wajib pada config/smb.conf dan config/exports
# lewat deploy/verifikasi-share.sh, entri config/fstab-share, pewarisan
# setgid pada folder share, serta penolakan tulis pada file read-only.
# Jalankan dari mana saja: bash tests/run-tests.sh

set -u

SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PROJ_DIR=$(dirname "$SCRIPT_DIR")

PASS=0
FAIL=0
WORK=""

# assert_eq <harapan> <nyata> <pesan>: bandingkan dua nilai.
assert_eq() {
    if [ "$1" = "$2" ]; then
        PASS=$((PASS + 1))
        printf 'OK    %s\n' "$3"
    else
        FAIL=$((FAIL + 1))
        printf 'GAGAL %s: harapan %s, nyata %s\n' "$3" "$1" "$2"
    fi
}

bersihkan() {
    if [ -n "$WORK" ]; then
        rm -rf "$WORK"
    fi
}
trap bersihkan EXIT

WORK=$(mktemp -d)
cd "$WORK" || exit 1

# --- uji 1: buat-struktur.sh membangun struktur yang sesuai ---------------
bash "$PROJ_DIR/deploy/buat-struktur.sh" latihan > /dev/null 2>&1
assert_eq 0 "$?" "buat-struktur.sh keluar 0"
for nama in dokumen media backup; do
    [ -d "latihan/share/$nama" ]
    assert_eq 0 "$?" "share $nama dibuat"
done
assert_eq 2770 "$(stat -c '%a' latihan/share/dokumen)" \
    "mode dokumen 2770"
assert_eq 2775 "$(stat -c '%a' latihan/share/media)" \
    "mode media 2775"
assert_eq 2770 "$(stat -c '%a' latihan/share/backup)" \
    "mode backup 2770"

# --- uji 2: fixture group tercatat, skrip idempoten ------------------------
grep -q '^keluarga:x:1100:budi,siti$' latihan/etc/group
assert_eq 0 "$?" "fixture group memuat keluarga:budi,siti"
grep -q '^tamu:x:1101:tamu1$' latihan/etc/group
assert_eq 0 "$?" "fixture group memuat tamu"
bash "$PROJ_DIR/deploy/buat-struktur.sh" latihan > /dev/null 2>&1
assert_eq 0 "$?" "buat-struktur.sh idempoten (dijalankan dua kali)"

# --- uji 3: konfigurasi bawaan lolos verifikasi-share.sh -------------------
bash "$PROJ_DIR/deploy/verifikasi-share.sh" "$PROJ_DIR/config" \
    > /dev/null 2>&1
assert_eq 0 "$?" "config/ lolos verifikasi-share.sh"

# --- uji 4: konfigurasi rusak tertangkap -----------------------------------
mkdir -p rusak
printf '[global]\n[dokumen]\npath = /srv/share/dokumen\nread only = no\n' \
    > rusak/smb.conf
printf '/srv/share/dokumen  192.0.2.0/24(rw,async)\n' > rusak/exports
bash "$PROJ_DIR/deploy/verifikasi-share.sh" rusak > /dev/null 2>&1
if [ "$?" -gt 0 ]; then
    PASS=$((PASS + 1))
    printf 'OK    konfigurasi rusak tertangkap (exit bukan 0)\n'
else
    FAIL=$((FAIL + 1))
    printf 'GAGAL konfigurasi rusak tertangkap: verifikasi-share.sh keluar 0\n'
fi

# --- uji 5: entri fstab-share memuat nofail dan enam field -----------------
grep -q 'nofail' "$PROJ_DIR/config/fstab-share"
assert_eq 0 "$?" "fstab-share memuat nofail"
jumlah=$(awk '!/^[[:space:]]*#/ && NF { print NF }' \
    "$PROJ_DIR/config/fstab-share" | sort -u)
assert_eq 6 "$jumlah" "entri fstab-share berisi enam field"
mnt=$(awk '$2 == "/srv/share" { print $2 }' "$PROJ_DIR/config/fstab-share")
assert_eq "/srv/share" "$mnt" "mountpoint fstab-share /srv/share"

# --- uji 6: setgid diwarisi oleh isi baru folder share ---------------------
mkdir -p latihan/share/dokumen/laporan
touch latihan/share/dokumen/laporan/catatan.txt
assert_eq "$(stat -c '%G' latihan/share/dokumen)" \
    "$(stat -c '%G' latihan/share/dokumen/laporan/catatan.txt)" \
    "file baru mewarisi group folder share"
mode_sub=$(stat -c '%a' latihan/share/dokumen/laporan)
if [ "$(( 8#$mode_sub & 8#2000 ))" -ne 0 ]; then
    PASS=$((PASS + 1))
    printf 'OK    folder baru mewarisi bit setgid (%s)\n' "$mode_sub"
else
    FAIL=$((FAIL + 1))
    printf 'GAGAL folder baru mewarisi bit setgid: mode %s tanpa bit 2000\n' \
        "$mode_sub"
fi

# --- uji 7: tulis ditolak pada file read-only ------------------------------
printf 'isi awal\n' > latihan/kunci.txt
chmod 444 latihan/kunci.txt
if (echo 'tambahan' >> latihan/kunci.txt) 2> /dev/null; then
    FAIL=$((FAIL + 1))
    printf 'GAGAL tulis ke file ro ditolak: append justru berhasil\n'
else
    PASS=$((PASS + 1))
    printf 'OK    tulis ke file ro ditolak\n'
fi
assert_eq 'isi awal' "$(cat latihan/kunci.txt)" "isi file ro tidak berubah"
chmod 644 latihan/kunci.txt
echo 'tambahan' >> latihan/kunci.txt
assert_eq 0 "$?" "tulis diizinkan kembali setelah mode rw"

# --- ringkasan ----------------------------------------------------------------
if [ "$FAIL" -eq 0 ]; then
    printf 'PASSED: %d/%d test lulus\n' "$PASS" "$PASS"
else
    printf 'FAILED: %d/%d test lulus\n' "$PASS" "$((PASS + FAIL))"
fi
[ "$FAIL" -eq 0 ]