Files
ThothII/tools/tht/internal/authconfig/projection_transaction_test.go
T
marcopan 610ae8c85a fix(auth): make local verification portable
Keep upstream identity visible while limiting logout to local auth. Inject the restore privilege gate so the deterministic core tests do not depend on the host OS, and confine descriptor-backed projection tests to Linux. Accept the real remaining Pi timeout budget instead of an exact millisecond.
2026-08-25 10:50:30 +02:00

268 lines
11 KiB
Go

//go:build linux
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
}