mounts: take an SMB source's username from its entry of sources, not a setting of its own

The live module check refused smb-users: a key naming a user is the
operator's value and may have no default, and a setting without one would
refuse to compose mounts on every node until set. The username now goes in
the source's URL (smb://<user>@<host>/<share>), percent-decoded; a
password there is refused unsaid, and a source without a user waits for
sources (hq ADR 0283, issue 386).
This commit is contained in:
jochen
2026-10-10 21:30:31 +02:00
parent c620173966
commit 6f78899827
8 changed files with 117 additions and 89 deletions
+41 -20
View File
@@ -149,9 +149,9 @@ func (f *fakeSystem) everything(except ...string) string {
}
const config = "shares=library=/srv/library downloads=/srv/downloads:ro\n" +
"sources=games=smb://192.0.2.76/Share@/mnt/games usb=disk:1234-ABCD@/mnt/usb\n" +
"sources=games=smb://root@192.0.2.76/Share@/mnt/games usb=disk:1234-ABCD@/mnt/usb\n" +
"server=homeserver\nat=192.0.2.10\nnode=workstation\naccount=operator\n" +
"smb-users=games=root\npasswords=/var/lib/mesh/mounts\nstate=/var/lib/mesh/mounts\n"
"passwords=/var/lib/mesh/mounts\nstate=/var/lib/mesh/mounts\n"
func exportsAnswer(t *testing.T, shares ...ServerShare) string {
raw, err := json.Marshal(ExportsAnswer{Server: "homeserver", At: "192.0.2.10", Shares: shares})
@@ -365,18 +365,18 @@ func TestASilentServerRemovesNoMount(t *testing.T) {
func TestASourceWithoutItsLoginWaitsForTheOperatorAndIsNotMounted(t *testing.T) {
for name, c := range map[string]struct {
users, password string
writePassword bool
waitsFor []string
user, password string
writePassword bool
waitsFor []string
}{
"no username": {"", testPassword, true, []string{"setting smb-users"}},
"no password": {"games=root", "", false, []string{"secret smb-password-games"}},
"an empty password file": {"games=root", "", true, []string{"secret smb-password-games"}},
"a line break only": {"games=root", "\n", true, []string{"secret smb-password-games"}},
"neither": {"other=x", "", false, []string{"setting smb-users", "secret smb-password-games"}},
"no username": {"", testPassword, true, []string{"setting sources"}},
"no password": {"root@", "", false, []string{"secret smb-password-games"}},
"an empty password file": {"root@", "", true, []string{"secret smb-password-games"}},
"a line break only": {"root@", "\n", true, []string{"secret smb-password-games"}},
"neither": {"", "", false, []string{"setting sources", "secret smb-password-games"}},
} {
f := newSystem(t)
f.write(ConfigFile, "sources=games=smb://192.0.2.76/Share@/mnt/games\nsmb-users="+c.users+"\npasswords=/var/lib/mesh/mounts\nstate=/var/lib/mesh/mounts\n")
f.write(ConfigFile, "sources=games=smb://"+c.user+"192.0.2.76/Share@/mnt/games\npasswords=/var/lib/mesh/mounts\nstate=/var/lib/mesh/mounts\n")
if c.writePassword {
f.write("/var/lib/mesh/mounts/smb-password-games.secret", c.password)
}
@@ -426,7 +426,7 @@ func TestAPasswordIsOneLineKeptAsTyped(t *testing.T) {
t.Errorf("two lines are refused, saying nothing typed: %v", err)
}
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(ConfigFile, "sources=games=smb://root@192.0.2.76/Share@/mnt/games\npasswords=/var/lib/mesh/mounts\nstate=/var/lib/mesh/mounts\n")
f.write("/var/lib/mesh/mounts/smb-password-games.secret", pw+"\n")
f.applier(time.Now()).Look(context.Background())
creds := filepath.Join(f.root, "/var/lib/mesh/mounts/smb-games.credentials")
@@ -449,13 +449,34 @@ func TestAPasswordIsOneLineKeptAsTyped(t *testing.T) {
}
}
func TestTheUsersSettingIsReadAndWhatCannotBeIsSaid(t *testing.T) {
users, problems := ParseSMBUsers("games=root nas=media-user bad Nas=x nas=again weird=a=b")
if users["games"] != "root" || users["nas"] != "media-user" || len(users) != 2 || len(problems) != 4 {
func TestAnSMBSourcesUserIsInItsEntryAndAPasswordThereIsRefusedUnsaid(t *testing.T) {
sources, problems := ParseSources("a=smb://root@192.0.2.76/Share@/mnt/a b=smb://192.0.2.76/Share@/mnt/b " +
"c=smb://WORKGROUP%5Cme@nas/Media@/mnt/c d=smb://me%40example@nas/Media@/mnt/d:ro")
users := map[string]string{}
for _, s := range sources {
users[s.Name] = s.User
if s.Host == "" || s.Share == "" {
t.Errorf("%s: %+v", s.Name, s)
}
}
if len(problems) != 0 || users["a"] != "root" || users["b"] != "" || users["c"] != `WORKGROUP\me` || users["d"] != "me@example" {
t.Errorf("users %v problems %q", users, problems)
}
if users, problems := ParseSMBUsers(None); len(users) != 0 || len(problems) != 0 {
t.Errorf("none: %v %q", users, problems)
for what, entry := range map[string]string{
"a password in the URL": "e=smb://root:" + testPassword + "@nas/Media@/mnt/e",
"a password with a slash": "e=smb://root:a/" + testPassword + "@nas/Media@/mnt/e",
"a user with a space, encoded": "e=smb://a%20" + testPassword + "@nas/Media@/mnt/e",
"a bad percent escape": "e=smb://%zz" + testPassword + "@nas/Media@/mnt/e",
"no mount point, with a password": "smb://root:" + testPassword + "@nas/Media",
} {
got, problems := ParseSources(entry)
said := strings.Join(problems, " ")
if len(got) != 0 || len(problems) != 1 || strings.Contains(said, testPassword) {
t.Errorf("%s: %+v %q", what, got, said)
}
}
if _, problems := ParseSources("e=smb://root:" + testPassword + "@nas/Media@/mnt/e"); !strings.Contains(problems[0], "smb-password-<source>") {
t.Errorf("a password in the URL is said with where it goes: %q", problems)
}
}
@@ -687,7 +708,7 @@ func TestAWaitBesideAFaultIsUnhealthy(t *testing.T) {
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(ConfigFile, "sources=games=smb://root@192.0.2.76/Share@/mnt/games\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") ||
@@ -697,8 +718,8 @@ func TestAPasswordThatCannotBeReadIsAFaultSaidWithoutValueOrPath(t *testing.T) {
// 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",
"sources=games=smb://root@192.0.2.76/Share@/mnt/games\n",
"sources=games=smb://root@192.0.2.76/Share@/mnt/games\npasswords=relative/dir\n",
} {
g := newSystem(t)
g.write(ConfigFile, cfg)