package authconfig import ( "bytes" "context" "errors" "fmt" "os" "path/filepath" "strings" "testing" "github.com/aritmolab/thothii/tools/tht/internal/authprojection" "github.com/aritmolab/thothii/tools/tht/internal/safeio" ) func TestRunProjectedMutationHoldsOuterLockAcrossCanonicalAndProjection(t *testing.T) { canonicalRoot := writeAuthFiles(t, defaultAuthYAML, registryYAML(adminUserYAML("admin", "Admin", true, "admin"))) spec := testProjectionSpec(t) entered := make(chan struct{}) release := make(chan struct{}) contended := make(chan struct{}, 8) restoreHooks := setProjectionCoordinatorHooksForTest(projectionCoordinatorHooks{ onOuterLockContention: func() { select { case contended <- struct{}{}: default: } }, }) t.Cleanup(restoreHooks) first := make(chan error, 1) go func() { first <- RunProjectedMutation(context.Background(), canonicalRoot, spec, func() error { close(entered) <-release return nil }) }() <-entered if _, err := authprojection.Inspect(toRuntimeSpec(spec)); !errors.Is(err, authprojection.ErrBlocked) { t.Fatalf("Inspect() error = %v, want blocked while mutation is inside the coordinator", err) } second := make(chan error, 1) go func() { second <- RunProjectedMutation(context.Background(), canonicalRoot, spec, func() error { return nil }) }() external := make(chan error, 1) go func() { transaction, err := BeginExternalProjectionTransaction(context.Background(), canonicalRoot, spec) if err == nil { err = transaction.Close() } external <- err }() <-contended <-contended close(release) if err := <-first; err != nil { t.Fatalf("first RunProjectedMutation() error = %v", err) } if err := <-second; err != nil { t.Fatalf("second RunProjectedMutation() error = %v", err) } if err := <-external; err != nil { t.Fatalf("BeginExternalProjectionTransaction() error = %v", err) } } func TestRunProjectedMutationPublishesExactLocalAndOIDCSnapshots(t *testing.T) { for _, fixture := range []struct{ name, auth, users string }{ {"local", defaultAuthYAML, registryYAML(adminUserYAML("admin", "Admin", true, "admin"))}, {"oidc", "version: 1\nmode: oidc\npublicUrl: https://example.invalid\n", ""}, } { t.Run(fixture.name, func(t *testing.T) { canonicalRoot := writeAuthFiles(t, fixture.auth, fixture.users) spec := testProjectionSpec(t) if err := RunProjectedMutation(context.Background(), canonicalRoot, spec, func() error { return nil }); err != nil { t.Fatalf("RunProjectedMutation() error = %v", err) } status, err := authprojection.Inspect(toRuntimeSpec(spec)) if err != nil { t.Fatalf("Inspect() error = %v", err) } if status.Snapshot.Mode != fixture.name || !bytes.Equal(status.Snapshot.Auth, []byte(fixture.auth)) { t.Fatal("published snapshot does not match canonical auth.yaml") } if fixture.name == "local" && !bytes.Equal(status.Snapshot.Users, []byte(fixture.users)) { t.Fatal("published local snapshot does not match canonical users.yaml") } if fixture.name == "oidc" && status.Snapshot.Users != nil { t.Fatal("published OIDC snapshot unexpectedly includes users.yaml") } }) } } func TestRunProjectedMutationRestoresPriorReadyWhenMutationFailsWithoutChangingCanonical(t *testing.T) { canonicalRoot := writeAuthFiles(t, defaultAuthYAML, registryYAML(adminUserYAML("admin", "Admin", true, "admin"))) spec := testProjectionSpec(t) before := publishCanonical(t, canonicalRoot, spec) if err := RunProjectedMutation(context.Background(), canonicalRoot, spec, func() error { return errors.New("mutation failed") }); err == nil { t.Fatal("RunProjectedMutation() succeeded after a failed mutation") } after := inspectCanonicalProjection(t, spec) if after.Generation != before.Generation { t.Fatalf("generation = %s, want restored %s", after.Generation, before.Generation) } } func TestRunProjectedMutationLeavesBlockedWhenMutationChangesCanonicalThenFails(t *testing.T) { canonicalRoot := writeAuthFiles(t, defaultAuthYAML, registryYAML(adminUserYAML("admin", "Admin", true, "admin"))) spec := testProjectionSpec(t) publishCanonical(t, canonicalRoot, spec) if err := RunProjectedMutation(context.Background(), canonicalRoot, spec, func() error { return writeCanonicalAuth(canonicalRoot, strings.Replace(defaultAuthYAML, "8080", "8181", 1), errors.New("mutation failed")) }); err == nil { t.Fatal("RunProjectedMutation() succeeded after changing canonical authentication then failing") } if _, err := authprojection.Inspect(toRuntimeSpec(spec)); !errors.Is(err, authprojection.ErrBlocked) { t.Fatalf("Inspect() error = %v, want blocked after divergent mutation failure", err) } } func TestRunProjectedMutationLeavesBlockedWhenPublicationOrVerificationFails(t *testing.T) { for _, fixture := range []struct { name string mutate func(string) error hooks projectionCoordinatorHooks }{ {"invalid canonical after mutation", func(directory string) error { return safeio.ReplaceCanonicalRegular(filepath.Join(directory, authFileName), []byte("not: [valid\n"), 0o600) }, projectionCoordinatorHooks{}}, {"post-commit equality verification", func(string) error { return nil }, projectionCoordinatorHooks{ verifyCommittedProjection: func(authprojection.Status, authprojection.Snapshot) error { return errors.New("synthetic verification failure") }, }}, } { t.Run(fixture.name, func(t *testing.T) { canonicalRoot := writeAuthFiles(t, defaultAuthYAML, registryYAML(adminUserYAML("admin", "Admin", true, "admin"))) spec := testProjectionSpec(t) publishCanonical(t, canonicalRoot, spec) restoreHooks := setProjectionCoordinatorHooksForTest(fixture.hooks) t.Cleanup(restoreHooks) if err := RunProjectedMutation(context.Background(), canonicalRoot, spec, func() error { return fixture.mutate(canonicalRoot) }); err == nil { t.Fatal("RunProjectedMutation() unexpectedly succeeded") } if _, err := authprojection.Inspect(toRuntimeSpec(spec)); !errors.Is(err, authprojection.ErrBlocked) { t.Fatalf("Inspect() error = %v, want blocked after failed publication", err) } }) } } func TestPublishProjectedCanonicalRepairsBlockedStateFromCanonicalOnly(t *testing.T) { canonicalRoot := writeAuthFiles(t, defaultAuthYAML, registryYAML(adminUserYAML("admin", "Admin", true, "admin"))) spec := testProjectionSpec(t) publishCanonical(t, canonicalRoot, spec) if err := RunProjectedMutation(context.Background(), canonicalRoot, spec, func() error { return writeCanonicalAuth(canonicalRoot, strings.Replace(defaultAuthYAML, "8080", "8181", 1), errors.New("fail after changing canonical bytes")) }); err == nil { t.Fatal("RunProjectedMutation() succeeded after a divergent failed mutation") } if _, err := authprojection.Inspect(toRuntimeSpec(spec)); !errors.Is(err, authprojection.ErrBlocked) { t.Fatalf("Inspect() error = %v, want blocked before canonical repair", err) } status, err := PublishProjectedCanonical(context.Background(), canonicalRoot, spec) if err != nil || !status.Equal || status.State != "ready" { t.Fatalf("PublishProjectedCanonical() = %#v, %v; want an equal ready projection", status, err) } } func TestExternalProjectionTransactionRepublishesRecoveredCanonicalUnderOneOuterLock(t *testing.T) { canonicalRoot := writeAuthFiles(t, defaultAuthYAML, registryYAML(adminUserYAML("admin", "Admin", true, "admin"))) spec := testProjectionSpec(t) initial := publishCanonical(t, canonicalRoot, spec) transaction, err := BeginExternalProjectionTransaction(context.Background(), canonicalRoot, spec) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = transaction.Close() }) if err := writeCanonicalAuth(canonicalRoot, strings.Replace(defaultAuthYAML, "8080", "8181", 1), nil); err != nil { t.Fatal(err) } candidate, err := transaction.PublishCanonical() if err != nil || !candidate.Equal || candidate.Generation == initial.Generation { t.Fatalf("candidate PublishCanonical() = %#v, %v", candidate, err) } if err := writeCanonicalAuth(canonicalRoot, defaultAuthYAML, nil); err != nil { t.Fatal(err) } recovered, err := transaction.PublishCanonical() if err != nil || !recovered.Equal || recovered.Generation != initial.Generation { t.Fatalf("recovery PublishCanonical() = %#v, %v; want original generation", recovered, err) } } func TestExternalProjectionTransactionCloseNeverMakesChangedCanonicalReady(t *testing.T) { canonicalRoot := writeAuthFiles(t, defaultAuthYAML, registryYAML(adminUserYAML("admin", "Admin", true, "admin"))) spec := testProjectionSpec(t) publishCanonical(t, canonicalRoot, spec) transaction, err := BeginExternalProjectionTransaction(context.Background(), canonicalRoot, spec) if err != nil { t.Fatal(err) } if err := writeCanonicalAuth(canonicalRoot, strings.Replace(defaultAuthYAML, "8080", "8181", 1), nil); err != nil { t.Fatal(err) } if err := transaction.Close(); err != nil { t.Fatal(err) } if _, err := authprojection.Inspect(toRuntimeSpec(spec)); !errors.Is(err, authprojection.ErrBlocked) { t.Fatalf("Inspect() error = %v, want blocked after closing with changed canonical bytes", err) } } func TestProjectionCoordinatorErrorsAndLogsNeverContainSyntheticPasswordsOrHashes(t *testing.T) { const secret = "synthetic-password-or-hash-must-not-leak" canonicalRoot := writeAuthFiles(t, defaultAuthYAML, registryYAML(adminUserYAML("admin", "Admin", true, "admin"))) err := RunProjectedMutation(context.Background(), canonicalRoot, testProjectionSpec(t), func() error { return fmt.Errorf("mutation failed: %s", secret) }) if err == nil || strings.Contains(err.Error(), secret) { t.Fatalf("RunProjectedMutation() error = %q, must be sanitized", err) } } func testProjectionSpec(t *testing.T) ProjectionSpec { t.Helper() runtimeRoot := t.TempDir() if err := os.Chmod(runtimeRoot, 0o700); err != nil { t.Fatal(err) } return ProjectionSpec{RuntimeRoot: runtimeRoot, UID: uint32(os.Getuid()), GID: uint32(os.Getgid())} } func toRuntimeSpec(spec ProjectionSpec) authprojection.Spec { return authprojection.Spec{RuntimeRoot: spec.RuntimeRoot, UID: spec.UID, GID: spec.GID} } func publishCanonical(t *testing.T, canonicalRoot string, spec ProjectionSpec) ProjectionStatus { t.Helper() status, err := PublishProjectedCanonical(context.Background(), canonicalRoot, spec) if err != nil || !status.Equal || status.State != "ready" { t.Fatalf("PublishProjectedCanonical() = %#v, %v", status, err) } return status } func inspectCanonicalProjection(t *testing.T, spec ProjectionSpec) ProjectionStatus { t.Helper() status, err := authprojection.Inspect(toRuntimeSpec(spec)) if err != nil { t.Fatal(err) } return ProjectionStatus{State: status.Selector.State, Generation: status.Snapshot.Generation, CanonicalRevision: status.Snapshot.CanonicalRevision, Equal: true} } func writeCanonicalAuth(directory, contents string, after error) error { if err := safeio.ReplaceCanonicalRegular(filepath.Join(directory, authFileName), []byte(contents), 0o600); err != nil { return err } return after }