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

Nested secrets handling fix for zookeeper and file based backend. #1964

Merged
Merged
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
65 changes: 35 additions & 30 deletions physical/file.go
Expand Up @@ -6,6 +6,7 @@ import (
"io"
"os"
"path/filepath"
"strings"
"sync"

log "github.com/mgutz/logxi/v1"
Expand Down Expand Up @@ -39,48 +40,52 @@ func newFileBackend(conf map[string]string, logger log.Logger) (Backend, error)
}, nil
}

func (b *FileBackend) Delete(k string) error {
func (b *FileBackend) Delete(path string) error {
Copy link
Member

Choose a reason for hiding this comment

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

Can we check if path is empty here and return nil?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

b.l.Lock()
defer b.l.Unlock()

path, key := b.path(k)
fullPath := filepath.Join(path, key)
basePath, key := b.path(path)
fullPath := filepath.Join(basePath, key)

// If the path doesn't exist return success; this is in line with Vault's
// expected behavior and we don't want to check for an empty directory if
// we couldn't even find the path in the first place.
err := os.Remove(fullPath)
if err != nil {
if os.IsNotExist(err) {
return nil
} else {
return err
}
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("Failed to remove `%s`: %v", fullPath, err)
Copy link
Member

Choose a reason for hiding this comment

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

%s can be replaced with %q

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

}

// Check for the directory being empty and remove if so, with another
// additional guard for the path not existing
dir, err := os.Open(path)
if err != nil {
if os.IsNotExist(err) {
return nil
} else {
return err
}
}
err = b.cleanupLogicalPath(path)
Copy link
Member

Choose a reason for hiding this comment

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

Can we make it return b.cleanupLogicalPath(path)?

Copy link
Contributor Author

@vespian vespian Oct 5, 2016

Choose a reason for hiding this comment

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

Would it be a problem if I leave it as-is ? Two things:
a) debugging - while stepping a program, you can inspect the output of err before escaping the function
b) there is clear separation between program/logic (doSomeStuff...) and the return statement

What do you think ?


list, err := dir.Readdir(1)
dir.Close()
if err != nil && err != io.EOF {
return err
}
return err
}

