Count a file that says nothing as trusted, and drop the refusal date
The fourth review (hq issue 339): the safe reading of a file that asks for a setting and does not say is that root or a consumer trusts it, so its settings are the terminal's; `"trusted": false` is the opt-out. With that, nothing unsafe is left to refuse: `module check` lists and counts the unmarked files and never refuses them.
This commit is contained in:
@@ -137,23 +137,11 @@ func moduleCheckFor(paths []string, longestMachine int, out io.Writer) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// **Every file that asks for a setting says whether it is trusted** (novox/hq issue 339): warned until the
|
// **A file that asks for a setting and does not say whether it is trusted counts as trusted** (novox/hq issue
|
||||||
// date, refused from it, and counted for the catalogue's merge check like the resources without health.
|
// 339): listed and counted, never refused, so an author can opt out a file nothing trusts.
|
||||||
unsaid := 0
|
unsaid := 0
|
||||||
trustRequired := !checkNow().Before(catalogue.TrustRequiredFrom)
|
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
missing := catalogue.UnsaidTrust(shelf[name])
|
unsaid += len(catalogue.UnsaidTrust(shelf[name]))
|
||||||
unsaid += len(missing)
|
|
||||||
if trustRequired {
|
|
||||||
for _, id := range missing {
|
|
||||||
fmt.Fprintf(out, "%s: the file %s asks for a setting and does not say whether it is trusted: say "+
|
|
||||||
"%q true or false (novox/hq issue 339)\n", name, id, catalogue.TrustedField)
|
|
||||||
}
|
|
||||||
if len(missing) > 0 {
|
|
||||||
failed += len(missing)
|
|
||||||
faulted[name] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
@@ -198,8 +186,9 @@ func moduleCheckFor(paths []string, longestMachine int, out io.Writer) error {
|
|||||||
fmt.Fprintf(out, ", ready: %s", strings.Join(checks, "; "))
|
fmt.Fprintf(out, ", ready: %s", strings.Join(checks, "; "))
|
||||||
}
|
}
|
||||||
if missing := catalogue.UnsaidTrust(m); len(missing) > 0 {
|
if missing := catalogue.UnsaidTrust(m); len(missing) > 0 {
|
||||||
fmt.Fprintf(out, "; WARNING: %s ask(s) for a setting and say(s) not whether it is trusted, refused from "+
|
fmt.Fprintf(out, "; WARNING: %s ask(s) for a setting and do(es) not say whether it is trusted, so it counts as "+
|
||||||
"%s (novox/hq issue 339)", strings.Join(missing, ", "), catalogue.TrustRequiredFrom.Format("2006-01-02"))
|
"trusted: set at the terminal alone; say %q false where nothing trusts it (novox/hq issue 339)",
|
||||||
|
strings.Join(missing, ", "), catalogue.TrustedField)
|
||||||
}
|
}
|
||||||
if missing := catalogue.Undeclared(m); len(missing) > 0 {
|
if missing := catalogue.Undeclared(m); len(missing) > 0 {
|
||||||
fmt.Fprintf(out, "; WARNING: %s stay(s) up and say(s) not how it is ready — judged by liveness alone, "+
|
fmt.Fprintf(out, "; WARNING: %s stay(s) up and say(s) not how it is ready — judged by liveness alone, "+
|
||||||
@@ -225,7 +214,7 @@ func moduleCheckFor(paths []string, longestMachine int, out io.Writer) error {
|
|||||||
const UndeclaredHealthLine = "long-running resources without health:"
|
const UndeclaredHealthLine = "long-running resources without health:"
|
||||||
|
|
||||||
// UnsaidTrustLine starts the line `module check` says the count of files that ask for a setting and do not say
|
// UnsaidTrustLine starts the line `module check` says the count of files that ask for a setting and do not say
|
||||||
// whether it is trusted (novox/hq issue 339).
|
// whether it is trusted, and so count as trusted (novox/hq issue 339).
|
||||||
const UnsaidTrustLine = "files asking for a setting without saying whether it is trusted:"
|
const UnsaidTrustLine = "files asking for a setting without saying whether it is trusted:"
|
||||||
|
|
||||||
// checkNow is the clock `module check` judges the date by; a test sets it.
|
// checkNow is the clock `module check` judges the date by; a test sets it.
|
||||||
|
|||||||
@@ -46,9 +46,9 @@ func TestModuleCheckCountsTheUndeclaredAndRefusesThemFromTheDate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A file that asks for a setting says whether it is trusted (novox/hq issue 339): `module check` warns and counts
|
// A file that asks for a setting without saying whether it is trusted counts as trusted (novox/hq issue 339):
|
||||||
// it before the date, and refuses it from the date; a file that says so passes either way.
|
// `module check` lists and counts it, and never refuses it — there is nothing unsafe to refuse.
|
||||||
func TestModuleCheckCountsUnsaidTrustAndRefusesItFromTheDate(t *testing.T) {
|
func TestModuleCheckListsUnmarkedFilesAndNeverRefusesThem(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
path := filepath.Join(dir, "module.json")
|
path := filepath.Join(dir, "module.json")
|
||||||
os.WriteFile(path, []byte(`{"module":"power","resources":[
|
os.WriteFile(path, []byte(`{"module":"power","resources":[
|
||||||
@@ -56,20 +56,17 @@ func TestModuleCheckCountsUnsaidTrustAndRefusesItFromTheDate(t *testing.T) {
|
|||||||
"content":"HandleLidSwitch=${setting:lid}\n"},
|
"content":"HandleLidSwitch=${setting:lid}\n"},
|
||||||
{"id":"note","type":"file","path":"/var/lib/power/note","mode":"0644","content":"${setting:greeting}\n"}]}`), 0o600)
|
{"id":"note","type":"file","path":"/var/lib/power/note","mode":"0644","content":"${setting:greeting}\n"}]}`), 0o600)
|
||||||
defer func() { checkNow = time.Now }()
|
defer func() { checkNow = time.Now }()
|
||||||
|
for _, at := range []time.Time{time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC), time.Date(2036, 1, 1, 0, 0, 0, 0, time.UTC)} {
|
||||||
checkNow = func() time.Time { return catalogue.TrustRequiredFrom.Add(-time.Hour) }
|
checkNow = func() time.Time { return at }
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
if err := moduleCheck([]string{path}, &out); err != nil {
|
if err := moduleCheck([]string{path}, &out); err != nil {
|
||||||
t.Fatalf("refused before the date: %v\n%s", err, out.String())
|
t.Fatalf("refused at %v: %v\n%s", at, err, out.String())
|
||||||
}
|
}
|
||||||
for _, want := range []string{"WARNING: note ask(s) for a setting", UnsaidTrustLine + " 1"} {
|
for _, want := range []string{"note ask(s) for a setting and do(es) not say whether it is trusted, so it counts as trusted",
|
||||||
if !strings.Contains(out.String(), want) {
|
UnsaidTrustLine + " 1"} {
|
||||||
t.Errorf("the check does not say %q:\n%s", want, out.String())
|
if !strings.Contains(out.String(), want) {
|
||||||
|
t.Errorf("the check does not say %q:\n%s", want, out.String())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
checkNow = func() time.Time { return catalogue.TrustRequiredFrom }
|
|
||||||
out.Reset()
|
|
||||||
if err := moduleCheck([]string{path}, &out); err == nil || !strings.Contains(out.String(), "power: the file note asks for a setting") {
|
|
||||||
t.Fatalf("an unsaid file passed after the date: %v\n%s", err, out.String())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,10 @@ func TestPlacesAndAccessesAreRefusedThroughEveryVerb(t *testing.T) {
|
|||||||
register(t, open, catalogue.Manifest{Module: "notes", Version: "1",
|
register(t, open, catalogue.Manifest{Module: "notes", Version: "1",
|
||||||
Accesses: []catalogue.Access{{ID: "media", Path: "/storage/media", Mode: "read"}},
|
Accesses: []catalogue.Access{{ID: "media", Path: "/storage/media", Mode: "read"}},
|
||||||
Resources: []map[string]any{{"id": "data", "type": "directory", "mode": "0755"},
|
Resources: []map[string]any{{"id": "data", "type": "directory", "mode": "0755"},
|
||||||
{"id": "rc", "type": "file", "path": "/etc/notes.conf", "mode": "0644", "content": "x = ${setting:x}\n"}}})
|
// Nothing trusts this file: said, so a verb may change what it asks for (an unmarked one counts as
|
||||||
|
// trusted, and only the terminal could).
|
||||||
|
{"id": "rc", "type": "file", "path": "/etc/notes.conf", "mode": "0644", "trusted": false,
|
||||||
|
"content": "x = ${setting:x}\n"}}})
|
||||||
if _, err := assign(ctx, open, "laptop", "notes"); err != nil {
|
if _, err := assign(ctx, open, "laptop", "notes"); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package catalogue
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Which settings are the controller's terminal's alone (novox/hq issue 339).
|
// Which settings are the controller's terminal's alone (novox/hq issue 339).
|
||||||
@@ -19,25 +18,22 @@ import (
|
|||||||
// database's port, an object store's scheme, a registry's port, an identity provider's issuer and token
|
// database's port, an object store's scheme, a registry's port, an identity provider's issuer and token
|
||||||
// path. Through a verb, any caller could point every consumer at a listener of its own and collect the
|
// path. Through a verb, any caller could point every consumer at a listener of its own and collect the
|
||||||
// credentials they present.
|
// credentials they present.
|
||||||
// 3. **Every setting a file marked `trusted` asks for**: a file root or a consumer trusts — a logind drop-in,
|
// 3. **Every setting a file asks for, unless the file says `"trusted": false`.** A file root or a consumer trusts —
|
||||||
// an env file that says which uid a container runs as, a script run as root. The manifest says so on the
|
// a logind drop-in, an env file that says which uid a container runs as, a script run as root — must not
|
||||||
// file (TrustedField), and `module check` names a file that asks for a setting without saying.
|
// change through a verb, and the safe reading of a file that says nothing is that it is one of them (fail
|
||||||
|
// closed). `"trusted": false` is the opt-out, for a file nothing trusts: a person's own notifier settings.
|
||||||
|
// `module check` lists the files that say nothing, so an author can opt one out where that is true.
|
||||||
//
|
//
|
||||||
// Derived from the manifest, never listed by hand, so a provider or a trusted file added tomorrow is covered.
|
// Derived from the manifest, never listed by hand, so a provider or a trusted file added tomorrow is covered.
|
||||||
|
|
||||||
// TrustedField is the key a file resource carries to say whether the settings it asks for are trusted: true
|
// TrustedField is the key a file resource carries to say whether the settings it asks for are trusted: true, or
|
||||||
// makes each a terminal key; false says, out loud, that none changes what root or a consumer trusts. Said in the
|
// absent, makes each a terminal key; false says, out loud, that none changes what root or a consumer trusts. Said in the
|
||||||
// catalogue, never on the machine: the composer takes it out before the node-engine, which parses strictly.
|
// catalogue, never on the machine: the composer takes it out before the node-engine, which parses strictly.
|
||||||
const TrustedField = "trusted"
|
const TrustedField = "trusted"
|
||||||
|
|
||||||
// TrustRequiredFrom is when `module check` refuses a file that asks for a setting and does not say whether it is
|
|
||||||
// trusted. Until then it is warned and counted: the catalogue's files get the field in their own change, which
|
|
||||||
// can only land once a controller that takes the field out of the declaration runs.
|
|
||||||
var TrustRequiredFrom = time.Date(2026, 10, 10, 0, 0, 0, 0, time.UTC)
|
|
||||||
|
|
||||||
// TerminalKeys are the settings keys of a module that are set at the controller's terminal alone: places and
|
// TerminalKeys are the settings keys of a module that are set at the controller's terminal alone: places and
|
||||||
// accesses, every key its provisions serve and every setting a served value asks for, and every setting a file
|
// accesses, every key its provisions serve and every setting a served value asks for, and every setting a file
|
||||||
// marked trusted asks for. Places and accesses first, then the rest sorted.
|
// asks for unless it says `"trusted": false`. Places and accesses first, then the rest sorted.
|
||||||
func TerminalKeys(m Manifest) []string {
|
func TerminalKeys(m Manifest) []string {
|
||||||
keys := map[string]bool{}
|
keys := map[string]bool{}
|
||||||
for _, served := range m.Serves {
|
for _, served := range m.Serves {
|
||||||
@@ -51,7 +47,10 @@ func TerminalKeys(m Manifest) []string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, r := range m.Resources {
|
for _, r := range m.Resources {
|
||||||
if trusted, _ := r[TrustedField].(bool); !trusted || fmt.Sprint(r["type"]) != "file" {
|
if fmt.Sprint(r["type"]) != "file" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if trusted, said := r[TrustedField].(bool); said && !trusted {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if content, ok := r["content"].(string); ok {
|
if content, ok := r["content"].(string); ok {
|
||||||
@@ -70,7 +69,8 @@ func TerminalKeys(m Manifest) []string {
|
|||||||
return append([]string{PlacesSetting, AccessesSetting}, rest...)
|
return append([]string{PlacesSetting, AccessesSetting}, rest...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnsaidTrust is every file of a module that asks for a setting and does not say whether it is trusted, by id.
|
// UnsaidTrust is every file of a module that asks for a setting and does not say whether it is trusted, by id:
|
||||||
|
// each counts as trusted, and is listed so an author can opt out a file nothing trusts.
|
||||||
func UnsaidTrust(m Manifest) []string {
|
func UnsaidTrust(m Manifest) []string {
|
||||||
var out []string
|
var out []string
|
||||||
for _, r := range m.Resources {
|
for _, r := range m.Resources {
|
||||||
|
|||||||
@@ -138,14 +138,16 @@ func TestTerminalKeysAreDerived(t *testing.T) {
|
|||||||
power := Manifest{Module: "power", Resources: []map[string]any{
|
power := Manifest{Module: "power", Resources: []map[string]any{
|
||||||
{"id": "logind", "type": "file", "path": "/etc/systemd/logind.conf.d/power.conf", "trusted": true,
|
{"id": "logind", "type": "file", "path": "/etc/systemd/logind.conf.d/power.conf", "trusted": true,
|
||||||
"content": "HandleLidSwitch=${setting:handle-lid-switch}\n"},
|
"content": "HandleLidSwitch=${setting:handle-lid-switch}\n"},
|
||||||
{"id": "note", "type": "file", "path": "/var/lib/power/note", "trusted": false, "content": "${setting:greeting}\n"}}}
|
{"id": "note", "type": "file", "path": "/var/lib/power/note", "trusted": false, "content": "${setting:greeting}\n"},
|
||||||
|
// Unmarked counts as trusted (fail closed): only `"trusted": false` lets a verb change what a file asks for.
|
||||||
|
{"id": "unmarked", "type": "file", "path": "/etc/power/unmarked", "content": "${setting:unmarked}\n"}}}
|
||||||
for _, c := range []struct {
|
for _, c := range []struct {
|
||||||
m Manifest
|
m Manifest
|
||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
{postgres, "places,accesses,port"},
|
{postgres, "places,accesses,port"},
|
||||||
{keycloak, "places,accesses,issuer,token-path"},
|
{keycloak, "places,accesses,issuer,token-path"},
|
||||||
{power, "places,accesses,handle-lid-switch"},
|
{power, "places,accesses,handle-lid-switch,unmarked"},
|
||||||
{Manifest{Module: "plain"}, "places,accesses"},
|
{Manifest{Module: "plain"}, "places,accesses"},
|
||||||
} {
|
} {
|
||||||
if got := strings.Join(TerminalKeys(c.m), ","); got != c.want {
|
if got := strings.Join(TerminalKeys(c.m), ","); got != c.want {
|
||||||
|
|||||||
Reference in New Issue
Block a user