feat(auth): coordinate canonical projection publication
This commit is contained in:
@@ -2,11 +2,14 @@ package authconfig
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/authprojection"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/safeio"
|
||||
"github.com/gofrs/flock"
|
||||
"gopkg.in/yaml.v3"
|
||||
@@ -53,6 +56,34 @@ func load(directory string) (Config, Registry, error) {
|
||||
return config, registry, nil
|
||||
}
|
||||
|
||||
// loadSnapshotBytes reads only canonical, validated authentication files and returns a detached,
|
||||
// bounded projection snapshot. Callers that coordinate a mutation hold .auth.lock around it.
|
||||
func loadSnapshotBytes(directory string) (authprojection.Snapshot, error) {
|
||||
if err := requirePrivateDirectory(directory); err != nil {
|
||||
return authprojection.Snapshot{}, err
|
||||
}
|
||||
authContents, err := readPrivateFile(filepath.Join(directory, authFileName))
|
||||
if err != nil {
|
||||
return authprojection.Snapshot{}, err
|
||||
}
|
||||
var config Config
|
||||
if err := decodeStrictYAML(authContents, &config); err != nil || !validConfig(config) {
|
||||
return authprojection.Snapshot{}, errInvalidAuthenticationConfig
|
||||
}
|
||||
var usersContents []byte
|
||||
if config.Mode == "local" {
|
||||
usersContents, err = readPrivateFile(filepath.Join(directory, config.Local.UsersFile))
|
||||
if err != nil {
|
||||
return authprojection.Snapshot{}, err
|
||||
}
|
||||
var registry Registry
|
||||
if err := decodeStrictYAML(usersContents, ®istry); err != nil || validateRegistry(registry) != nil {
|
||||
return authprojection.Snapshot{}, errInvalidAuthenticationConfig
|
||||
}
|
||||
}
|
||||
return authprojection.NewSnapshot(config.Mode, append([]byte(nil), authContents...), append([]byte(nil), usersContents...))
|
||||
}
|
||||
|
||||
// MutateUsers serializes the entire read-check-write transaction under the configuration lock.
|
||||
// The resulting registry is revalidated and atomically replaced only after all invariants hold.
|
||||
func MutateUsers(directory string, mutate func(*Registry) error) error {
|
||||
@@ -137,6 +168,13 @@ func readPrivateFile(path string) ([]byte, error) {
|
||||
}
|
||||
|
||||
func acquireLock(directory string) (*flock.Flock, error) {
|
||||
return acquireLockContext(context.Background(), directory)
|
||||
}
|
||||
|
||||
func acquireLockContext(ctx context.Context, directory string) (*flock.Flock, error) {
|
||||
if ctx == nil || ctx.Err() != nil {
|
||||
return nil, errInvalidAuthenticationConfig
|
||||
}
|
||||
path := filepath.Join(directory, lockFileName)
|
||||
if _, err := os.Lstat(path); errors.Is(err, os.ErrNotExist) {
|
||||
if err := safeio.WriteCanonicalNewFile(path, nil, 0o600); err != nil {
|
||||
@@ -153,9 +191,19 @@ func acquireLock(directory string) (*flock.Flock, error) {
|
||||
return nil, err
|
||||
}
|
||||
lock := flock.New(path, flock.SetPermissions(0o600))
|
||||
if err := lock.Lock(); err != nil {
|
||||
locked, err := lock.TryLock()
|
||||
if err != nil {
|
||||
return nil, errInvalidAuthenticationConfig
|
||||
}
|
||||
if !locked {
|
||||
if hook := coordinatorHooks().onCanonicalLockContention; hook != nil {
|
||||
hook()
|
||||
}
|
||||
locked, err = lock.TryLockContext(ctx, 10*time.Millisecond)
|
||||
if err != nil || !locked {
|
||||
return nil, errInvalidAuthenticationConfig
|
||||
}
|
||||
}
|
||||
if _, err := readPrivateFile(path); err != nil {
|
||||
_ = lock.Unlock()
|
||||
return nil, err
|
||||
|
||||
Reference in New Issue
Block a user