// If we have no entries, it's an empty directory; remove it
if err == io.EOF || list == nil || len(list) == 0 {
err = os.Remove(path)
// cleanupLogicalPath is used to remove all empty nodes, begining with deepest
// one, aborting on first non-empty one, up to top-level node.
func (b *FileBackend) cleanupLogicalPath(path string) error {
nodes := strings.Split(path, "/")
Copy link
Member

Choose a reason for hiding this comment

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

Can we use os.PathSeparator here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

True :)

Copy link
Member

Choose a reason for hiding this comment

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

If we check for path being empty in the Delete method above (left a comment there as well), then we don't let i becoming -1 here. Otherwise there is a risk for a panic here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am not sure I understand - if i becomes -1 then the loop will not be executed: https://play.golang.org/p/TH1DHWW3h6

for i := len(nodes) - 1; i > 0; i-- {
fullPath := b.Path + "/" + strings.Join(nodes[:i], "/")
Copy link
Member

Choose a reason for hiding this comment

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

Can we employ a combination of os.PathSeparator and filepath.Join() to achieve this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍


dir, err := os.Open(fullPath)
if err != nil {
if os.IsNotExist(err) {
return nil
} else {
return err
}
}

list, err := dir.Readdir(1)
dir.Close()
if err != nil && err != io.EOF {
return err
}

// If we have no entries, it's an empty directory; remove it
if err == io.EOF || list == nil || len(list) == 0 {
err = os.Remove(fullPath)
if err != nil {
return err
}
}
}

return nil
Expand Down
26 changes: 26 additions & 0 deletions physical/physical_test.go
Expand Up @@ -151,6 +151,32 @@ func testBackend(t *testing.T, b Backend) {
t.Fatalf("missing child")
}

// Removal of nested secret should not leave artifacts
e = &Entry{Key: "foo/nested1/nested2/nested3", Value: []byte("baz")}
err = b.Put(e)
if err != nil {
t.Fatalf("err: %v", err)
}

err = b.Delete("foo/nested1/nested2/nested3")
if err != nil {
t.Fatalf("failed to remove nested secret: %v", err)
}

keys, err = b.List("foo/")
if err != nil {
t.Fatalf("err: %v", err)
}

if len(keys) != 1 {
t.Fatalf("there should be only one key left after deleting nested "+
"secret: %v", keys)
}

if keys[0] != "bar" {
t.Fatalf("bad keys after deleting nested: %v", keys)
}

// Make a second nested entry to test prefix removal
e = &Entry{Key: "foo/zip", Value: []byte("zap")}
err = b.Put(e)
Expand Down
62 changes: 55 additions & 7 deletions physical/zookeeper.go
Expand Up @@ -10,7 +10,7 @@ import (

log "github.com/mgutz/logxi/v1"

"github.com/armon/go-metrics"
metrics "github.com/armon/go-metrics"
"github.com/samuel/go-zookeeper/zk"
)

Expand Down Expand Up @@ -159,7 +159,39 @@ func (c *ZookeeperBackend) ensurePath(path string, value []byte) error {
return nil
}

// nodePath returns an etcd filepath based on the given key.
// cleanupLogicalPath is used to remove all empty nodes, begining with deepest one,
// aborting on first non-empty one, up to top-level node.
func (c *ZookeeperBackend) cleanupLogicalPath(path string) error {
nodes := strings.Split(path, "/")
for i := len(nodes) - 1; i > 0; i-- {
fullPath := c.path + strings.Join(nodes[:i], "/")

_, stat, err := c.client.Exists(fullPath)
if err != nil {
return fmt.Errorf("Failed to acquire node data: %s", err)
}

if stat.DataLength > 0 && stat.NumChildren > 0 {
msgFmt := "Node %s is both of data and leaf type ??"
panic(fmt.Sprintf(msgFmt, fullPath))
} else if stat.DataLength > 0 {
msgFmt := "Node %s is a data node, this is either a bug or " +
"backend data is corrupted"
panic(fmt.Sprintf(msgFmt, fullPath))
} else if stat.NumChildren > 0 {
return nil
} else {
// Empty node, lets clean it up!
if err := c.client.Delete(fullPath, -1); err != nil {
msgFmt := "Removal of node `%s` failed: `%v`"
return fmt.Errorf(msgFmt, fullPath, err)
}
}
}
return nil
}

// nodePath returns an zk path based on the given key.
func (c *ZookeeperBackend) nodePath(key string) string {
return filepath.Join(c.path, filepath.Dir(key), ZKNodeFilePrefix+filepath.Base(key))
}
Expand Down Expand Up @@ -215,9 +247,12 @@ func (c *ZookeeperBackend) Delete(key string) error {
err := c.client.Delete(fullPath, -1)

// Mask if the node does not exist
if err == zk.ErrNoNode {
err = nil
if err != nil && err != zk.ErrNoNode {
return fmt.Errorf("Failed to remove `%s`: %v", fullPath, err)
}

err = c.cleanupLogicalPath(key)

return err
}

Expand All @@ -233,15 +268,28 @@ func (c *ZookeeperBackend) List(prefix string) ([]string, error) {
// If the path nodes are missing, no children!
if err == zk.ErrNoNode {
return []string{}, nil
} else if err != nil {
return []string{}, err
}

children := []string{}
for _, key := range result {
// Check if this entry has any child entries,
childPath := fullPath + "/" + key
_, stat, err := c.client.Exists(childPath)
if err != nil {
// Node is ought to exists, so it must be something different
return []string{}, err
}

// Check if this entry is a leaf of a node,
// and append the slash which is what Vault depends on
// for iteration
nodeChildren, _, _ := c.client.Children(fullPath + "/" + key)
if nodeChildren != nil && len(nodeChildren) > 0 {
if stat.DataLength > 0 && stat.NumChildren > 0 {
msgFmt := "Node %s is both of data and leaf type ??"
panic(fmt.Sprintf(msgFmt, childPath))
} else if stat.DataLength == 0 {
// No, we cannot differentiate here on number of children as node
// can have all it leafs remoed, and it still is a node.
children = append(children, key+"/")
} else {
children = append(children, key[1:])
Expand Down
3 changes: 3 additions & 0 deletions physical/zookeeper_test.go
Expand Up @@ -33,6 +33,9 @@ func TestZookeeperBackend(t *testing.T) {
}

defer func() {
client.Delete(randPath+"/foo/nested1/nested2/nested3", -1)
client.Delete(randPath+"/foo/nested1/nested2", -1)
client.Delete(randPath+"/foo/nested1", -1)
client.Delete(randPath+"/foo/bar/baz", -1)
client.Delete(randPath+"/foo/bar", -1)
client.Delete(randPath+"/foo", -1)
Expand Down