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:
+11
-11
@@ -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://<host>/<share>@<mountpoint>` or `name=disk:<filesystem uuid>@<mountpoint>`.
|
||||
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://[<user>@]<host>/<share>@<mountpoint>` or `name=disk:<filesystem uuid>@<mountpoint>`.
|
||||
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 <node>`) and the
|
||||
Both are trusted settings: an agent proposes them (`settings propose mounts … --node <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.
|
||||
|
||||
@@ -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-<source>.
|
||||
// Waits is what a waiting source waits for: its username in the setting sources, or its secret
|
||||
// smb-password-<source>.
|
||||
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) {
|
||||
|
||||
@@ -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-<source>, 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-<source>, 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-<source>.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-<source>.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://<user>@<host>/<share>`), 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=<where>@<mountpoint>", e))
|
||||
if strings.Contains(e, "://") {
|
||||
problems = append(problems, "an entry of sources is not name=<where>@<mountpoint>")
|
||||
} else {
|
||||
problems = append(problems, fmt.Sprintf("the source %q is not name=<where>@<mountpoint>", 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://<host>/<share>", 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-<source>` (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: `[<user>@]<host>/<share>`. 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://[<user>@]<host>/<share>"
|
||||
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-<source>, 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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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-<source>) 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 },
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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://<host>/<share>@<mountpoint> or name=disk:<uuid>@<mountpoint>, 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-<source> (hq ADR 0283)"
|
||||
"why": "a machine has no occasional source until its assignment names one: space-separated name=smb://[<user>@]<host>/<share>@<mountpoint> or name=disk:<uuid>@<mountpoint>, each optionally :ro (hq ADR 0263 rule 6); an SMB source's username is in its entry, its password the secret smb-password-<name> (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",
|
||||
|
||||
Reference in New Issue
Block a user