b0c5ef4823
SMBSettings + SMBShare registry in settings (password never stored — only UserSet); NetBIOS-safe name validation. Pure infra renderers: RenderSambaConfig (hardened global block: SMB2 floor, bind interfaces only=lo eth0, disable netbios=no, force user block) + RenderSambaCompose (network_mode host, pinned image, :ro bind for read-only shares, passdb volume). Exact smb.conf golden + CRUD/validation tests.
141 lines
4.2 KiB
Go
141 lines
4.2 KiB
Go
package settings
|
|
|
|
import (
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func newSMBTestSettings(t *testing.T) (*Settings, string) {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "settings.json")
|
|
s, err := Load(path, log.New(os.Stderr, "", 0))
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
return s, path
|
|
}
|
|
|
|
func TestValidateSMBServerName(t *testing.T) {
|
|
good := []string{"FELHOM", "Otthon-1", "box_2", "A", "sixteencharnam"} // last = 14 chars
|
|
for _, n := range good {
|
|
if err := ValidateSMBServerName(n); err != nil {
|
|
t.Errorf("%q should be valid: %v", n, err)
|
|
}
|
|
}
|
|
bad := []string{"", " ", "sixteencharname1", "has space", "bad/slash", "-lead", "trail-", "dot.name", "back\\slash"}
|
|
for _, n := range bad {
|
|
if err := ValidateSMBServerName(n); err == nil {
|
|
t.Errorf("%q should be invalid", n)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidateSMBShareName(t *testing.T) {
|
|
good := []string{"dokumentumok", "filmek", "csalad_kepek", "A1"}
|
|
for _, n := range good {
|
|
if err := ValidateSMBShareName(n); err != nil {
|
|
t.Errorf("%q should be valid: %v", n, err)
|
|
}
|
|
}
|
|
// Scenario C: "../x", >15-char, and invalid NetBIOS charset must all refuse.
|
|
bad := []string{"", "../x", "a/b", "a\\b", "file.txt", "way-too-long-nam", "has space", "-x", "x-"}
|
|
for _, n := range bad {
|
|
if err := ValidateSMBShareName(n); err == nil {
|
|
t.Errorf("%q should be invalid", n)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSMBShareCRUD(t *testing.T) {
|
|
s, path := newSMBTestSettings(t)
|
|
|
|
sh := SMBShare{Name: "dokumentumok", Path: "/mnt/felhom-drives/scratch1/shares/dokumentumok", Offsite: true}
|
|
if err := s.AddSMBShare(sh); err != nil {
|
|
t.Fatalf("AddSMBShare: %v", err)
|
|
}
|
|
got := s.GetSMBShares()
|
|
if len(got) != 1 || got[0].Name != "dokumentumok" || !got[0].Offsite {
|
|
t.Fatalf("share not stored as expected: %+v", got)
|
|
}
|
|
if got[0].CreatedAt == "" {
|
|
t.Error("CreatedAt not stamped on add")
|
|
}
|
|
|
|
// Case-insensitive collision is refused AND does not mutate the registry (Scenario C non-effect).
|
|
if err := s.AddSMBShare(SMBShare{Name: "DOKUMENTUMOK", Path: "/other"}); err == nil {
|
|
t.Error("case-insensitive name collision should be refused")
|
|
}
|
|
if len(s.GetSMBShares()) != 1 {
|
|
t.Error("refused collision must not mutate the registry")
|
|
}
|
|
|
|
// Offsite (Felhőmentés) toggle persists.
|
|
if err := s.SetSMBShareOffsite("dokumentumok", false); err != nil {
|
|
t.Fatalf("SetSMBShareOffsite: %v", err)
|
|
}
|
|
if s.GetSMBShares()[0].Offsite {
|
|
t.Error("offsite toggle did not persist")
|
|
}
|
|
|
|
// Persistence across reload.
|
|
s2, err := Load(path, log.New(os.Stderr, "", 0))
|
|
if err != nil {
|
|
t.Fatalf("reload: %v", err)
|
|
}
|
|
if r := s2.GetSMBShares(); len(r) != 1 || r[0].Name != "dokumentumok" {
|
|
t.Errorf("share did not persist across reload: %+v", r)
|
|
}
|
|
|
|
// Remove is config-only + errors on a missing name.
|
|
if err := s.RemoveSMBShare("dokumentumok"); err != nil {
|
|
t.Fatalf("RemoveSMBShare: %v", err)
|
|
}
|
|
if len(s.GetSMBShares()) != 0 {
|
|
t.Error("share not removed")
|
|
}
|
|
if err := s.RemoveSMBShare("nope"); err == nil {
|
|
t.Error("removing a missing share should error")
|
|
}
|
|
}
|
|
|
|
func TestSMBSettingsDefaultsAndNoPassword(t *testing.T) {
|
|
s, path := newSMBTestSettings(t)
|
|
|
|
// Unconfigured → disabled with the default server name (never nil).
|
|
def := s.GetSMBSettings()
|
|
if def.Enabled || def.ServerName != DefaultSMBServerName {
|
|
t.Errorf("default SMB settings wrong: %+v", def)
|
|
}
|
|
|
|
if err := s.SetSMBEnabled(true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.SetSMBServerName("OTTHON"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.SetSMBUserSet(true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cur := s.GetSMBSettings()
|
|
if !cur.Enabled || cur.ServerName != "OTTHON" || !cur.UserSet {
|
|
t.Errorf("SMB settings not stored: %+v", cur)
|
|
}
|
|
|
|
// The household SMB password must NEVER be persisted (rule 4). The struct has no field for it;
|
|
// prove the on-disk file carries only the UserSet flag, no plaintext secret.
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(string(raw), "password") && strings.Contains(string(raw), "smb") {
|
|
// there is a claim_code_hash etc.; assert no *smb* password key specifically
|
|
if strings.Contains(string(raw), "smb_password") || strings.Contains(string(raw), "\"password\": \"") {
|
|
t.Errorf("settings.json appears to store an SMB password:\n%s", raw)
|
|
}
|
|
}
|
|
}
|