From 745df0a88c82c061c0e46b213a35f4a222d90d6e Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sat, 7 Jan 2017 15:18:22 -0800 Subject: [PATCH] Adding interface methods to logical.Backend for parity (#2242) --- logical/framework/backend.go | 14 +++++++++++ logical/logical.go | 7 ++++++ logical/storage.go | 6 +++++ logical/system_view.go | 10 ++++++++ vault/barrier_view.go | 13 +++++++--- vault/barrier_view_test.go | 32 +++++++++++++++++++++++++ vault/dynamic_system_view_ext.go | 8 +++++++ vault/rollback_test.go | 5 +++- vault/router.go | 41 ++++++++++++++++++++++++++++---- vault/router_test.go | 33 ++++++++++++++++++++++--- vault/testing.go | 4 ++++ vault/token_store_test.go | 6 +++-- 12 files changed, 165 insertions(+), 14 deletions(-) create mode 100644 vault/dynamic_system_view_ext.go diff --git a/logical/framework/backend.go b/logical/framework/backend.go index bcaae5b53971..eebe2b910bbd 100644 --- a/logical/framework/backend.go +++ b/logical/framework/backend.go @@ -68,6 +68,9 @@ type Backend struct { // to the backend, if required. Clean CleanupFunc + // Invalidate is called when a keys is modified if required + Invalidate InvalidateFunc + // AuthRenew is the callback to call when a RenewRequest for an // authentication comes in. By default, renewal won't be allowed. // See the built-in AuthRenew helpers in lease.go for common callbacks. @@ -92,6 +95,9 @@ type WALRollbackFunc func(*logical.Request, string, interface{}) error // CleanupFunc is the callback for backend unload. type CleanupFunc func() +// InvalidateFunc is the callback for backend key invalidation. +type InvalidateFunc func(string) + func (b *Backend) HandleExistenceCheck(req *logical.Request) (checkFound bool, exists bool, err error) { b.once.Do(b.init) @@ -218,12 +224,20 @@ func (b *Backend) Setup(config *logical.BackendConfig) (logical.Backend, error) return b, nil } +// Cleanup is used to release resources and prepare to stop the backend func (b *Backend) Cleanup() { if b.Clean != nil { b.Clean() } } +// InvalidateKey is used to clear caches and reset internal state on key changes +func (b *Backend) InvalidateKey(key string) { + if b.Invalidate != nil { + b.Invalidate(key) + } +} + // Logger can be used to get the logger. If no logger has been set, // the logs will be discarded. func (b *Backend) Logger() log.Logger { diff --git a/logical/logical.go b/logical/logical.go index 69178c805b6e..9c0dd38cbb19 100644 --- a/logical/logical.go +++ b/logical/logical.go @@ -35,7 +35,14 @@ type Backend interface { // existence check function was found, the item exists or not. HandleExistenceCheck(*Request) (bool, bool, error) + // Cleanup is invoked during an unmount of a backend to allow it to + // handle any cleanup like connection closing or releasing of file handles. Cleanup() + + // InvalidateKey may be invoked when an object is modified that belongs + // to the backend. The backend can use this to clear any caches or reset + // internal state as needed. + InvalidateKey(key string) } // BackendConfig is provided to the factory to initialize the backend diff --git a/logical/storage.go b/logical/storage.go index 8c15f4bf638b..51487ded9eb3 100644 --- a/logical/storage.go +++ b/logical/storage.go @@ -1,12 +1,18 @@ package logical import ( + "errors" "fmt" "strings" "github.com/hashicorp/vault/helper/jsonutil" ) +// ErrReadOnly is returned when a backend does not support +// writing. This can be caused by a read-only replica or secondary +// cluster operation. +var ErrReadOnly = errors.New("Cannot write to readonly storage") + // Storage is the way that logical backends are able read/write data. type Storage interface { List(prefix string) ([]string, error) diff --git a/logical/system_view.go b/logical/system_view.go index d20bf0c37311..551aac659d55 100644 --- a/logical/system_view.go +++ b/logical/system_view.go @@ -30,6 +30,11 @@ type SystemView interface { // Returns true if caching is disabled. If true, no caches should be used, // despite known slowdowns. CachingDisabled() bool + + // IsPrimary checks if this is a primary Vault instance. This + // can be used to avoid writes on secondaries and to avoid doing + // lazy upgrades which may cause writes. + IsPrimary() bool } type StaticSystemView struct { @@ -38,6 +43,7 @@ type StaticSystemView struct { SudoPrivilegeVal bool TaintedVal bool CachingDisabledVal bool + Primary bool } func (d StaticSystemView) DefaultLeaseTTL() time.Duration { @@ -59,3 +65,7 @@ func (d StaticSystemView) Tainted() bool { func (d StaticSystemView) CachingDisabled() bool { return d.CachingDisabledVal } + +func (d StaticSystemView) IsPrimary() bool { + return d.Primary +} diff --git a/vault/barrier_view.go b/vault/barrier_view.go index 67964c6468a9..b0dbbf2d1d38 100644 --- a/vault/barrier_view.go +++ b/vault/barrier_view.go @@ -15,8 +15,9 @@ import ( // BarrierView implements logical.Storage so it can be passed in as the // durable storage mechanism for logical views. type BarrierView struct { - barrier BarrierStorage - prefix string + barrier BarrierStorage + prefix string + readonly bool } // NewBarrierView takes an underlying security barrier and returns @@ -68,6 +69,9 @@ func (v *BarrierView) Get(key string) (*logical.StorageEntry, error) { // logical.Storage impl. func (v *BarrierView) Put(entry *logical.StorageEntry) error { + if v.readonly { + return logical.ErrReadOnly + } if err := v.sanityCheck(entry.Key); err != nil { return err } @@ -80,6 +84,9 @@ func (v *BarrierView) Put(entry *logical.StorageEntry) error { // logical.Storage impl. func (v *BarrierView) Delete(key string) error { + if v.readonly { + return logical.ErrReadOnly + } if err := v.sanityCheck(key); err != nil { return err } @@ -89,7 +96,7 @@ func (v *BarrierView) Delete(key string) error { // SubView constructs a nested sub-view using the given prefix func (v *BarrierView) SubView(prefix string) *BarrierView { sub := v.expandKey(prefix) - return &BarrierView{barrier: v.barrier, prefix: sub} + return &BarrierView{barrier: v.barrier, prefix: sub, readonly: v.readonly} } // expandKey is used to expand to the full key path with the prefix diff --git a/vault/barrier_view_test.go b/vault/barrier_view_test.go index 6b80c54cea19..59a0c38b73c6 100644 --- a/vault/barrier_view_test.go +++ b/vault/barrier_view_test.go @@ -282,3 +282,35 @@ func TestBarrierView_ClearView(t *testing.T) { t.Fatalf("have keys: %#v", out) } } +func TestBarrierView_Readonly(t *testing.T) { + _, barrier, _ := mockBarrier(t) + view := NewBarrierView(barrier, "foo/") + + // Add a key before enabling read-only + entry := &Entry{Key: "test", Value: []byte("test")} + if err := view.Put(entry.Logical()); err != nil { + t.Fatalf("err: %v", err) + } + + // Enable read only mode + view.readonly = true + + // Put should fail in readonly mode + if err := view.Put(entry.Logical()); err != logical.ErrReadOnly { + t.Fatalf("err: %v", err) + } + + // Delete nested + if err := view.Delete("test"); err != logical.ErrReadOnly { + t.Fatalf("err: %v", err) + } + + // Check the non-nested key + e, err := view.Get("test") + if err != nil { + t.Fatalf("err: %v", err) + } + if e == nil { + t.Fatalf("key test missing") + } +} diff --git a/vault/dynamic_system_view_ext.go b/vault/dynamic_system_view_ext.go new file mode 100644 index 000000000000..c4edaeb12016 --- /dev/null +++ b/vault/dynamic_system_view_ext.go @@ -0,0 +1,8 @@ +// +build vault + +package vault + +// IsPrimary checks if this is a primary Vault instance. +func (d dynamicSystemView) IsPrimary() bool { + return true +} diff --git a/vault/rollback_test.go b/vault/rollback_test.go index c9a5c468e232..797993a9dd4b 100644 --- a/vault/rollback_test.go +++ b/vault/rollback_test.go @@ -17,6 +17,9 @@ func mockRollback(t *testing.T) (*RollbackManager, *NoopBackend) { mounts := new(MountTable) router := NewRouter() + _, barrier, _ := mockBarrier(t) + view := NewBarrierView(barrier, "logical/") + mounts.Entries = []*MountEntry{ &MountEntry{ Path: "foo", @@ -26,7 +29,7 @@ func mockRollback(t *testing.T) (*RollbackManager, *NoopBackend) { if err != nil { t.Fatal(err) } - if err := router.Mount(backend, "foo", &MountEntry{UUID: meUUID}, nil); err != nil { + if err := router.Mount(backend, "foo", &MountEntry{UUID: meUUID}, view); err != nil { t.Fatalf("err: %s", err) } diff --git a/vault/router.go b/vault/router.go index ea0b9775b358..cbec432fd7e9 100644 --- a/vault/router.go +++ b/vault/router.go @@ -17,12 +17,18 @@ type Router struct { l sync.RWMutex root *radix.Tree tokenStoreSalt *salt.Salt + + // storagePrefix maps the prefix used for storage (ala the BarrierView) + // to the backend. This is used to map a key back into the backend that owns it. + // For example, logical/uuid1/foobar -> secrets/ (generic backend) + foobar + storagePrefix *radix.Tree } // NewRouter returns a new router func NewRouter() *Router { r := &Router{ - root: radix.New(), + root: radix.New(), + storagePrefix: radix.New(), } return r } @@ -69,6 +75,7 @@ func (r *Router) Mount(backend logical.Backend, prefix string, mountEntry *Mount loginPaths: pathsToRadix(paths.Unauthenticated), } r.root.Insert(prefix, re) + r.storagePrefix.Insert(storageView.prefix, re) return nil } @@ -78,12 +85,19 @@ func (r *Router) Unmount(prefix string) error { r.l.Lock() defer r.l.Unlock() - // Call backend's Cleanup routine - re, ok := r.root.Get(prefix) - if ok { - re.(*routeEntry).backend.Cleanup() + // Fast-path out if the backend doesn't exist + raw, ok := r.root.Get(prefix) + if !ok { + return nil } + + // Call backend's Cleanup routine + re := raw.(*routeEntry) + re.backend.Cleanup() + + // Purge from the radix trees r.root.Delete(prefix) + r.storagePrefix.Delete(re.storageView.prefix) return nil } @@ -182,6 +196,23 @@ func (r *Router) MatchingSystemView(path string) logical.SystemView { return raw.(*routeEntry).backend.System() } +// MatchingStoragePrefix returns the mount path matching and storage prefix +// matching the given path +func (r *Router) MatchingStoragePrefix(path string) (string, string, bool) { + r.l.RLock() + _, raw, ok := r.storagePrefix.LongestPrefix(path) + r.l.RUnlock() + if !ok { + return "", "", false + } + + // Extract the mount path and storage prefix + re := raw.(*routeEntry) + mountPath := re.mountEntry.Path + prefix := re.storageView.prefix + return mountPath, prefix, true +} + // Route is used to route a given request func (r *Router) Route(req *logical.Request) (*logical.Response, error) { resp, _, _, err := r.routeCommon(req, false) diff --git a/vault/router_test.go b/vault/router_test.go index 328fe36064e3..5b8f1769ab04 100644 --- a/vault/router_test.go +++ b/vault/router_test.go @@ -57,6 +57,10 @@ func (n *NoopBackend) Cleanup() { // noop } +func (n *NoopBackend) InvalidateKey(string) { + // noop +} + func TestRouter_Mount(t *testing.T) { r := NewRouter() _, barrier, _ := mockBarrier(t) @@ -67,7 +71,7 @@ func TestRouter_Mount(t *testing.T) { t.Fatal(err) } n := &NoopBackend{} - err = r.Mount(n, "prod/aws/", &MountEntry{UUID: meUUID}, view) + err = r.Mount(n, "prod/aws/", &MountEntry{Path: "prod/aws/", UUID: meUUID}, view) if err != nil { t.Fatalf("err: %v", err) } @@ -97,6 +101,14 @@ func TestRouter_Mount(t *testing.T) { t.Fatalf("bad: %s", v) } + mount, prefix, ok := r.MatchingStoragePrefix("logical/foo") + if !ok { + t.Fatalf("missing storage prefix") + } + if mount != "prod/aws/" || prefix != "logical/" { + t.Fatalf("Bad: %v - %v", mount, prefix) + } + req := &logical.Request{ Path: "prod/aws/foo", } @@ -124,7 +136,7 @@ func TestRouter_Unmount(t *testing.T) { t.Fatal(err) } n := &NoopBackend{} - err = r.Mount(n, "prod/aws/", &MountEntry{UUID: meUUID}, view) + err = r.Mount(n, "prod/aws/", &MountEntry{Path: "prod/aws/", UUID: meUUID}, view) if err != nil { t.Fatalf("err: %v", err) } @@ -141,6 +153,10 @@ func TestRouter_Unmount(t *testing.T) { if !strings.Contains(err.Error(), "unsupported path") { t.Fatalf("err: %v", err) } + + if _, _, ok := r.MatchingStoragePrefix("logical/foo"); ok { + t.Fatalf("should not have matching storage prefix") + } } func TestRouter_Remount(t *testing.T) { @@ -153,11 +169,13 @@ func TestRouter_Remount(t *testing.T) { t.Fatal(err) } n := &NoopBackend{} - err = r.Mount(n, "prod/aws/", &MountEntry{UUID: meUUID}, view) + me := &MountEntry{Path: "prod/aws/", UUID: meUUID} + err = r.Mount(n, "prod/aws/", me, view) if err != nil { t.Fatalf("err: %v", err) } + me.Path = "stage/aws/" err = r.Remount("prod/aws/", "stage/aws/") if err != nil { t.Fatalf("err: %v", err) @@ -188,6 +206,15 @@ func TestRouter_Remount(t *testing.T) { if len(n.Paths) != 1 || n.Paths[0] != "foo" { t.Fatalf("bad: %v", n.Paths) } + + // Check the resolve from storage still works + mount, prefix, _ := r.MatchingStoragePrefix("logical/foobar") + if mount != "stage/aws/" { + t.Fatalf("bad mount: %s", mount) + } + if prefix != "logical/" { + t.Fatalf("Bad prefix: %s", prefix) + } } func TestRouter_RootPath(t *testing.T) { diff --git a/vault/testing.go b/vault/testing.go index 3c50d0265ffb..38519150790b 100644 --- a/vault/testing.go +++ b/vault/testing.go @@ -381,6 +381,10 @@ func (n *rawHTTP) Cleanup() { // noop } +func (n *rawHTTP) InvalidateKey(string) { + // noop +} + func GenerateRandBytes(length int) ([]byte, error) { if length < 0 { return nil, fmt.Errorf("length must be >= 0") diff --git a/vault/token_store_test.go b/vault/token_store_test.go index 2caab6037a1e..226bd432733a 100644 --- a/vault/token_store_test.go +++ b/vault/token_store_test.go @@ -593,11 +593,13 @@ func TestTokenStore_Revoke(t *testing.T) { } func TestTokenStore_Revoke_Leases(t *testing.T) { - _, ts, _, _ := TestCoreWithTokenStore(t) + c, ts, _, _ := TestCoreWithTokenStore(t) + + view := NewBarrierView(c.barrier, "noop/") // Mount a noop backend noop := &NoopBackend{} - ts.expiration.router.Mount(noop, "", &MountEntry{UUID: ""}, nil) + ts.expiration.router.Mount(noop, "", &MountEntry{UUID: ""}, view) ent := &TokenEntry{Path: "test", Policies: []string{"dev", "ops"}} if err := ts.create(ent); err != nil {