diff --git a/plugins/database/mongodb/mongodb.go b/plugins/database/mongodb/mongodb.go index 795d0b2911..bfd8d4a3ca 100644 --- a/plugins/database/mongodb/mongodb.go +++ b/plugins/database/mongodb/mongodb.go @@ -7,9 +7,11 @@ import ( "io" "strings" - dbplugin "github.com/hashicorp/vault/sdk/database/dbplugin/v5" - "github.com/hashicorp/vault/sdk/database/helper/credsutil" "github.com/hashicorp/vault/sdk/database/helper/dbutil" + "github.com/hashicorp/vault/sdk/helper/strutil" + "github.com/hashicorp/vault/sdk/helper/template" + + dbplugin "github.com/hashicorp/vault/sdk/database/dbplugin/v5" "github.com/mitchellh/mapstructure" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" @@ -18,11 +20,17 @@ import ( "go.mongodb.org/mongo-driver/x/mongo/driver/connstring" ) -const mongoDBTypeName = "mongodb" +const ( + mongoDBTypeName = "mongodb" + + defaultUserNameTemplate = `{{ printf "v-%s-%s-%s-%s" (.DisplayName | truncate 15) (.RoleName | truncate 15) (random 20) (unix_time) | truncate 100 }}` +) // MongoDB is an implementation of Database interface type MongoDB struct { *mongoDBConnectionProducer + + usernameProducer template.StringTemplate } var _ dbplugin.Database = &MongoDB{} @@ -64,7 +72,26 @@ func (m *MongoDB) Initialize(ctx context.Context, req dbplugin.InitializeRequest m.RawConfig = req.Config - err := mapstructure.WeakDecode(req.Config, m.mongoDBConnectionProducer) + usernameTemplate, err := strutil.GetString(req.Config, "username_template") + if err != nil { + return dbplugin.InitializeResponse{}, fmt.Errorf("failed to retrieve username_template: %w", err) + } + if usernameTemplate == "" { + usernameTemplate = defaultUserNameTemplate + } + + up, err := template.NewTemplate(template.Template(usernameTemplate)) + if err != nil { + return dbplugin.InitializeResponse{}, fmt.Errorf("unable to initialize username template: %w", err) + } + m.usernameProducer = up + + _, err = m.usernameProducer.Generate(dbplugin.UsernameMetadata{}) + if err != nil { + return dbplugin.InitializeResponse{}, fmt.Errorf("invalid username template: %w", err) + } + + err = mapstructure.WeakDecode(req.Config, m.mongoDBConnectionProducer) if err != nil { return dbplugin.InitializeResponse{}, err } @@ -116,12 +143,7 @@ func (m *MongoDB) NewUser(ctx context.Context, req dbplugin.NewUserRequest) (dbp return dbplugin.NewUserResponse{}, dbutil.ErrEmptyCreationStatement } - username, err := credsutil.GenerateUsername( - credsutil.DisplayName(req.UsernameConfig.DisplayName, 15), - credsutil.RoleName(req.UsernameConfig.RoleName, 15), - credsutil.MaxLength(100), - credsutil.Separator("-"), - ) + username, err := m.usernameProducer.Generate(req.UsernameConfig) if err != nil { return dbplugin.NewUserResponse{}, err } diff --git a/plugins/database/mongodb/mongodb_test.go b/plugins/database/mongodb/mongodb_test.go index b07cc75f08..cb08889248 100644 --- a/plugins/database/mongodb/mongodb_test.go +++ b/plugins/database/mongodb/mongodb_test.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/vault/helper/testhelpers/mongodb" dbplugin "github.com/hashicorp/vault/sdk/database/dbplugin/v5" dbtesting "github.com/hashicorp/vault/sdk/database/dbplugin/v5/testing" + "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "go.mongodb.org/mongo-driver/mongo/readpref" @@ -51,6 +52,78 @@ func TestMongoDB_Initialize(t *testing.T) { } } +func TestNewUser_usernameTemplate(t *testing.T) { + type testCase struct { + usernameTemplate string + + newUserReq dbplugin.NewUserRequest + expectedUsernameRegex string + } + + tests := map[string]testCase{ + "default username template": { + usernameTemplate: "", + + newUserReq: dbplugin.NewUserRequest{ + UsernameConfig: dbplugin.UsernameMetadata{ + DisplayName: "token", + RoleName: "testrolenamewithmanycharacters", + }, + Statements: dbplugin.Statements{ + Commands: []string{mongoAdminRole}, + }, + Password: "98yq3thgnakjsfhjkl", + Expiration: time.Now().Add(time.Minute), + }, + + expectedUsernameRegex: "^v-token-testrolenamewit-[a-zA-Z0-9]{20}-[0-9]{10}$", + }, + "custom username template": { + usernameTemplate: "{{random 2 | uppercase}}_{{unix_time}}_{{.RoleName | uppercase}}_{{.DisplayName | uppercase}}", + + newUserReq: dbplugin.NewUserRequest{ + UsernameConfig: dbplugin.UsernameMetadata{ + DisplayName: "token", + RoleName: "testrolenamewithmanycharacters", + }, + Statements: dbplugin.Statements{ + Commands: []string{mongoAdminRole}, + }, + Password: "98yq3thgnakjsfhjkl", + Expiration: time.Now().Add(time.Minute), + }, + + expectedUsernameRegex: "^[A-Z0-9]{2}_[0-9]{10}_TESTROLENAMEWITHMANYCHARACTERS_TOKEN$", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + cleanup, connURL := mongodb.PrepareTestContainer(t, "latest") + defer cleanup() + + db := new() + defer dbtesting.AssertClose(t, db) + + initReq := dbplugin.InitializeRequest{ + Config: map[string]interface{}{ + "connection_url": connURL, + "username_template": test.usernameTemplate, + }, + VerifyConnection: true, + } + dbtesting.AssertInitialize(t, db, initReq) + + ctx := context.Background() + newUserResp, err := db.NewUser(ctx, test.newUserReq) + require.NoError(t, err) + require.Regexp(t, test.expectedUsernameRegex, newUserResp.Username) + + assertCredsExist(t, newUserResp.Username, test.newUserReq.Password, connURL) + }) + } +} + func TestMongoDB_CreateUser(t *testing.T) { cleanup, connURL := mongodb.PrepareTestContainer(t, "latest") defer cleanup()