diff --git a/modules/mounts/README.md b/modules/mounts/README.md index 06a37d22..b775de58 100644 --- a/modules/mounts/README.md +++ b/modules/mounts/README.md @@ -69,7 +69,8 @@ All three are preferences whose default, `none`, names nothing (ADR 0262 allows For example `games=smb://192.0.2.76/Share@/mnt/games`. - `smb-users`: the username each SMB source logs in with, space-separated `name=username`, for example `games=root`. A username is not a secret, so it is a setting (novox/hq ADR 0283). It holds no space, - comma or `=`. + comma or `=`. A device that needs a domain takes it in the username, as `DOMAIN\user` or `user@domain`: + the credentials file has no `domain=` line. A mount point is an absolute, plainly written path, not a directory of the system's own (`/etc`, `/usr`, `/run`, …). An entry that cannot be read is said, and only that entry is left out. diff --git a/modules/mounts/cmd/mounts/apply.go b/modules/mounts/cmd/mounts/apply.go index 02f25dad..65a73039 100644 --- a/modules/mounts/cmd/mounts/apply.go +++ b/modules/mounts/cmd/mounts/apply.go @@ -714,10 +714,15 @@ func (a *Applier) login(cfg Config, source string) (SMBCredential, []health.Wait if !hasUser { waits = append(waits, health.Wait{Part: part, Setting: "smb-users", What: "the username of the source " + source}) } + // Never a relative path: without the directory the mesh places the passwords in, nothing is read. + if !filepath.IsAbs(cfg.Passwords) { + return SMBCredential{}, nil, "the module's configuration names no directory for its passwords, so " + + PasswordSecret(source) + " cannot be read" + } var password string raw, err := a.M.read(PasswordFile(cfg.Passwords, source)) switch { - case cfg.Passwords == "" || errors.Is(err, fs.ErrNotExist): + case errors.Is(err, fs.ErrNotExist): waits = append(waits, health.Wait{Part: part, Secret: PasswordSecret(source), What: "the password of the source " + source}) case err != nil: return SMBCredential{}, nil, "its password " + PasswordSecret(source) + " could not be read: " + bare(err) diff --git a/modules/mounts/cmd/mounts/write_test.go b/modules/mounts/cmd/mounts/write_test.go index 6a4bfa61..bd9e7b1d 100644 --- a/modules/mounts/cmd/mounts/write_test.go +++ b/modules/mounts/cmd/mounts/write_test.go @@ -681,3 +681,36 @@ func TestAWaitBesideAFaultIsUnhealthy(t *testing.T) { t.Errorf("a wait alone: %+v", h) } } + +// A password that exists and cannot be read, and a configuration naming no directory for the passwords, are +// refused as faults: said without the value and without the path, and nothing is read from a relative path. +func TestAPasswordThatCannotBeReadIsAFaultSaidWithoutValueOrPath(t *testing.T) { + // A directory where the file should be: read fails, and not as "not there" (as root a mode would not stop it). + f := newSystem(t) + f.write(ConfigFile, "sources=games=smb://192.0.2.76/Share@/mnt/games\nsmb-users=games=root\npasswords=/var/lib/mesh/mounts\nstate=/var/lib/mesh/mounts\n") + f.write("/var/lib/mesh/mounts/smb-password-games.secret/inside", testPassword) + st := f.applier(time.Now()).Look(context.Background()) + if len(st.Shares) != 1 || st.Shares[0].State != StateFailed || !strings.Contains(st.Shares[0].Why, "could not be read") || + strings.Contains(st.Shares[0].Why, "/var/lib") || strings.Contains(st.Shares[0].Why, testPassword) { + t.Fatalf("an unreadable password: %+v", st.Shares) + } + + // No passwords= and no state=: the source is not set up, and no relative path is read. + for _, cfg := range []string{ + "sources=games=smb://192.0.2.76/Share@/mnt/games\nsmb-users=games=root\n", + "sources=games=smb://192.0.2.76/Share@/mnt/games\nsmb-users=games=root\npasswords=relative/dir\n", + } { + g := newSystem(t) + g.write(ConfigFile, cfg) + g.write("smb-password-games.secret", testPassword) + g.write("relative/dir/smb-password-games.secret", testPassword) + st := g.applier(time.Now()).Look(context.Background()) + if len(st.Shares) != 1 || st.Shares[0].State != StateFailed || !strings.Contains(st.Shares[0].Why, "names no directory for its passwords") || + strings.Contains(st.Shares[0].Why, testPassword) || strings.Contains(st.Shares[0].Why, "relative") { + t.Errorf("%q: %+v", cfg, st.Shares) + } + if _, err := os.Stat(filepath.Join(g.root, "/var/lib/mesh/mounts/smb-games.credentials")); !os.IsNotExist(err) { + t.Errorf("%q: a credentials file was written", cfg) + } + } +}