From 6f78899827ed16ae6fed5577df041ec7159b67b9 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 10 Oct 2026 21:30:31 +0200 Subject: [PATCH] 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://@/), percent-decoded; a password there is refused unsaid, and a source without a user waits for sources (hq ADR 0283, issue 386). --- modules/mounts/README.md | 22 +++--- modules/mounts/cmd/mounts/apply.go | 14 ++-- modules/mounts/cmd/mounts/config.go | 87 ++++++++++++---------- modules/mounts/cmd/mounts/failed_test.go | 4 +- modules/mounts/cmd/mounts/manifest_test.go | 7 +- modules/mounts/cmd/mounts/verbs.go | 2 +- modules/mounts/cmd/mounts/write_test.go | 61 ++++++++++----- modules/mounts/module.json | 9 +-- 8 files changed, 117 insertions(+), 89 deletions(-) diff --git a/modules/mounts/README.md b/modules/mounts/README.md index b775de58..76fad833 100644 --- a/modules/mounts/README.md +++ b/modules/mounts/README.md @@ -12,7 +12,7 @@ provision `nfs-share`, which `nfs-server` provides on the machine that shares. - The packages `nfs-utils` and `cifs-utils`. - `/etc/mesh-mounts/mounts.conf`, placed by the mesh from the assignment: the settings `shares`, - `sources` and `smb-users`, the binding of `nfs-share` (`from`, the sharing machine, and `at`, where it is + and `sources`, the binding of `nfs-share` (`from`, the sharing machine, and `at`, where it is reached on the private network), the operator account, and where the module's secrets are placed. - One `.mount` and one `.automount` unit per share or source, in `/etc/systemd/system`, named as `systemd-escape --path` names the mount point and opened by the marker @@ -59,23 +59,23 @@ read-write share is mounted hard, so a write is never dropped while the server i ## Settings -All three are preferences whose default, `none`, names nothing (ADR 0262 allows no empty default). +Both are preferences whose default, `none`, names nothing (ADR 0262 allows no empty default). - `shares`: space-separated `name=mountpoint`, each optionally `:ro`. The name is the share's on the sharing machine; its path there comes from that machine over the bus, never from this setting. For example `library=/srv/library downloads=/srv/downloads:ro`. - `sources`: space-separated occasional sources, each optionally `:ro`: - `name=smb:///@` or `name=disk:@`. - 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 `=`. A device that needs a domain takes it in the username, as `DOMAIN\user` or `user@domain`: - the credentials file has no `domain=` line. + `name=smb://[@]/@` or `name=disk:@`. + For example `games=smb://root@192.0.2.76/Share@/mnt/games`. An SMB source's username is in its entry: it + is not a secret (novox/hq ADR 0283). It is percent-decoded, so a device that needs a domain takes it as + `WORKGROUP%5Cuser` (for `WORKGROUP\user`), and an `@` in it is written `%40`; it holds no space, comma or + `=`. A password never goes in the entry: `user:password@` is refused, saying so and never what was + written. An SMB entry that cannot be read is said without what it holds. 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. -All three are trusted settings: an agent proposes them (`settings propose mounts … --node `) and the +Both are trusted settings: an agent proposes them (`settings propose mounts … --node `) and the operator approves the change on their phone; it is set on that approval and pushed after (ADR 0277). ## The secrets @@ -154,8 +154,8 @@ password of more than one line), or when `watch` has not looked for two minutes. is absent is healthy, and said as not present since when. A source waiting for its username or password is not a fault. With nothing else found, the answer is -*waits*, with one wait per thing not given: the setting `smb-users` ("the username of the source games") -or the secret `smb-password-games` ("the password of the source games"). The node-engine states the +*waits*, with one wait per thing not given: the setting `sources` ("the username of the source games, in +its entry of sources") or the secret `smb-password-games` ("the password of the source games"). The node-engine states the resource `waiting`, the controller says it to the operator as needing them, naming the act, and the first-node gate does not fail a build for it. Beside a fault, the waits are said and the answer is unhealthy. diff --git a/modules/mounts/cmd/mounts/apply.go b/modules/mounts/cmd/mounts/apply.go index 65a73039..97cb80da 100644 --- a/modules/mounts/cmd/mounts/apply.go +++ b/modules/mounts/cmd/mounts/apply.go @@ -81,7 +81,8 @@ type ShareStatus struct { Why string `json:"why,omitempty"` MountUnit string `json:"mount_unit,omitempty"` AutoUnit string `json:"automount_unit,omitempty"` - // Waits is what a waiting source waits for: the setting smb-users, or its secret smb-password-. + // Waits is what a waiting source waits for: its username in the setting sources, or its secret + // smb-password-. Waits []health.Wait `json:"waits,omitempty"` } @@ -292,7 +293,7 @@ func (a *Applier) Look(ctx context.Context) Status { case "disk": pairs = append(pairs, DiskPair(s)) case "smb": - c, waits, why := a.login(cfg, s.Name) + c, waits, why := a.login(cfg, s) switch { case why != "": notSet[s.Name] = ShareStatus{Name: s.Name, Kind: "smb", Occasional: true, Mountpoint: s.Mountpoint, State: StateFailed, Why: why} @@ -707,12 +708,13 @@ func orInterval(d, otherwise time.Duration) time.Duration { // login is an SMB source's login, or what it waits for, or why it cannot be read. The password is read as // one line from the source's own secret; it is never said, and neither is anything the file holds. -func (a *Applier) login(cfg Config, source string) (SMBCredential, []health.Wait, string) { +func (a *Applier) login(cfg Config, s Source) (SMBCredential, []health.Wait, string) { var waits []health.Wait + source, user := s.Name, s.User part := "the source " + source - user, hasUser := cfg.SMBUsers[source] - if !hasUser { - waits = append(waits, health.Wait{Part: part, Setting: "smb-users", What: "the username of the source " + source}) + if user == "" { + waits = append(waits, health.Wait{Part: part, Setting: "sources", + What: "the username of the source " + source + ", in its entry of sources"}) } // Never a relative path: without the directory the mesh places the passwords in, nothing is read. if !filepath.IsAbs(cfg.Passwords) { diff --git a/modules/mounts/cmd/mounts/config.go b/modules/mounts/cmd/mounts/config.go index 1c085b05..1e8b1f13 100644 --- a/modules/mounts/cmd/mounts/config.go +++ b/modules/mounts/cmd/mounts/config.go @@ -6,11 +6,12 @@ package main // The mesh renders ConfigFile from the assignment: the settings `shares` and `sources`, and the binding // of the provision `nfs-share` (`from`, the machine that shares, and `at`, where it is reached). The // long-running process reads it as root and writes the mount units; the bundle reads it as the operator -// account to read what the sharing machine exports from the bus. Nothing in it is secret: an SMB source's username is the -// setting smb-users, and its password the module's own secret smb-password-, in a file only root reads. +// account to read what the sharing machine exports from the bus. Nothing in it is secret: an SMB source's username is in its +// entry of sources, and its password the module's own secret smb-password-, in a file only root reads. import ( "fmt" + "net/url" "path/filepath" "regexp" "strings" @@ -41,13 +42,12 @@ type Config struct { // reached on the private network (the binding's `at`). Server string `json:"server,omitempty"` At string `json:"at,omitempty"` - // SMBUsers is the setting `smb-users`: each SMB source's username. Passwords is the directory where - // the mesh places each source's password, `smb-password-.secret` (ADR 0283); State is the - // module's state directory, where the per-source credentials files are written. - SMBUsers map[string]string `json:"smb_users,omitempty"` - Passwords string `json:"passwords,omitempty"` - State string `json:"state,omitempty"` - Account string `json:"account,omitempty"` + // Passwords is the directory where the mesh places each SMB source's password, + // `smb-password-.secret` (ADR 0283); State is the module's state directory, where the + // per-source credentials files are written. + Passwords string `json:"passwords,omitempty"` + State string `json:"state,omitempty"` + Account string `json:"account,omitempty"` // Node is this machine's name: a share this machine serves itself is never mounted onto it. Node string `json:"node,omitempty"` // Problems are what in the file could not be read; each is said, and its share is left out. @@ -67,10 +67,13 @@ type Wanted struct { // Source is one occasional source: a device that is not a node and is not always there. type Source struct { - Name string `json:"name"` - Kind string `json:"kind"` // smb or disk - Host string `json:"host,omitempty"` - Share string `json:"share,omitempty"` + Name string `json:"name"` + Kind string `json:"kind"` // smb or disk + Host string `json:"host,omitempty"` + Share string `json:"share,omitempty"` + // User is an SMB source's username, from its entry (`smb://@/`), percent-decoded; + // empty when the entry names none, and the source then waits for it (ADR 0283). Not a secret. + User string `json:"user,omitempty"` UUID string `json:"uuid,omitempty"` Mountpoint string `json:"mountpoint"` ReadOnly bool `json:"read_only"` @@ -162,7 +165,11 @@ func ParseSources(v string) ([]Source, []string) { for _, e := range entries(v) { name, rest, ok := strings.Cut(e, "=") if !ok { - problems = append(problems, fmt.Sprintf("the source %q is not name=@", e)) + if strings.Contains(e, "://") { + problems = append(problems, "an entry of sources is not name=@") + } else { + problems = append(problems, fmt.Sprintf("the source %q is not name=@", e)) + } continue } if !nameRe.MatchString(name) { @@ -183,12 +190,13 @@ func ParseSources(v string) ([]Source, []string) { s := Source{Name: name, Mountpoint: point, ReadOnly: ro} switch { case strings.HasPrefix(where, "smb://"): - host, share, ok := strings.Cut(strings.TrimPrefix(where, "smb://"), "/") - if !ok || !hostRe.MatchString(host) || !shareRe.MatchString(share) { - problems = append(problems, fmt.Sprintf("the source %s: %q is not smb:///", name, where)) + // Nothing of an SMB entry is said back: it may carry a password where the user goes. + user, host, share, problem := parseSMB(strings.TrimPrefix(where, "smb://")) + if problem != "" { + problems = append(problems, fmt.Sprintf("the source %s: %s", name, problem)) continue } - s.Kind, s.Host, s.Share = "smb", host, share + s.Kind, s.User, s.Host, s.Share = "smb", user, host, share case strings.HasPrefix(where, "disk:"): uuid := strings.TrimPrefix(where, "disk:") if !uuidRe.MatchString(uuid) { @@ -231,8 +239,6 @@ func ParseConfig(text string) Config { c.Sources, p = ParseSources(kv["sources"]) c.Problems = append(c.Problems, p...) c.Unreadable = append(c.Unreadable, p...) - c.SMBUsers, p = ParseSMBUsers(kv["smb-users"]) - c.Problems = append(c.Problems, p...) c.Server, c.At, c.Passwords, c.State, c.Account = kv["server"], kv["at"], kv["passwords"], kv["state"], kv["account"] if c.Passwords == "" { c.Passwords = c.State @@ -270,7 +276,7 @@ func (c Config) ServerNode() string { return first } -// SMBCredential is one source's login: the username from the setting `smb-users`, the password from the +// SMBCredential is one source's login: the username from its entry of `sources`, the password from the // source's own secret `smb-password-` (novox/hq ADR 0283). type SMBCredential struct { Username string @@ -290,27 +296,28 @@ func PasswordFile(dir, source string) string { // would end the line early. var userRe = regexp.MustCompile(`^[^\s=,]{1,64}$`) -// ParseSMBUsers reads the setting `smb-users`: space-separated `name=username`, one per SMB source. Not a -// secret, so it is a setting (ADR 0283). What cannot be read is said, and only that entry is left out. -func ParseSMBUsers(v string) (map[string]string, []string) { - out := map[string]string{} - var problems []string - for _, e := range entries(v) { - name, user, ok := strings.Cut(e, "=") - switch { - case !ok: - problems = append(problems, fmt.Sprintf("the smb-users entry %q is not name=username", e)) - case !nameRe.MatchString(name): - problems = append(problems, fmt.Sprintf("the smb-users name %q is not lower-case letters, digits and dashes", name)) - case !userRe.MatchString(user): - problems = append(problems, fmt.Sprintf("the smb-users entry for %s is not a username without spaces, commas or =", name)) - case out[name] != "": - problems = append(problems, fmt.Sprintf("the source %s is named twice in smb-users; the first is kept", name)) - default: - out[name] = user +// parseSMB reads what follows smb:// in a source's entry: `[@]/`. The user is +// percent-decoded, so `DOMAIN%5Cuser` or a user holding `@` can be written; a password in its place +// (`user:pass@`) is refused, and no problem says anything of what was written. +func parseSMB(body string) (user, host, share, problem string) { + const form = "is not smb://[@]/" + if at := strings.LastIndex(body, "@"); at >= 0 { + raw := body[:at] + body = body[at+1:] + if strings.Contains(raw, ":") { + return "", "", "", "its entry carries a password in the URL; a password is given as the secret smb-password-, never in a setting" } + decoded, err := url.PathUnescape(raw) + if err != nil || !userRe.MatchString(decoded) { + return "", "", "", "its user is not a username without spaces, commas or = (percent-encode a \\ or an @ in it)" + } + user = decoded } - return out, problems + host, share, ok := strings.Cut(body, "/") + if !ok || !hostRe.MatchString(host) || !shareRe.MatchString(share) { + return "", "", "", form + } + return user, host, share, "" } // ReadPassword reads a source's password secret as one line, as typed at the desk's hidden prompt (ADR diff --git a/modules/mounts/cmd/mounts/failed_test.go b/modules/mounts/cmd/mounts/failed_test.go index 72843c75..38431990 100644 --- a/modules/mounts/cmd/mounts/failed_test.go +++ b/modules/mounts/cmd/mounts/failed_test.go @@ -41,9 +41,9 @@ func TestTheFailureOfTheUnitTheModuleTakesOverIsCleared(t *testing.T) { func TestAMountThatFailedBecauseItsSourceWasAwayIsCleared(t *testing.T) { f := newSystem(t) - f.write(ConfigFile, "shares=library=/srv/library\nsources=games=smb://192.0.2.76/Share@/mnt/games\n"+ + f.write(ConfigFile, "shares=library=/srv/library\nsources=games=smb://root@192.0.2.76/Share@/mnt/games\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") f.write(ExportsFile, exportsAnswer(t, library)) f.write("/var/lib/mesh/mounts/smb-password-games.secret", testPassword+"\n") a := f.applier(time.Date(2026, 10, 9, 14, 0, 0, 0, time.UTC)) diff --git a/modules/mounts/cmd/mounts/manifest_test.go b/modules/mounts/cmd/mounts/manifest_test.go index 9e61ac99..ea95543a 100644 --- a/modules/mounts/cmd/mounts/manifest_test.go +++ b/modules/mounts/cmd/mounts/manifest_test.go @@ -91,7 +91,10 @@ func TestTheManifestNamesWhatTheBundleServesAndWhatItNeeds(t *testing.T) { if got := PasswordFile("${dir:state}", "games"); got != strings.Replace(sec["path"], "*", "games", 1) { t.Errorf("the process reads %s, the mesh places %s", got, sec["path"]) } - for _, key := range []string{"shares", "sources", "smb-users"} { + if _, has := m.Settings["smb-users"]; has || len(m.Settings) != 2 { + t.Errorf("an SMB source's username is in its entry of sources, not a setting of its own: %v", m.Settings) + } + for _, key := range []string{"shares", "sources"} { s := m.Settings[key] if s["kind"] != "preference" || s["default"] != None || s["why"] == "" || !strings.Contains(raw, "${setting:"+key+"}") { t.Errorf("setting %s: %v", key, s) @@ -139,7 +142,7 @@ func TestTheProcessesAreRootAndJudgedByTheirUnitAndTheHealthTool(t *testing.T) { if len(cfg.Problems) != 0 && !strings.Contains(strings.Join(cfg.Problems, " "), "${") { t.Errorf("the config's template reads: %v", cfg.Problems) } - for _, want := range []string{"server=${bound:nfs-share:from}", "at=${bound:nfs-share:at}", "passwords=${dir:state}", "smb-users=${setting:smb-users}"} { + for _, want := range []string{"server=${bound:nfs-share:from}", "at=${bound:nfs-share:at}", "passwords=${dir:state}"} { if !strings.Contains(content, want) { t.Errorf("the config lacks %s", want) } diff --git a/modules/mounts/cmd/mounts/verbs.go b/modules/mounts/cmd/mounts/verbs.go index ceda87dd..12d07c29 100644 --- a/modules/mounts/cmd/mounts/verbs.go +++ b/modules/mounts/cmd/mounts/verbs.go @@ -146,7 +146,7 @@ func Verbs(m *Machine) []stdio.Tool { "unhealthy when a share that should always be there has been unreachable or failed for five minutes, an automount " + "is not armed, something the settings name cannot be set up (a unit file that is not the module's), or the " + "module's process has not looked for two minutes; with why, one sentence per finding. An occasional source that " + - "is absent is healthy, and said as not present since when. A source whose username (the setting smb-users) or " + + "is absent is healthy, and said as not present since when. A source whose username (in its entry of the setting sources) or " + "password (the secret smb-password-) is not given waits for the operator: with nothing else found the " + "answer is waits, naming each, never unhealthy. (r)", Run: func(map[string]any) (any, error) { return m.Health(time.Now()), nil }, diff --git a/modules/mounts/cmd/mounts/write_test.go b/modules/mounts/cmd/mounts/write_test.go index bd9e7b1d..24a3002a 100644 --- a/modules/mounts/cmd/mounts/write_test.go +++ b/modules/mounts/cmd/mounts/write_test.go @@ -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-") { + 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) diff --git a/modules/mounts/module.json b/modules/mounts/module.json index fa9ccf91..108133c9 100644 --- a/modules/mounts/module.json +++ b/modules/mounts/module.json @@ -47,12 +47,7 @@ "sources": { "kind": "preference", "default": "none", - "why": "a machine has no occasional source until its assignment names one: space-separated name=smb:///@ or name=disk:@, each optionally :ro (hq ADR 0263 rule 6)" - }, - "smb-users": { - "kind": "preference", - "default": "none", - "why": "the username each SMB source logs in with: space-separated name=username; not a secret, so a setting, beside each source's password in its own secret smb-password- (hq ADR 0283)" + "why": "a machine has no occasional source until its assignment names one: space-separated name=smb://[@]/@ or name=disk:@, each optionally :ro (hq ADR 0263 rule 6); an SMB source's username is in its entry, its password the secret smb-password- (hq ADR 0283)" } }, "own-secrets": { @@ -89,7 +84,7 @@ "type": "file", "path": "/etc/mesh-mounts/mounts.conf", "mode": "0644", - "content": "# Written by the mesh (module mounts, novox/hq ADR 0263) from this machine's assignment: the settings\n# shares, sources and smb-users, and the binding of nfs-share. Replaced on every push; change the settings, not this.\n# The module's process reads it as root every 30 seconds and writes one .mount and .automount per entry.\nshares=${setting:shares}\nsources=${setting:sources}\nsmb-users=${setting:smb-users}\nserver=${bound:nfs-share:from}\nat=${bound:nfs-share:at}\naccount=${machine:account}\nnode=${machine:name}\npasswords=${dir:state}\nstate=${dir:state}\n" + "content": "# Written by the mesh (module mounts, novox/hq ADR 0263) from this machine's assignment: the settings\n# shares and sources, and the binding of nfs-share. Replaced on every push; change the settings, not this.\n# The module's process reads it as root every 30 seconds and writes one .mount and .automount per entry.\nshares=${setting:shares}\nsources=${setting:sources}\nserver=${bound:nfs-share:from}\nat=${bound:nfs-share:at}\naccount=${machine:account}\nnode=${machine:name}\npasswords=${dir:state}\nstate=${dir:state}\n" }, { "id": "bus-dir-parent",