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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user