291 lines
12 KiB
Go
291 lines
12 KiB
Go
package main
|
|
|
|
// Two things a host holding `read` on a folder must never reach through the
|
|
// file ops (list, stat, read_range), nor move with a plan:
|
|
// - what an exclude of several folders names. The Host's default excludes
|
|
// carry ".config/gcloud", and a pattern with a "/" was compared with one
|
|
// folder name at a time, so it matched nothing: a shared home folder
|
|
// served its gcloud credentials;
|
|
// - the local search agent's own folder, .agent, which holds the plaintext
|
|
// of every file the agent read, whatever the grant excludes.
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gorilla/websocket"
|
|
)
|
|
|
|
// dcnDefaultExcludes is the exclude list the Host proposes for a new folder.
|
|
var dcnDefaultExcludes = []string{
|
|
".ssh", ".gnupg", ".aws", ".azure", ".config/gcloud", ".kube",
|
|
".env", ".env.*", "*.pem", "*.key", "*.p12", "*.pfx", "*.keystore",
|
|
"id_rsa*", "id_ed25519*", "*.token", "credentials*", ".netrc",
|
|
".git-credentials", "*history",
|
|
}
|
|
|
|
// syncExcluding installs a read grant for the store with these excludes,
|
|
// through the real grant_sync op.
|
|
func (fx *dcnStoreFixture) syncExcluding(t *testing.T, excludes []string) {
|
|
t.Helper()
|
|
rec := signedRec(t, fx.priv, map[string]any{"kind": "dcn_store_grant", "class": "machine",
|
|
"root": fx.store, "mask": []string{"read"}, "excludes": excludes,
|
|
"expires": time.Now().Add(24 * time.Hour).UTC().Format(time.RFC3339)})
|
|
payload, _ := json.Marshal(map[string]any{"store_id": 1, "terms_canonical": rec.TermsCanonical,
|
|
"signature": rec.Signature, "verify_pubkey": base64.StdEncoding.EncodeToString(fx.pub)})
|
|
_, why, exit := dcnRunOp(t, func(ws *websocket.Conn) {
|
|
handleDcnGrantSync(ws, fx.dir, []string{base64.StdEncoding.EncodeToString(payload)})
|
|
})
|
|
if exit != 0 {
|
|
t.Fatalf("grant_sync: exit %d: %s", exit, why)
|
|
}
|
|
}
|
|
|
|
func (fx *dcnStoreFixture) put(t *testing.T, rel, body string) {
|
|
t.Helper()
|
|
p := filepath.Join(fx.store, filepath.FromSlash(rel))
|
|
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(p, []byte(body), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// listed is what dcn_list returns under prefix, and its exit code.
|
|
func (fx *dcnStoreFixture) listed(t *testing.T, prefix string) ([]string, int) {
|
|
t.Helper()
|
|
obj, _, exit := fx.op(t, handleDcnList, map[string]any{"prefix": prefix})
|
|
var paths []string
|
|
files, _ := obj["files"].([]any)
|
|
for _, f := range files {
|
|
if m, ok := f.(map[string]any); ok {
|
|
p, _ := m["path"].(string)
|
|
paths = append(paths, p)
|
|
}
|
|
}
|
|
return paths, exit
|
|
}
|
|
|
|
// A pattern with a "/" names that run of folders, wherever it sits in the
|
|
// path, each folder compared as the file system compares names. Judged per
|
|
// platform from any OS.
|
|
func TestDcnAMultiFolderExcludeMatchesThatRunAnywhere(t *testing.T) {
|
|
for _, c := range []struct {
|
|
goos, path string
|
|
want bool
|
|
}{
|
|
{"linux", ".config/gcloud/access_tokens.db", true},
|
|
{"linux", ".config/gcloud", true},
|
|
{"linux", "home/.config/gcloud/x", true},
|
|
{"linux", "a/b/.config/gcloud/configurations/config_default", true},
|
|
{"linux", ".config/gcloudx", false},
|
|
{"linux", ".config/gcloudx/access_tokens.db", false},
|
|
{"linux", ".configx/gcloud/x", false},
|
|
{"linux", ".config/other/gcloud/x", false},
|
|
{"linux", "gcloud/.config/x", false},
|
|
{"linux", ".config", false},
|
|
{"linux", ".config/settings.ini", false},
|
|
{"linux", ".Config/gcloud/x", false}, // another folder on Linux
|
|
{"darwin", ".Config/GCloud/access_tokens.db", true},
|
|
{"darwin", "home/.CONFIG/gcloud/x", true},
|
|
{"darwin", ".config/gcloudx/x", false},
|
|
{"windows", ".CONFIG/GCLOUD/access_tokens.db", true},
|
|
{"windows", "home/.config./gcloud /x", true},
|
|
{"windows", ".config/gcloudx/x", false},
|
|
// one-folder patterns as before
|
|
{"linux", "deep/.ssh/config", true},
|
|
{"linux", "certs/server.key", true},
|
|
{"linux", "notes/ssh-notes.txt", false},
|
|
{"windows", ".SSH/id_rsa", true},
|
|
} {
|
|
if got := dcnExcludedOn(c.goos, dcnDefaultExcludes, c.path); got != c.want {
|
|
t.Errorf("%s: %q excluded=%v, want %v", c.goos, c.path, got, c.want)
|
|
}
|
|
}
|
|
// A pattern's own slashes at either end, or doubled, name the same run.
|
|
for _, pat := range []string{".config/gcloud/", "/.config/gcloud", ".config//gcloud"} {
|
|
if !dcnExcludedOn("linux", []string{pat}, "home/.config/gcloud/x") {
|
|
t.Errorf("%q must exclude home/.config/gcloud/x", pat)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The default exclude of gcloud's folder holds on every op a host sends, at the
|
|
// top of a home folder and inside one, and however the path is spelled.
|
|
func TestDcnTheDefaultGcloudExcludeHoldsOnEveryOp(t *testing.T) {
|
|
fx := newDcnStoreFixture(t)
|
|
fx.put(t, ".config/gcloud/access_tokens.db", "GCLOUD-TOKEN")
|
|
fx.put(t, "home/.config/gcloud/credentials.db", "NESTED-TOKEN")
|
|
fx.put(t, ".config/gcloudx/ok.txt", "NEAR-MISS")
|
|
fx.put(t, ".config/settings.ini", "SETTINGS")
|
|
fx.syncExcluding(t, dcnDefaultExcludes)
|
|
|
|
paths, exit := fx.listed(t, "")
|
|
if exit != 0 {
|
|
t.Fatalf("list: exit %d", exit)
|
|
}
|
|
have := map[string]bool{}
|
|
for _, p := range paths {
|
|
have[p] = true
|
|
if strings.Contains(p, ".config/gcloud/") {
|
|
t.Errorf("list returned %q, under an excluded folder", p)
|
|
}
|
|
}
|
|
for _, p := range []string{".config/gcloudx/ok.txt", ".config/settings.ini", "a.txt"} {
|
|
if !have[p] {
|
|
t.Errorf("list left out %q, which no exclude names (got %v)", p, paths)
|
|
}
|
|
}
|
|
if paths, exit := fx.listed(t, "home/.config"); exit != 0 || len(paths) != 0 {
|
|
t.Errorf("list under home/.config: exit %d, %v; want nothing", exit, paths)
|
|
}
|
|
for _, prefix := range []string{".config/gcloud", "home/.config/gcloud"} {
|
|
if _, exit := fx.listed(t, prefix); exit != 126 {
|
|
t.Errorf("list of %q: exit %d, want 126", prefix, exit)
|
|
}
|
|
}
|
|
for _, p := range []string{".config/gcloud", ".config/gcloud/access_tokens.db",
|
|
"home/.config/gcloud/credentials.db", " .config/gcloud/access_tokens.db",
|
|
"docs/../.config/gcloud/access_tokens.db"} {
|
|
if obj, _, exit := fx.op(t, handleDcnStat, map[string]any{"path": p}); exit != 126 {
|
|
t.Errorf("stat %q: exit %d %v, want 126", p, exit, obj)
|
|
}
|
|
if got, _, exit := fx.read(t, p); exit != 126 || strings.Contains(got, "TOKEN") {
|
|
t.Errorf("read_range %q: exit %d %q, want 126 and nothing", p, exit, got)
|
|
}
|
|
}
|
|
if got, why, exit := fx.read(t, ".config/gcloudx/ok.txt"); exit != 0 || got != "NEAR-MISS" {
|
|
t.Errorf("a folder only named like gcloud's must read: exit %d %q %s", exit, got, why)
|
|
}
|
|
if problems := dcnCheckPlan(fx.store, dcnDefaultExcludes, []dcnMove{
|
|
{From: ".config/gcloud/access_tokens.db", To: "out.db"}}); len(problems) == 0 {
|
|
t.Error("a move out of gcloud's folder must be refused")
|
|
}
|
|
if problems := dcnCheckPlan(fx.store, dcnDefaultExcludes, []dcnMove{
|
|
{From: "a.txt", To: "home/.config/gcloud/a.txt"}}); len(problems) == 0 {
|
|
t.Error("a move into gcloud's folder must be refused")
|
|
}
|
|
if err := os.Symlink(filepath.Join(".config", "gcloud"), filepath.Join(fx.store, "gc")); err != nil {
|
|
t.Skipf("symlinks unavailable: %v", err)
|
|
}
|
|
if got, _, exit := fx.read(t, "gc/access_tokens.db"); exit != 126 || got == "GCLOUD-TOKEN" {
|
|
t.Errorf("read through a link to gcloud's folder: exit %d %q, want 126", exit, got)
|
|
}
|
|
}
|
|
|
|
// The agent's folder is refused on every op a host sends: at the store's top,
|
|
// inside a folder another grant shares (where that store's agent keeps its
|
|
// own), through a link, and however it is spelled. The grant here excludes
|
|
// nothing, so the refusal is not the grant's to give or take.
|
|
func TestDcnTheAgentsFolderIsNeverServed(t *testing.T) {
|
|
fx := newDcnStoreFixture(t)
|
|
fx.put(t, ".agent/extract/ab/report.txt", "PLAINTEXT-OF-A-FILE")
|
|
fx.put(t, ".agent/manifest.yaml", "manifest")
|
|
fx.put(t, "proj/.agent/extract/x.txt", "NESTED-PLAINTEXT")
|
|
fx.put(t, "proj/readme.md", "README")
|
|
fx.syncExcluding(t, nil)
|
|
|
|
paths, exit := fx.listed(t, "")
|
|
if exit != 0 {
|
|
t.Fatalf("list: exit %d", exit)
|
|
}
|
|
have := map[string]bool{}
|
|
for _, p := range paths {
|
|
have[p] = true
|
|
if strings.Contains("/"+p, "/.agent/") {
|
|
t.Errorf("list returned %q, inside an agent's folder", p)
|
|
}
|
|
}
|
|
if !have["a.txt"] || !have["proj/readme.md"] {
|
|
t.Errorf("list left out the folder's own files: %v", paths)
|
|
}
|
|
for _, prefix := range []string{".agent", ".agent/extract", "proj/.agent"} {
|
|
if _, exit := fx.listed(t, prefix); exit != 126 {
|
|
t.Errorf("list of %q: exit %d, want 126", prefix, exit)
|
|
}
|
|
}
|
|
for _, p := range []string{".agent", ".agent/extract/ab/report.txt", ".agent/manifest.yaml",
|
|
"proj/.agent/extract/x.txt", " .agent/manifest.yaml", "docs/../.agent/extract/ab/report.txt"} {
|
|
if obj, _, exit := fx.op(t, handleDcnStat, map[string]any{"path": p}); exit != 126 {
|
|
t.Errorf("stat %q: exit %d %v, want 126", p, exit, obj)
|
|
}
|
|
if got, _, exit := fx.read(t, p); exit != 126 || strings.Contains(got, "PLAINTEXT") {
|
|
t.Errorf("read_range %q: exit %d %q, want 126 and nothing", p, exit, got)
|
|
}
|
|
}
|
|
for _, m := range []dcnMove{{From: ".agent/extract/ab/report.txt", To: "out.txt"},
|
|
{From: "a.txt", To: ".agent/a.txt"}, {From: "a.txt", To: "proj/.agent/a.txt"}} {
|
|
if problems := dcnCheckPlan(fx.store, nil, []dcnMove{m}); len(problems) == 0 {
|
|
t.Errorf("a move %q -> %q must be refused", m.From, m.To)
|
|
}
|
|
}
|
|
|
|
// The bastion's own report of a plan it applied still goes into
|
|
// .agent/plans, through the real plan op.
|
|
hn, _ := os.Hostname()
|
|
rec := signedRec(t, fx.priv, map[string]any{"kind": "dcn_move_plan", "store_id": 1, "plan_id": 41,
|
|
"server": hn, "root": fx.store, "archive": "Archive",
|
|
"moves": []map[string]any{{"from": "a.txt", "to": "Archive/a.txt"}},
|
|
"expires": time.Now().Add(time.Hour).UTC().Format(time.RFC3339)})
|
|
payload, _ := json.Marshal(map[string]any{"terms_canonical": rec.TermsCanonical,
|
|
"signature": rec.Signature, "mode": "apply"})
|
|
out, why, exit := dcnRunOp(t, func(ws *websocket.Conn) {
|
|
handleDcnPlanApply(ws, fx.dir, []string{"1", base64.StdEncoding.EncodeToString(payload)})
|
|
})
|
|
if exit != 0 || !strings.Contains(out, `"ok"`) {
|
|
t.Fatalf("plan apply: exit %d %s %s", exit, out, why)
|
|
}
|
|
if b, err := os.ReadFile(filepath.Join(fx.store, ".agent", "plans", "41.json")); err != nil ||
|
|
!strings.Contains(string(b), `"state": "ok"`) {
|
|
t.Fatalf("the plan's report was not written into .agent/plans: %q %v", b, err)
|
|
}
|
|
|
|
if err := os.Symlink(filepath.Join(".agent", "extract", "ab", "report.txt"), filepath.Join(fx.store, "notes.txt")); err != nil {
|
|
t.Skipf("symlinks unavailable: %v", err)
|
|
}
|
|
if err := os.Symlink(".agent", filepath.Join(fx.store, "ag")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, p := range []string{"notes.txt", "ag/extract/ab/report.txt", "ag/manifest.yaml"} {
|
|
if got, _, exit := fx.read(t, p); exit != 126 || strings.Contains(got, "PLAINTEXT") {
|
|
t.Errorf("read_range %q, a link into .agent: exit %d %q, want 126", p, exit, got)
|
|
}
|
|
}
|
|
if _, exit := fx.listed(t, "ag"); exit != 126 {
|
|
t.Errorf("list of a link to .agent: exit %d, want 126", exit)
|
|
}
|
|
}
|
|
|
|
// The agent's folder is named as the file system names it, and on every OS
|
|
// regardless of case, as the agent's own walker treats it: a ".Agent" left on
|
|
// a volume that ignores case is the agent's folder.
|
|
func TestDcnTheAgentsFolderIsMatchedHoweverTheNameIsCased(t *testing.T) {
|
|
for _, c := range []struct {
|
|
goos, path string
|
|
want bool
|
|
}{
|
|
{"linux", ".agent/extract/x", true},
|
|
{"linux", ".AGENT/extract/x", true},
|
|
{"darwin", ".Agent/vectors.db", true},
|
|
{"windows", ".AGENT/extract/x", true},
|
|
{"windows", ".agent./extract/x", true},
|
|
{"windows", ".agent /extract/x", true},
|
|
{"windows", ".agent::$INDEX_ALLOCATION/x", true},
|
|
{"linux", "deep/.agent", true},
|
|
{"linux", ".agentx/x", false},
|
|
{"linux", "agent/x", false},
|
|
{"linux", "notes/.agent-old/x", false},
|
|
{"darwin", "docs/readme.md", false},
|
|
} {
|
|
if got := dcnExcludedOn(c.goos, nil, c.path); got != c.want {
|
|
t.Errorf("%s: %q refused=%v, want %v", c.goos, c.path, got, c.want)
|
|
}
|
|
}
|
|
}
|