-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Nested secrets handling fix for zookeeper and file based backend. #1964
Conversation
@@ -151,43 +151,31 @@ func testBackend(t *testing.T, b Backend) { | |||
t.Fatalf("missing child") | |||
} | |||
|
|||
// Make a second nested entry to test prefix removal |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi,
This looks pretty good but one request: you changed the tests here but actually took out some functionality. The previous version was testing that removing one key from a prefix with two keys under it would not remove the prefix. You should add your test in addition to that, not instead of.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed. Please have another look.
e016013
to
a6a8da8
Compare
Current tests were not checking if backends are properly removing nested secrets. We follow here the behaviour of Consul backend, where empty "directories/prefixes" are automatically removed by Consul itself.
This patch fixes two bugs in Zookeeper backends: * backend was determining if the node is a leaf or not basing on the number of the childer given node has. This is incorrect if you consider the fact that deleteing nested node can leave empty prefixes/dirs behind which have neither children nor data inside. The fix changes this situation by testing if the node has any data set - if not then it is not a leaf. * zookeeper does not delete nodes that do not have childern just like consul does and this leads to leaving empty nodes behind. In order to fix it, we scan the logical path of a secret being deleted for empty dirs/prefixes and remove them up until first non-empty one.
This patch makes file backend properly remove nested secrets, without leaving empty directory artifacts, no matter how nested directories were.
a6a8da8
to
3156098
Compare
return err | ||
} | ||
if err != nil && !os.IsNotExist(err) { | ||
return fmt.Errorf("Failed to remove `%s`: %v", fullPath, err) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
return err | ||
} | ||
} | ||
err = b.cleanupLogicalPath(path) |
There was a problem hiding this comment.
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)
?
There was a problem hiding this comment.
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 ?
// 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, "/") |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True :)
@@ -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 { |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
// 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, "/") |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
func (b *FileBackend) cleanupLogicalPath(path string) error { | ||
nodes := strings.Split(path, "/") | ||
for i := len(nodes) - 1; i > 0; i-- { | ||
fullPath := b.Path + "/" + strings.Join(nodes[:i], "/") |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
@vespian I left a few comments on the file backend. Please also assume the same comments for ZK wherever applicable. |
Thank you @vespian for submitting this! I'll be merging this in hopes to try to get this into our next release. |
@vishalnayak I have have made changes you requested in #1969 Could you please have a look and merge if applicable ? Thanks! |
Hi!
Some time ago we discovered that nested secrets (e.g. "foo/bar/baz") are inproperly handled in zookeeper backend, after some research it turned out that:
List
method improperly calssifies nodes, basing only on the number of children given node has. This was causing "ghost" entries inList
call results for ZK nodes which were not removed when all their children wereWhile rebasing, I have noticed that there was already some patching in file based backend (https://github.com/hashicorp/vault/pull/1821/files). Unfortunately neither the fix nor the extra tests are not recursive. Lets assume that we have following hierarchy:
now let's create secret
foo/bar/baz/gaz/naz
with valuetest1234
, elementsbar
,baz
,gaz
are empty,foo/suz
andnaz
hold a secret. We have following hierarchy now:Removing
foo/bar/baz/gaz/naz
will result in directoriesbar
,baz
,gaz
left on the disk. In case of the Zookeeper, user would get additional entry offoo/bar/baz/az
in the result ofList
call (first character is stripped as the backend thinks this is a data node with_
prefix).So this PR refactors tests in order to cover mentioned corner-cases and adds recursive deletion of directoires/nodes that are empty. It has been verified/tested on Zookeeper, Consul and File backend. I am not sure if other backends do not share this bug (i.e. PSQL, MySQL, ETCd, S3, etc...).
Thanks in advance for feedback.
pr