-
Notifications
You must be signed in to change notification settings - Fork 4.2k
/
version_wrapper.go
271 lines (232 loc) · 8.85 KB
/
version_wrapper.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
package database
import (
"context"
"crypto/rand"
"fmt"
log "github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/vault/helper/random"
v4 "github.com/hashicorp/vault/sdk/database/dbplugin"
v5 "github.com/hashicorp/vault/sdk/database/dbplugin/v5"
"github.com/hashicorp/vault/sdk/helper/pluginutil"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type databaseVersionWrapper struct {
v4 v4.Database
v5 v5.Database
}
// 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 := v5.PluginFactory(ctx, pluginName, sys, logger)
if err == nil {
dbw = databaseVersionWrapper{
v5: newDB,
}
return dbw, nil
}
merr := &multierror.Error{}
merr = multierror.Append(merr, err)
legacyDB, err := v4.PluginFactory(ctx, pluginName, sys, logger)
if err == nil {
dbw = databaseVersionWrapper{
v4: legacyDB,
}
return dbw, nil
}
merr = multierror.Append(merr, err)
return dbw, fmt.Errorf("invalid database version: %s", merr)
}
// Initialize the underlying database. This is analogous to a constructor on the database plugin object.
// Errors if the wrapper does not contain an underlying database.
func (d databaseVersionWrapper) Initialize(ctx context.Context, req v5.InitializeRequest) (v5.InitializeResponse, error) {
if !d.isV5() && !d.isV4() {
return v5.InitializeResponse{}, fmt.Errorf("no underlying database specified")
}
// v5 Database
if d.isV5() {
return d.v5.Initialize(ctx, req)
}
// v4 Database
saveConfig, err := d.v4.Init(ctx, req.Config, req.VerifyConnection)
if err != nil {
return v5.InitializeResponse{}, err
}
resp := v5.InitializeResponse{
Config: saveConfig,
}
return resp, nil
}
// NewUser in the database. This is different from the v5 Database in that it returns a password as well.
// This is done because the v4 Database is expected to generate a password and return it. The NewUserResponse
// 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 v5.NewUserRequest) (resp v5.NewUserResponse, password string, err error) {
if !d.isV5() && !d.isV4() {
return v5.NewUserResponse{}, "", fmt.Errorf("no underlying database specified")
}
// v5 Database
if d.isV5() {
resp, err = d.v5.NewUser(ctx, req)
return resp, req.Password, err
}
// v4 Database
stmts := v4.Statements{
Creation: req.Statements.Commands,
Rollback: req.RollbackStatements.Commands,
}
usernameConfig := v4.UsernameConfig{
DisplayName: req.UsernameConfig.DisplayName,
RoleName: req.UsernameConfig.RoleName,
}
username, password, err := d.v4.CreateUser(ctx, stmts, usernameConfig, req.Expiration)
if err != nil {
return resp, "", err
}
resp = v5.NewUserResponse{
Username: username,
}
return resp, password, nil
}
// UpdateUser in the underlying database. This is used to update any information currently supported
// in the UpdateUserRequest such as password credentials or user TTL.
// Errors if the wrapper does not contain an underlying database.
func (d databaseVersionWrapper) UpdateUser(ctx context.Context, req v5.UpdateUserRequest, isRootUser bool) (saveConfig map[string]interface{}, err error) {
if !d.isV5() && !d.isV4() {
return nil, fmt.Errorf("no underlying database specified")
}
// v5 Database
if d.isV5() {
_, err := d.v5.UpdateUser(ctx, req)
return nil, err
}
// v4 Database
if req.Password == nil && req.Expiration == nil {
return nil, fmt.Errorf("missing change to be sent to the database")
}
if req.Password != nil && req.Expiration != nil {
// We could support this, but it would require handling partial
// errors which I'm punting on since we don't need it for now
return nil, fmt.Errorf("cannot specify both password and expiration change at the same time")
}
// Change password
if req.Password != nil {
return d.changePasswordLegacy(ctx, req.Username, req.Password, isRootUser)
}
// Change expiration date
if req.Expiration != nil {
stmts := v4.Statements{
Renewal: req.Expiration.Statements.Commands,
}
err := d.v4.RenewUser(ctx, stmts, req.Username, req.Expiration.NewExpiration)
return nil, err
}
return nil, nil
}
// changePasswordLegacy attempts to use SetCredentials to change the password for the user with the password provided
// in ChangePassword. If that user is the root user and SetCredentials is unimplemented, it will fall back to using
// RotateRootCredentials. If not the root user, this will not use RotateRootCredentials.
func (d databaseVersionWrapper) changePasswordLegacy(ctx context.Context, username string, passwordChange *v5.ChangePassword, isRootUser bool) (saveConfig map[string]interface{}, err error) {
err = d.changeUserPasswordLegacy(ctx, username, passwordChange)
// If changing the root user's password but SetCredentials is unimplemented, fall back to RotateRootCredentials
if isRootUser && (err == v4.ErrPluginStaticUnsupported || status.Code(err) == codes.Unimplemented) {
saveConfig, err = d.changeRootUserPasswordLegacy(ctx, passwordChange)
if err != nil {
return nil, err
}
return saveConfig, nil
}
if err != nil {
return nil, err
}
return nil, nil
}
func (d databaseVersionWrapper) changeUserPasswordLegacy(ctx context.Context, username string, passwordChange *v5.ChangePassword) (err error) {
stmts := v4.Statements{
Rotation: passwordChange.Statements.Commands,
}
staticConfig := v4.StaticUserConfig{
Username: username,
Password: passwordChange.NewPassword,
}
_, _, err = d.v4.SetCredentials(ctx, stmts, staticConfig)
return err
}
func (d databaseVersionWrapper) changeRootUserPasswordLegacy(ctx context.Context, passwordChange *v5.ChangePassword) (saveConfig map[string]interface{}, err error) {
return d.v4.RotateRootCredentials(ctx, passwordChange.Statements.Commands)
}
// DeleteUser in the underlying database. Errors if the wrapper does not contain an underlying database.
func (d databaseVersionWrapper) DeleteUser(ctx context.Context, req v5.DeleteUserRequest) (v5.DeleteUserResponse, error) {
if !d.isV5() && !d.isV4() {
return v5.DeleteUserResponse{}, fmt.Errorf("no underlying database specified")
}
// v5 Database
if d.isV5() {
return d.v5.DeleteUser(ctx, req)
}
// v4 Database
stmts := v4.Statements{
Revocation: req.Statements.Commands,
}
err := d.v4.RevokeUser(ctx, stmts, req.Username)
return v5.DeleteUserResponse{}, err
}
// Type of the underlying database. Errors if the wrapper does not contain an underlying database.
func (d databaseVersionWrapper) Type() (string, error) {
if !d.isV5() && !d.isV4() {
return "", fmt.Errorf("no underlying database specified")
}
// v5 Database
if d.isV5() {
return d.v5.Type()
}
// v4 Database
return d.v4.Type()
}
// Close the underlying database. Errors if the wrapper does not contain an underlying database.
func (d databaseVersionWrapper) Close() error {
if !d.isV5() && !d.isV4() {
return fmt.Errorf("no underlying database specified")
}
// v5 Database
if d.isV5() {
return d.v5.Close()
}
// v4 Database
return d.v4.Close()
}
// /////////////////////////////////////////////////////////////////////////////////
// Password generation
// /////////////////////////////////////////////////////////////////////////////////
type passwordGenerator interface {
GeneratePasswordFromPolicy(ctx context.Context, policyName string) (password string, err error)
}
var defaultPasswordGenerator = random.DefaultStringGenerator
// GeneratePassword either from the v4 database or by using the provided password policy. If using a v5 database
// and no password policy is specified, this will have a reasonable default password generator.
func (d databaseVersionWrapper) GeneratePassword(ctx context.Context, generator passwordGenerator, passwordPolicy string) (password string, err error) {
if !d.isV5() && !d.isV4() {
return "", fmt.Errorf("no underlying database specified")
}
// If using the legacy database, use GenerateCredentials instead of password policies
// This will keep the existing behavior even though passwords can be generated with a policy
if d.isV4() {
password, err := d.v4.GenerateCredentials(ctx)
if err != nil {
return "", err
}
return password, nil
}
if passwordPolicy == "" {
return defaultPasswordGenerator.Generate(ctx, rand.Reader)
}
return generator.GeneratePasswordFromPolicy(ctx, passwordPolicy)
}
func (d databaseVersionWrapper) isV5() bool {
return d.v5 != nil
}
func (d databaseVersionWrapper) isV4() bool {
return d.v4 != nil
}