| 1 | //go:build linux |
| 2 | |
| 3 | package provisioner |
| 4 | |
| 5 | import ( |
| 6 | "bytes" |
| 7 | "context" |
| 8 | "errors" |
| 9 | "os" |
| 10 | "os/exec" |
| 11 | "strings" |
| 12 | "testing" |
| 13 | "time" |
| 14 | |
| 15 | "amadan.net/oficina/home/internal/operator" |
| 16 | "github.com/go-json-experiment/json" |
| 17 | "github.com/go-json-experiment/json/jsontext" |
| 18 | ) |
| 19 | |
| 20 | func planFixture() InstallationPlan { |
| 21 | target := func(host, origin string) InstallationTarget { |
| 22 | return InstallationTarget{Console: "https://console.example.test", Account: "synthetic-account", App: "oficina/home", Host: host, Origin: origin, Release: strings.Repeat("a", 40), Artifact: strings.Repeat("b", 64), Environment: strings.Repeat("c", 64), DataKey: strings.Repeat("d", 64)} |
| 23 | } |
| 24 | return InstallationPlan{1, testToken(20), testToken(21), target("credential.example.test", "https://id.example.test"), target("directory.example.test", "https://home.example.test")} |
| 25 | } |
| 26 | func stateFixture(t *testing.T) (*journal, InstallationPlan) { |
| 27 | t.Helper() |
| 28 | j := journalFixture(t) |
| 29 | p := planFixture() |
| 30 | j.now = func() time.Time { return time.Unix(2000000000, 0) } |
| 31 | if e := j.enroll(context.Background(), p); e != nil { |
| 32 | t.Fatal(e) |
| 33 | } |
| 34 | return j, p |
| 35 | } |
| 36 | func snapshot(t *testing.T, j *journal) *installationBook { |
| 37 | t.Helper() |
| 38 | j.mu.Lock() |
| 39 | defer j.mu.Unlock() |
| 40 | b, e := j.bookLocked() |
| 41 | if e != nil { |
| 42 | t.Fatal(e) |
| 43 | } |
| 44 | return b |
| 45 | } |
| 46 | func initialization(op string) operator.InitializeInput { |
| 47 | return operator.InitializeInput{InputBase: operator.InputBase{Version: 1, Operation: "directory.initialize", OperationID: op}, Generation: testToken(22), Baseline: "operator-attests-never-provisioned"} |
| 48 | } |
| 49 | func TestInstallationPreparationReplayAndIndependentRoles(t *testing.T) { |
| 50 | j, p := stateFixture(t) |
| 51 | ctx := context.Background() |
| 52 | if e := j.enroll(ctx, p); e != nil { |
| 53 | t.Fatal(e) |
| 54 | } |
| 55 | changed := p |
| 56 | changed.Directory.Environment = strings.Repeat("e", 64) |
| 57 | if e := j.enroll(ctx, changed); e != ErrRefused { |
| 58 | t.Fatal("changed baseline", e) |
| 59 | } |
| 60 | raw, e := j.prepare(ctx, p.Installation, testToken(23), "directory.initialize", initialization(testToken(23))) |
| 61 | if e != nil { |
| 62 | t.Fatal(e) |
| 63 | } |
| 64 | before := snapshot(t, j) |
| 65 | req := before.installations[p.Installation].Operations[testToken(23)].Request |
| 66 | if req.Authorization().JournalSequence != 2 || req.Authorization().Sequence != 1 { |
| 67 | t.Fatal("independent sequences") |
| 68 | } |
| 69 | if before.installations[p.Installation].Generation != "" || before.installations[p.Installation].Heads["directory"].Sequence != 0 { |
| 70 | t.Fatal("preparation advanced authority") |
| 71 | } |
| 72 | j.now = func() time.Time { return time.Unix(2000001000, 0) } |
| 73 | repeated, e := j.prepare(ctx, p.Installation, testToken(23), "directory.initialize", initialization(testToken(23))) |
| 74 | if e != nil || !bytes.Equal(raw, repeated) { |
| 75 | t.Fatal("exact expired prepared request changed", e) |
| 76 | } |
| 77 | changedInput := initialization(testToken(23)) |
| 78 | changedInput.Generation = testToken(29) |
| 79 | if _, e = j.prepare(ctx, p.Installation, testToken(23), "directory.initialize", changedInput); e != ErrRefused { |
| 80 | t.Fatal("changed input", e) |
| 81 | } |
| 82 | if _, e = j.prepare(ctx, p.Installation, testToken(24), "directory.initialize", initialization(testToken(24))); e != ErrRefused { |
| 83 | t.Fatal("replaced unresolved attempt", e) |
| 84 | } |
| 85 | if e = j.markInFlight(ctx, p.Installation, testToken(25), testToken(23)); e != ErrRefused { |
| 86 | t.Fatal("expired dispatch", e) |
| 87 | } |
| 88 | credential, e := j.prepare(ctx, p.Installation, testToken(26), "credential.owner-seed", struct { |
| 89 | Version int `json:"version"` |
| 90 | }{1}) |
| 91 | if e != nil || len(credential) == 0 { |
| 92 | t.Fatal("independent credential role", e) |
| 93 | } |
| 94 | after := snapshot(t, j) |
| 95 | if after.installations[p.Installation].Operations[testToken(26)].Request.Authorization().Sequence != 1 || len(after.installations[p.Installation].Heads) != 0 { |
| 96 | t.Fatal("role advanced during preparation") |
| 97 | } |
| 98 | } |
| 99 | func TestInstallationAmbiguityNeverReleasesSlot(t *testing.T) { |
| 100 | j, p := stateFixture(t) |
| 101 | ctx := context.Background() |
| 102 | op := testToken(23) |
| 103 | if _, e := j.prepare(ctx, p.Installation, op, "directory.initialize", initialization(op)); e != nil { |
| 104 | t.Fatal(e) |
| 105 | } |
| 106 | if e := j.markInFlight(ctx, p.Installation, testToken(24), op); e != nil { |
| 107 | t.Fatal(e) |
| 108 | } |
| 109 | if e := j.markInFlight(ctx, p.Installation, testToken(24), op); e != nil { |
| 110 | t.Fatal("event replay", e) |
| 111 | } |
| 112 | if e := j.transition(ctx, p.Installation, testToken(25), "provisioner.outcome", "in-flight", localTransition{Operation: op, Kind: "ambiguous"}); e != nil { |
| 113 | t.Fatal(e) |
| 114 | } |
| 115 | j.now = func() time.Time { return time.Unix(2000001000, 0) } |
| 116 | if e := j.transition(ctx, p.Installation, testToken(26), "provisioner.outcome", "in-flight", localTransition{Operation: op, Kind: "refused"}); e != ErrRefused { |
| 117 | t.Fatal("ambiguous became refused", e) |
| 118 | } |
| 119 | if _, e := j.prepare(ctx, p.Installation, testToken(27), "directory.initialize", initialization(testToken(27))); e != ErrRefused { |
| 120 | t.Fatal("replaced ambiguous", e) |
| 121 | } |
| 122 | } |
| 123 | func TestInstallationRefusalRequiresExpiryBeforeReplacement(t *testing.T) { |
| 124 | j, p := stateFixture(t) |
| 125 | ctx := context.Background() |
| 126 | op := testToken(23) |
| 127 | if _, e := j.prepare(ctx, p.Installation, op, "directory.initialize", initialization(op)); e != nil { |
| 128 | t.Fatal(e) |
| 129 | } |
| 130 | if e := j.markInFlight(ctx, p.Installation, testToken(24), op); e != nil { |
| 131 | t.Fatal(e) |
| 132 | } |
| 133 | tr := localTransition{Operation: op, Kind: "refused"} |
| 134 | if e := j.transition(ctx, p.Installation, testToken(25), "provisioner.outcome", "in-flight", tr); e != ErrRefused { |
| 135 | t.Fatal("fresh slot released", e) |
| 136 | } |
| 137 | j.now = func() time.Time { return time.Unix(2000000300, 0) } |
| 138 | if e := j.transition(ctx, p.Installation, testToken(25), "provisioner.outcome", "in-flight", tr); e != nil { |
| 139 | t.Fatal(e) |
| 140 | } |
| 141 | if _, e := j.prepare(ctx, p.Installation, testToken(26), "directory.initialize", initialization(testToken(26))); e != nil { |
| 142 | t.Fatal(e) |
| 143 | } |
| 144 | s := snapshot(t, j).installations[p.Installation] |
| 145 | if s.Operations[testToken(26)].Request.Authorization().Sequence != 1 || s.Heads["directory"].Sequence != 0 { |
| 146 | t.Fatal("refusal consumed role sequence") |
| 147 | } |
| 148 | } |
| 149 | func TestInstallationExactLargeResultDoesNotAdvanceUntilTransition(t *testing.T) { |
| 150 | j, p := stateFixture(t) |
| 151 | ctx := context.Background() |
| 152 | op := testToken(23) |
| 153 | if _, e := j.prepare(ctx, p.Installation, op, "directory.initialize", initialization(op)); e != nil { |
| 154 | t.Fatal(e) |
| 155 | } |
| 156 | if e := j.markInFlight(ctx, p.Installation, testToken(24), op); e != nil { |
| 157 | t.Fatal(e) |
| 158 | } |
| 159 | result := operator.Result{Version: 1, OperationID: op, Outcome: "committed", Generation: testToken(22), Receipt: jsontext.Value(`"x"`)} |
| 160 | small, _ := json.Marshal(result, json.Deterministic(true)) |
| 161 | result.Receipt = jsontext.Value(`"` + strings.Repeat("x", (64<<10)-len(small)+1) + `"`) |
| 162 | exact, e := json.Marshal(result, json.Deterministic(true)) |
| 163 | if e != nil || len(exact) != 64<<10 { |
| 164 | t.Fatal("boundary", len(exact), e) |
| 165 | } |
| 166 | record, e := j.append(ctx, p.Installation, testToken(25), "provisioner.result", "in-flight", exact) |
| 167 | if e != nil { |
| 168 | t.Fatal(e) |
| 169 | } |
| 170 | s := snapshot(t, j).installations[p.Installation] |
| 171 | if s.Generation != "" || s.Heads["directory"].Sequence != 0 { |
| 172 | t.Fatal("orphan result advanced") |
| 173 | } |
| 174 | tr := localTransition{Operation: op, Kind: "accepted", ResultRecord: testToken(25), ResultHead: testToken(99)} |
| 175 | if e = j.transition(ctx, p.Installation, testToken(26), "provisioner.outcome", "in-flight", tr); e != ErrRefused { |
| 176 | t.Fatal("tampered result reference", e) |
| 177 | } |
| 178 | tr.ResultHead = hash(record) |
| 179 | if e = j.transition(ctx, p.Installation, testToken(26), "provisioner.outcome", "in-flight", tr); e != nil { |
| 180 | t.Fatal(e) |
| 181 | } |
| 182 | if e = j.transition(ctx, p.Installation, testToken(26), "provisioner.outcome", "in-flight", tr); e != nil { |
| 183 | t.Fatal("outcome exact replay", e) |
| 184 | } |
| 185 | s = snapshot(t, j).installations[p.Installation] |
| 186 | if s.Generation != testToken(22) || s.Heads["directory"].Sequence != 1 || !bytes.Equal(s.Operations[op].Result, exact) { |
| 187 | t.Fatal("accepted result changed") |
| 188 | } |
| 189 | if _, e = j.prepare(ctx, p.Installation, testToken(27), "directory.seed-team", struct { |
| 190 | Generation string `json:"expected_generation"` |
| 191 | }{s.Generation}); e != nil { |
| 192 | t.Fatal(e) |
| 193 | } |
| 194 | s = snapshot(t, j).installations[p.Installation] |
| 195 | if s.Operations[testToken(27)].Request.Authorization().Previous != s.Heads["directory"].Head { |
| 196 | t.Fatal("accepted chain not bound") |
| 197 | } |
| 198 | } |
| 199 | func TestInstallationPreparationCrashBoundaries(t *testing.T) { |
| 200 | for _, boundary := range []string{"prepared", "floor", "finalized"} { |
| 201 | t.Run(boundary, func(t *testing.T) { |
| 202 | j, p := stateFixture(t) |
| 203 | ctx := context.Background() |
| 204 | stop := errors.New("synthetic crash") |
| 205 | j.boundary = func(stage string) error { |
| 206 | if stage == boundary { |
| 207 | return stop |
| 208 | } |
| 209 | return nil |
| 210 | } |
| 211 | if _, e := j.prepare(ctx, p.Installation, testToken(23), "directory.initialize", initialization(testToken(23))); e != stop { |
| 212 | t.Fatal(e) |
| 213 | } |
| 214 | j.close() |
| 215 | reopened, e := openJournal(j.dir, j.floor, j.key, false) |
| 216 | if e != nil { |
| 217 | t.Fatal(e) |
| 218 | } |
| 219 | defer reopened.close() |
| 220 | reopened.now = func() time.Time { return time.Unix(2000001000, 0) } |
| 221 | raw, e := reopened.prepare(ctx, p.Installation, testToken(23), "directory.initialize", initialization(testToken(23))) |
| 222 | if e != nil || len(raw) == 0 { |
| 223 | t.Fatal("recover signed bytes", e) |
| 224 | } |
| 225 | s := snapshot(t, reopened).installations[p.Installation] |
| 226 | if len(s.Operations) != 1 || s.Heads["directory"].Sequence != 0 || s.Operations[testToken(23)].Request.Authorization().JournalSequence != 2 { |
| 227 | t.Fatal("crash reserved twice") |
| 228 | } |
| 229 | }) |
| 230 | } |
| 231 | } |
| 232 | |
| 233 | // These subprocesses exercise local custody and journal transitions only. They |
| 234 | // do not impersonate a worker or manufacture an authenticated remote outcome. |
| 235 | func TestNativeEnrollmentCLIAndProcessDeath(t *testing.T) { |
| 236 | if mode := os.Getenv("OFICINA_STATE_CHILD"); mode != "" { |
| 237 | state, floor := os.Getenv("OFICINA_STATE_PATH"), os.Getenv("OFICINA_FLOOR_PATH") |
| 238 | if mode == "enroll" { |
| 239 | if e := Run([]string{"provision", "enroll", "--state", state, "--floor-dir", floor, "--password-fd", "3", "--plan-fd", "4"}, os.Stdout); e != nil { |
| 240 | os.Exit(1) |
| 241 | } |
| 242 | os.Exit(0) |
| 243 | } |
| 244 | password, e := privatePassword(3) |
| 245 | if e != nil { |
| 246 | os.Exit(2) |
| 247 | } |
| 248 | j, e := existingJournal(state, floor, password) |
| 249 | clear(password) |
| 250 | if e != nil { |
| 251 | os.Exit(3) |
| 252 | } |
| 253 | defer closeExisting(j) |
| 254 | j.boundary = func(stage string) error { |
| 255 | if stage == mode { |
| 256 | os.Exit(72) |
| 257 | } |
| 258 | return nil |
| 259 | } |
| 260 | _, e = j.prepare(context.Background(), testToken(20), testToken(23), "directory.initialize", initialization(testToken(23))) |
| 261 | if e != nil { |
| 262 | os.Exit(4) |
| 263 | } |
| 264 | os.Exit(5) |
| 265 | } |
| 266 | for _, boundary := range []string{"prepared", "floor", "finalized"} { |
| 267 | t.Run(boundary, func(t *testing.T) { |
| 268 | state, floor := persistentDirectory(t), persistentDirectory(t) |
| 269 | password := []byte("synthetic-state-private-password") |
| 270 | if _, e := Initialize(state, floor, password); e != nil { |
| 271 | t.Fatal(e) |
| 272 | } |
| 273 | plan, _ := canonicalJSON(planFixture()) |
| 274 | run := func(mode string) ([]byte, error) { |
| 275 | r, w, e := os.Pipe() |
| 276 | if e != nil { |
| 277 | t.Fatal(e) |
| 278 | } |
| 279 | pr, pw, e := os.Pipe() |
| 280 | if e != nil { |
| 281 | t.Fatal(e) |
| 282 | } |
| 283 | defer r.Close() |
| 284 | defer pr.Close() |
| 285 | cmd := exec.Command(os.Args[0], "-test.run=^TestNativeEnrollmentCLIAndProcessDeath$") |
| 286 | cmd.Env = append(os.Environ(), "OFICINA_STATE_CHILD="+mode, "OFICINA_STATE_PATH="+state, "OFICINA_FLOOR_PATH="+floor) |
| 287 | cmd.ExtraFiles = []*os.File{r, pr} |
| 288 | var out bytes.Buffer |
| 289 | cmd.Stdout = &out |
| 290 | cmd.Stderr = &out |
| 291 | if e = cmd.Start(); e != nil { |
| 292 | t.Fatal(e) |
| 293 | } |
| 294 | r.Close() |
| 295 | pr.Close() |
| 296 | _, e = w.Write(password) |
| 297 | w.Close() |
| 298 | if e != nil { |
| 299 | t.Fatal(e) |
| 300 | } |
| 301 | if mode == "enroll" { |
| 302 | if _, e = pw.Write(plan); e != nil { |
| 303 | t.Fatal(e) |
| 304 | } |
| 305 | } |
| 306 | pw.Close() |
| 307 | e = cmd.Wait() |
| 308 | if bytes.Contains(out.Bytes(), password) || bytes.Contains(out.Bytes(), []byte(planFixture().Credential.Host)) { |
| 309 | t.Fatal("private output") |
| 310 | } |
| 311 | return out.Bytes(), e |
| 312 | } |
| 313 | first, e := run("enroll") |
| 314 | if e != nil { |
| 315 | t.Fatal("enroll CLI", e) |
| 316 | } |
| 317 | second, e := run("enroll") |
| 318 | if e != nil || !bytes.Equal(first, second) { |
| 319 | t.Fatal("plan replay", e) |
| 320 | } |
| 321 | j, e := existingJournal(state, floor, password) |
| 322 | if e != nil { |
| 323 | t.Fatal(e) |
| 324 | } |
| 325 | if _, e = run("enroll"); e == nil { |
| 326 | t.Fatal("second process bypassed registry lock") |
| 327 | } |
| 328 | closeExisting(j) |
| 329 | _, e = run(boundary) |
| 330 | var exit *exec.ExitError |
| 331 | if !errors.As(e, &exit) || exit.ExitCode() != 72 { |
| 332 | t.Fatal("process death boundary", e) |
| 333 | } |
| 334 | reopened, e := existingJournal(state, floor, password) |
| 335 | if e != nil { |
| 336 | t.Fatal(e) |
| 337 | } |
| 338 | defer closeExisting(reopened) |
| 339 | reopened.now = func() time.Time { return time.Now().Add(time.Hour) } |
| 340 | raw, e := reopened.prepare(context.Background(), testToken(20), testToken(23), "directory.initialize", initialization(testToken(23))) |
| 341 | if e != nil || len(raw) == 0 { |
| 342 | t.Fatal("recover after process death", e) |
| 343 | } |
| 344 | s := snapshot(t, reopened).installations[testToken(20)] |
| 345 | if len(s.Operations) != 1 || s.Heads["directory"].Sequence != 0 { |
| 346 | t.Fatal("process crash advanced authority") |
| 347 | } |
| 348 | }) |
| 349 | } |
| 350 | } |
| 351 | |
| 352 | func TestInstallationResultAndAcceptanceCrashBoundaries(t *testing.T) { |
| 353 | for _, phase := range []string{"result", "acceptance"} { |
| 354 | for _, boundary := range []string{"prepared", "floor", "finalized"} { |
| 355 | t.Run(phase+"/"+boundary, func(t *testing.T) { |
| 356 | j, p := stateFixture(t) |
| 357 | ctx := context.Background() |
| 358 | op := testToken(23) |
| 359 | if _, e := j.prepare(ctx, p.Installation, op, "directory.initialize", initialization(op)); e != nil { |
| 360 | t.Fatal(e) |
| 361 | } |
| 362 | if e := j.markInFlight(ctx, p.Installation, testToken(24), op); e != nil { |
| 363 | t.Fatal(e) |
| 364 | } |
| 365 | result, _ := json.Marshal(operator.Result{Version: 1, OperationID: op, Outcome: "committed", Generation: testToken(22)}, json.Deterministic(true)) |
| 366 | stop := errors.New("synthetic interruption") |
| 367 | inject := func() { |
| 368 | j.boundary = func(stage string) error { |
| 369 | if stage == boundary { |
| 370 | return stop |
| 371 | } |
| 372 | return nil |
| 373 | } |
| 374 | } |
| 375 | if phase == "result" { |
| 376 | inject() |
| 377 | } |
| 378 | record, e := j.append(ctx, p.Installation, testToken(25), "provisioner.result", "in-flight", result) |
| 379 | if phase == "result" && e != stop || phase == "acceptance" && e != nil { |
| 380 | t.Fatal(e) |
| 381 | } |
| 382 | if phase == "acceptance" { |
| 383 | inject() |
| 384 | if e = j.transition(ctx, p.Installation, testToken(26), "provisioner.outcome", "in-flight", localTransition{Operation: op, Kind: "accepted", ResultRecord: testToken(25), ResultHead: hash(record)}); e != stop { |
| 385 | t.Fatal(e) |
| 386 | } |
| 387 | } |
| 388 | j.close() |
| 389 | reopened, e := openJournal(j.dir, j.floor, j.key, false) |
| 390 | if e != nil { |
| 391 | t.Fatal(e) |
| 392 | } |
| 393 | defer reopened.close() |
| 394 | b := snapshot(t, reopened) |
| 395 | s := b.installations[p.Installation] |
| 396 | if phase == "result" && (s.Generation != "" || s.Heads["directory"].Sequence != 0) { |
| 397 | t.Fatal("orphan accepted") |
| 398 | } |
| 399 | if phase == "acceptance" && (s.Generation != testToken(22) || s.Heads["directory"].Sequence != 1) { |
| 400 | t.Fatal("accepted event lost") |
| 401 | } |
| 402 | tr := localTransition{Operation: op, Kind: "accepted", ResultRecord: testToken(25), ResultHead: b.heads[testToken(25)]} |
| 403 | if phase == "result" { |
| 404 | missing := tr |
| 405 | missing.ResultRecord = testToken(30) |
| 406 | if e = reopened.transition(ctx, p.Installation, testToken(26), "provisioner.outcome", "in-flight", missing); e != ErrRefused { |
| 407 | t.Fatal("missing result accepted", e) |
| 408 | } |
| 409 | } |
| 410 | if e = reopened.transition(ctx, p.Installation, testToken(26), "provisioner.outcome", "in-flight", tr); e != nil { |
| 411 | t.Fatal("exact acceptance recovery", e) |
| 412 | } |
| 413 | if snapshot(t, reopened).installations[p.Installation].Heads["directory"].Sequence != 1 { |
| 414 | t.Fatal("accepted twice") |
| 415 | } |
| 416 | }) |
| 417 | } |
| 418 | } |
| 419 | } |
| 420 | func TestInstallationRestoredOldJournalCannotForgetAcceptedHead(t *testing.T) { |
| 421 | j, p := stateFixture(t) |
| 422 | ctx := context.Background() |
| 423 | op := testToken(23) |
| 424 | if _, e := j.prepare(ctx, p.Installation, op, "directory.initialize", initialization(op)); e != nil { |
| 425 | t.Fatal(e) |
| 426 | } |
| 427 | if _, e := j.db.Exec("PRAGMA wal_checkpoint(TRUNCATE)"); e != nil { |
| 428 | t.Fatal(e) |
| 429 | } |
| 430 | path := j.dir.file.Name() + "/registry.db" |
| 431 | old, e := os.ReadFile(path) |
| 432 | if e != nil { |
| 433 | t.Fatal(e) |
| 434 | } |
| 435 | if e = j.markInFlight(ctx, p.Installation, testToken(24), op); e != nil { |
| 436 | t.Fatal(e) |
| 437 | } |
| 438 | result, _ := json.Marshal(operator.Result{Version: 1, OperationID: op, Outcome: "committed", Generation: testToken(22)}, json.Deterministic(true)) |
| 439 | record, e := j.append(ctx, p.Installation, testToken(25), "provisioner.result", "in-flight", result) |
| 440 | if e != nil { |
| 441 | t.Fatal(e) |
| 442 | } |
| 443 | if e = j.transition(ctx, p.Installation, testToken(26), "provisioner.outcome", "in-flight", localTransition{Operation: op, Kind: "accepted", ResultRecord: testToken(25), ResultHead: hash(record)}); e != nil { |
| 444 | t.Fatal(e) |
| 445 | } |
| 446 | j.close() |
| 447 | if e = os.WriteFile(path, old, 0600); e != nil { |
| 448 | t.Fatal(e) |
| 449 | } |
| 450 | reopened, e := openJournal(j.dir, j.floor, j.key, false) |
| 451 | if reopened != nil { |
| 452 | reopened.close() |
| 453 | } |
| 454 | if e != ErrRefused { |
| 455 | t.Fatal("old snapshot reset role authority", e) |
| 456 | } |
| 457 | } |
| 458 | |
| 459 | func TestInstallationPrivatePlanAndRequestStayEncrypted(t *testing.T) { |
| 460 | j, p := stateFixture(t) |
| 461 | input := operator.OwnerInput{InputBase: operator.InputBase{Version: 1, Operation: "credential.owner-seed", OperationID: testToken(23)}, Directory: p.Directory.Origin, TeamID: 1, TeamSQID: "Uk", Address: "native-state-private-32@example.invalid", ExistingOnly: true} |
| 462 | if _, e := j.prepare(context.Background(), p.Installation, testToken(23), "credential.owner-seed", input); e != nil { |
| 463 | t.Fatal(e) |
| 464 | } |
| 465 | for _, suffix := range []string{"", "-wal", "-shm"} { |
| 466 | raw, e := os.ReadFile(j.dir.file.Name() + "/registry.db" + suffix) |
| 467 | if e != nil && !os.IsNotExist(e) { |
| 468 | t.Fatal(e) |
| 469 | } |
| 470 | for _, canary := range []string{input.Address, p.Directory.Host, p.Credential.Host, p.Directory.Environment, p.Directory.Origin} { |
| 471 | if bytes.Contains(raw, []byte(canary)) { |
| 472 | t.Fatal("private journal plaintext") |
| 473 | } |
| 474 | } |
| 475 | } |
| 476 | for _, action := range []string{"accept", "refuse", "complete", "mark-stopped"} { |
| 477 | if e := Run([]string{"provision", action}, &bytes.Buffer{}); e != ErrRefused { |
| 478 | t.Fatal("CLI manufactured completion", e) |
| 479 | } |
| 480 | } |
| 481 | } |
| 482 | |