Files
ThothII/tools/tht/internal/authconfig/projection_transaction_linux_test.go

301 lines
9.5 KiB
Go

//go:build linux
package authconfig
import (
"context"
"errors"
"os"
"path/filepath"
"sync/atomic"
"testing"
"time"
"github.com/aritmolab/thothii/tools/tht/internal/authprojection"
"github.com/aritmolab/thothii/tools/tht/internal/safeio"
"github.com/aritmolab/thothii/tools/tht/internal/testsupport"
)
func TestBeginExternalTransactionReleasesOuterLockAfterRuntimeCancellation(t *testing.T) {
canonicalRoot := writeAuthFiles(t, defaultAuthYAML, registryYAML(adminUserYAML("admin", "Admin", true, "admin")))
spec := testProjectionSpec(t)
raw, err := authprojection.Begin(toRuntimeSpec(spec), nil, false)
if err != nil {
t.Fatal(err)
}
defer func() {
if raw != nil {
_ = raw.Close()
}
}()
admitted := make(chan struct{})
restore := setRuntimeProjectionBeginForTest(func(ctx context.Context, runtimeSpec authprojection.Spec, before *authprojection.Snapshot, requireReadyMatch bool) (*authprojection.Transaction, error) {
close(admitted)
return authprojection.BeginContext(ctx, runtimeSpec, before, requireReadyMatch)
})
t.Cleanup(restore)
ctx, cancel := context.WithCancel(context.Background())
result := make(chan error, 1)
go func() {
transaction, err := BeginExternalProjectionTransaction(ctx, canonicalRoot, spec)
if err == nil {
err = transaction.Close()
}
result <- err
}()
<-admitted
cancel()
select {
case err := <-result:
if err == nil {
t.Fatal("BeginExternalProjectionTransaction() unexpectedly succeeded after cancellation")
}
case <-time.After(2 * time.Second):
t.Fatal("BeginExternalProjectionTransaction() did not return after runtime cancellation")
}
restore()
if err := raw.Close(); err != nil {
t.Fatal(err)
}
raw = nil
next, err := BeginExternalProjectionTransaction(context.Background(), canonicalRoot, spec)
if err != nil {
t.Fatalf("outer lock remained held after runtime cancellation: %v", err)
}
if err := next.Close(); err != nil {
t.Fatal(err)
}
}
func TestExternalPublishContextReturnsWhenCanonicalLockIsHeld(t *testing.T) {
canonicalRoot := writeAuthFiles(t, defaultAuthYAML, registryYAML(adminUserYAML("admin", "Admin", true, "admin")))
ctx, cancel := context.WithCancel(context.Background())
transaction, err := BeginExternalProjectionTransaction(ctx, canonicalRoot, testProjectionSpec(t))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = transaction.Close() })
canonicalLock, err := acquireLock(canonicalRoot)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = canonicalLock.Unlock() })
contended := make(chan struct{}, 1)
reblocked := make(chan struct{}, 1)
restore := setProjectionCoordinatorHooksForTest(projectionCoordinatorHooks{
onCanonicalLockContention: func() { contended <- struct{}{} },
beforeProjectionBegin: func() { reblocked <- struct{}{} },
})
t.Cleanup(restore)
result := make(chan error, 1)
go func() {
_, err := transaction.PublishCanonical()
result <- err
}()
<-contended
if _, err := authprojection.Inspect(toRuntimeSpec(testProjectionSpecFromTransaction(transaction))); !errors.Is(err, authprojection.ErrBlocked) {
t.Fatalf("Inspect() error = %v, want blocked while waiting for canonical lock", err)
}
cancel()
select {
case <-reblocked:
t.Fatal("publishCanonicalContext() re-blocked even though Commit was never reached")
case err := <-result:
if !errors.Is(err, errProjectionPublish) {
t.Fatalf("publishCanonicalContext() error = %v, want sanitized publish failure", err)
}
case <-time.After(2 * time.Second):
t.Fatal("publishCanonicalContext() did not return after cancellation")
}
}
func TestExternalRestoreContextReturnsWhenCanonicalLockIsHeld(t *testing.T) {
canonicalRoot := writeAuthFiles(t, defaultAuthYAML, registryYAML(adminUserYAML("admin", "Admin", true, "admin")))
spec := testProjectionSpec(t)
publishCanonical(t, canonicalRoot, spec)
ctx, cancel := context.WithCancel(context.Background())
transaction, err := BeginExternalProjectionTransaction(ctx, canonicalRoot, spec)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = transaction.Close() })
canonicalLock, err := acquireLock(canonicalRoot)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = canonicalLock.Unlock() })
contended := make(chan struct{}, 1)
restoreHooks := setProjectionCoordinatorHooksForTest(projectionCoordinatorHooks{
onCanonicalLockContention: func() { contended <- struct{}{} },
})
t.Cleanup(restoreHooks)
result := make(chan error, 1)
go func() { result <- transaction.RestorePriorIfCanonicalUnchanged() }()
<-contended
if _, err := authprojection.Inspect(toRuntimeSpec(spec)); !errors.Is(err, authprojection.ErrBlocked) {
t.Fatalf("Inspect() error = %v, want blocked while waiting for canonical lock", err)
}
cancel()
select {
case err := <-result:
if !errors.Is(err, errProjectionIntegrity) {
t.Fatalf("RestorePriorIfCanonicalUnchanged() error = %v, want sanitized integrity failure", err)
}
case <-time.After(2 * time.Second):
t.Fatal("RestorePriorIfCanonicalUnchanged() did not return after cancellation")
}
}
func TestAcquireTransactionLockRejectsSwapBetweenOpenAndRevalidation(t *testing.T) {
canonicalRoot := writeAuthFiles(t, defaultAuthYAML, registryYAML(adminUserYAML("admin", "Admin", true, "admin")))
opened := make(chan struct{})
release := make(chan struct{})
var firstOpen atomic.Bool
restore := setProjectionLockHooksForTest(projectionLockHooks{afterOpen: func() {
if firstOpen.CompareAndSwap(false, true) {
close(opened)
<-release
}
}})
t.Cleanup(restore)
first := make(chan struct {
lock *transactionLock
err error
}, 1)
go func() {
lock, err := acquireTransactionLock(context.Background(), canonicalRoot)
first <- struct {
lock *transactionLock
err error
}{lock, err}
}()
<-opened
replacement := filepath.Join(canonicalRoot, ".replacement-lock")
if err := safeio.WriteCanonicalNewFile(replacement, nil, 0o600); err != nil {
t.Fatal(err)
}
if err := os.Rename(replacement, filepath.Join(canonicalRoot, transactionLockFileName)); err != nil {
t.Fatal(err)
}
second, err := acquireTransactionLock(context.Background(), canonicalRoot)
if err != nil {
t.Fatalf("replacement-domain acquisition error = %v", err)
}
t.Cleanup(func() { _ = second.Close() })
close(release)
firstResult := <-first
if firstResult.lock != nil || !errors.Is(firstResult.err, errProjectionIntegrity) {
t.Fatalf("swapped original acquisition = %#v, %v; want no old-domain lock", firstResult.lock, firstResult.err)
}
}
func TestAcquireTransactionLockRejectsUnsafeMetadata(t *testing.T) {
for _, fixture := range []struct {
name string
arrange func(*testing.T, string)
}{
{
name: "symlink",
arrange: func(t *testing.T, path string) {
target := path + ".target"
if err := safeio.WriteCanonicalNewFile(target, nil, 0o600); err != nil {
t.Fatal(err)
}
testsupport.SymlinkOrSkip(t, target, path)
},
},
{
name: "hardlink",
arrange: func(t *testing.T, path string) {
if err := safeio.WriteCanonicalNewFile(path, nil, 0o600); err != nil {
t.Fatal(err)
}
if err := os.Link(path, path+".linked"); err != nil {
t.Fatal(err)
}
},
},
{
name: "non-0600 mode",
arrange: func(t *testing.T, path string) {
if err := safeio.WriteCanonicalNewFile(path, nil, 0o600); err != nil {
t.Fatal(err)
}
if err := os.Chmod(path, 0o640); err != nil {
t.Fatal(err)
}
},
},
{
name: "wrong owner",
arrange: func(t *testing.T, path string) {
if os.Geteuid() != 0 {
t.Skip("requires root to create a wrong numeric owner")
}
if err := safeio.WriteCanonicalNewFile(path, nil, 0o600); err != nil {
t.Fatal(err)
}
if err := os.Chown(path, 10001, 10001); err != nil {
t.Fatal(err)
}
},
},
} {
t.Run(fixture.name, func(t *testing.T) {
canonicalRoot := writeAuthFiles(t, defaultAuthYAML, registryYAML(adminUserYAML("admin", "Admin", true, "admin")))
fixture.arrange(t, filepath.Join(canonicalRoot, transactionLockFileName))
lock, err := acquireTransactionLock(context.Background(), canonicalRoot)
if lock != nil || !errors.Is(err, errProjectionIntegrity) {
t.Fatalf("acquireTransactionLock() = %#v, %v; want unsafe metadata rejection", lock, err)
}
})
}
}
func TestAcquireTransactionLockAcceptsSafeCreatorRace(t *testing.T) {
canonicalRoot := writeAuthFiles(t, defaultAuthYAML, registryYAML(adminUserYAML("admin", "Admin", true, "admin")))
paused := make(chan struct{})
release := make(chan struct{})
var firstCreate atomic.Bool
restore := setProjectionLockHooksForTest(projectionLockHooks{beforeCreate: func() {
if firstCreate.CompareAndSwap(false, true) {
close(paused)
<-release
}
}})
t.Cleanup(restore)
first := make(chan struct {
lock *transactionLock
err error
}, 1)
go func() {
lock, err := acquireTransactionLock(context.Background(), canonicalRoot)
first <- struct {
lock *transactionLock
err error
}{lock, err}
}()
<-paused
second, err := acquireTransactionLock(context.Background(), canonicalRoot)
if err != nil {
t.Fatalf("creator-race winner error = %v", err)
}
if err := second.Close(); err != nil {
t.Fatal(err)
}
close(release)
result := <-first
if result.err != nil || result.lock == nil {
t.Fatalf("creator-race loser = %#v, %v; want same validated lock domain", result.lock, result.err)
}
if err := result.lock.Close(); err != nil {
t.Fatal(err)
}
}
func testProjectionSpecFromTransaction(transaction *ExternalProjectionTransaction) ProjectionSpec {
return ProjectionSpec{RuntimeRoot: transaction.spec.RuntimeRoot, UID: transaction.spec.UID, GID: transaction.spec.GID}
}