301 lines
9.5 KiB
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}
|
|
}
|