From 11b654499bb67c2b22fe54e0de663c188b6808ab Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 4 Oct 2026 12:42:00 +0200 Subject: [PATCH] Several modules may add groups to one account; its shell and home stay one module's The host only ever adds groups, so the container runtime's module can put the operator in its group while the shell's module sets the same account's shell. --- internal/catalogue/resolve.go | 22 ++++++++++++ internal/catalogue/shared_account_test.go | 42 +++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 internal/catalogue/shared_account_test.go diff --git a/internal/catalogue/resolve.go b/internal/catalogue/resolve.go index a825a3a..bece43e 100644 --- a/internal/catalogue/resolve.go +++ b/internal/catalogue/resolve.go @@ -857,6 +857,28 @@ func checkResources(modules []Manifest) []string { // does not exist is the manifest's own problem, refused where it was made. dirs := dirsFor(m, Rendering{}) for _, r := range m.Resources { + if fmt.Sprint(r["type"]) == "user" { + // **An account is shared; what it is set to is not.** Several modules may need one + // login: the shell's module sets its shell, the container runtime's puts it in the + // `docker` group. The host only ever adds groups — it never takes the account out of + // one, not even when the resource that named it is undeclared — so groups from + // several modules cannot contradict each other and are not owned. A shell or a home + // is one value, and two modules setting it would each be undone by the other's + // apply: each stays one module's per node, and two are refused naming both. + name, _ := r["name"].(string) + for _, field := range []string{"shell", "home"} { + if v, ok := r[field].(string); !ok || v == "" || name == "" { + continue + } + key := "user " + field + " " + name + if other, taken := owner[key]; taken && other != m.Module { + problems = append(problems, fmt.Sprintf( + "%s and %s both set the %s of the user %q", other, m.Module, field, name)) + } + owner[key] = m.Module + } + continue + } for _, field := range []string{"path", "unit", "name", "package"} { value, ok := r[field].(string) if !ok || value == "" { diff --git a/internal/catalogue/shared_account_test.go b/internal/catalogue/shared_account_test.go new file mode 100644 index 0000000..16a7f74 --- /dev/null +++ b/internal/catalogue/shared_account_test.go @@ -0,0 +1,42 @@ +package catalogue + +import ( + "strings" + "testing" +) + +// One account, several modules: the shell's module sets its shell, the container runtime's adds it +// to a group. Groups are only ever added by the host, so they are contributed; a shell or a home is +// one value, owned by one module per node. + +func userResource(fields map[string]any) map[string]any { + r := map[string]any{"id": "operator", "type": "user", "name": "op"} + for k, v := range fields { + r[k] = v + } + return r +} + +func TestAShellAndAGroupOnOneAccountFromTwoModulesResolve(t *testing.T) { + zsh := Manifest{Module: "zsh", Resources: []map[string]any{userResource(map[string]any{"shell": "/usr/bin/zsh"})}} + docker := Manifest{Module: "docker", Resources: []map[string]any{userResource(map[string]any{"groups": []any{"docker"}})}} + other := Manifest{Module: "media", Resources: []map[string]any{userResource(map[string]any{"groups": []any{"video"}})}} + if problems := checkResources([]Manifest{zsh, docker, other}); len(problems) != 0 { + t.Fatalf("a shell and two modules' groups on one account were refused: %v", problems) + } + if _, err := Resolve(shelf(zsh, docker, other), []string{"zsh", "docker", "media"}, workstation(), World{}); err != nil { + t.Fatalf("the three did not resolve together: %v", err) + } +} + +func TestTwoModulesSettingOneAccountsShellOrHomeAreRefused(t *testing.T) { + for _, field := range []string{"shell", "home"} { + a := Manifest{Module: "zsh", Resources: []map[string]any{userResource(map[string]any{field: "/one"})}} + b := Manifest{Module: "fish", Resources: []map[string]any{userResource(map[string]any{field: "/two", "groups": []any{"x"}})}} + problems := checkResources([]Manifest{a, b}) + want := `zsh and fish both set the ` + field + ` of the user "op"` + if len(problems) != 1 || !strings.Contains(problems[0], want) { + t.Errorf("two modules setting %s gave %v, want %q", field, problems, want) + } + } +}