brother: the admin process becomes the device process, brother-device, with device-asks and device-answers (issue 1453, ADR 0468 clause 1)
This commit is contained in:
@@ -59,7 +59,7 @@ makes it (mode 0755) when it files the first scan.
|
||||
| verb (tool) | does |
|
||||
|---|---|
|
||||
| `printer.status` | without a login: over IPP (`Get-Printer-Attributes`, encoded by hand, on port 631) the state (idle, processing, stopped), the state reasons (such as `toner-low-warning`) and the markers' names, levels (percent; below 0 is unknown) and colours; over eSCL the scanner's state and its feeder's. A part that does not answer is `answers: false` with why. Needs `printer` and `scanner` |
|
||||
| `printer.settings` | asks the admin process (below) to log in, read one admin page and log out, and waits up to 60 s for its answer. Argument `page`: a path on the admin site, starting with `/` and ending in `.html` (default `/general/status.html`). Answers the page's title, links (href and text) and forms with their fields (name, type, value, label); a password field's value is never given. Read-only: it sends the login and the logout and nothing else |
|
||||
| `printer.settings` | asks the device process (below) to log in, read one admin page and log out, and waits up to 60 s for its answer. Argument `page`: a path on the admin site, starting with `/` and ending in `.html` (default `/general/status.html`). Answers the page's title, links (href and text) and forms with their fields (name, type, value, label); a password field's value is never given. Read-only: it sends the login and the logout and nothing else |
|
||||
|
||||
### Trust and the password
|
||||
|
||||
@@ -69,18 +69,18 @@ to it; the error names both fingerprints. Get the fingerprint from the device's
|
||||
upper-case hex pairs joined by colons.
|
||||
|
||||
The admin password is the module's own secret `admin-password` (ADR 0467 clause 4; ADR 0283): the operator gives it
|
||||
at their desk, and the mesh never generates or rotates it. It is owned by the account `brother-admin`, which the
|
||||
at their desk, and the mesh never generates or rotates it. It is owned by the account `brother-device`, which the
|
||||
module makes, because the tools run as the operator account and must never read it (ADR 0436 §1, §12; the shape of
|
||||
mesh-mail's sender). The file is `${dir:state}/admin-password.secret`, mode 0600, and `BROTHER_ADMIN_PASSWORD` names
|
||||
it for the admin process alone; the tools' bundle is given no such name, and `admin_boundary_test.go` checks that
|
||||
it for the device process alone; the tools' bundle is given no such name, and `admin_boundary_test.go` checks that
|
||||
no tool file imports the package that reads it. While it is not given, `printer.settings` answers that the secret
|
||||
is not given, and the printer is not contacted.
|
||||
|
||||
### The admin process
|
||||
### The device process
|
||||
|
||||
`brother admin` runs as `brother-admin` with a bus account of its own (the own secret `broker-admin`), never a
|
||||
seat. It consumes the work-queue stream `printer-asks` (event `printer.asked`, body `id`, `page`, `asked-at`) and
|
||||
writes each answer, once, into the state `printer-answers` under the ask's id (ten minutes' life): the page's
|
||||
`brother device` runs as `brother-device` with a bus account of its own (the own secret `broker-device`), never a
|
||||
seat. It consumes the work-queue stream `device-asks` (event `device.asked`, body `id`, `page`, `asked-at`) and
|
||||
writes each answer, once, into the state `device-answers` under the ask's id (ten minutes' life): the page's
|
||||
title, links and form fields, or `error`. It alone reads the password, pins the certificate, logs in, reads the
|
||||
page and logs out. An ask older than two minutes is dropped without contacting the printer; at most three
|
||||
deliveries. It reads `settings.json` (mode 0644, no secret in it) and the state directory is 0711 so that it can
|
||||
|
||||
+9
-9
@@ -1,7 +1,7 @@
|
||||
package main
|
||||
|
||||
// The tools never open the admin password (novox/hq ADR 0436 §12, ADR 0467 clause 4): only the admin process
|
||||
// reads it, as its own account, brother-admin. The build seat runs every test under one account, so no
|
||||
// The tools never open the admin password (novox/hq ADR 0436 §12, ADR 0467 clause 4): only the device process
|
||||
// reads it, as its own account, brother-device. The build seat runs every test under one account, so no
|
||||
// permission test can show the split; this is the file-level proxy mesh-mail's secret_boundary_test.go uses
|
||||
// instead: an AST check that the tools' files import no password-reading package and name no password file.
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
const secretPackage = "brother/cmd/brother/internal/secret"
|
||||
|
||||
// toolFiles are the files the tools' bundle runs (the scanner's, the printer.status's and the printer.settings'
|
||||
// ask), as opposed to the admin process's: adminproc.go, admin.go, pin.go and html.go.
|
||||
// ask), as opposed to the device process's: deviceproc.go, admin.go, pin.go and html.go.
|
||||
var toolFiles = []string{"main.go", "printer.go", "scan.go", "settings.go", "ipp.go", "escl.go", "file.go", "crop.go", "pdf.go"}
|
||||
|
||||
// 1453: none of the tools' files imports the password-reading package or names the password's file.
|
||||
@@ -29,7 +29,7 @@ func TestTheToolsNeverOpenTheAdminPassword(t *testing.T) {
|
||||
}
|
||||
for _, imp := range f.Imports {
|
||||
if strings.Trim(imp.Path.Value, `"`) == secretPackage {
|
||||
t.Errorf("%s imports %s, which only the admin process may (ADR 0436 §12)", file, secretPackage)
|
||||
t.Errorf("%s imports %s, which only the device process may (ADR 0436 §12)", file, secretPackage)
|
||||
}
|
||||
}
|
||||
src, err := os.ReadFile(file)
|
||||
@@ -37,16 +37,16 @@ func TestTheToolsNeverOpenTheAdminPassword(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(src), `Getenv("BROTHER_ADMIN_PASSWORD")`) {
|
||||
t.Errorf("%s reads BROTHER_ADMIN_PASSWORD, the admin process's alone", file)
|
||||
t.Errorf("%s reads BROTHER_ADMIN_PASSWORD, the device process's alone", file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1453: the admin process does read it through the package, and the package is the one place a password file is
|
||||
// 1453: the device process does read it through the package, and the package is the one place a password file is
|
||||
// opened.
|
||||
func TestTheAdminProcessReadsThePasswordThroughThePackage(t *testing.T) {
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, "adminproc.go", nil, parser.ImportsOnly)
|
||||
f, err := parser.ParseFile(fset, "deviceproc.go", nil, parser.ImportsOnly)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -57,9 +57,9 @@ func TestTheAdminProcessReadsThePasswordThroughThePackage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("adminproc.go does not import %s", secretPackage)
|
||||
t.Errorf("deviceproc.go does not import %s", secretPackage)
|
||||
}
|
||||
for _, file := range append(append([]string{}, toolFiles...), "admin.go", "adminproc.go", "pin.go", "html.go") {
|
||||
for _, file := range append(append([]string{}, toolFiles...), "admin.go", "deviceproc.go", "pin.go", "html.go") {
|
||||
src, _ := os.ReadFile(file)
|
||||
if strings.Contains(string(src), "os.ReadFile") && file != "settings.go" && file != "file.go" && file != "escl.go" {
|
||||
t.Errorf("%s reads a file: only settings.json (settings.go) and the scan folder's PDFs are read outside the secret package", file)
|
||||
+8
-8
@@ -2,9 +2,9 @@
|
||||
|
||||
package main
|
||||
|
||||
// The admin process over a real bus (novox/hq ADR 0467 clause 4; the shape of mesh-mail's
|
||||
// The device process over a real bus (novox/hq ADR 0467 clause 4; the shape of mesh-mail's
|
||||
// sender_integration_test.go, ADR 0436 §7): a JetStream server of the test's own, the work-queue stream
|
||||
// printer-asks and its consumer, the state bucket printer-answers, and a fake printer. An ask published on the
|
||||
// device-asks and its consumer, the state bucket device-answers, and a fake printer. An ask published on the
|
||||
// stream is answered in the state under its id and acknowledged. Compiled by the merge gate's
|
||||
// `go vet -tags integration`; never run on this machine.
|
||||
|
||||
@@ -46,7 +46,7 @@ func newTestBus(t *testing.T) string {
|
||||
return s.ClientURL()
|
||||
}
|
||||
|
||||
// 1453: an ask on printer-asks is answered under its id in printer-answers by the admin process, over the
|
||||
// 1453: an ask on device-asks is answered under its id in device-answers by the device process, over the
|
||||
// printer's pinned HTTPS and the login, and acknowledged; stopping it leaves nothing in hand.
|
||||
func TestTheAdminProcessAnswersAnAskOverARealBus(t *testing.T) {
|
||||
url := newTestBus(t)
|
||||
@@ -64,7 +64,7 @@ func TestTheAdminProcessAnswersAnAskOverARealBus(t *testing.T) {
|
||||
Retention: jetstream.WorkQueuePolicy, Discard: jetstream.DiscardNew, MaxAge: 24 * time.Hour, MaxBytes: 8 << 20}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
account := process.Account{URL: url, Node: "g14", Module: "brother", User: "g14.brother." + AdminProcessID, Password: "pw"}
|
||||
account := process.Account{URL: url, Node: "g14", Module: "brother", User: "g14.brother." + DeviceProcessID, Password: "pw"}
|
||||
if _, err := js.CreateConsumer(ctx, AsksStream, jetstream.ConsumerConfig{Durable: account.QueueConsumerName(),
|
||||
AckPolicy: jetstream.AckExplicitPolicy, AckWait: 30 * time.Second, MaxDeliver: 3}); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -80,14 +80,14 @@ func TestTheAdminProcessAnswersAnAskOverARealBus(t *testing.T) {
|
||||
os.WriteFile(settings, []byte(`{"printer":"`+f.host()+`","printer-certificate":"`+f.print()+`"}`), 0o600)
|
||||
pwFile := filepath.Join(dir, "admin-password.secret")
|
||||
os.WriteFile(pwFile, []byte("pw\n"), 0o600)
|
||||
w := &adminWorker{
|
||||
w := &deviceWorker{
|
||||
Settings: func() (Settings, error) { return LoadSettings(settings) },
|
||||
Password: func() (string, error) { return secret.Read(pwFile) },
|
||||
Now: time.Now,
|
||||
}
|
||||
runCtx, cancel := context.WithCancel(ctx)
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- runAdmin(runCtx, account, w) }()
|
||||
go func() { done <- runDevice(runCtx, account, w) }()
|
||||
|
||||
body, _ := json.Marshal(Ask{ID: "ask-1", Page: "/general/other.html", At: time.Now()})
|
||||
if _, err := js.Publish(ctx, printerAskedSubject, body, jetstream.WithMsgID("ask-1")); err != nil {
|
||||
@@ -111,10 +111,10 @@ func TestTheAdminProcessAnswersAnAskOverARealBus(t *testing.T) {
|
||||
select {
|
||||
case err := <-done:
|
||||
if err == nil {
|
||||
t.Fatal("runAdmin returned nil, want ctx.Err() once its context was cancelled")
|
||||
t.Fatal("runDevice returned nil, want ctx.Err() once its context was cancelled")
|
||||
}
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Fatal("runAdmin did not stop once its context was cancelled")
|
||||
t.Fatal("runDevice did not stop once its context was cancelled")
|
||||
}
|
||||
cons, err := js.Consumer(ctx, AsksStream, account.QueueConsumerName())
|
||||
if err != nil {
|
||||
+24
-24
@@ -1,9 +1,9 @@
|
||||
package main
|
||||
|
||||
// mesh brother admin (novox/hq ADR 0467 clause 4, in the shape of ADR 0436 §7): the admin process, a process
|
||||
// with a bus account of its own (brother-admin), never a seat. It alone of the module's parts reads the admin
|
||||
// password and logs in to the printer's admin page; the tools queue an ask on the work-queue stream printer-asks
|
||||
// and read its answer in the state printer-answers. The admin password is never in an answer, an event or a log.
|
||||
// mesh brother device (novox/hq ADR 0467 clause 4, in the shape of ADR 0436 §7): the device process, a process
|
||||
// with a bus account of its own (brother-device), never a seat. It alone of the module's parts reads the admin
|
||||
// password and logs in to the printer's admin page; the tools queue an ask on the work-queue stream device-asks
|
||||
// and read its answer in the state device-answers. The admin password is never in an answer, an event or a log.
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -35,9 +35,9 @@ const (
|
||||
actionTerm
|
||||
)
|
||||
|
||||
// adminWorker answers the asks. Settings and the password are read at each ask, so a changed config value or a
|
||||
// deviceWorker answers the asks. Settings and the password are read at each ask, so a changed config value or a
|
||||
// password given later is the next ask's.
|
||||
type adminWorker struct {
|
||||
type deviceWorker struct {
|
||||
Settings func() (Settings, error)
|
||||
Password func() (string, error)
|
||||
Answers Answers
|
||||
@@ -45,7 +45,7 @@ type adminWorker struct {
|
||||
Logf func(format string, a ...any)
|
||||
}
|
||||
|
||||
func (w *adminWorker) logf(format string, a ...any) {
|
||||
func (w *deviceWorker) logf(format string, a ...any) {
|
||||
if w.Logf != nil {
|
||||
w.Logf(format, a...)
|
||||
}
|
||||
@@ -55,14 +55,14 @@ func (w *adminWorker) logf(format string, a ...any) {
|
||||
// for is acknowledged and the printer not contacted; every other outcome, a failure included, is written as the
|
||||
// answer under the ask's id and the message acknowledged; only an answer that could not be written is
|
||||
// redelivered (the bus's limit ends that).
|
||||
func (w *adminWorker) Handle(body []byte) (action, time.Duration) {
|
||||
func (w *deviceWorker) Handle(body []byte) (action, time.Duration) {
|
||||
var ask Ask
|
||||
if err := json.Unmarshal(body, &ask); err != nil || ask.ID == "" {
|
||||
w.logf("[brother] admin: a queued ask could not be read: %v", err)
|
||||
w.logf("[brother] device: a queued ask could not be read: %v", err)
|
||||
return actionTerm, 0
|
||||
}
|
||||
if w.Now().Sub(ask.At) > StaleAfter {
|
||||
w.logf("[brother] admin: ask %s is %s old; nobody waits for it, the printer is not contacted", ask.ID, w.Now().Sub(ask.At).Round(time.Second))
|
||||
w.logf("[brother] device: ask %s is %s old; nobody waits for it, the printer is not contacted", ask.ID, w.Now().Sub(ask.At).Round(time.Second))
|
||||
return actionAck, 0
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
@@ -74,7 +74,7 @@ func (w *adminWorker) Handle(body []byte) (action, time.Duration) {
|
||||
answer = page
|
||||
}
|
||||
if _, err := w.Answers.Create(ask.ID, answer); err != nil && !errors.Is(err, stdio.ErrChanged) {
|
||||
w.logf("[brother] admin: the answer to %s could not be written: %v", ask.ID, err)
|
||||
w.logf("[brother] device: the answer to %s could not be written: %v", ask.ID, err)
|
||||
return actionRetry, 5 * time.Second
|
||||
}
|
||||
return actionAck, 0
|
||||
@@ -83,7 +83,7 @@ func (w *adminWorker) Handle(body []byte) (action, time.Duration) {
|
||||
// read logs in, reads one page of the admin site and logs out. It is read-only: the login (and the logout) are
|
||||
// the only things it sends, and a password field's value is never in the answer. The printer is not contacted
|
||||
// before the settings and the password are known to be given (1453/AC4).
|
||||
func (w *adminWorker) read(ctx context.Context, page string) (map[string]any, error) {
|
||||
func (w *deviceWorker) read(ctx context.Context, page string) (map[string]any, error) {
|
||||
if page == "" {
|
||||
page = DefaultPage
|
||||
}
|
||||
@@ -143,40 +143,40 @@ func nonNilForms(f []Form) []Form {
|
||||
return f
|
||||
}
|
||||
|
||||
// admin is `brother admin`: it reads its own bus account, connects, and consumes printer-asks until stopped.
|
||||
// admin is `brother device`: it reads its own bus account, connects, and consumes device-asks until stopped.
|
||||
// It refuses to start naming what is missing when the account cannot be read, and never logs a password.
|
||||
func admin() int {
|
||||
func device() int {
|
||||
settings, passwordFile, brokerFile := os.Getenv("BROTHER_SETTINGS"), os.Getenv("BROTHER_ADMIN_PASSWORD"), os.Getenv("BROTHER_BROKER")
|
||||
if brokerFile == "" {
|
||||
fmt.Fprintln(os.Stderr, "[brother] admin was started without BROTHER_BROKER, its bus account's file")
|
||||
fmt.Fprintln(os.Stderr, "[brother] device was started without BROTHER_BROKER, its bus account's file")
|
||||
return 2
|
||||
}
|
||||
account, err := process.ReadAccount(brokerFile)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[brother] admin: %v\n", err)
|
||||
fmt.Fprintf(os.Stderr, "[brother] device: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
w := &adminWorker{
|
||||
w := &deviceWorker{
|
||||
Settings: func() (Settings, error) { return LoadSettings(settings) },
|
||||
Password: func() (string, error) { return secret.Read(passwordFile) },
|
||||
Now: time.Now,
|
||||
Logf: func(f string, a ...any) { fmt.Fprintf(os.Stderr, f+"\n", a...) },
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "[brother] admin is consuming %s as %s\n", AsksStream, account.User)
|
||||
err = runAdmin(ctx, account, w)
|
||||
fmt.Fprintf(os.Stderr, "[brother] device is consuming %s as %s\n", AsksStream, account.User)
|
||||
err = runDevice(ctx, account, w)
|
||||
if err != nil && ctx.Err() == nil {
|
||||
fmt.Fprintf(os.Stderr, "[brother] admin stopped: %v\n", err)
|
||||
fmt.Fprintf(os.Stderr, "[brother] device stopped: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// runAdmin connects as the admin process's own account and consumes printer-asks until ctx ends, answering each
|
||||
// ask in the state printer-answers on that connection and then settling the message as Handle says. Apart from
|
||||
// runDevice connects as the device process's own account and consumes device-asks until ctx ends, answering each
|
||||
// ask in the state device-answers on that connection and then settling the message as Handle says. Apart from
|
||||
// admin so a test can drive it over a bus of its own.
|
||||
func runAdmin(ctx context.Context, account process.Account, w *adminWorker) error {
|
||||
func runDevice(ctx context.Context, account process.Account, w *deviceWorker) error {
|
||||
conn, err := process.Connect(account)
|
||||
if err != nil {
|
||||
return fmt.Errorf("the bus cannot be reached: %w", err)
|
||||
@@ -195,7 +195,7 @@ func runAdmin(ctx context.Context, account process.Account, w *adminWorker) erro
|
||||
aerr = msg.Term()
|
||||
}
|
||||
if aerr != nil {
|
||||
w.logf("[brother] admin: settling %s: %v", msg.Subject, aerr)
|
||||
w.logf("[brother] device: settling %s: %v", msg.Subject, aerr)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) > 1 && os.Args[1] == "admin" {
|
||||
os.Exit(admin())
|
||||
if len(os.Args) > 1 && os.Args[1] == "device" {
|
||||
os.Exit(device())
|
||||
}
|
||||
if err := stdio.Serve("", tools(os.Getenv("BROTHER_SETTINGS"), emit, time.Now)); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
@@ -54,7 +54,7 @@ func tools(settingsFile string, say Emitter, now func() time.Time) []stdio.Tool
|
||||
},
|
||||
{
|
||||
Name: "printer.settings",
|
||||
Description: "Read one page of the printer's admin site, read-only: the module's admin process logs in with the " +
|
||||
Description: "Read one page of the printer's admin site, read-only: the module's device process logs in with the " +
|
||||
"secret admin-password (the operator's; it trusts the printer's pinned certificate alone), gets the page and logs " +
|
||||
"out; this tool asks it and waits up to a minute for its answer. Answers the " +
|
||||
"page's title, its links (href and text) and its forms with their fields (name, type, value, label); a " +
|
||||
|
||||
@@ -190,9 +190,9 @@ func resource(t *testing.T, m manifest, id string) map[string]any {
|
||||
}
|
||||
|
||||
// 1453/AC4 (ADR 0467 clause 4, ADR 0436 §1): the admin password is the module's own secret, issued by the
|
||||
// operator (never generated), and owned by an account the module makes, brother-admin: the tools' runner
|
||||
// (the operator account) cannot read it, the admin process (that account) can. The process's bus account is
|
||||
// the own secret broker-admin, owned by the same account.
|
||||
// operator (never generated), and owned by an account the module makes, brother-device: the tools' runner
|
||||
// (the operator account) cannot read it, the device process (that account) can. The process's bus account is
|
||||
// the own secret broker-device, owned by the same account.
|
||||
func TestTheAdminPasswordIsTheOperatorsOwnSecretOwnedByTheAdminAccount(t *testing.T) {
|
||||
m := readManifest(t)
|
||||
s, ok := m.OwnSecrets["admin-password"]
|
||||
@@ -202,35 +202,35 @@ func TestTheAdminPasswordIsTheOperatorsOwnSecretOwnedByTheAdminAccount(t *testin
|
||||
if s["issued-by"] != "outside" {
|
||||
t.Errorf("issued-by = %v, want outside: the mesh never generates the device's password", s["issued-by"])
|
||||
}
|
||||
if s["owner"] != "brother-admin" {
|
||||
t.Errorf("owner = %v, want brother-admin (never root or ${machine:account}: ADR 0436 §1)", s["owner"])
|
||||
if s["owner"] != "brother-device" {
|
||||
t.Errorf("owner = %v, want brother-device (never root or ${machine:account}: ADR 0436 §1)", s["owner"])
|
||||
}
|
||||
if p, _ := s["prompt"].(string); p != "the admin password of the Brother printer" {
|
||||
t.Errorf("prompt = %q", p)
|
||||
}
|
||||
if b := m.OwnSecrets["broker-admin"]; b["owner"] != "brother-admin" || b["path"] != "${dir:state}/broker-admin.secret" {
|
||||
t.Errorf("broker-admin = %v", b)
|
||||
if b := m.OwnSecrets["broker-device"]; b["owner"] != "brother-device" || b["path"] != "${dir:state}/broker-device.secret" {
|
||||
t.Errorf("broker-device = %v", b)
|
||||
}
|
||||
acct := resource(t, m, "account")
|
||||
if acct["type"] != "user" || acct["name"] != "brother-admin" || acct["shell"] != "/usr/bin/nologin" {
|
||||
if acct["type"] != "user" || acct["name"] != "brother-device" || acct["shell"] != "/usr/bin/nologin" {
|
||||
t.Errorf("account = %v", acct)
|
||||
}
|
||||
}
|
||||
|
||||
// 1453 (ADR 0467 clause 4): the admin process runs as brother-admin with the bus access of ADR 0436 §7: it
|
||||
// consumes the work queue printer-asks and writes the state printer-answers, and is judged by its unit. The
|
||||
// 1453 (ADR 0467 clause 4): the device process runs as brother-device with the bus access of ADR 0436 §7: it
|
||||
// consumes the work queue device-asks and writes the state device-answers, and is judged by its unit. The
|
||||
// tools' bundle is given no password file.
|
||||
func TestTheAdminProcessIsDeclaredAsMeshMailsSenderIs(t *testing.T) {
|
||||
m := readManifest(t)
|
||||
p := resource(t, m, "admin")
|
||||
if p["type"] != "process" || p["name"] != "brother-admin" || p["user"] != "brother-admin" || p["artifact"] != "tools" {
|
||||
p := resource(t, m, "device")
|
||||
if p["type"] != "process" || p["name"] != "brother-device" || p["user"] != "brother-device" || p["artifact"] != "tools" {
|
||||
t.Errorf("process = %v", p)
|
||||
}
|
||||
if !reflect.DeepEqual(p["run"], []any{"./brother", "admin"}) {
|
||||
if !reflect.DeepEqual(p["run"], []any{"./brother", "device"}) {
|
||||
t.Errorf("run = %v", p["run"])
|
||||
}
|
||||
bus, _ := p["bus"].(map[string]any)
|
||||
if !reflect.DeepEqual(bus["queues"], []any{"printer-asks"}) || !reflect.DeepEqual(bus["state"], []any{"printer-answers"}) {
|
||||
if !reflect.DeepEqual(bus["queues"], []any{"device-asks"}) || !reflect.DeepEqual(bus["state"], []any{"device-answers"}) {
|
||||
t.Errorf("bus = %v", bus)
|
||||
}
|
||||
if h, _ := p["health"].(map[string]any); h["kind"] != "unit" {
|
||||
@@ -240,7 +240,7 @@ func TestTheAdminProcessIsDeclaredAsMeshMailsSenderIs(t *testing.T) {
|
||||
want := map[string]any{
|
||||
"BROTHER_SETTINGS": "${dir:state}/settings.json",
|
||||
"BROTHER_ADMIN_PASSWORD": m.OwnSecrets["admin-password"]["path"],
|
||||
"BROTHER_BROKER": m.OwnSecrets["broker-admin"]["path"],
|
||||
"BROTHER_BROKER": m.OwnSecrets["broker-device"]["path"],
|
||||
}
|
||||
if !reflect.DeepEqual(env, want) {
|
||||
t.Errorf("env = %v, want %v", env, want)
|
||||
@@ -260,7 +260,7 @@ func TestTheAdminProcessIsDeclaredAsMeshMailsSenderIs(t *testing.T) {
|
||||
t.Errorf("state mode = %v: the admin account must reach its files in it", r["mode"])
|
||||
}
|
||||
for _, r := range m.Resources {
|
||||
if r["id"] != "admin" {
|
||||
if r["id"] != "device" {
|
||||
if r["takes-config"] != nil {
|
||||
for _, k := range r["takes-config"].([]any) {
|
||||
if k == "admin-password" {
|
||||
@@ -272,7 +272,7 @@ func TestTheAdminProcessIsDeclaredAsMeshMailsSenderIs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 1453 (ADR 0467 clause 4): the stream printer-asks is a work queue like mesh-mail's mail-sends, and the
|
||||
// 1453 (ADR 0467 clause 4): the stream device-asks is a work queue like mesh-mail's mail-sends, and the
|
||||
// answers are a state that expires.
|
||||
func TestThePrinterAsksStreamIsAWorkQueue(t *testing.T) {
|
||||
raw, _ := os.ReadFile(filepath.Join("..", "..", "module.json"))
|
||||
@@ -287,7 +287,7 @@ func TestThePrinterAsksStreamIsAWorkQueue(t *testing.T) {
|
||||
t.Fatalf("streams = %v", m.Streams)
|
||||
}
|
||||
st := m.Streams[0]
|
||||
if st["name"] != "printer-asks" || st["retention"] != "work-queue" || st["max-deliveries"] != float64(3) ||
|
||||
if st["name"] != "device-asks" || st["retention"] != "work-queue" || st["max-deliveries"] != float64(3) ||
|
||||
!reflect.DeepEqual(st["events"], []any{EventAsked}) {
|
||||
t.Errorf("stream = %v", st)
|
||||
}
|
||||
|
||||
@@ -20,24 +20,24 @@ var pagePattern = regexp.MustCompile(`^/[A-Za-z0-9_./-]*\.html$`)
|
||||
// DefaultPage is the page printer.settings reads when none is asked for.
|
||||
const DefaultPage = "/general/status.html"
|
||||
|
||||
// The ask printer.settings puts to the admin process (ADR 0467 clause 4; the shape of ADR 0436's mail-sends):
|
||||
// the tool never holds the admin password, so it queues the ask on the work-queue stream printer-asks and the
|
||||
// admin process, an account of its own that alone reads the password, answers under the ask's id in the state
|
||||
// printer-answers.
|
||||
// The ask printer.settings puts to the device process (ADR 0467 clause 4; the shape of ADR 0436's mail-sends):
|
||||
// the tool never holds the admin password, so it queues the ask on the work-queue stream device-asks and the
|
||||
// device process, an account of its own that alone reads the password, answers under the ask's id in the state
|
||||
// device-answers.
|
||||
const (
|
||||
// EventAsked is the event on the work queue; the stream printer-asks carries it.
|
||||
EventAsked = "printer.asked"
|
||||
// AsksStream is the work-queue stream's name on the bus: the manifest's local name printer-asks, capitalised
|
||||
// EventAsked is the event on the work queue; the stream device-asks carries it.
|
||||
EventAsked = "device.asked"
|
||||
// AsksStream is the work-queue stream's name on the bus: the manifest's local name device-asks, capitalised
|
||||
// with its hyphen an underscore, as the controller composes a stream's own bus name.
|
||||
AsksStream = "PRINTER_ASKS"
|
||||
// AnswersState is the state the admin process writes its answers in.
|
||||
AnswersState = "printer-answers"
|
||||
// AdminProcessID is the admin process's id as module.json's resource "admin" names it: the controller
|
||||
// delivers its bus account as the own secret broker-admin.
|
||||
AdminProcessID = "admin"
|
||||
// AnswerWait is how long printer.settings waits for the admin process.
|
||||
AsksStream = "DEVICE_ASKS"
|
||||
// AnswersState is the state the device process writes its answers in.
|
||||
AnswersState = "device-answers"
|
||||
// DeviceProcessID is the device process's id as module.json's resource "device" names it: the controller
|
||||
// delivers its bus account as the own secret broker-device.
|
||||
DeviceProcessID = "device"
|
||||
// AnswerWait is how long printer.settings waits for the device process.
|
||||
AnswerWait = 60 * time.Second
|
||||
// StaleAfter is how old an ask may be when the admin process takes it: older than twice the wait, nobody waits
|
||||
// StaleAfter is how old an ask may be when the device process takes it: older than twice the wait, nobody waits
|
||||
// for it any more, so the printer is not contacted for it.
|
||||
StaleAfter = 2 * AnswerWait
|
||||
)
|
||||
@@ -49,7 +49,7 @@ type Ask struct {
|
||||
At time.Time `json:"asked-at"`
|
||||
}
|
||||
|
||||
// AskBus is how the tool reaches the admin process: it queues an ask and reads an answer back.
|
||||
// AskBus is how the tool reaches the device process: it queues an ask and reads an answer back.
|
||||
type AskBus interface {
|
||||
Publish(id string, body any) error
|
||||
Answer(id string) (json.RawMessage, error)
|
||||
@@ -84,7 +84,7 @@ func newAskID() string {
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
// PrinterSettings asks the admin process to log in, read one page of the admin site and log out, and waits for
|
||||
// PrinterSettings asks the device process to log in, read one page of the admin site and log out, and waits for
|
||||
// its answer. It checks the page and that printer and printer-certificate are set, and reads, opens and sends
|
||||
// nothing of the admin password (ADR 0467 clause 4; ADR 0436 §12): nothing is queued before those checks pass.
|
||||
func (s *Service) PrinterSettings(ctx context.Context, page string) (map[string]any, error) {
|
||||
@@ -106,19 +106,19 @@ func (s *Service) PrinterSettings(ctx context.Context, page string) (map[string]
|
||||
}
|
||||
ask := Ask{ID: newAskID(), Page: page, At: s.Now().UTC()}
|
||||
if err := s.Asks.Publish(ask.ID, ask); err != nil {
|
||||
return nil, fmt.Errorf("the ask for the admin process could not be queued: %w", err)
|
||||
return nil, fmt.Errorf("the ask for the device process could not be queued: %w", err)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, wait)
|
||||
defer cancel()
|
||||
for {
|
||||
raw, err := s.Asks.Answer(ask.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the admin process's answers cannot be read: %w", err)
|
||||
return nil, fmt.Errorf("the device process's answers cannot be read: %w", err)
|
||||
}
|
||||
if raw != nil {
|
||||
var out map[string]any
|
||||
if err := json.Unmarshal(raw, &out); err != nil {
|
||||
return nil, fmt.Errorf("the admin process's answer is not JSON: %w", err)
|
||||
return nil, fmt.Errorf("the device process's answer is not JSON: %w", err)
|
||||
}
|
||||
if msg, bad := out["error"].(string); bad {
|
||||
return nil, fmt.Errorf("%s", msg)
|
||||
@@ -127,7 +127,7 @@ func (s *Service) PrinterSettings(ctx context.Context, page string) (map[string]
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("the admin process did not answer within %s: it may not be running, or the printer is slow (ask %s)",
|
||||
return nil, fmt.Errorf("the device process did not answer within %s: it may not be running, or the printer is slow (ask %s)",
|
||||
wait, ask.ID)
|
||||
case <-time.After(poll):
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"brother/cmd/brother/internal/secret"
|
||||
)
|
||||
|
||||
// memAnswers is the state printer-answers, in memory.
|
||||
// memAnswers is the state device-answers, in memory.
|
||||
type memAnswers struct {
|
||||
mu sync.Mutex
|
||||
m map[string]json.RawMessage
|
||||
@@ -46,10 +46,10 @@ func (a *memAnswers) get(key string) json.RawMessage {
|
||||
return a.m[key]
|
||||
}
|
||||
|
||||
// fakeAskBus is the bus between the tool and the admin process: an ask published on it is handed to the
|
||||
// worker, as the work queue hands it to the admin process, and the worker's answers are what the tool reads.
|
||||
// fakeAskBus is the bus between the tool and the device process: an ask published on it is handed to the
|
||||
// worker, as the work queue hands it to the device process, and the worker's answers are what the tool reads.
|
||||
type fakeAskBus struct {
|
||||
worker *adminWorker // nil: nobody consumes the queue
|
||||
worker *deviceWorker // nil: nobody consumes the queue
|
||||
answers *memAnswers
|
||||
mu sync.Mutex
|
||||
published []Ask
|
||||
@@ -81,7 +81,7 @@ func (b *fakeAskBus) asks() int {
|
||||
type printerRig struct {
|
||||
run func(args map[string]any) (any, error)
|
||||
bus *fakeAskBus
|
||||
worker *adminWorker
|
||||
worker *deviceWorker
|
||||
answers *memAnswers
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ func newRig(t *testing.T, settings, password string) *printerRig {
|
||||
}
|
||||
file := settingsFile(t, settings)
|
||||
answers := &memAnswers{}
|
||||
w := &adminWorker{
|
||||
w := &deviceWorker{
|
||||
Settings: func() (Settings, error) { return LoadSettings(file) },
|
||||
Password: func() (string, error) { return secret.Read(secretFile) },
|
||||
Answers: answers,
|
||||
@@ -231,7 +231,7 @@ func TestPrinterSettingsSaysWhichConfigIsUnsetAndQueuesNothing(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 1453/AC4: the secret not given is said by name, by the admin process, and the printer is not contacted.
|
||||
// 1453/AC4: the secret not given is said by name, by the device process, and the printer is not contacted.
|
||||
func TestPrinterSettingsSaysTheSecretIsNotGivenAndContactsNothing(t *testing.T) {
|
||||
f := newFakePrinter(t, "B1891", "pw")
|
||||
_, run := printerTools(t, f, "")
|
||||
@@ -263,7 +263,7 @@ func TestPrinterSettingsRefusesAPageOutsideTheSite(t *testing.T) {
|
||||
t.Errorf("page %q: error = %v", p, err)
|
||||
}
|
||||
if _, err := r.worker.read(t.Context(), p); err == nil || !strings.Contains(err.Error(), "not a page") {
|
||||
t.Errorf("the admin process, page %q: error = %v", p, err)
|
||||
t.Errorf("the device process, page %q: error = %v", p, err)
|
||||
}
|
||||
}
|
||||
if _, err := r.run(map[string]any{"page": 5}); err == nil {
|
||||
@@ -274,14 +274,14 @@ func TestPrinterSettingsRefusesAPageOutsideTheSite(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 1453: nobody consuming the queue is said as the admin process not answering, naming the wait.
|
||||
// 1453: nobody consuming the queue is said as the device process not answering, naming the wait.
|
||||
func TestPrinterSettingsSaysWhenTheAdminProcessDoesNotAnswer(t *testing.T) {
|
||||
f := newFakePrinter(t, "B1891", "pw")
|
||||
r := newRig(t, printerSettingsJSON(f), "pw")
|
||||
r.bus.worker = nil
|
||||
answerWait = 100 * time.Millisecond
|
||||
_, err := r.run(nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "admin process did not answer") {
|
||||
if err == nil || !strings.Contains(err.Error(), "device process did not answer") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
if r.bus.asks() != 1 {
|
||||
@@ -289,7 +289,7 @@ func TestPrinterSettingsSaysWhenTheAdminProcessDoesNotAnswer(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 1453: the admin process terminates an ask it cannot read, drops one nobody waits for any more without
|
||||
// 1453: the device process terminates an ask it cannot read, drops one nobody waits for any more without
|
||||
// contacting the printer, acknowledges one already answered, and asks for redelivery when it cannot write its
|
||||
// answer.
|
||||
func TestTheAdminWorkerSettlesEachAsk(t *testing.T) {
|
||||
|
||||
@@ -23,9 +23,9 @@ type Service struct {
|
||||
Node string
|
||||
Now func() time.Time
|
||||
Emit Emitter
|
||||
// Asks is how printer.settings reaches the admin process, which alone logs in to the printer.
|
||||
// Asks is how printer.settings reaches the device process, which alone logs in to the printer.
|
||||
Asks AskBus
|
||||
// AnswerWait is how long printer.settings waits for the admin process's answer; AnswerPoll how often it looks.
|
||||
// AnswerWait is how long printer.settings waits for the device process's answer; AnswerPoll how often it looks.
|
||||
AnswerWait, AnswerPoll time.Duration
|
||||
}
|
||||
|
||||
|
||||
+15
-15
@@ -67,18 +67,18 @@
|
||||
"path": "${dir:state}/admin-password.secret",
|
||||
"issued-by": "outside",
|
||||
"prompt": "the admin password of the Brother printer",
|
||||
"owner": "brother-admin"
|
||||
"owner": "brother-device"
|
||||
},
|
||||
"broker-admin": {
|
||||
"path": "${dir:state}/broker-admin.secret",
|
||||
"owner": "brother-admin"
|
||||
"broker-device": {
|
||||
"path": "${dir:state}/broker-device.secret",
|
||||
"owner": "brother-device"
|
||||
}
|
||||
},
|
||||
"streams": [
|
||||
{
|
||||
"name": "printer-asks",
|
||||
"name": "device-asks",
|
||||
"events": [
|
||||
"printer.asked"
|
||||
"device.asked"
|
||||
],
|
||||
"max-age-days": 1,
|
||||
"max-mib": 8,
|
||||
@@ -88,7 +88,7 @@
|
||||
],
|
||||
"state": [
|
||||
{
|
||||
"name": "printer-answers",
|
||||
"name": "device-answers",
|
||||
"ttl-seconds": 600
|
||||
}
|
||||
],
|
||||
@@ -141,7 +141,7 @@
|
||||
{
|
||||
"id": "account",
|
||||
"type": "user",
|
||||
"name": "brother-admin",
|
||||
"name": "brother-device",
|
||||
"shell": "/usr/bin/nologin"
|
||||
},
|
||||
{
|
||||
@@ -171,29 +171,29 @@
|
||||
"template": "files/settings.json"
|
||||
},
|
||||
{
|
||||
"id": "admin",
|
||||
"id": "device",
|
||||
"type": "process",
|
||||
"name": "brother-admin",
|
||||
"name": "brother-device",
|
||||
"artifact": "tools",
|
||||
"run": [
|
||||
"./brother",
|
||||
"admin"
|
||||
"device"
|
||||
],
|
||||
"user": "brother-admin",
|
||||
"user": "brother-device",
|
||||
"env": {
|
||||
"BROTHER_SETTINGS": "${dir:state}/settings.json",
|
||||
"BROTHER_ADMIN_PASSWORD": "${dir:state}/admin-password.secret",
|
||||
"BROTHER_BROKER": "${dir:state}/broker-admin.secret"
|
||||
"BROTHER_BROKER": "${dir:state}/broker-device.secret"
|
||||
},
|
||||
"restart-on": [
|
||||
"settings"
|
||||
],
|
||||
"bus": {
|
||||
"queues": [
|
||||
"printer-asks"
|
||||
"device-asks"
|
||||
],
|
||||
"state": [
|
||||
"printer-answers"
|
||||
"device-answers"
|
||||
]
|
||||
},
|
||||
"health": {
|
||||
|
||||
Reference in New Issue
Block a user