[DBPW 4/5] Update DB engine to support v4 and v5 interfaces with password policies#9878
Conversation
This mirrors what DBv4 is doing, but with the updated interface
This continues to support backwards compatibility with V4
| client.Close() | ||
| return consts.PluginTypeDatabase, nil | ||
| } else { | ||
| logger.Warn(fmt.Sprintf("received %s attempting as db plugin, attempting as auth/secret plugin", err)) |
There was a problem hiding this comment.
This statement is not entirely accurate (and might print twice if it's not a db plugin by both this line and L88). We should skip logging this warning, or move the V4 check into this else block.
There was a problem hiding this comment.
👍 Moved Database type checking into its own function
|
|
||
| type databaseVersionWrapper struct { | ||
| database newdbplugin.Database | ||
| legacyDatabase dbplugin.Database |
There was a problem hiding this comment.
Even though there's the intent to leave the v4 nomenclature out out of code references, I think that terms like legacy, old, new, or similar have its own set of problems too. If we were to introduce the next iteration of database plugins (which is unlikely but not impossible), this terminology might make things confusing as it would be come the "legacy" database, and the legacyDatabase is really a oldLegacyDatabase, which would be just as ambiguous if not more so.
There was a problem hiding this comment.
Unfortunately I don't have a good way of differentiating between versions other than referring to the gRPC version number. I've switched this over to using that nomenclature within the version wrapping struct since it has more forwards compatibility.
| // Change new user | ||
| // ///////////////////////////////////////////////////////////////////////////////// | ||
|
|
||
| func createUser(ctx context.Context, dbw databaseVersionWrapper, pg passwordGenerator, statements dbplugin.Statements, displayName, roleName string, expiration time.Time, passwordPolicy string) (username, password string, err error) { |
There was a problem hiding this comment.
Can we make these funcs be part of dbPluginInstance's methods?
There was a problem hiding this comment.
This has been put into a separate wrapper struct in version_wrapper.go rather than on the dbPluginInstance.
| } | ||
| } | ||
|
|
||
| func (b *databaseBackend) deleteWal(ctx context.Context, storage logical.Storage, walID string) { |
There was a problem hiding this comment.
I don't see this being re-used elsewhere, can we move it back to the func above?
There was a problem hiding this comment.
Done. I think at one point I had this doing more but it's not useful as a separate function any more.
| // Attempt to get a connection with the WAL entry new password. | ||
| config.ConnectionDetails["password"] = entry.NewPassword | ||
| dbc, err := b.GetConnectionWithConfig(ctx, entry.ConnectionName, config) | ||
| db, err := b.GetConnectionWithConfig(ctx, entry.ConnectionName, config) |
There was a problem hiding this comment.
Nit: can we name the var dbi to have it be consistent with similar calls?
There was a problem hiding this comment.
Done. I also updated the other places where GetConnection is being called to use dbi.
| v5Client.Close() | ||
| return nil | ||
| } | ||
| merr = multierror.Append(merr, err) |
There was a problem hiding this comment.
Does the error from there indicate which database version was attempted? If not, might be useful to wrap the error message with this bit of info.
There was a problem hiding this comment.
It does not but that's because the error will only be returned if both v5 and v4 fail.
Holding off on doing anything with this until we figure out what to do with the log message that this error is used in.
There was a problem hiding this comment.
Wrapped the error to indicate which error failed on which database version. This will bubble up to the log message referenced in the next comment thread.
| if err == nil { | ||
| return consts.PluginTypeDatabase, nil | ||
| } | ||
| logger.Warn(fmt.Sprintf("received %s attempting as db plugin, attempting as auth/secret plugin", err)) |
There was a problem hiding this comment.
The error might look a bit off in here now since it can be a multierror, so consider appending error to the end of the string. https://play.golang.org/p/rOofMYebB2d
There was a problem hiding this comment.
I ended up keeping this warning since it was there in the previous version, however I don't think this message makes sense as a warning. Every plugin goes through this path, and if the plugin isn't a database plugin you're going to get a warning saying the plugin isn't a database plugin. What do you think of changing this to a debug (or trace?) log? I'm also tempted to not include the error message at all as I'm skeptical that it's useful in this context. Thoughts?
There was a problem hiding this comment.
getPluginTypeFromUnknown shouldn't be called that often -- once when setting up the plugin catalog if a plugin has not been upgraded yet, and when a plugin with unknown type is added to the catalog (which shouldn't happen since we're now enforcing the type to be provided via the API). The latter is more of a backward compat behavior, so new plugin registration operation shouldn't be hitting this path.
There was a problem hiding this comment.
For posterity:
@calvn & I talked offline and agreed that we should keep this as a warning, but move it to the end of the function if we are unable to determine the type of the plugin. I also changed it so the message is a bit more descriptive differentiating between an unknown type vs one that isn't recognized by this particular function. That will help guard against adding new types to the logical.BackendType enum.
| }, | ||
| "allowed_roles": []string{"*"}, | ||
| "root_credentials_rotate_statements": []string{}, | ||
| "password_policy": "", |
There was a problem hiding this comment.
Is this required to be provided?
There was a problem hiding this comment.
No, password policies are optional.
There was a problem hiding this comment.
Is there a reason why we're explicitly setting this to "" on every case? Not sure if I missed this test case, but we could test various permutation of how this value is passed in (missing, empty, valid, invalid).
There was a problem hiding this comment.
Nvm, this is the expected value returned! Additional tests on password_policy would be nice though since this is a new param on the config, on both v4 and v5 test cases to ensure that defaults are used (if empty and on v5) or ignored (if on v4).
There was a problem hiding this comment.
The key needs to exist because the DatabaseConfig doesn't have omitempty on this field.
| // does not have a way of returning the password so this function signature needs to be different. | ||
| // The password returned here should be considered the source of truth, not the provided password. | ||
| // Errors if the wrapper does not contain an underlying database. | ||
| func (d databaseVersionWrapper) NewUser(ctx context.Context, req newdbplugin.NewUserRequest) (resp newdbplugin.NewUserResponse, password string, err error) { |
There was a problem hiding this comment.
This probably okay as-is, but I also wanted to bring up a suggestion that was tossed around, which is the idea of having the password be part of newdbplugin.NewUserResponse (and its proto equivalent), but marking that field as being there solely for backward compatibility purposes. This should address the issue of NewUser being unable to return the password string if we had a wrapper around v4. We could probably do something in the same vein (e.g. an UpdateType or similar on the request payload) for UpdateUser so that the v4 wrapper knows what method to call.
There was a problem hiding this comment.
I don't think this is a good idea for three reasons:
- It conflates the two interfaces. We'd be stuck with the new interface returning a password even if we removed the old interface.
- It confuses plugin authors.
NewUseris receiving a password and returning one? - We're explicitly moving password generation out of plugins and into Vault. By having the response return a password, we revert that decision.
Unfortunately, we cannot make the version selection code simpler because we are dramatically changing what parts of the system are responsible for what (in particular password generation).
There was a problem hiding this comment.
That's right, I'm satisfied with the current approach since there are quite a few downsides for little gain going with the other suggestion. Documentation around fields should hopefully help alleviate with the confusion on the return value. However, leaking some of the old behavior that will be unused in v5 and having dangling fields when v4 is removed is certainly non-ideal.
|
|
||
| ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) | ||
| defer cancel() | ||
| password, err := dbw.GeneratePassword(ctx, passGen, "test_policy") |
There was a problem hiding this comment.
Looks like this is always testing for a password to be provided at the wrapper layer. Can we also test for "" so that we make sure the call to defaultPasswordGenerator.Generate(ctx, rand.Reader) is exercised?
There was a problem hiding this comment.
Added this test. It was being exercised in another test, but it wasn't explicit.
| // newDatabaseWrapper figures out which version of the database the pluginName is referring to and returns a wrapper object | ||
| // that can be used to make operations on the underlying database plugin. | ||
| func newDatabaseWrapper(ctx context.Context, pluginName string, sys pluginutil.LookRunnerUtil, logger log.Logger) (dbw databaseVersionWrapper, err error) { | ||
| newDB, err := newdbplugin.PluginFactory(ctx, pluginName, sys, logger) |
There was a problem hiding this comment.
Will attempt! This will end up being in a separate PR though since it will be a part of the SDK code rather than the engine.
Overview
This PR is part of a larger feature adding support for password policies into the combined database engine. This feature is being split into multiple PRs to make for smaller reviews & earlier feedback.
Updates the combined database secrets engine to serve both Database v5 and v4 interfaces. Most of the logic is located in
versioning.go. This adds a new type:databaseVersionWrapperthat is essentially a one-of type. It can either contain the v4 Database type or a v5 Database type. It should never contain both. This is then used everywhere the v4 Database was previously referenced in order to switch on the version of the interface. This was the simplest approach I could come up with since the two Database interfaces don't have useful overlap that would allow us to define a shared interface.If using the v5 interface, this also supports using password policies for password generation. Setting
password_policy=<string>when configuring the database will use the specified password policy for password generation. This is a change in current behavior. In the v4 Database, the database plugin is responsible for generating passwords for dynamic & static users, as well as the root user. In the v5 Database, this responsibility is handled by Vault such that passwords are generated in Vault & passed into the plugin's functions.PR Size
This PR looks huge (20k+ lines!) but it's actually much smaller. Most of those lines are from vendoring:
Prerequisites
mastermasterRelated PRs
Original password policies PR
1/X - Database interface & gRPC
2/X - Middleware
3/X - Plugin management