-
Notifications
You must be signed in to change notification settings - Fork 31
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
Add subkeys endpoint #59
Conversation
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.
Looking really good, just a few questions/comments!
helpers_test.go
Outdated
@@ -0,0 +1,53 @@ | |||
package kv |
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 think these helpers should be moved to a file that doesn't end with test.go
? As far as I remember, only files containing tests are named *_test.go.
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.
Ah, whoops. You're right, thanks! I can move these to a non _test.go
file in a separate test helpers package to make it obvious and import it directly
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.
Actually, after a bit of research this might not be true. Forgive me, since I'm new to Go. It seems that a file ending in _test.go
indicates that the file should be packaged up when running go test
. I don't think it necessarily dictates that the file must contain tests. Creating a file that doesn't end in _test.go
would mean that it would be packaged into the binary which I don't think we want. Thoughts?
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.
All that you're saying is true. Nonetheless, we typically put helpers in non-_test.go files, because otherwise they can't be imported by other packages.
The Go linker is pretty smart, I don't think it'll include the helper code in the prod binary unless prod code invokes it.
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.
Of course if the helpers aren't public anyway, it's up to you.
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.
That makes sense. I would like to follow our general practices so I will move things. Thanks for the feedback everyone.
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.
So now looking at this more, having a separate package will result in cyclic imports as the getBackend
function requires the VersionedKVFactory
. I didn't like that the getBackend
function was defined in a path_data_test.go
and instead wanted it in a separate file. Though it's all the same test package so maybe I'm just complicating things and might just revert all that and go back to way things were prior to this PR.
helpers_test.go
Outdated
} | ||
|
||
// Wait for the upgrade to finish | ||
time.Sleep(time.Second) |
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 to make sure the upgrade finishes instead of using the sleep statement 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.
The getBackend
method existed before this PR. It is used by the unit tests to instantiate a KV backend. I moved it from one file to another. That said, I agree that it would be good to ensure that the upgrade finishes rather than a single hopeful sleep.
The backend itself has a flag that states that the upgrade is pending. I think it would be cleaner to add polling with a timeout as you said. That's what we do for the kv
CLI tests in the vault
repo. I can try to get something like that in place 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 added a ticker/timeout which ticks per second. Each tick, a read request will be submitted to the config endpoint to ensure that it doesn't result in an error.
helpers_test.go
Outdated
} | ||
|
||
// getKeySet will produce a set of the keys that exist in m | ||
func getKeySet(m map[string]interface{}) map[string]struct{} { |
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.
Why are we changing the tests to check the keySet now? Shouldn't we check the nested structure as well?
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.
This was a pre-existing function that I added in the last release cycle to verify the structure of response bodies. I had it defined in a specific test file and moved it to a more central location. It is used to check the top-level keys of the subkeys response (subkeys
and metadata
) and then the keys underneath metadata
See:
vault-plugin-secrets-kv/path_subkeys_test.go
Lines 82 to 84 in 9f2cb0e
if diff := deep.Equal(getKeySet(resp.Data), expectedRespKeys); len(diff) > 0 { | |
t.Fatalf("metadata map keys mismatch, diff: %#v", diff) | |
} |
vault-plugin-secrets-kv/path_subkeys_test.go
Lines 86 to 89 in 9f2cb0e
metadata, ok := resp.Data["metadata"].(map[string]interface{}) | |
if !ok { | |
t.Fatalf("expected metadata to be map, actual: %#v", metadata) | |
} |
I just noticed, however, that the error message for the top-level keys check is incorrect and I'll have to change that.
An example of a check for the full nested structure can be found:
vault-plugin-secrets-kv/path_subkeys_test.go
Lines 95 to 114 in 9f2cb0e
expectedSubkeys := map[string]interface{}{ | |
"foo": nil, | |
"bar": map[string]interface{}{ | |
"a": map[string]interface{}{ | |
"c": map[string]interface{}{ | |
"d": nil, | |
}, | |
}, | |
"b": nil, | |
}, | |
"baz": map[string]interface{}{ | |
"e": nil, | |
}, | |
"quux": nil, | |
"quuz": nil, | |
} | |
if diff := deep.Equal(resp.Data["subkeys"], expectedSubkeys); len(diff) > 0 { | |
t.Fatalf("resp and expected data mismatch, diff: %#v", diff) | |
} |
}, | ||
}, | ||
Callbacks: map[logical.Operation]framework.OperationFunc{ | ||
logical.ReadOperation: b.upgradeCheck(b.pathSubkeysRead()), |
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.
What's the significance of b.upgradeCheck
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.
Every endpoint is wrapped with b.upgradeCheck
. This is to ensure an error is returned if the backend is being upgraded from v1 (non-versioned entries) to v2 (versioned entries). In other words, it ensures the backend is inaccessible while the backend's storage is updated to fit the new structure of v2. This is outlined in the Vault docs here. Does that answer your question?
var walk func(interface{}, int) | ||
|
||
walk = func(in interface{}, depth int) { | ||
val := reflect.ValueOf(in) |
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.
Do we need to check IsValid
on 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.
Ah, yes! Good catch.
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.
Added IsValid
checks prior to any method calls on any reflect.Value
vars. How does that look?
} | ||
|
||
versionNum := meta.CurrentVersion | ||
versionParam := data.Get("version").(int) |
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 could be wrong on this, but if memory serves we need to do GetOk
for parameters that are not required?
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 think that GetOk
will actually result in a false value for the ok
return val if the field is not provided. Additional logic would then be required to default it appropriately. The Get method will call GetOk
under the hood ultimately defaulting to the default or zero value for the field. The version
field does not have a default defined thus the zero value (i.e. 0) will be used. A 0 for this field denotes "use the current version." Note that on line 105 versionNum
is initialized as meta.CurrentVersion
. It is only overridden on line 109 if versionParam > 0
.
} | ||
|
||
if deletionTime.Before(time.Now()) { | ||
return logical.RespondWithStatusCode(resp, req, http.StatusNotFound) |
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.
Do we want to return the response here? What about returning logical.ErrorResponse
instead?
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 believe that returning a logical.ErrorResponse
would result in a 400 status code, correct? The desire is to return a 404. I believe the helper function logical.RespondWithStatusCode
is required to achieve that. It takes a logical.Response
, the incoming logical.Request
and an HTTP status code. It will add HTTPStatusCode
to the Data
field in the logical.Response
. Vault's response handling logic will pull this out and use it as the response status. This is a means to ensure that a 404 response is provided if the secret entry has been deleted (we also check if it is destroyed). This check is consistent with some of the other KV endpoints. See the read for data/:path
:
vault-plugin-secrets-kv/path_data.go
Lines 126 to 143 in 9f2cb0e
// If the version has been deleted return metadata with a 404 | |
if vm.DeletionTime != nil { | |
deletionTime, err := ptypes.Timestamp(vm.DeletionTime) | |
if err != nil { | |
return nil, err | |
} | |
if deletionTime.Before(time.Now()) { | |
return logical.RespondWithStatusCode(resp, req, http.StatusNotFound) | |
} | |
} | |
// If the version has been destroyed return metadata with a 404 | |
if vm.Destroyed { | |
return logical.RespondWithStatusCode(resp, req, http.StatusNotFound) | |
} |
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.
Nice work!
// provide all subkeys with nesting fully intact. The modifications are made | ||
// to the input in-place. maxDepth will denote how deep to traverse. A maxDepth | ||
// of 0 is the equivalent of no limit. | ||
func removeValues(input map[string]interface{}, maxDepth int) { |
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.
You might like https://github.com/mitchellh/reflectwalk. (I'm not asking you to use it, just pointing out it exists. For your use case it might be simpler to do as you're doing.)
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 did look at that a bit, actually! It's a very intriguing library. I was hesitant to use it though. My thought was that the added boilerplate might outweigh the overall benefit based on the fairly narrow problem scope.
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.
looks good!
"metadata": {}, | ||
} | ||
|
||
if diff := deep.Equal(getKeySet(resp.Data), expectedRespKeys); len(diff) > 0 { |
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.
does this not check the values in the map? like is it only checking that the keys are present and the same?
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.
Yup, this is just checking top-level keys to verify that the response structure matches our expectations. The metadata
keys (but not the associated values) are then checked below on line 91. The actual structure of the subkeys
map is then checked on line 112. All these checks are done separately to verify the overall structure of the response body.
Overview
KVv2 added
patch
support in #49 which allows users to update a subset of a secret entries subkeys without the need to first read the read the data. The knowledge of a secret entry's structure (i.e. its subkeys) will allow users to more confidently perform patch operations while still not having to retrieve the underlying secret data. This PR adds a newsubkeys/:path
endpoint to achieve this. Its response structure resembles that of thedata/:path
endpoint except the underlying values of each leaf subkey is replaced withnil
.The
subkeys
endpoint remains consistent with thedata
endpoint in multiple ways. Like thedata/:path
endpoint, the current version of the secret entry stored at:path
will be read. A specific version can be read by using theversion
parameter. If the requested version has been deleted or destroyed, a nilLogical.Response
is returned which ultimately leads to a status of404 Not Found
. Both provide the samemetadata
structure in the response.The
depth
parameter can be used to limit the depth of the structure. If thedepth
is not provided or is equal to 0, the full structure is returned. Adepth
of 1, for example, will provide only the top-level subkeys.Example secret entry:
Example
subkeys
response:Design of Change
The
pathSubkeysRead
method has been added to handleReadOperation
requests for thesubkeys/:path
endpoint. The underlying logic closely resembles that of thepathDataRead
method which handlesReadOperation
requests for thedata/:path
endpoint. The secret data fetched from storage is run throughremoveValues
which walks the map obtained from unmarshaling the JSON entry and replaces any values for leaf keys withnil
. The map is modified in place.Leaf keys are defined as those whose type is not a
map[string]interface{}
or is amap[string]interface{}
but has no underlying keys. Thedepth
parameter will also artificially treat keys as leaves if they reside at the associated level within the map.Related Issues/Pull Requests
vault API docs PR #13893