| 1 | //go:build unix |
| 2 | |
| 3 | package hostbox |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "errors" |
| 8 | "os" |
| 9 | "path/filepath" |
| 10 | "testing" |
| 11 | |
| 12 | "github.com/carlosframework/platform/internal/hostclaim" |
| 13 | "github.com/carlosframework/platform/internal/registry" |
| 14 | ) |
| 15 | |
| 16 | func TestEmptyCensusClassifiesSharedEntriesExactly(t *testing.T) { |
| 17 | ctx := context.Background() |
| 18 | base := t.TempDir() |
| 19 | db, err := OpenStopInventoryDB(filepath.Join(base, "inventory.db")) |
| 20 | if err != nil { |
| 21 | t.Fatal(err) |
| 22 | } |
| 23 | defer db.Close() |
| 24 | reg, err := registry.Open(filepath.Join(base, "registry.db")) |
| 25 | if err != nil { |
| 26 | t.Fatal(err) |
| 27 | } |
| 28 | defer reg.Close() |
| 29 | launches, err := OpenLaunchJournal(filepath.Join(base, "launch.db")) |
| 30 | if err != nil { |
| 31 | t.Fatal(err) |
| 32 | } |
| 33 | defer launches.Close() |
| 34 | roots := map[string]string{} |
| 35 | for _, name := range []string{"data", "opt", "units"} { |
| 36 | roots[name] = filepath.Join(base, name) |
| 37 | if err = os.Mkdir(roots[name], 0o700); err != nil { |
| 38 | t.Fatal(err) |
| 39 | } |
| 40 | } |
| 41 | at := func(root, rel string) string { return filepath.Join(roots[root], rel) } |
| 42 | write := func(path string) { |
| 43 | t.Helper() |
| 44 | if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { |
| 45 | t.Fatal(err) |
| 46 | } |
| 47 | if err := os.WriteFile(path, []byte("x"), 0o644); err != nil { |
| 48 | t.Fatal(err) |
| 49 | } |
| 50 | } |
| 51 | link := func(target, path string) { |
| 52 | t.Helper() |
| 53 | if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { |
| 54 | t.Fatal(err) |
| 55 | } |
| 56 | if err := os.Symlink(target, path); err != nil { |
| 57 | t.Fatal(err) |
| 58 | } |
| 59 | } |
| 60 | write(at("data", "opaque/anything")) |
| 61 | write(at("opt", "bin")) |
| 62 | write(at("opt", "store/v1/app")) |
| 63 | write(at("units", "a.service")) |
| 64 | link(at("units", "a.service"), at("units", "multi-user.target.wants/a.service")) |
| 65 | shared := map[string][]string{ |
| 66 | "data": {"opaque"}, |
| 67 | "opt": {CensusFile("bin"), CensusTree("store")}, |
| 68 | "units": {CensusFile("a.service"), CensusLink("multi-user.target.wants/a.service", at("units", "a.service"))}, |
| 69 | } |
| 70 | c := EmptyInstallationCensus{Inventory: db, Registry: reg, Launches: launches, Box: "box", Enrollment: "enrollment", DirectoryRevision: "revision", Roots: roots, SharedPaths: shared} |
| 71 | obs := hostclaim.Observed{Host: "shop.example", Claim: &hostclaim.Claim{Holder: &hostclaim.Holder{Incarnation: "inc", Stamp: 1}}} |
| 72 | selected := RecordSelection{Host: obs.Host, Incarnation: "inc"} |
| 73 | if err = c.CheckLocal(ctx, obs, selected); err != nil { |
| 74 | t.Fatalf("classified box: %v", err) |
| 75 | } |
| 76 | refuse := func(name string, mutate func() func()) { |
| 77 | t.Helper() |
| 78 | restore := mutate() |
| 79 | if err := c.CheckLocal(ctx, obs, selected); !errors.Is(err, ErrTypedInventory) { |
| 80 | t.Fatalf("%s: %v", name, err) |
| 81 | } |
| 82 | restore() |
| 83 | if err := c.CheckLocal(ctx, obs, selected); err != nil { |
| 84 | t.Fatalf("%s: not restored: %v", name, err) |
| 85 | } |
| 86 | } |
| 87 | remove := func(path string) func() { |
| 88 | return func() { |
| 89 | if err := os.RemoveAll(path); err != nil { |
| 90 | t.Fatal(err) |
| 91 | } |
| 92 | } |
| 93 | } |
| 94 | created := func(path string, put func(string)) func() func() { |
| 95 | return func() func() { put(path); return remove(path) } |
| 96 | } |
| 97 | swapped := func(path, target string) func() func() { |
| 98 | return func() func() { |
| 99 | old, err := os.Readlink(path) |
| 100 | if err != nil { |
| 101 | t.Fatal(err) |
| 102 | } |
| 103 | remove(path)() |
| 104 | link(target, path) |
| 105 | return func() { remove(path)(); link(old, path) } |
| 106 | } |
| 107 | } |
| 108 | refuse("symlink in a shared tree", created(at("opt", "store/v1/sh"), func(p string) { link("/bin/sh", p) })) |
| 109 | refuse("host name in a shared tree", created(at("opt", "store/v1/shop.example"), write)) |
| 110 | refuse("unclassified file", created(at("opt", "other"), write)) |
| 111 | refuse("substituted link target", swapped(at("units", "multi-user.target.wants/a.service"), at("units", "b.service"))) |
| 112 | refuse("relative link target", swapped(at("units", "multi-user.target.wants/a.service"), "../a.service")) |
| 113 | refuse("foreign link into a managed root", created(at("units", "multi-user.target.wants/b.service"), func(p string) { link(at("opt", "bin"), p) })) |
| 114 | refuse("foreign link below a unit-root directory", created(at("units", "multi-user.target.wants/c.service"), func(p string) { link(at("units", "multi-user.target.wants/ssh.service"), p) })) |
| 115 | refuse("foreign link naming the platform", created(at("units", "d.service"), func(p string) { link("/srv/carlos/d.service", p) })) |
| 116 | refuse("foreign file naming the host", created(at("units", "e.service"), func(p string) { |
| 117 | if err := os.WriteFile(p, []byte("ExecStart=/srv/shop.example\n"), 0o644); err != nil { |
| 118 | t.Fatal(err) |
| 119 | } |
| 120 | })) |
| 121 | refuse("host-named foreign unit", created(at("units", "x@shop.example.service"), write)) |
| 122 | refuse("file where a link is classified", func() func() { |
| 123 | path := at("units", "multi-user.target.wants/a.service") |
| 124 | remove(path)() |
| 125 | write(path) |
| 126 | return func() { remove(path)(); link(at("units", "a.service"), path) } |
| 127 | }) |
| 128 | if err = os.Remove(at("opt", "bin")); err != nil { |
| 129 | t.Fatal(err) |
| 130 | } |
| 131 | link(at("opt", "store/v1/app"), at("opt", "bin")) |
| 132 | if err = c.CheckLocal(ctx, obs, selected); !errors.Is(err, ErrTypedInventory) { |
| 133 | t.Fatalf("symlink where a file is classified: %v", err) |
| 134 | } |
| 135 | remove(at("opt", "bin"))() |
| 136 | for name, bad := range map[string][]string{"relative link target": {CensusLink("x", "y")}, "escaping entry": {CensusFile("../x")}, "host entry": {CensusFile("shop.example")}, "conflicting entries": {CensusFile("x"), CensusTree("x")}} { |
| 137 | c.SharedPaths = map[string][]string{"data": shared["data"], "opt": append(append([]string{}, shared["opt"]...), bad...), "units": shared["units"]} |
| 138 | if err = c.CheckLocal(ctx, obs, selected); !errors.Is(err, ErrTypedInventory) { |
| 139 | t.Fatalf("%s accepted: %v", name, err) |
| 140 | } |
| 141 | } |
| 142 | } |
| 143 | |