-
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
[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
vault/plugin_catalog.go
Outdated
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 Moved Database type checking into its own function
|
||
type databaseVersionWrapper struct { | ||
database newdbplugin.Database | ||
legacyDatabase dbplugin.Database |
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we make these funcs be part of dbPluginInstance's
methods?
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 has been put into a separate wrapper struct in version_wrapper.go
rather than on the dbPluginInstance
.
return nil, nil | ||
} | ||
} | ||
|
||
func (b *databaseBackend) deleteWal(ctx context.Context, storage logical.Storage, walID string) { |
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 don't see this being re-used elsewhere, can we move it back to the func above?
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.
Done. I think at one point I had this doing more but it's not useful as a separate function any more.
builtin/logical/database/rollback.go
Outdated
// a connection to the database using the WAL entry new password in | ||
// order to alter the password to be the WAL entry old password. | ||
func (b *databaseBackend) rollbackDatabaseCredentials(ctx context.Context, config *DatabaseConfig, entry rotateRootCredentialsWAL) error { | ||
// 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: can we name the var dbi
to have it be consistent with similar calls?
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.
Done. I also updated the other places where GetConnection
is being called to use dbi
.
vault/plugin_catalog.go
Outdated
v5Client.Close() | ||
return nil | ||
} | ||
merr = multierror.Append(merr, 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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
vault/plugin_catalog.go
Outdated
} | ||
logger.Warn(fmt.Sprintf("received %s attempting as db plugin, attempting as auth/secret plugin", 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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
@@ -289,6 +289,7 @@ func TestBackend_config_connection(t *testing.T) { | |||
}, | |||
"allowed_roles": []string{"*"}, | |||
"root_credentials_rotate_statements": []string{}, | |||
"password_policy": "", |
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.
Is this required to be provided?
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.
No, password policies are optional.
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
NewUser
is 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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:databaseVersionWrapper
that 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
master
master
Related PRs
Original password policies PR
1/X - Database interface & gRPC
2/X - Middleware
3/X - Plugin management