test(auth): gate local and OIDC authentication release
This commit is contained in:
@@ -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, ¤t) != nil || current.Token == "" || current.Token != lock.token {
|
||||
return ErrOwnership
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user