test(auth): gate local and OIDC authentication release

This commit is contained in:
2026-08-18 06:02:25 +02:00
parent 7cf7d9db6b
commit 8a3fa5031d
23 changed files with 1711 additions and 168 deletions
+68 -2
View File
@@ -16,8 +16,10 @@ import (
)
var (
ErrLocked = errors.New("another lifecycle operation is already running for this installation")
ErrOwnership = errors.New("lifecycle lock ownership changed; refusing to remove it")
ErrLocked = errors.New("another lifecycle operation is already running for this installation")
ErrOwnership = errors.New("lifecycle lock ownership changed; refusing to remove it")
ErrTransactionInactive = errors.New("lifecycle transaction capability is not active")
ErrTransactionInstallation = errors.New("lifecycle transaction capability belongs to another installation")
)
const lockFileName = "lifecycle.lock.owner.json"
@@ -36,6 +38,14 @@ type Lock struct {
released bool
}
// Transaction is an opaque, installation-bound capability for work that must run while a
// lifecycle lock remains owned. Its fields are deliberately private so callers can obtain one
// only through AcquireTransaction.
type Transaction struct {
lock *Lock
controlDirectory string
}
// Acquire obtains the shared lock used by backup, restore, Pi lifecycle and product updates.
func Acquire(installation config.Installation) (*Lock, error) {
directory := installation.ControlDirectory()
@@ -76,6 +86,39 @@ func Acquire(installation config.Installation) (*Lock, error) {
return &Lock{path: path, token: token}, nil
}
// AcquireTransaction obtains a lifecycle lock and returns the capability required by callers
// that perform nested work inside the same non-reentrant transaction.
func AcquireTransaction(installation config.Installation) (*Transaction, error) {
lock, err := Acquire(installation)
if err != nil {
return nil, err
}
return &Transaction{
lock: lock,
controlDirectory: filepath.Clean(installation.ControlDirectory()),
}, nil
}
// Verify refuses a nil, released, replaced, or foreign-installation capability before a nested
// lifecycle operation can begin.
func (transaction *Transaction) Verify(installation config.Installation) error {
if transaction == nil || transaction.lock == nil {
return ErrTransactionInactive
}
if transaction.controlDirectory != filepath.Clean(installation.ControlDirectory()) {
return ErrTransactionInstallation
}
return transaction.lock.verifyHeld()
}
// Release relinquishes the lifecycle lock associated with this transaction capability.
func (transaction *Transaction) Release() error {
if transaction == nil || transaction.lock == nil {
return nil
}
return transaction.lock.Release()
}
// Path returns the installation-private owner-file path for diagnostics and tests.
func (lock *Lock) Path() string {
if lock == nil {
@@ -111,3 +154,26 @@ func (lock *Lock) Release() error {
lock.released = true
return nil
}
func (lock *Lock) verifyHeld() error {
if lock == nil {
return ErrTransactionInactive
}
lock.mu.Lock()
defer lock.mu.Unlock()
if lock.released {
return ErrTransactionInactive
}
contents, err := os.ReadFile(lock.path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return ErrOwnership
}
return fmt.Errorf("read lifecycle lock owner: %w", err)
}
var current owner
if json.Unmarshal(contents, &current) != nil || current.Token == "" || current.Token != lock.token {
return ErrOwnership
}
return nil
}
+24
View File
@@ -48,3 +48,27 @@ func TestLifecycleLockReleaseDoesNotRemoveAnotherOwnersFile(t *testing.T) {
t.Fatalf("foreign lock was removed: %v", err)
}
}
func TestTransactionCapabilityIsInstallationBoundAndExpiresOnRelease(t *testing.T) {
root := t.TempDir()
installation := config.Installation{ProjectDirectory: root, Path: filepath.Join(root, "thothii-installation.yaml")}
transaction, err := AcquireTransaction(installation)
if err != nil {
t.Fatal(err)
}
if err := transaction.Verify(installation); err != nil {
t.Fatalf("Verify() active capability error = %v", err)
}
otherRoot := t.TempDir()
other := config.Installation{ProjectDirectory: otherRoot, Path: filepath.Join(otherRoot, "thothii-installation.yaml")}
if err := transaction.Verify(other); !errors.Is(err, ErrTransactionInstallation) {
t.Fatalf("Verify() for another installation error = %v, want ErrTransactionInstallation", err)
}
if err := transaction.Release(); err != nil {
t.Fatal(err)
}
if err := transaction.Verify(installation); !errors.Is(err, ErrTransactionInactive) {
t.Fatalf("Verify() after Release() error = %v, want ErrTransactionInactive", err)
}
}