266 lines
11 KiB
Go
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
|
|
}
|