diff --git a/builtin/logical/database/backend.go b/builtin/logical/database/backend.go index 2a4f3fcf5a9c..4c97935ce584 100644 --- a/builtin/logical/database/backend.go +++ b/builtin/logical/database/backend.go @@ -34,6 +34,7 @@ const ( databaseRolePath = "role/" databaseStaticRolePath = "static-role/" minRootCredRollbackAge = 1 * time.Minute + scheduleOptionsDefault = cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow ) type dbPluginInstance struct { @@ -129,12 +130,6 @@ func Backend(conf *logical.BackendConfig) *databaseBackend { b.queueCtx, b.cancelQueueCtx = context.WithCancel(context.Background()) b.roleLocks = locksutil.CreateLocks() - // TODO(JM): don't allow seconds in production, this is helpful for - // development/testing though - parser := cron.NewParser( - cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.DowOptional, - ) - b.scheduleParser = parser return &b } @@ -185,7 +180,25 @@ type databaseBackend struct { gaugeCollectionProcess *metricsutil.GaugeCollectionProcess gaugeCollectionProcessStop sync.Once - scheduleParser cron.Parser + // scheduleOptionsOverride is used by tests to set a custom ParseOption with seconds enabled + scheduleOptionsOverride cron.ParseOption +} + +func (b *databaseBackend) ParseSchedule(rotationSchedule string) (*cron.SpecSchedule, error) { + scheduleOptions := scheduleOptionsDefault + if b.scheduleOptionsOverride != 0 { + scheduleOptions = b.scheduleOptionsOverride + } + parser := cron.NewParser(scheduleOptions) + schedule, err := parser.Parse(rotationSchedule) + if err != nil { + return nil, err + } + sched, ok := schedule.(*cron.SpecSchedule) + if !ok { + return nil, fmt.Errorf("invalid rotation schedule") + } + return sched, nil } func (b *databaseBackend) DatabaseConfig(ctx context.Context, s logical.Storage, name string) (*DatabaseConfig, error) { diff --git a/builtin/logical/database/path_roles.go b/builtin/logical/database/path_roles.go index 5366ac711c0c..43417f000a74 100644 --- a/builtin/logical/database/path_roles.go +++ b/builtin/logical/database/path_roles.go @@ -591,16 +591,12 @@ func (b *databaseBackend) pathStaticRoleCreateUpdate(ctx context.Context, req *l } if rotationScheduleOk { - schedule, err := b.scheduleParser.Parse(rotationSchedule) + parsedSchedule, err := b.ParseSchedule(rotationSchedule) if err != nil { return logical.ErrorResponse("could not parse rotation_schedule", "error", err), nil } role.StaticAccount.RotationSchedule = rotationSchedule - sched, ok := schedule.(*cron.SpecSchedule) - if !ok { - return logical.ErrorResponse("could not parse rotation_schedule"), nil - } - role.StaticAccount.Schedule = *sched + role.StaticAccount.Schedule = *parsedSchedule if rotationWindowOk { rotationWindowSeconds := rotationWindowSecondsRaw.(int) diff --git a/builtin/logical/database/path_roles_test.go b/builtin/logical/database/path_roles_test.go index 4cc142d783c9..bb7b5b93a90e 100644 --- a/builtin/logical/database/path_roles_test.go +++ b/builtin/logical/database/path_roles_test.go @@ -17,7 +17,6 @@ import ( postgreshelper "github.com/hashicorp/vault/helper/testhelpers/postgresql" v5 "github.com/hashicorp/vault/sdk/database/dbplugin/v5" "github.com/hashicorp/vault/sdk/logical" - "github.com/robfig/cron/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -344,6 +343,14 @@ func TestBackend_StaticRole_Config(t *testing.T) { path: "disallowed-role", err: errors.New("\"disallowed-role\" is not an allowed role"), }, + "fails to parse cronSpec with seconds": { + account: map[string]interface{}{ + "username": dbUser, + "rotation_schedule": "*/10 * * * * *", + }, + path: "plugin-role-test-1", + errContains: "could not parse rotation_schedule", + }, } for name, tc := range testCases { @@ -1145,16 +1152,12 @@ func TestIsInsideRotationWindow(t *testing.T) { testTime := tc.now if tc.data["rotation_schedule"] != nil && tc.timeModifier != nil { rotationSchedule := tc.data["rotation_schedule"].(string) - schedule, err := b.scheduleParser.Parse(rotationSchedule) + schedule, err := b.ParseSchedule(rotationSchedule) if err != nil { t.Fatalf("could not parse rotation_schedule: %s", err) } - sched, ok := schedule.(*cron.SpecSchedule) - if !ok { - t.Fatalf("could not parse rotation_schedule") - } - next1 := sched.Next(tc.now) // the next rotation time we expect - next2 := sched.Next(next1) // the next rotation time after that + next1 := schedule.Next(tc.now) // the next rotation time we expect + next2 := schedule.Next(next1) // the next rotation time after that testTime = tc.timeModifier(next2) } diff --git a/builtin/logical/database/rotation_test.go b/builtin/logical/database/rotation_test.go index 4e7b1dbfba77..4ee3c6d775f0 100644 --- a/builtin/logical/database/rotation_test.go +++ b/builtin/logical/database/rotation_test.go @@ -25,6 +25,7 @@ import ( "github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/sdk/queue" _ "github.com/jackc/pgx/v4/stdlib" + "github.com/robfig/cron/v3" "github.com/stretchr/testify/mock" mongodbatlasapi "go.mongodb.org/atlas/mongodbatlas" "go.mongodb.org/mongo-driver/mongo" @@ -32,8 +33,9 @@ import ( ) const ( - dbUser = "vaultstatictest" - dbUserDefaultPassword = "password" + dbUser = "vaultstatictest" + dbUserDefaultPassword = "password" + testScheduleOptionsSeconds = cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow ) func TestBackend_StaticRole_Rotation_basic(t *testing.T) { @@ -54,6 +56,8 @@ func TestBackend_StaticRole_Rotation_basic(t *testing.T) { } defer b.Cleanup(context.Background()) + b.scheduleOptionsOverride = testScheduleOptionsSeconds + cleanup, connURL := postgreshelper.PrepareTestContainer(t, "") defer cleanup() @@ -1293,6 +1297,7 @@ func TestStoredWALsCorrectlyProcessed(t *testing.T) { t.Fatal(err) } b.credRotationQueue = queue.New() + b.scheduleOptionsOverride = testScheduleOptionsSeconds configureDBMount(t, config.StorageView) createRoleWithData(t, b, config.StorageView, mockDB, tc.wal.RoleName, tc.data) role, err := b.StaticRole(ctx, config.StorageView, "hashicorp") @@ -1453,6 +1458,7 @@ func getBackend(t *testing.T) (*databaseBackend, logical.Storage, *mockNewDataba if err := b.Setup(context.Background(), config); err != nil { t.Fatal(err) } + b.scheduleOptionsOverride = testScheduleOptionsSeconds b.credRotationQueue = queue.New() b.populateQueue(context.Background(), config.StorageView)