Files
ThothII/tools/tht/internal/authconfig/projection_transaction_test.go
T

266 lines
11 KiB
Go

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
}