fix: run DWH verification with read-only registry access
This commit is contained in:
@@ -47,6 +47,8 @@ var (
|
||||
ErrConflict = errors.New("credential record already exists")
|
||||
// ErrIntegrity means an unsafe or malformed registry object was observed.
|
||||
ErrIntegrity = errors.New("registry integrity failure")
|
||||
// ErrReadOnly means a runtime reader was asked to mutate registry state.
|
||||
ErrReadOnly = errors.New("registry is read-only")
|
||||
)
|
||||
|
||||
// PublicRecord is the redacted inventory form of a persisted record.
|
||||
@@ -64,11 +66,12 @@ type PublicRecord struct {
|
||||
|
||||
// Store owns protected descriptors for one registry root.
|
||||
type Store struct {
|
||||
mu sync.RWMutex
|
||||
now func() time.Time
|
||||
root *securefile.Dir
|
||||
active *securefile.Dir
|
||||
revoked *securefile.Dir
|
||||
mu sync.RWMutex
|
||||
now func() time.Time
|
||||
root *securefile.Dir
|
||||
active *securefile.Dir
|
||||
revoked *securefile.Dir
|
||||
readOnly bool
|
||||
}
|
||||
|
||||
type storedRecord struct {
|
||||
@@ -94,9 +97,56 @@ func Open(root string) (*Store, error) {
|
||||
_ = rootDir.Close()
|
||||
return nil, integrity(err)
|
||||
}
|
||||
lock, err := rootDir.Lock(registryLockName)
|
||||
if err != nil {
|
||||
_ = revoked.Close()
|
||||
_ = active.Close()
|
||||
_ = rootDir.Close()
|
||||
return nil, integrity(err)
|
||||
}
|
||||
if err := lock.Close(); err != nil {
|
||||
_ = revoked.Close()
|
||||
_ = active.Close()
|
||||
_ = rootDir.Close()
|
||||
return nil, integrity(err)
|
||||
}
|
||||
return &Store{root: rootDir, active: active, revoked: revoked, now: time.Now}, nil
|
||||
}
|
||||
|
||||
// OpenReadOnly opens only a complete preprovisioned protected registry. It
|
||||
// neither creates registry paths nor permits mutations through the Store.
|
||||
func OpenReadOnly(root string) (*Store, error) {
|
||||
rootDir, err := securefile.OpenDir(root)
|
||||
if err != nil {
|
||||
return nil, integrity(err)
|
||||
}
|
||||
active, err := rootDir.OpenDir(string(StateActive))
|
||||
if err != nil {
|
||||
_ = rootDir.Close()
|
||||
return nil, integrity(err)
|
||||
}
|
||||
revoked, err := rootDir.OpenDir(string(StateRevoked))
|
||||
if err != nil {
|
||||
_ = active.Close()
|
||||
_ = rootDir.Close()
|
||||
return nil, integrity(err)
|
||||
}
|
||||
lock, err := rootDir.LockShared(registryLockName)
|
||||
if err != nil {
|
||||
_ = revoked.Close()
|
||||
_ = active.Close()
|
||||
_ = rootDir.Close()
|
||||
return nil, integrity(err)
|
||||
}
|
||||
if err := lock.Close(); err != nil {
|
||||
_ = revoked.Close()
|
||||
_ = active.Close()
|
||||
_ = rootDir.Close()
|
||||
return nil, integrity(err)
|
||||
}
|
||||
return &Store{root: rootDir, active: active, revoked: revoked, now: time.Now, readOnly: true}, nil
|
||||
}
|
||||
|
||||
// Close closes descriptors held by the store.
|
||||
func (s *Store) Close() error {
|
||||
if s == nil {
|
||||
@@ -119,6 +169,9 @@ func (s *Store) Close() error {
|
||||
// Add validates and atomically publishes one active record. Existing active or
|
||||
// revoked records cannot be overwritten or resurrected.
|
||||
func (s *Store) Add(value record.Record) error {
|
||||
if s != nil && s.readOnly {
|
||||
return ErrReadOnly
|
||||
}
|
||||
if err := validateForState(value, StateActive); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -261,6 +314,9 @@ func (s *Store) listUnlocked() ([]PublicRecord, error) {
|
||||
// Revoke publishes a validated revoked record and fsyncs it before removing the
|
||||
// active record. If deletion then fails, Find still returns ErrRevoked.
|
||||
func (s *Store) Revoke(keyID, reason string, at time.Time) error {
|
||||
if s != nil && s.readOnly {
|
||||
return ErrReadOnly
|
||||
}
|
||||
if !validKeyID(keyID) {
|
||||
return ErrNotFound
|
||||
}
|
||||
|
||||
@@ -112,6 +112,82 @@ func TestOpenRejectsSymlinkedRegistryRootAndDirectories(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestOpenReadOnlyRequiresCompletePreprovisionedRegistryAndNeverCreates(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
before, err := os.ReadDir(root)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadDir(before) error = %v", err)
|
||||
}
|
||||
if _, err := OpenReadOnly(root); err == nil {
|
||||
t.Fatal("OpenReadOnly(incomplete) error = nil, want refusal")
|
||||
}
|
||||
after, err := os.ReadDir(root)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadDir(after) error = %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(before, after) {
|
||||
t.Fatalf("OpenReadOnly(incomplete) changed root entries: before=%v after=%v", before, after)
|
||||
}
|
||||
|
||||
admin := openStore(t, root)
|
||||
item := syntheticRecord(13)
|
||||
if err := admin.Add(item); err != nil {
|
||||
t.Fatalf("Add() error = %v", err)
|
||||
}
|
||||
if err := admin.Close(); err != nil {
|
||||
t.Fatalf("Close(admin) error = %v", err)
|
||||
}
|
||||
for _, path := range []string{root, filepath.Join(root, "active"), filepath.Join(root, "revoked")} {
|
||||
if err := os.Chmod(path, 0o750|os.ModeSetgid); err != nil {
|
||||
t.Fatalf("Chmod(%q, 2750) error = %v", path, err)
|
||||
}
|
||||
}
|
||||
if info, err := os.Stat(filepath.Join(root, registryLockName)); err != nil || info.Mode().Perm() != 0o640 {
|
||||
t.Fatalf("registry lock = (%v, %v), want 0640", info, err)
|
||||
}
|
||||
entriesBefore := registryEntryModes(t, root)
|
||||
reader, err := OpenReadOnly(root)
|
||||
if err != nil {
|
||||
t.Fatalf("OpenReadOnly() error = %v", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
if found, err := reader.Find(item.KeyID); err != nil || found.KeyID != item.KeyID {
|
||||
t.Fatalf("Find() = (%#v, %v), want protected record", found, err)
|
||||
}
|
||||
if err := reader.Check(); err != nil {
|
||||
t.Fatalf("Check() error = %v", err)
|
||||
}
|
||||
if err := reader.Add(syntheticRecord(14)); err == nil {
|
||||
t.Fatal("read-only Add() error = nil, want refusal")
|
||||
}
|
||||
if err := reader.Revoke(item.KeyID, "synthetic", item.CreatedAt.Add(time.Hour)); err == nil {
|
||||
t.Fatal("read-only Revoke() error = nil, want refusal")
|
||||
}
|
||||
if got := registryEntryModes(t, root); !reflect.DeepEqual(got, entriesBefore) {
|
||||
t.Fatalf("read-only operations changed registry entries: before=%v after=%v", entriesBefore, got)
|
||||
}
|
||||
}
|
||||
|
||||
func registryEntryModes(t *testing.T, root string) []string {
|
||||
t.Helper()
|
||||
var result []string
|
||||
for _, directory := range []string{root, filepath.Join(root, "active"), filepath.Join(root, "revoked")} {
|
||||
entries, err := os.ReadDir(directory)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadDir(%q) error = %v", directory, err)
|
||||
}
|
||||
for _, entry := range entries {
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
t.Fatalf("Info(%q) error = %v", entry.Name(), err)
|
||||
}
|
||||
result = append(result, filepath.Join(directory, entry.Name())+":"+info.Mode().String())
|
||||
}
|
||||
}
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
func TestCheckRejectsUnsafeModesAndSymlinkedRecords(t *testing.T) {
|
||||
t.Run("unsafe active mode", func(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
|
||||
Reference in New Issue
Block a user