v0.329.0: each app reaches only its own appdata folder; empty allow-list; drive retirement asks the agent first; plain no-durable-id refusal; :latest only for a release
gates / gates (push) Successful in 45s
gates / gates (push) Successful in 45s
R-1142: the HDD_PATH root is appdata/<own folder>, with a compiled-in folder map (paperless-ngx -> paperless). R-981: the allow-list is empty (homepage's socket served no Felhom feature). R-1063: finalizeDecommissionWith asks the agent before it marks the drive or moves the default; the 403/409 come back typed and reach the household as a plain 409. R-1069: the agent's no-durable-id format refusal is ErrNoDurableID and a plain sentence. R-1143: build.sh carries :latest only for x.y.z. Each pinned by a red-proofed test. MinAgent 0.158.1.
This commit is contained in:
1 parent
5d67041d6d
commit
2fe634141f
20 files changed
+504
-78
No files matched your search
@@ -21,7 +21,9 @@
|
||||
//
|
||||
// The data roots are the values the controller itself gives the app: HDD_PATH, USERDATA_PATH and IMPORT_PATH, each
|
||||
// an absolute clean path under /mnt/ or under the system data path. A bind source may use only those three variables,
|
||||
// and only as its leading token.
|
||||
// and only as its leading token. Under HDD_PATH the root is the app's OWN folder, ${HDD_PATH}/appdata/<folder>/ (R-1142):
|
||||
// <folder> is the app's name, or its entry in the list's appdata_folders (paperless-ngx keeps appdata/paperless) — so a
|
||||
// catalog push cannot hand one app another app's files.
|
||||
//
|
||||
// The controller's own infra stacks (traefik, cloudflared, filebrowser, samba) and its override files (setup gate,
|
||||
// family gate, install hold, media library) are rendered by the controller, not the catalog: out of scope.
|
||||
@@ -69,11 +71,14 @@ type Entry struct {
|
||||
type allowList struct {
|
||||
Version int `json:"version"`
|
||||
Entries []Entry `json:"entries"`
|
||||
// AppdataFolders names an app's folder under ${HDD_PATH}/appdata/ when it is not the app's own name (R-1142). It is
|
||||
// compiled in with the entries for the same reason: the catalog — the threat — must not be able to name the folder.
|
||||
AppdataFolders map[string]string `json:"appdata_folders"`
|
||||
}
|
||||
|
||||
var allowed = mustLoad(allowListJSON)
|
||||
var allowed, appdataFolders = mustLoad(allowListJSON)
|
||||
|
||||
func mustLoad(b []byte) map[string]Entry {
|
||||
func mustLoad(b []byte) (map[string]Entry, map[string]string) {
|
||||
var l allowList
|
||||
if err := json.Unmarshal(b, &l); err != nil {
|
||||
panic("privpolicy: allowlist.json does not parse: " + err.Error())
|
||||
@@ -85,7 +90,29 @@ func mustLoad(b []byte) map[string]Entry {
|
||||
}
|
||||
m[key(e.App, e.Service, e.Setting)] = e
|
||||
}
|
||||
return m
|
||||
for app, f := range l.AppdataFolders {
|
||||
if !oneSegment(f) {
|
||||
panic("privpolicy: allowlist.json appdata folder of " + app + " is not one folder name: " + f)
|
||||
}
|
||||
}
|
||||
return m, l.AppdataFolders
|
||||
}
|
||||
|
||||
// oneSegment: a single folder name — no separator, not "." or "..", not empty.
|
||||
func oneSegment(s string) bool {
|
||||
return s != "" && s != "." && s != ".." && !strings.ContainsAny(s, "/\\$")
|
||||
}
|
||||
|
||||
// AppdataFolder is the app's own folder under ${HDD_PATH}/appdata/ (R-1142): its appdata_folders entry, else its name.
|
||||
// "" when the app has no usable name — then HDD_PATH gives it no root at all (fail closed).
|
||||
func AppdataFolder(app string) string {
|
||||
if f, ok := appdataFolders[app]; ok {
|
||||
return f
|
||||
}
|
||||
if !oneSegment(app) {
|
||||
return ""
|
||||
}
|
||||
return app
|
||||
}
|
||||
|
||||
func key(app, svc, setting string) string { return app + "\x00" + svc + "\x00" + setting }
|
||||
@@ -156,7 +183,7 @@ func PlaceholderEnv() map[string]string {
|
||||
// error = the compose could not be read, which refuses too (fail closed: a definition the check cannot read is not a
|
||||
// definition it has checked).
|
||||
func Admit(app string, compose []byte, env map[string]string, p Policy) error {
|
||||
vs, err := Check(compose, env, p)
|
||||
vs, err := Check(app, compose, env, p)
|
||||
if err != nil {
|
||||
return fmt.Errorf("the catalog definition of %s could not be checked: %w", app, err)
|
||||
}
|
||||
@@ -230,13 +257,13 @@ func imageRepo(ref string) string {
|
||||
return ref
|
||||
}
|
||||
|
||||
// Check lists every setting in a class, allowed or not, sorted.
|
||||
func Check(compose []byte, env map[string]string, p Policy) ([]Violation, error) {
|
||||
// Check lists every setting in a class, allowed or not, sorted. app decides the HDD_PATH root (R-1142).
|
||||
func Check(app string, compose []byte, env map[string]string, p Policy) ([]Violation, error) {
|
||||
var doc map[string]any
|
||||
if err := yaml.Unmarshal(compose, &doc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c := &checker{env: env, p: p}
|
||||
c := &checker{env: env, p: p, app: app}
|
||||
c.roots = c.validRoots()
|
||||
if _, ok := doc["include"]; ok {
|
||||
c.add("", "include")
|
||||
@@ -276,6 +303,7 @@ func Check(compose []byte, env map[string]string, p Policy) ([]Violation, error)
|
||||
}
|
||||
|
||||
type checker struct {
|
||||
app string
|
||||
env map[string]string
|
||||
p Policy
|
||||
roots []string
|
||||
@@ -516,7 +544,12 @@ func (c *checker) validRoots() []string {
|
||||
// round 3: HDD_PATH is the drive's whole namespace — its backups/ (recovery units carry app secrets)
|
||||
// and every other top-level folder are siblings an app has no business binding. Only appdata/ is a
|
||||
// root; the household's folders come through USERDATA_PATH.
|
||||
v = filepath.Join(v, "appdata")
|
||||
// R-1142: and only the app's OWN folder in it — appdata/<other app>/ is another app's files.
|
||||
f := AppdataFolder(c.app)
|
||||
if f == "" {
|
||||
continue
|
||||
}
|
||||
v = filepath.Join(v, "appdata", f)
|
||||
}
|
||||
out = append(out, v)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user