Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix panic on renewing a renewable KV v1 secret #118

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend,
var err error
switch version {
case "1", "":
return LeaseSwitchedPassthroughBackend(ctx, conf, conf.Config["leased_passthrough"] == "true")
return LeaseSwitchedPassthroughBackendFactory(ctx, conf, conf.Config["leased_passthrough"] == "true")
case "2":
b, err = VersionedKVFactory(ctx, conf)
}
Expand Down
48 changes: 26 additions & 22 deletions passthrough.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,18 @@ import (
// PassthroughBackendFactory returns a PassthroughBackend
// with leases switched off
func PassthroughBackendFactory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
return LeaseSwitchedPassthroughBackend(ctx, conf, false)
return LeaseSwitchedPassthroughBackendFactory(ctx, conf, false)
}

// LeasedPassthroughBackendFactory returns a PassthroughBackend
// with leases switched on
func LeasedPassthroughBackendFactory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
return LeaseSwitchedPassthroughBackend(ctx, conf, true)
return LeaseSwitchedPassthroughBackendFactory(ctx, conf, true)
}

// LeaseSwitchedPassthroughBackend returns a PassthroughBackend
// LeaseSwitchedPassthroughBackendFactory returns a PassthroughBackend
// with leases switched on or off
func LeaseSwitchedPassthroughBackend(ctx context.Context, conf *logical.BackendConfig, leases bool) (logical.Backend, error) {
func LeaseSwitchedPassthroughBackendFactory(ctx context.Context, conf *logical.BackendConfig, leases bool) (logical.Backend, error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to reviewer: The changes above are just an opportunistic cleanup whilst I was here ... one of the three factory functions was inconsistently missing the Factory suffix.

Compatibility: Should not really be an issue, no-one other than Vault should be importing this as a library and Vault doesn't (yet) call these.

b := &PassthroughBackend{
generateLeases: leases,
}
Expand Down Expand Up @@ -61,11 +61,15 @@ func LeaseSwitchedPassthroughBackend(ctx context.Context, conf *logical.BackendC
},
},

// The regex and field definition above are purely for the benefit of OpenAPI and generated
// documentation. The actual request processing functions consult req.Path directly.
// See documentation of handleReadOrRenew for more detail.

TakesArbitraryInput: true,

Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
Callback: b.handleRead(),
Callback: b.handleReadOrRenew(),
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "read",
},
Expand Down Expand Up @@ -127,7 +131,7 @@ func LeaseSwitchedPassthroughBackend(ctx context.Context, conf *logical.BackendC
{
Type: "kv",

Renew: b.handleRead(),
Renew: b.handleReadOrRenew(),
Revoke: func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
// This is a no-op
return nil, nil
Expand Down Expand Up @@ -156,9 +160,7 @@ type PassthroughBackend struct {

func (b *PassthroughBackend) handleExistenceCheck() framework.ExistenceFunc {
return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) {
key := data.Get("path").(string)

out, err := req.Storage.Get(ctx, key)
out, err := req.Storage.Get(ctx, req.Path)
if err != nil {
return false, fmt.Errorf("existence check failed: %w", err)
}
Expand All @@ -167,12 +169,17 @@ func (b *PassthroughBackend) handleExistenceCheck() framework.ExistenceFunc {
}
}

func (b *PassthroughBackend) handleRead() framework.OperationFunc {
// handleReadOrRenew is called both for ReadOperations and RenewOperations. The RenewOperation case only applies when
// using the rather obscure feature of mounting the backend with option leased_passthrough=true, and storing a duration
// within the secret data itself, at the key "ttl" (preferred) or "lease" (deprecated). When that is done, we return a
// renewable lease, and treat invoking the renew as a request to re-read the same path that was originally read. We use
// req.Path, instead of a more conventional data.Get("path").(string), to read the requested path because the data
// attached to a RenewOperation is NOT the original request data - it is the response data! Technically we need only do
// this for this function, but for consistency we also use req.Path throughout the other handlers for this path pattern.
func (b *PassthroughBackend) handleReadOrRenew() framework.OperationFunc {
return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
key := data.Get("path").(string)

// Read the path
out, err := req.Storage.Get(ctx, key)
out, err := req.Storage.Get(ctx, req.Path)
if err != nil {
return nil, fmt.Errorf("read failed: %w", err)
}
Expand Down Expand Up @@ -235,8 +242,7 @@ func (b *PassthroughBackend) handleRead() framework.OperationFunc {

func (b *PassthroughBackend) handleWrite() framework.OperationFunc {
return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
key := data.Get("path").(string)
if key == "" {
if req.Path == "" {
return logical.ErrorResponse("missing path"), nil
}

Expand All @@ -253,29 +259,27 @@ func (b *PassthroughBackend) handleWrite() framework.OperationFunc {

// Write out a new key
entry := &logical.StorageEntry{
Key: key,
Key: req.Path,
Value: buf,
}
if err := req.Storage.Put(ctx, entry); err != nil {
return nil, fmt.Errorf("failed to write: %w", err)
}

kvEvent(ctx, b.Backend, "write", key, key, true, 1)
kvEvent(ctx, b.Backend, "write", req.Path, req.Path, true, 1)

return nil, nil
}
}

func (b *PassthroughBackend) handleDelete() framework.OperationFunc {
return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
key := data.Get("path").(string)

// Delete the key at the request path
if err := req.Storage.Delete(ctx, key); err != nil {
if err := req.Storage.Delete(ctx, req.Path); err != nil {
return nil, err
}

kvEvent(ctx, b.Backend, "delete", key, "", true, 1)
kvEvent(ctx, b.Backend, "delete", req.Path, "", true, 1)

return nil, nil
}
Expand All @@ -286,7 +290,7 @@ func (b *PassthroughBackend) handleList() framework.OperationFunc {
// Right now we only handle directories, so ensure it ends with /; however,
// some physical backends may not handle the "/" case properly, so only add
// it if we're not listing the root
path := data.Get("path").(string)
path := req.Path
if path != "" && !strings.HasSuffix(path, "/") {
path = path + "/"
}
Expand Down
34 changes: 34 additions & 0 deletions passthrough_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,40 @@ func TestPassthroughBackend_Revoke(t *testing.T) {
test(b)
}

func TestPassthroughBackend_Renew(t *testing.T) {
b := testPassthroughLeasedBackend()

req := logical.TestRequest(t, logical.CreateOperation, "foo")
req.Data = map[string]interface{}{
"ttl": "4h",
"payload": "alpha",
}
storage := req.Storage
if _, err := b.HandleRequest(context.Background(), req); err != nil {
t.Fatalf("err: %v", err)
}

req = logical.TestRequest(t, logical.RenewOperation, "foo")
req.Storage = storage
req.Secret = &logical.Secret{
InternalData: map[string]interface{}{
"secret_type": "kv",
},
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to reviewer: This is a bit fake, hacking together just enough of what a real RenewOperation request would look like. Ideally I'd like to run a real Core and generate a real lease renewal, but the relevant test helpers are not exposed for out-of-tree plugins to use. This is probably good enough, I guess.

resp, err := b.HandleRequest(context.Background(), req)
if err != nil {
t.Fatalf("err: %v", err)
}

expected := map[string]interface{}{
"ttl": "4h",
"payload": "alpha",
}
if !reflect.DeepEqual(resp.Data, expected) {
t.Fatalf("bad response.\n\nexpected: %#v\n\nGot: %#v", expected, resp)
}
}

func testPassthroughBackend() logical.Backend {
return testPassthroughBackendWithEvents(nil)
}
Expand Down