mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-29 09:42:25 +00:00
secrets/database: advanced TTL management for static roles (#22484)
* add rotation_schedule field to db backend * add cron schedule field * use priority queue with scheduled rotation types * allow marshalling of cron schedule type * return warning on use of mutually exclusive fields * handle mutual exclusion of rotation fields (#22306) * handle mutual exclusion of rotation fields * fix import * adv ttl mgmt: add rotation_window field (#22303) * adv ttl mgmt: add rotation_window field * do some rotation_window validation and add unit tests * adv ttl mgmt: Ensure initialization sets appropriate rotation schedule (#22341) * general cleanup and refactor rotation type checks * make NextRotationTime account for the rotation type * add comments * add unit tests to handle mutual exclusion (#22352) * add unit tests to handle mutual exclusion * revert rotation_test.go and add missing test case to path_roles_test.go * adv ttl mgmt: add tests for init queue (#22376) * Vault 18908/handle manual rotation (#22389) * support manual rotation for schedule based roles * update description and naming * adv ttl mgmt: consider rotation window (#22448) * consider rotation window ensure rotations only occur within a rotation window for schedule-based rotations * use helper method to set priority in rotateCredential * fix bug with priority check * remove test for now * add and remove comments * add unit tests for manual rotation (#22453) * adv ttl mgmt: add tests for rotation_window * adv ttl mgmt: refactor window tests (#22472) * Handle GET static-creds endpoint (#22476) * update read static-creds endpoint to include correct resp data * return rotation_window if set * update * add changelog * add unit test for static-creds read endpoint (#22505) --------- Co-authored-by: Milena Zlaticanin <60530402+Zlaticanin@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
749b0f7948
commit
83f3e391c2
@@ -25,6 +25,7 @@ import (
|
|||||||
"github.com/hashicorp/vault/sdk/helper/locksutil"
|
"github.com/hashicorp/vault/sdk/helper/locksutil"
|
||||||
"github.com/hashicorp/vault/sdk/logical"
|
"github.com/hashicorp/vault/sdk/logical"
|
||||||
"github.com/hashicorp/vault/sdk/queue"
|
"github.com/hashicorp/vault/sdk/queue"
|
||||||
|
"github.com/robfig/cron/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -127,6 +128,13 @@ func Backend(conf *logical.BackendConfig) *databaseBackend {
|
|||||||
b.connections = syncmap.NewSyncMap[string, *dbPluginInstance]()
|
b.connections = syncmap.NewSyncMap[string, *dbPluginInstance]()
|
||||||
b.queueCtx, b.cancelQueueCtx = context.WithCancel(context.Background())
|
b.queueCtx, b.cancelQueueCtx = context.WithCancel(context.Background())
|
||||||
b.roleLocks = locksutil.CreateLocks()
|
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
|
return &b
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,6 +184,8 @@ type databaseBackend struct {
|
|||||||
// the running gauge collection process
|
// the running gauge collection process
|
||||||
gaugeCollectionProcess *metricsutil.GaugeCollectionProcess
|
gaugeCollectionProcess *metricsutil.GaugeCollectionProcess
|
||||||
gaugeCollectionProcessStop sync.Once
|
gaugeCollectionProcessStop sync.Once
|
||||||
|
|
||||||
|
scheduleParser cron.Parser
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *databaseBackend) DatabaseConfig(ctx context.Context, s logical.Storage, name string) (*DatabaseConfig, error) {
|
func (b *databaseBackend) DatabaseConfig(ctx context.Context, s logical.Storage, name string) (*DatabaseConfig, error) {
|
||||||
|
|||||||
@@ -249,10 +249,18 @@ func (b *databaseBackend) pathStaticCredsRead() framework.OperationFunc {
|
|||||||
respData := map[string]interface{}{
|
respData := map[string]interface{}{
|
||||||
"username": role.StaticAccount.Username,
|
"username": role.StaticAccount.Username,
|
||||||
"ttl": role.StaticAccount.CredentialTTL().Seconds(),
|
"ttl": role.StaticAccount.CredentialTTL().Seconds(),
|
||||||
"rotation_period": role.StaticAccount.RotationPeriod.Seconds(),
|
|
||||||
"last_vault_rotation": role.StaticAccount.LastVaultRotation,
|
"last_vault_rotation": role.StaticAccount.LastVaultRotation,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if role.StaticAccount.UsesRotationPeriod() {
|
||||||
|
respData["rotation_period"] = role.StaticAccount.RotationPeriod.Seconds()
|
||||||
|
} else if role.StaticAccount.UsesRotationSchedule() {
|
||||||
|
respData["rotation_schedule"] = role.StaticAccount.RotationSchedule
|
||||||
|
if role.StaticAccount.RotationWindow.Seconds() != 0 {
|
||||||
|
respData["rotation_window"] = role.StaticAccount.RotationWindow.Seconds()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch role.CredentialType {
|
switch role.CredentialType {
|
||||||
case v5.CredentialTypePassword:
|
case v5.CredentialTypePassword:
|
||||||
respData["password"] = role.StaticAccount.Password
|
respData["password"] = role.StaticAccount.Password
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/hashicorp/vault/sdk/helper/locksutil"
|
"github.com/hashicorp/vault/sdk/helper/locksutil"
|
||||||
"github.com/hashicorp/vault/sdk/logical"
|
"github.com/hashicorp/vault/sdk/logical"
|
||||||
"github.com/hashicorp/vault/sdk/queue"
|
"github.com/hashicorp/vault/sdk/queue"
|
||||||
|
"github.com/robfig/cron/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func pathListRoles(b *databaseBackend) []*framework.Path {
|
func pathListRoles(b *databaseBackend) []*framework.Path {
|
||||||
@@ -196,7 +197,18 @@ func staticFields() map[string]*framework.FieldSchema {
|
|||||||
Type: framework.TypeDurationSecond,
|
Type: framework.TypeDurationSecond,
|
||||||
Description: `Period for automatic
|
Description: `Period for automatic
|
||||||
credential rotation of the given username. Not valid unless used with
|
credential rotation of the given username. Not valid unless used with
|
||||||
"username".`,
|
"username". Mutually exclusive with "rotation_schedule."`,
|
||||||
|
},
|
||||||
|
"rotation_schedule": {
|
||||||
|
Type: framework.TypeString,
|
||||||
|
Description: `Schedule for automatic credential rotation of the
|
||||||
|
given username. Mutually exclusive with "rotation_period."`,
|
||||||
|
},
|
||||||
|
"rotation_window": {
|
||||||
|
Type: framework.TypeDurationSecond,
|
||||||
|
Description: `The window of time in which rotations are allowed to
|
||||||
|
occur starting from a given "rotation_schedule". Requires "rotation_schedule"
|
||||||
|
to be specified`,
|
||||||
},
|
},
|
||||||
"rotation_statements": {
|
"rotation_statements": {
|
||||||
Type: framework.TypeStringSlice,
|
Type: framework.TypeStringSlice,
|
||||||
@@ -293,10 +305,20 @@ func (b *databaseBackend) pathStaticRoleRead(ctx context.Context, req *logical.R
|
|||||||
if role.StaticAccount != nil {
|
if role.StaticAccount != nil {
|
||||||
data["username"] = role.StaticAccount.Username
|
data["username"] = role.StaticAccount.Username
|
||||||
data["rotation_statements"] = role.Statements.Rotation
|
data["rotation_statements"] = role.Statements.Rotation
|
||||||
data["rotation_period"] = role.StaticAccount.RotationPeriod.Seconds()
|
|
||||||
if !role.StaticAccount.LastVaultRotation.IsZero() {
|
if !role.StaticAccount.LastVaultRotation.IsZero() {
|
||||||
data["last_vault_rotation"] = role.StaticAccount.LastVaultRotation
|
data["last_vault_rotation"] = role.StaticAccount.LastVaultRotation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// only return one of the mutually exclusive fields in the response
|
||||||
|
if role.StaticAccount.UsesRotationPeriod() {
|
||||||
|
data["rotation_period"] = role.StaticAccount.RotationPeriod.Seconds()
|
||||||
|
} else if role.StaticAccount.UsesRotationSchedule() {
|
||||||
|
data["rotation_schedule"] = role.StaticAccount.RotationSchedule
|
||||||
|
// rotation_window is only valid with rotation_schedule
|
||||||
|
if role.StaticAccount.RotationWindow != 0 {
|
||||||
|
data["rotation_window"] = role.StaticAccount.RotationWindow.Seconds()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(role.CredentialConfig) > 0 {
|
if len(role.CredentialConfig) > 0 {
|
||||||
@@ -480,6 +502,7 @@ func (b *databaseBackend) pathRoleCreateUpdate(ctx context.Context, req *logical
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *databaseBackend) pathStaticRoleCreateUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
func (b *databaseBackend) pathStaticRoleCreateUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||||
|
response := &logical.Response{}
|
||||||
name := data.Get("name").(string)
|
name := data.Get("name").(string)
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return logical.ErrorResponse("empty role name attribute given"), nil
|
return logical.ErrorResponse("empty role name attribute given"), nil
|
||||||
@@ -536,12 +559,18 @@ func (b *databaseBackend) pathStaticRoleCreateUpdate(ctx context.Context, req *l
|
|||||||
}
|
}
|
||||||
role.StaticAccount.Username = username
|
role.StaticAccount.Username = username
|
||||||
|
|
||||||
// If it's a Create operation, both username and rotation_period must be included
|
rotationPeriodSecondsRaw, rotationPeriodOk := data.GetOk("rotation_period")
|
||||||
rotationPeriodSecondsRaw, ok := data.GetOk("rotation_period")
|
rotationSchedule := data.Get("rotation_schedule").(string)
|
||||||
if !ok && createRole {
|
rotationScheduleOk := rotationSchedule != ""
|
||||||
return logical.ErrorResponse("rotation_period is required to create static accounts"), nil
|
rotationWindowSecondsRaw, rotationWindowOk := data.GetOk("rotation_window")
|
||||||
|
|
||||||
|
if rotationScheduleOk && rotationPeriodOk {
|
||||||
|
return logical.ErrorResponse("mutually exclusive fields rotation_period and rotation_schedule were both specified; only one of them can be provided"), nil
|
||||||
|
} else if createRole && (!rotationScheduleOk && !rotationPeriodOk) {
|
||||||
|
return logical.ErrorResponse("one of rotation_schedule or rotation_period must be provided to create a static account"), nil
|
||||||
}
|
}
|
||||||
if ok {
|
|
||||||
|
if rotationPeriodOk {
|
||||||
rotationPeriodSeconds := rotationPeriodSecondsRaw.(int)
|
rotationPeriodSeconds := rotationPeriodSecondsRaw.(int)
|
||||||
if rotationPeriodSeconds < defaultQueueTickSeconds {
|
if rotationPeriodSeconds < defaultQueueTickSeconds {
|
||||||
// If rotation frequency is specified, and this is an update, the value
|
// If rotation frequency is specified, and this is an update, the value
|
||||||
@@ -550,6 +579,40 @@ func (b *databaseBackend) pathStaticRoleCreateUpdate(ctx context.Context, req *l
|
|||||||
return logical.ErrorResponse(fmt.Sprintf("rotation_period must be %d seconds or more", defaultQueueTickSeconds)), nil
|
return logical.ErrorResponse(fmt.Sprintf("rotation_period must be %d seconds or more", defaultQueueTickSeconds)), nil
|
||||||
}
|
}
|
||||||
role.StaticAccount.RotationPeriod = time.Duration(rotationPeriodSeconds) * time.Second
|
role.StaticAccount.RotationPeriod = time.Duration(rotationPeriodSeconds) * time.Second
|
||||||
|
|
||||||
|
if rotationWindowOk {
|
||||||
|
return logical.ErrorResponse("rotation_window is invalid with use of rotation_period"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unset rotation schedule and window if rotation period is set since
|
||||||
|
// these are mutually exclusive
|
||||||
|
role.StaticAccount.RotationSchedule = ""
|
||||||
|
role.StaticAccount.RotationWindow = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if rotationScheduleOk {
|
||||||
|
schedule, err := b.scheduleParser.Parse(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
|
||||||
|
|
||||||
|
if rotationWindowOk {
|
||||||
|
rotationWindowSeconds := rotationWindowSecondsRaw.(int)
|
||||||
|
if rotationWindowSeconds < minRotationWindowSeconds {
|
||||||
|
return logical.ErrorResponse(fmt.Sprintf("rotation_window must be %d seconds or more", minRotationWindowSeconds)), nil
|
||||||
|
}
|
||||||
|
role.StaticAccount.RotationWindow = time.Duration(rotationWindowSeconds) * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unset rotation period if rotation schedule is set since these are
|
||||||
|
// mutually exclusive
|
||||||
|
role.StaticAccount.RotationPeriod = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if rotationStmtsRaw, ok := data.GetOk("rotation_statements"); ok {
|
if rotationStmtsRaw, ok := data.GetOk("rotation_statements"); ok {
|
||||||
@@ -623,14 +686,23 @@ func (b *databaseBackend) pathStaticRoleCreateUpdate(ctx context.Context, req *l
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
item.Priority = lvr.Add(role.StaticAccount.RotationPeriod).Unix()
|
_, rotationPeriodChanged := data.Raw["rotation_period"]
|
||||||
|
_, rotationScheduleChanged := data.Raw["rotation_schedule"]
|
||||||
|
if rotationPeriodChanged {
|
||||||
|
b.logger.Debug("init priority for RotationPeriod", "lvr", lvr, "next", lvr.Add(role.StaticAccount.RotationPeriod))
|
||||||
|
item.Priority = lvr.Add(role.StaticAccount.RotationPeriod).Unix()
|
||||||
|
} else if rotationScheduleChanged {
|
||||||
|
next := role.StaticAccount.Schedule.Next(lvr)
|
||||||
|
b.logger.Debug("init priority for Schedule", "lvr", lvr, "next", next)
|
||||||
|
item.Priority = role.StaticAccount.Schedule.Next(lvr).Unix()
|
||||||
|
}
|
||||||
|
|
||||||
// Add their rotation to the queue
|
// Add their rotation to the queue
|
||||||
if err := b.pushItem(item); err != nil {
|
if err := b.pushItem(item); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type roleEntry struct {
|
type roleEntry struct {
|
||||||
@@ -730,24 +802,103 @@ type staticAccount struct {
|
|||||||
// LastVaultRotation represents the last time Vault rotated the password
|
// LastVaultRotation represents the last time Vault rotated the password
|
||||||
LastVaultRotation time.Time `json:"last_vault_rotation"`
|
LastVaultRotation time.Time `json:"last_vault_rotation"`
|
||||||
|
|
||||||
|
// NextVaultRotation represents the next time Vault is expected to rotate
|
||||||
|
// the password
|
||||||
|
NextVaultRotation time.Time `json:"next_vault_rotation"`
|
||||||
|
|
||||||
// RotationPeriod is number in seconds between each rotation, effectively a
|
// RotationPeriod is number in seconds between each rotation, effectively a
|
||||||
// "time to live". This value is compared to the LastVaultRotation to
|
// "time to live". This value is compared to the LastVaultRotation to
|
||||||
// determine if a password needs to be rotated
|
// determine if a password needs to be rotated
|
||||||
RotationPeriod time.Duration `json:"rotation_period"`
|
RotationPeriod time.Duration `json:"rotation_period"`
|
||||||
|
|
||||||
|
// RotationSchedule is a "chron style" string representing the allowed
|
||||||
|
// schedule for each rotation.
|
||||||
|
// e.g. "1 0 * * *" would rotate at one minute past midnight (00:01) every
|
||||||
|
// day.
|
||||||
|
RotationSchedule string `json:"rotation_schedule"`
|
||||||
|
|
||||||
|
// RotationWindow is number in seconds in which rotations are allowed to
|
||||||
|
// occur starting from a given rotation_schedule.
|
||||||
|
RotationWindow time.Duration `json:"rotation_window"`
|
||||||
|
|
||||||
|
// Schedule holds the parsed "chron style" string representing the allowed
|
||||||
|
// schedule for each rotation.
|
||||||
|
Schedule cron.SpecSchedule `json:"schedule"`
|
||||||
|
|
||||||
// RevokeUser is a boolean flag to indicate if Vault should revoke the
|
// RevokeUser is a boolean flag to indicate if Vault should revoke the
|
||||||
// database user when the role is deleted
|
// database user when the role is deleted
|
||||||
RevokeUserOnDelete bool `json:"revoke_user_on_delete"`
|
RevokeUserOnDelete bool `json:"revoke_user_on_delete"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NextRotationTime calculates the next rotation by adding the Rotation Period
|
// NextRotationTime calculates the next rotation for period and schedule-based
|
||||||
// to the last known vault rotation
|
// rotations.
|
||||||
|
//
|
||||||
|
// Period-based expiries are calculated by adding the Rotation Period to the
|
||||||
|
// last known vault rotation. Schedule-based expiries are calculated by
|
||||||
|
// querying for the next schedule expiry since the last known vault rotation.
|
||||||
func (s *staticAccount) NextRotationTime() time.Time {
|
func (s *staticAccount) NextRotationTime() time.Time {
|
||||||
return s.LastVaultRotation.Add(s.RotationPeriod)
|
if s.UsesRotationPeriod() {
|
||||||
|
return s.LastVaultRotation.Add(s.RotationPeriod)
|
||||||
|
}
|
||||||
|
return s.Schedule.Next(s.LastVaultRotation)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextRotationTimeFromInput calculates the next rotation time for period and
|
||||||
|
// schedule-based roles based on the input.
|
||||||
|
func (s *staticAccount) NextRotationTimeFromInput(input time.Time) time.Time {
|
||||||
|
if s.UsesRotationPeriod() {
|
||||||
|
return input.Add(s.RotationPeriod)
|
||||||
|
}
|
||||||
|
return s.Schedule.Next(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UsesRotationSchedule returns true if the given static account has been
|
||||||
|
// configured to rotate credentials on a schedule (i.e. NOT on a rotation period).
|
||||||
|
func (s *staticAccount) UsesRotationSchedule() bool {
|
||||||
|
return s.RotationSchedule != "" && s.RotationPeriod == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// UsesRotationPeriod returns true if the given static account has been
|
||||||
|
// configured to rotate credentials on a period (i.e. NOT on a rotation schedule).
|
||||||
|
func (s *staticAccount) UsesRotationPeriod() bool {
|
||||||
|
return s.RotationPeriod != 0 && s.RotationSchedule == ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsInsideRotationWindow returns true if the current time t is within a given
|
||||||
|
// static account's rotation window.
|
||||||
|
//
|
||||||
|
// Returns true if the rotation window is not set. In this case, the rotation
|
||||||
|
// window is effectively the span of time between two consecutive rotation
|
||||||
|
// schedules and we should not prevent rotation.
|
||||||
|
func (s *staticAccount) IsInsideRotationWindow(t time.Time) bool {
|
||||||
|
if s.UsesRotationSchedule() && s.RotationWindow != 0 {
|
||||||
|
return t.Before(s.NextVaultRotation.Add(s.RotationWindow))
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldRotate returns true if a given static account should have its
|
||||||
|
// credentials rotated.
|
||||||
|
//
|
||||||
|
// This will return true when the priority <= the current Unix time. If this
|
||||||
|
// static account is schedule-based with a rotation window, this method will
|
||||||
|
// return false if now is outside the rotation window.
|
||||||
|
func (s *staticAccount) ShouldRotate(priority int64) bool {
|
||||||
|
now := time.Now()
|
||||||
|
return priority <= now.Unix() && s.IsInsideRotationWindow(now)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNextVaultRotation
|
||||||
|
func (s *staticAccount) SetNextVaultRotation(t time.Time) {
|
||||||
|
if s.UsesRotationPeriod() {
|
||||||
|
s.NextVaultRotation = t.Add(s.RotationPeriod)
|
||||||
|
} else {
|
||||||
|
s.NextVaultRotation = s.Schedule.Next(t)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CredentialTTL calculates the approximate time remaining until the credential is
|
// CredentialTTL calculates the approximate time remaining until the credential is
|
||||||
// no longer valid. This is approximate because the periodic rotation is only
|
// no longer valid. This is approximate because the rotation expiry is only
|
||||||
// checked approximately every 5 seconds, and each rotation can take a small
|
// checked approximately every 5 seconds, and each rotation can take a small
|
||||||
// amount of time to process. This can result in a negative TTL time while the
|
// amount of time to process. This can result in a negative TTL time while the
|
||||||
// rotation function processes the Static Role and performs the rotation. If the
|
// rotation function processes the Static Role and performs the rotation. If the
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -16,6 +17,7 @@ import (
|
|||||||
postgreshelper "github.com/hashicorp/vault/helper/testhelpers/postgresql"
|
postgreshelper "github.com/hashicorp/vault/helper/testhelpers/postgresql"
|
||||||
v5 "github.com/hashicorp/vault/sdk/database/dbplugin/v5"
|
v5 "github.com/hashicorp/vault/sdk/database/dbplugin/v5"
|
||||||
"github.com/hashicorp/vault/sdk/logical"
|
"github.com/hashicorp/vault/sdk/logical"
|
||||||
|
"github.com/robfig/cron/v3"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
)
|
)
|
||||||
@@ -254,6 +256,8 @@ func TestBackend_StaticRole_Config(t *testing.T) {
|
|||||||
path string
|
path string
|
||||||
expected map[string]interface{}
|
expected map[string]interface{}
|
||||||
err error
|
err error
|
||||||
|
// use this field to check partial error strings, otherwise use err
|
||||||
|
errContains string
|
||||||
}{
|
}{
|
||||||
"basic": {
|
"basic": {
|
||||||
account: map[string]interface{}{
|
account: map[string]interface{}{
|
||||||
@@ -266,12 +270,71 @@ func TestBackend_StaticRole_Config(t *testing.T) {
|
|||||||
"rotation_period": float64(5400),
|
"rotation_period": float64(5400),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"missing rotation period": {
|
"missing required fields": {
|
||||||
account: map[string]interface{}{
|
account: map[string]interface{}{
|
||||||
"username": dbUser,
|
"username": dbUser,
|
||||||
},
|
},
|
||||||
path: "plugin-role-test",
|
path: "plugin-role-test",
|
||||||
err: errors.New("rotation_period is required to create static accounts"),
|
err: errors.New("one of rotation_schedule or rotation_period must be provided to create a static account"),
|
||||||
|
},
|
||||||
|
"rotation_period with rotation_schedule": {
|
||||||
|
account: map[string]interface{}{
|
||||||
|
"username": dbUser,
|
||||||
|
"rotation_period": "5400s",
|
||||||
|
"rotation_schedule": "* * * * *",
|
||||||
|
},
|
||||||
|
path: "plugin-role-test",
|
||||||
|
err: errors.New("mutually exclusive fields rotation_period and rotation_schedule were both specified; only one of them can be provided"),
|
||||||
|
},
|
||||||
|
"rotation window invalid with rotation_period": {
|
||||||
|
account: map[string]interface{}{
|
||||||
|
"username": dbUser,
|
||||||
|
"rotation_period": "5400s",
|
||||||
|
"rotation_window": "3600s",
|
||||||
|
},
|
||||||
|
path: "disallowed-role",
|
||||||
|
err: errors.New("rotation_window is invalid with use of rotation_period"),
|
||||||
|
},
|
||||||
|
"happy path for rotation_schedule": {
|
||||||
|
account: map[string]interface{}{
|
||||||
|
"username": dbUser,
|
||||||
|
"rotation_schedule": "* * * * *",
|
||||||
|
},
|
||||||
|
path: "plugin-role-test",
|
||||||
|
expected: map[string]interface{}{
|
||||||
|
"username": dbUser,
|
||||||
|
"rotation_schedule": "* * * * *",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"happy path for rotation_schedule and rotation_window": {
|
||||||
|
account: map[string]interface{}{
|
||||||
|
"username": dbUser,
|
||||||
|
"rotation_schedule": "* * * * *",
|
||||||
|
"rotation_window": "3600s",
|
||||||
|
},
|
||||||
|
path: "plugin-role-test",
|
||||||
|
expected: map[string]interface{}{
|
||||||
|
"username": dbUser,
|
||||||
|
"rotation_schedule": "* * * * *",
|
||||||
|
"rotation_window": float64(3600),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"error parsing rotation_schedule": {
|
||||||
|
account: map[string]interface{}{
|
||||||
|
"username": dbUser,
|
||||||
|
"rotation_schedule": "foo",
|
||||||
|
},
|
||||||
|
path: "plugin-role-test",
|
||||||
|
errContains: "could not parse rotation_schedule",
|
||||||
|
},
|
||||||
|
"rotation_window invalid": {
|
||||||
|
account: map[string]interface{}{
|
||||||
|
"username": dbUser,
|
||||||
|
"rotation_schedule": "* * * * *",
|
||||||
|
"rotation_window": "59s",
|
||||||
|
},
|
||||||
|
path: "plugin-role-test",
|
||||||
|
err: errors.New(fmt.Sprintf("rotation_window must be %d seconds or more", minRotationWindowSeconds)),
|
||||||
},
|
},
|
||||||
"disallowed role config": {
|
"disallowed role config": {
|
||||||
account: map[string]interface{}{
|
account: map[string]interface{}{
|
||||||
@@ -305,7 +368,12 @@ func TestBackend_StaticRole_Config(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
|
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
|
||||||
if err != nil || (resp != nil && resp.IsError()) {
|
if tc.errContains != "" {
|
||||||
|
if !strings.Contains(resp.Error().Error(), tc.errContains) {
|
||||||
|
t.Fatalf("expected err message: (%s), got (%s), response error: (%s)", tc.err, err, resp.Error())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} else if err != nil || (resp != nil && resp.IsError()) {
|
||||||
if tc.err == nil {
|
if tc.err == nil {
|
||||||
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
}
|
}
|
||||||
@@ -341,7 +409,14 @@ func TestBackend_StaticRole_Config(t *testing.T) {
|
|||||||
|
|
||||||
expected := tc.expected
|
expected := tc.expected
|
||||||
actual := make(map[string]interface{})
|
actual := make(map[string]interface{})
|
||||||
dataKeys := []string{"username", "password", "last_vault_rotation", "rotation_period"}
|
dataKeys := []string{
|
||||||
|
"username",
|
||||||
|
"password",
|
||||||
|
"last_vault_rotation",
|
||||||
|
"rotation_period",
|
||||||
|
"rotation_schedule",
|
||||||
|
"rotation_window",
|
||||||
|
}
|
||||||
for _, key := range dataKeys {
|
for _, key := range dataKeys {
|
||||||
if v, ok := resp.Data[key]; ok {
|
if v, ok := resp.Data[key]; ok {
|
||||||
actual[key] = v
|
actual[key] = v
|
||||||
@@ -388,6 +463,186 @@ func TestBackend_StaticRole_Config(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBackend_StaticRole_ReadCreds(t *testing.T) {
|
||||||
|
cluster, sys := getCluster(t)
|
||||||
|
defer cluster.Cleanup()
|
||||||
|
|
||||||
|
config := logical.TestBackendConfig()
|
||||||
|
config.StorageView = &logical.InmemStorage{}
|
||||||
|
config.System = sys
|
||||||
|
|
||||||
|
lb, err := Factory(context.Background(), config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
b, ok := lb.(*databaseBackend)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("could not convert to db backend")
|
||||||
|
}
|
||||||
|
defer b.Cleanup(context.Background())
|
||||||
|
|
||||||
|
cleanup, connURL := postgreshelper.PrepareTestContainer(t, "")
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
// create the database user
|
||||||
|
createTestPGUser(t, connURL, dbUser, dbUserDefaultPassword, testRoleStaticCreate)
|
||||||
|
|
||||||
|
verifyPgConn(t, dbUser, dbUserDefaultPassword, connURL)
|
||||||
|
|
||||||
|
// Configure a connection
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"connection_url": connURL,
|
||||||
|
"plugin_name": "postgresql-database-plugin",
|
||||||
|
"verify_connection": false,
|
||||||
|
"allowed_roles": []string{"*"},
|
||||||
|
"name": "plugin-test",
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &logical.Request{
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Path: "config/plugin-test",
|
||||||
|
Storage: config.StorageView,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
account map[string]interface{}
|
||||||
|
path string
|
||||||
|
expected map[string]interface{}
|
||||||
|
}{
|
||||||
|
"happy path for rotation_period": {
|
||||||
|
account: map[string]interface{}{
|
||||||
|
"username": dbUser,
|
||||||
|
"rotation_period": "5400s",
|
||||||
|
},
|
||||||
|
path: "plugin-role-test",
|
||||||
|
expected: map[string]interface{}{
|
||||||
|
"username": dbUser,
|
||||||
|
"rotation_period": float64(5400),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"happy path for rotation_schedule": {
|
||||||
|
account: map[string]interface{}{
|
||||||
|
"username": dbUser,
|
||||||
|
"rotation_schedule": "* * * * *",
|
||||||
|
},
|
||||||
|
path: "plugin-role-test",
|
||||||
|
expected: map[string]interface{}{
|
||||||
|
"username": dbUser,
|
||||||
|
"rotation_schedule": "* * * * *",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"happy path for rotation_schedule and rotation_window": {
|
||||||
|
account: map[string]interface{}{
|
||||||
|
"username": dbUser,
|
||||||
|
"rotation_schedule": "* * * * *",
|
||||||
|
"rotation_window": "3600s",
|
||||||
|
},
|
||||||
|
path: "plugin-role-test",
|
||||||
|
expected: map[string]interface{}{
|
||||||
|
"username": dbUser,
|
||||||
|
"rotation_schedule": "* * * * *",
|
||||||
|
"rotation_window": float64(3600),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
data = map[string]interface{}{
|
||||||
|
"name": "plugin-role-test",
|
||||||
|
"db_name": "plugin-test",
|
||||||
|
"rotation_statements": testRoleStaticUpdate,
|
||||||
|
"username": dbUser,
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range tc.account {
|
||||||
|
data[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
req = &logical.Request{
|
||||||
|
Operation: logical.CreateOperation,
|
||||||
|
Path: "static-roles/plugin-role-test",
|
||||||
|
Storage: config.StorageView,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the creds
|
||||||
|
data = map[string]interface{}{}
|
||||||
|
req = &logical.Request{
|
||||||
|
Operation: logical.ReadOperation,
|
||||||
|
Path: "static-creds/plugin-role-test",
|
||||||
|
Storage: config.StorageView,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := tc.expected
|
||||||
|
actual := make(map[string]interface{})
|
||||||
|
dataKeys := []string{
|
||||||
|
"username",
|
||||||
|
"password",
|
||||||
|
"last_vault_rotation",
|
||||||
|
"rotation_period",
|
||||||
|
"rotation_schedule",
|
||||||
|
"rotation_window",
|
||||||
|
"ttl",
|
||||||
|
}
|
||||||
|
for _, key := range dataKeys {
|
||||||
|
if v, ok := resp.Data[key]; ok {
|
||||||
|
actual[key] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tc.expected) > 0 {
|
||||||
|
// verify a password is returned, but we don't care what it's value is
|
||||||
|
if actual["password"] == "" {
|
||||||
|
t.Fatalf("expected result to contain password, but none found")
|
||||||
|
}
|
||||||
|
if actual["ttl"] == "" {
|
||||||
|
t.Fatalf("expected result to contain ttl, but none found")
|
||||||
|
}
|
||||||
|
if v, ok := actual["last_vault_rotation"].(time.Time); !ok {
|
||||||
|
t.Fatalf("expected last_vault_rotation to be set to time.Time type, got: %#v", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete these values before the comparison, since we can't know them in
|
||||||
|
// advance
|
||||||
|
delete(actual, "password")
|
||||||
|
delete(actual, "ttl")
|
||||||
|
delete(actual, "last_vault_rotation")
|
||||||
|
if diff := deep.Equal(expected, actual); diff != nil {
|
||||||
|
t.Fatal(diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete role for next run
|
||||||
|
req = &logical.Request{
|
||||||
|
Operation: logical.DeleteOperation,
|
||||||
|
Path: "static-roles/plugin-role-test",
|
||||||
|
Storage: config.StorageView,
|
||||||
|
}
|
||||||
|
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBackend_StaticRole_Updates(t *testing.T) {
|
func TestBackend_StaticRole_Updates(t *testing.T) {
|
||||||
cluster, sys := getCluster(t)
|
cluster, sys := getCluster(t)
|
||||||
defer cluster.Cleanup()
|
defer cluster.Cleanup()
|
||||||
@@ -828,6 +1083,97 @@ func TestWALsDeletedOnRoleDeletion(t *testing.T) {
|
|||||||
requireWALs(t, storage, 1)
|
requireWALs(t, storage, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIsInsideRotationWindow(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
expected bool
|
||||||
|
data map[string]interface{}
|
||||||
|
now time.Time
|
||||||
|
timeModifier func(t time.Time) time.Time
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"always returns true for rotation_period type",
|
||||||
|
true,
|
||||||
|
map[string]interface{}{
|
||||||
|
"rotation_period": "86400s",
|
||||||
|
},
|
||||||
|
time.Now(),
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"always returns true for rotation_schedule when no rotation_window set",
|
||||||
|
true,
|
||||||
|
map[string]interface{}{
|
||||||
|
"rotation_schedule": "0 0 */2 * * *",
|
||||||
|
},
|
||||||
|
time.Now(),
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"returns true for rotation_schedule when inside rotation_window",
|
||||||
|
true,
|
||||||
|
map[string]interface{}{
|
||||||
|
"rotation_schedule": "0 0 */2 * * *",
|
||||||
|
"rotation_window": "3600s",
|
||||||
|
},
|
||||||
|
time.Now(),
|
||||||
|
func(t time.Time) time.Time {
|
||||||
|
// set current time just inside window
|
||||||
|
return t.Add(-3640 * time.Second)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"returns false for rotation_schedule when outside rotation_window",
|
||||||
|
false,
|
||||||
|
map[string]interface{}{
|
||||||
|
"rotation_schedule": "0 0 */2 * * *",
|
||||||
|
"rotation_window": "3600s",
|
||||||
|
},
|
||||||
|
time.Now(),
|
||||||
|
func(t time.Time) time.Time {
|
||||||
|
// set current time just outside window
|
||||||
|
return t.Add(-3560 * time.Second)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
b, s, mockDB := getBackend(t)
|
||||||
|
defer b.Cleanup(ctx)
|
||||||
|
configureDBMount(t, s)
|
||||||
|
|
||||||
|
testTime := tc.now
|
||||||
|
if tc.data["rotation_schedule"] != nil && tc.timeModifier != nil {
|
||||||
|
rotationSchedule := tc.data["rotation_schedule"].(string)
|
||||||
|
schedule, err := b.scheduleParser.Parse(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
|
||||||
|
testTime = tc.timeModifier(next2)
|
||||||
|
}
|
||||||
|
|
||||||
|
tc.data["username"] = "hashicorp"
|
||||||
|
tc.data["db_name"] = "mockv5"
|
||||||
|
createRoleWithData(t, b, s, mockDB, "test-role", tc.data)
|
||||||
|
role, err := b.StaticRole(ctx, s, "test-role")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
isInsideWindow := role.StaticAccount.IsInsideRotationWindow(testTime)
|
||||||
|
if tc.expected != isInsideWindow {
|
||||||
|
t.Fatalf("expected %t, got %t", tc.expected, isInsideWindow)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func createRole(t *testing.T, b *databaseBackend, storage logical.Storage, mockDB *mockNewDatabase, roleName string) {
|
func createRole(t *testing.T, b *databaseBackend, storage logical.Storage, mockDB *mockNewDatabase, roleName string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
mockDB.On("UpdateUser", mock.Anything, mock.Anything).
|
mockDB.On("UpdateUser", mock.Anything, mock.Anything).
|
||||||
@@ -848,6 +1194,22 @@ func createRole(t *testing.T, b *databaseBackend, storage logical.Storage, mockD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createRoleWithData(t *testing.T, b *databaseBackend, s logical.Storage, mockDB *mockNewDatabase, roleName string, data map[string]interface{}) {
|
||||||
|
t.Helper()
|
||||||
|
mockDB.On("UpdateUser", mock.Anything, mock.Anything).
|
||||||
|
Return(v5.UpdateUserResponse{}, nil).
|
||||||
|
Once()
|
||||||
|
resp, err := b.HandleRequest(context.Background(), &logical.Request{
|
||||||
|
Operation: logical.CreateOperation,
|
||||||
|
Path: "static-roles/" + roleName,
|
||||||
|
Storage: s,
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatal(resp, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const testRoleStaticCreate = `
|
const testRoleStaticCreate = `
|
||||||
CREATE ROLE "{{name}}" WITH
|
CREATE ROLE "{{name}}" WITH
|
||||||
LOGIN
|
LOGIN
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ func (b *databaseBackend) pathRotateRoleCredentialsUpdate() framework.OperationF
|
|||||||
item.Value = resp.WALID
|
item.Value = resp.WALID
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
item.Priority = resp.RotationTime.Add(role.StaticAccount.RotationPeriod).Unix()
|
item.Priority = role.StaticAccount.NextRotationTimeFromInput(resp.RotationTime).Unix()
|
||||||
// Clear any stored WAL ID as we must have successfully deleted our WAL to get here.
|
// Clear any stored WAL ID as we must have successfully deleted our WAL to get here.
|
||||||
item.Value = ""
|
item.Value = ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ const (
|
|||||||
// Default interval to check the queue for items needing rotation
|
// Default interval to check the queue for items needing rotation
|
||||||
defaultQueueTickSeconds = 5
|
defaultQueueTickSeconds = 5
|
||||||
|
|
||||||
|
// Minimum allowed value for rotation_window
|
||||||
|
minRotationWindowSeconds = 3600
|
||||||
|
|
||||||
// Config key to set an alternate interval
|
// Config key to set an alternate interval
|
||||||
queueTickIntervalKey = "rotation_queue_tick_interval"
|
queueTickIntervalKey = "rotation_queue_tick_interval"
|
||||||
|
|
||||||
@@ -91,6 +94,8 @@ func (b *databaseBackend) populateQueue(ctx context.Context, s logical.Storage)
|
|||||||
log.Warn("unable to delete WAL", "error", err, "WAL ID", walEntry.walID)
|
log.Warn("unable to delete WAL", "error", err, "WAL ID", walEntry.walID)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// previous rotation attempt was interrupted, so we set the
|
||||||
|
// Priority as highest to be processed immediately
|
||||||
log.Info("found WAL for role", "role", item.Key, "WAL ID", walEntry.walID)
|
log.Info("found WAL for role", "role", item.Key, "WAL ID", walEntry.walID)
|
||||||
item.Value = walEntry.walID
|
item.Value = walEntry.walID
|
||||||
item.Priority = time.Now().Unix()
|
item.Priority = time.Now().Unix()
|
||||||
@@ -215,9 +220,8 @@ func (b *databaseBackend) rotateCredential(ctx context.Context, s logical.Storag
|
|||||||
|
|
||||||
logger = logger.With("database", role.DBName)
|
logger = logger.With("database", role.DBName)
|
||||||
|
|
||||||
// If "now" is less than the Item priority, then this item does not need to
|
if !role.StaticAccount.ShouldRotate(item.Priority) {
|
||||||
// be rotated
|
// do not rotate now, push item back onto queue to be rotated later
|
||||||
if time.Now().Unix() < item.Priority {
|
|
||||||
if err := b.pushItem(item); err != nil {
|
if err := b.pushItem(item); err != nil {
|
||||||
logger.Error("unable to push item on to queue", "error", err)
|
logger.Error("unable to push item on to queue", "error", err)
|
||||||
}
|
}
|
||||||
@@ -264,8 +268,8 @@ func (b *databaseBackend) rotateCredential(ctx context.Context, s logical.Storag
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update priority and push updated Item to the queue
|
// Update priority and push updated Item to the queue
|
||||||
nextRotation := lvr.Add(role.StaticAccount.RotationPeriod)
|
item.Priority = role.StaticAccount.NextRotationTimeFromInput(lvr).Unix()
|
||||||
item.Priority = nextRotation.Unix()
|
|
||||||
if err := b.pushItem(item); err != nil {
|
if err := b.pushItem(item); err != nil {
|
||||||
logger.Warn("unable to push item on to queue", "error", err)
|
logger.Warn("unable to push item on to queue", "error", err)
|
||||||
}
|
}
|
||||||
@@ -490,6 +494,7 @@ func (b *databaseBackend) setStaticAccount(ctx context.Context, s logical.Storag
|
|||||||
// lvr is the known LastVaultRotation
|
// lvr is the known LastVaultRotation
|
||||||
lvr := time.Now()
|
lvr := time.Now()
|
||||||
input.Role.StaticAccount.LastVaultRotation = lvr
|
input.Role.StaticAccount.LastVaultRotation = lvr
|
||||||
|
input.Role.StaticAccount.SetNextVaultRotation(lvr)
|
||||||
output.RotationTime = lvr
|
output.RotationTime = lvr
|
||||||
|
|
||||||
entry, err := logical.StorageEntryJSON(databaseStaticRolePath+input.RoleName, input.Role)
|
entry, err := logical.StorageEntryJSON(databaseStaticRolePath+input.RoleName, input.Role)
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const (
|
|||||||
dbUserDefaultPassword = "password"
|
dbUserDefaultPassword = "password"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBackend_StaticRole_Rotate_basic(t *testing.T) {
|
func TestBackend_StaticRole_Rotation_basic(t *testing.T) {
|
||||||
cluster, sys := getCluster(t)
|
cluster, sys := getCluster(t)
|
||||||
defer cluster.Cleanup()
|
defer cluster.Cleanup()
|
||||||
|
|
||||||
@@ -82,109 +82,168 @@ func TestBackend_StaticRole_Rotate_basic(t *testing.T) {
|
|||||||
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
data = map[string]interface{}{
|
testCases := map[string]struct {
|
||||||
"name": "plugin-role-test",
|
account map[string]interface{}
|
||||||
"db_name": "plugin-test",
|
path string
|
||||||
"rotation_statements": testRoleStaticUpdate,
|
expected map[string]interface{}
|
||||||
"username": dbUser,
|
waitTime time.Duration
|
||||||
"rotation_period": "5400s",
|
}{
|
||||||
|
"basic with rotation_period": {
|
||||||
|
account: map[string]interface{}{
|
||||||
|
"username": dbUser,
|
||||||
|
"rotation_period": "5400s",
|
||||||
|
},
|
||||||
|
path: "plugin-role-test-1",
|
||||||
|
expected: map[string]interface{}{
|
||||||
|
"username": dbUser,
|
||||||
|
"rotation_period": float64(5400),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"rotation_schedule is set and expires": {
|
||||||
|
account: map[string]interface{}{
|
||||||
|
"username": dbUser,
|
||||||
|
"rotation_schedule": "*/10 * * * * *",
|
||||||
|
},
|
||||||
|
path: "plugin-role-test-2",
|
||||||
|
expected: map[string]interface{}{
|
||||||
|
"username": dbUser,
|
||||||
|
"rotation_schedule": "*/10 * * * * *",
|
||||||
|
},
|
||||||
|
waitTime: 20 * time.Second,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
req = &logical.Request{
|
for name, tc := range testCases {
|
||||||
Operation: logical.CreateOperation,
|
t.Run(name, func(t *testing.T) {
|
||||||
Path: "static-roles/plugin-role-test",
|
data = map[string]interface{}{
|
||||||
Storage: config.StorageView,
|
"name": "plugin-role-test",
|
||||||
Data: data,
|
"db_name": "plugin-test",
|
||||||
}
|
"rotation_statements": testRoleStaticUpdate,
|
||||||
|
"username": dbUser,
|
||||||
|
}
|
||||||
|
|
||||||
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
|
for k, v := range tc.account {
|
||||||
if err != nil || (resp != nil && resp.IsError()) {
|
data[k] = v
|
||||||
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Read the creds
|
req = &logical.Request{
|
||||||
data = map[string]interface{}{}
|
Operation: logical.CreateOperation,
|
||||||
req = &logical.Request{
|
Path: "static-roles/" + tc.path,
|
||||||
Operation: logical.ReadOperation,
|
Storage: config.StorageView,
|
||||||
Path: "static-creds/plugin-role-test",
|
Data: data,
|
||||||
Storage: config.StorageView,
|
}
|
||||||
Data: data,
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
|
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
|
||||||
if err != nil || (resp != nil && resp.IsError()) {
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
username := resp.Data["username"].(string)
|
// Read the creds
|
||||||
password := resp.Data["password"].(string)
|
data = map[string]interface{}{}
|
||||||
if username == "" || password == "" {
|
req = &logical.Request{
|
||||||
t.Fatalf("empty username (%s) or password (%s)", username, password)
|
Operation: logical.ReadOperation,
|
||||||
}
|
Path: "static-creds/" + tc.path,
|
||||||
|
Storage: config.StorageView,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
|
||||||
// Verify username/password
|
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
|
||||||
verifyPgConn(t, dbUser, password, connURL)
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
// Re-read the creds, verifying they aren't changing on read
|
username := resp.Data["username"].(string)
|
||||||
data = map[string]interface{}{}
|
password := resp.Data["password"].(string)
|
||||||
req = &logical.Request{
|
if username == "" || password == "" {
|
||||||
Operation: logical.ReadOperation,
|
t.Fatalf("empty username (%s) or password (%s)", username, password)
|
||||||
Path: "static-creds/plugin-role-test",
|
}
|
||||||
Storage: config.StorageView,
|
|
||||||
Data: data,
|
|
||||||
}
|
|
||||||
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
|
|
||||||
if err != nil || (resp != nil && resp.IsError()) {
|
|
||||||
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
if username != resp.Data["username"].(string) || password != resp.Data["password"].(string) {
|
// Verify username/password
|
||||||
t.Fatal("expected re-read username/password to match, but didn't")
|
verifyPgConn(t, dbUser, password, connURL)
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger rotation
|
// Re-read the creds, verifying they aren't changing on read
|
||||||
data = map[string]interface{}{"name": "plugin-role-test"}
|
data = map[string]interface{}{}
|
||||||
req = &logical.Request{
|
req = &logical.Request{
|
||||||
Operation: logical.UpdateOperation,
|
Operation: logical.ReadOperation,
|
||||||
Path: "rotate-role/plugin-role-test",
|
Path: "static-creds/" + tc.path,
|
||||||
Storage: config.StorageView,
|
Storage: config.StorageView,
|
||||||
Data: data,
|
Data: data,
|
||||||
}
|
}
|
||||||
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
|
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
|
||||||
if err != nil || (resp != nil && resp.IsError()) {
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp != nil {
|
if username != resp.Data["username"].(string) || password != resp.Data["password"].(string) {
|
||||||
t.Fatalf("Expected empty response from rotate-role: (%#v)", resp)
|
t.Fatal("expected re-read username/password to match, but didn't")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-Read the creds
|
// Trigger rotation
|
||||||
data = map[string]interface{}{}
|
data = map[string]interface{}{"name": "plugin-role-test"}
|
||||||
req = &logical.Request{
|
req = &logical.Request{
|
||||||
Operation: logical.ReadOperation,
|
Operation: logical.UpdateOperation,
|
||||||
Path: "static-creds/plugin-role-test",
|
Path: "rotate-role/" + tc.path,
|
||||||
Storage: config.StorageView,
|
Storage: config.StorageView,
|
||||||
Data: data,
|
Data: data,
|
||||||
}
|
}
|
||||||
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
|
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
|
||||||
if err != nil || (resp != nil && resp.IsError()) {
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
newPassword := resp.Data["password"].(string)
|
if resp != nil {
|
||||||
if password == newPassword {
|
t.Fatalf("Expected empty response from rotate-role: (%#v)", resp)
|
||||||
t.Fatalf("expected passwords to differ, got (%s)", newPassword)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Verify new username/password
|
// Re-Read the creds
|
||||||
verifyPgConn(t, username, newPassword, connURL)
|
data = map[string]interface{}{}
|
||||||
|
req = &logical.Request{
|
||||||
|
Operation: logical.ReadOperation,
|
||||||
|
Path: "static-creds/" + tc.path,
|
||||||
|
Storage: config.StorageView,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
newPassword := resp.Data["password"].(string)
|
||||||
|
if password == newPassword {
|
||||||
|
t.Fatalf("expected passwords to differ, got (%s)", newPassword)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify new username/password
|
||||||
|
verifyPgConn(t, username, newPassword, connURL)
|
||||||
|
|
||||||
|
if tc.waitTime > 0 {
|
||||||
|
time.Sleep(tc.waitTime)
|
||||||
|
// Re-Read the creds after schedule expiration
|
||||||
|
data = map[string]interface{}{}
|
||||||
|
req = &logical.Request{
|
||||||
|
Operation: logical.ReadOperation,
|
||||||
|
Path: "static-creds/" + tc.path,
|
||||||
|
Storage: config.StorageView,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkPassword := resp.Data["password"].(string)
|
||||||
|
if newPassword == checkPassword {
|
||||||
|
t.Fatalf("expected passwords to differ, got (%s)", checkPassword)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanity check to make sure we don't allow an attempt of rotating credentials
|
// Sanity check to make sure we don't allow an attempt of rotating credentials
|
||||||
// for non-static accounts, which doesn't make sense anyway, but doesn't hurt to
|
// for non-static accounts, which doesn't make sense anyway, but doesn't hurt to
|
||||||
// verify we return an error
|
// verify we return an error
|
||||||
func TestBackend_StaticRole_Rotate_NonStaticError(t *testing.T) {
|
func TestBackend_StaticRole_Rotation_NonStaticError(t *testing.T) {
|
||||||
cluster, sys := getCluster(t)
|
cluster, sys := getCluster(t)
|
||||||
defer cluster.Cleanup()
|
defer cluster.Cleanup()
|
||||||
|
|
||||||
@@ -288,7 +347,7 @@ func TestBackend_StaticRole_Rotate_NonStaticError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBackend_StaticRole_Revoke_user(t *testing.T) {
|
func TestBackend_StaticRole_Rotation_Revoke_user(t *testing.T) {
|
||||||
cluster, sys := getCluster(t)
|
cluster, sys := getCluster(t)
|
||||||
defer cluster.Cleanup()
|
defer cluster.Cleanup()
|
||||||
|
|
||||||
@@ -466,7 +525,7 @@ func verifyPgConn(t *testing.T, username, password, connURL string) {
|
|||||||
// WAL testing
|
// WAL testing
|
||||||
//
|
//
|
||||||
// First scenario, WAL contains a role name that does not exist.
|
// First scenario, WAL contains a role name that does not exist.
|
||||||
func TestBackend_Static_QueueWAL_discard_role_not_found(t *testing.T) {
|
func TestBackend_StaticRole_Rotation_QueueWAL_discard_role_not_found(t *testing.T) {
|
||||||
cluster, sys := getCluster(t)
|
cluster, sys := getCluster(t)
|
||||||
defer cluster.Cleanup()
|
defer cluster.Cleanup()
|
||||||
|
|
||||||
@@ -507,7 +566,7 @@ func TestBackend_Static_QueueWAL_discard_role_not_found(t *testing.T) {
|
|||||||
|
|
||||||
// Second scenario, WAL contains a role name that does exist, but the role's
|
// Second scenario, WAL contains a role name that does exist, but the role's
|
||||||
// LastVaultRotation is greater than the WAL has
|
// LastVaultRotation is greater than the WAL has
|
||||||
func TestBackend_Static_QueueWAL_discard_role_newer_rotation_date(t *testing.T) {
|
func TestBackend_StaticRole_Rotation_QueueWAL_discard_role_newer_rotation_date(t *testing.T) {
|
||||||
cluster, sys := getCluster(t)
|
cluster, sys := getCluster(t)
|
||||||
defer cluster.Cleanup()
|
defer cluster.Cleanup()
|
||||||
|
|
||||||
@@ -695,7 +754,7 @@ func assertWALCount(t *testing.T, s logical.Storage, expected int, key string) {
|
|||||||
|
|
||||||
type userCreator func(t *testing.T, username, password string)
|
type userCreator func(t *testing.T, username, password string)
|
||||||
|
|
||||||
func TestBackend_StaticRole_Rotations_PostgreSQL(t *testing.T) {
|
func TestBackend_StaticRole_Rotation_PostgreSQL(t *testing.T) {
|
||||||
cleanup, connURL := postgreshelper.PrepareTestContainer(t, "13.4-buster")
|
cleanup, connURL := postgreshelper.PrepareTestContainer(t, "13.4-buster")
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
uc := userCreator(func(t *testing.T, username, password string) {
|
uc := userCreator(func(t *testing.T, username, password string) {
|
||||||
@@ -707,7 +766,7 @@ func TestBackend_StaticRole_Rotations_PostgreSQL(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBackend_StaticRole_Rotations_MongoDB(t *testing.T) {
|
func TestBackend_StaticRole_Rotation_MongoDB(t *testing.T) {
|
||||||
cleanup, connURL := mongodb.PrepareTestContainerWithDatabase(t, "5.0.10", "vaulttestdb")
|
cleanup, connURL := mongodb.PrepareTestContainerWithDatabase(t, "5.0.10", "vaulttestdb")
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -720,7 +779,7 @@ func TestBackend_StaticRole_Rotations_MongoDB(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBackend_StaticRole_Rotations_MongoDBAtlas(t *testing.T) {
|
func TestBackend_StaticRole_Rotation_MongoDBAtlas(t *testing.T) {
|
||||||
// To get the project ID, connect to cloud.mongodb.com, go to the vault-test project and
|
// To get the project ID, connect to cloud.mongodb.com, go to the vault-test project and
|
||||||
// look at Project Settings.
|
// look at Project Settings.
|
||||||
projID := os.Getenv("VAULT_MONGODBATLAS_PROJECT_ID")
|
projID := os.Getenv("VAULT_MONGODBATLAS_PROJECT_ID")
|
||||||
@@ -944,7 +1003,7 @@ type createUserCommand struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Demonstrates a bug fix for the credential rotation not releasing locks
|
// Demonstrates a bug fix for the credential rotation not releasing locks
|
||||||
func TestBackend_StaticRole_LockRegression(t *testing.T) {
|
func TestBackend_StaticRole_Rotation_LockRegression(t *testing.T) {
|
||||||
cluster, sys := getCluster(t)
|
cluster, sys := getCluster(t)
|
||||||
defer cluster.Cleanup()
|
defer cluster.Cleanup()
|
||||||
|
|
||||||
@@ -1023,7 +1082,7 @@ func TestBackend_StaticRole_LockRegression(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBackend_StaticRole_Rotate_Invalid_Role(t *testing.T) {
|
func TestBackend_StaticRole_Rotation_Invalid_Role(t *testing.T) {
|
||||||
cluster, sys := getCluster(t)
|
cluster, sys := getCluster(t)
|
||||||
defer cluster.Cleanup()
|
defer cluster.Cleanup()
|
||||||
|
|
||||||
@@ -1160,10 +1219,18 @@ func TestRollsPasswordForwardsUsingWAL(t *testing.T) {
|
|||||||
|
|
||||||
func TestStoredWALsCorrectlyProcessed(t *testing.T) {
|
func TestStoredWALsCorrectlyProcessed(t *testing.T) {
|
||||||
const walNewPassword = "new-password-from-wal"
|
const walNewPassword = "new-password-from-wal"
|
||||||
|
|
||||||
|
rotationPeriodData := map[string]interface{}{
|
||||||
|
"username": "hashicorp",
|
||||||
|
"db_name": "mockv5",
|
||||||
|
"rotation_period": "86400s",
|
||||||
|
}
|
||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
name string
|
name string
|
||||||
shouldRotate bool
|
shouldRotate bool
|
||||||
wal *setCredentialsWAL
|
wal *setCredentialsWAL
|
||||||
|
data map[string]interface{}
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"WAL is kept and used for roll forward",
|
"WAL is kept and used for roll forward",
|
||||||
@@ -1174,6 +1241,7 @@ func TestStoredWALsCorrectlyProcessed(t *testing.T) {
|
|||||||
NewPassword: walNewPassword,
|
NewPassword: walNewPassword,
|
||||||
LastVaultRotation: time.Now().Add(time.Hour),
|
LastVaultRotation: time.Now().Add(time.Hour),
|
||||||
},
|
},
|
||||||
|
rotationPeriodData,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"zero-time WAL is discarded on load",
|
"zero-time WAL is discarded on load",
|
||||||
@@ -1184,9 +1252,10 @@ func TestStoredWALsCorrectlyProcessed(t *testing.T) {
|
|||||||
NewPassword: walNewPassword,
|
NewPassword: walNewPassword,
|
||||||
LastVaultRotation: time.Time{},
|
LastVaultRotation: time.Time{},
|
||||||
},
|
},
|
||||||
|
rotationPeriodData,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"empty-password WAL is kept but a new password is generated",
|
"rotation_period empty-password WAL is kept but a new password is generated",
|
||||||
true,
|
true,
|
||||||
&setCredentialsWAL{
|
&setCredentialsWAL{
|
||||||
RoleName: "hashicorp",
|
RoleName: "hashicorp",
|
||||||
@@ -1194,6 +1263,22 @@ func TestStoredWALsCorrectlyProcessed(t *testing.T) {
|
|||||||
NewPassword: "",
|
NewPassword: "",
|
||||||
LastVaultRotation: time.Now().Add(time.Hour),
|
LastVaultRotation: time.Now().Add(time.Hour),
|
||||||
},
|
},
|
||||||
|
rotationPeriodData,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rotation_schedule empty-password WAL is kept but a new password is generated",
|
||||||
|
true,
|
||||||
|
&setCredentialsWAL{
|
||||||
|
RoleName: "hashicorp",
|
||||||
|
Username: "hashicorp",
|
||||||
|
NewPassword: "",
|
||||||
|
LastVaultRotation: time.Now().Add(time.Hour),
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"username": "hashicorp",
|
||||||
|
"db_name": "mockv5",
|
||||||
|
"rotation_schedule": "*/10 * * * * *",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
@@ -1209,7 +1294,7 @@ func TestStoredWALsCorrectlyProcessed(t *testing.T) {
|
|||||||
}
|
}
|
||||||
b.credRotationQueue = queue.New()
|
b.credRotationQueue = queue.New()
|
||||||
configureDBMount(t, config.StorageView)
|
configureDBMount(t, config.StorageView)
|
||||||
createRole(t, b, config.StorageView, mockDB, "hashicorp")
|
createRoleWithData(t, b, config.StorageView, mockDB, tc.wal.RoleName, tc.data)
|
||||||
role, err := b.StaticRole(ctx, config.StorageView, "hashicorp")
|
role, err := b.StaticRole(ctx, config.StorageView, "hashicorp")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -1247,6 +1332,7 @@ func TestStoredWALsCorrectlyProcessed(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nextRotationTime := role.StaticAccount.NextRotationTime()
|
||||||
if tc.shouldRotate {
|
if tc.shouldRotate {
|
||||||
if tc.wal.NewPassword != "" {
|
if tc.wal.NewPassword != "" {
|
||||||
// Should use WAL's new_password field
|
// Should use WAL's new_password field
|
||||||
@@ -1262,11 +1348,11 @@ func TestStoredWALsCorrectlyProcessed(t *testing.T) {
|
|||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Ensure the role was not promoted for early rotation
|
||||||
|
assertPriorityUnchanged(t, item.Priority, nextRotationTime)
|
||||||
} else {
|
} else {
|
||||||
// Ensure the role was not promoted for early rotation
|
// Ensure the role was not promoted for early rotation
|
||||||
if item.Priority < time.Now().Add(time.Hour).Unix() {
|
assertPriorityUnchanged(t, item.Priority, nextRotationTime)
|
||||||
t.Fatal("priority should be for about a week away, but was", item.Priority)
|
|
||||||
}
|
|
||||||
if role.StaticAccount.Password != initialPassword {
|
if role.StaticAccount.Password != initialPassword {
|
||||||
t.Fatal("password should not have been rotated yet")
|
t.Fatal("password should not have been rotated yet")
|
||||||
}
|
}
|
||||||
@@ -1447,3 +1533,12 @@ func newBoolPtr(b bool) *bool {
|
|||||||
v := b
|
v := b
|
||||||
return &v
|
return &v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// assertPriorityUnchanged is a helper to verify that the priority is the
|
||||||
|
// expected value for a given rotation time
|
||||||
|
func assertPriorityUnchanged(t *testing.T, priority int64, nextRotationTime time.Time) {
|
||||||
|
t.Helper()
|
||||||
|
if priority != nextRotationTime.Unix() {
|
||||||
|
t.Fatalf("expected next rotation at %s, but got %s", nextRotationTime, time.Unix(priority, 0).String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
4
changelog/22484.txt
Normal file
4
changelog/22484.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
```release-note:feature
|
||||||
|
**Database Static Role Advanced TTL Management**: Adds the ability to rotate
|
||||||
|
static roles on a defined schedule.
|
||||||
|
```
|
||||||
1
go.mod
1
go.mod
@@ -191,6 +191,7 @@ require (
|
|||||||
github.com/prometheus/client_golang v1.14.0
|
github.com/prometheus/client_golang v1.14.0
|
||||||
github.com/prometheus/common v0.37.0
|
github.com/prometheus/common v0.37.0
|
||||||
github.com/rboyer/safeio v0.2.1
|
github.com/rboyer/safeio v0.2.1
|
||||||
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/ryanuber/columnize v2.1.0+incompatible
|
github.com/ryanuber/columnize v2.1.0+incompatible
|
||||||
github.com/ryanuber/go-glob v1.0.0
|
github.com/ryanuber/go-glob v1.0.0
|
||||||
github.com/sasha-s/go-deadlock v0.2.0
|
github.com/sasha-s/go-deadlock v0.2.0
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -2575,6 +2575,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qq
|
|||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/renier/xmlrpc v0.0.0-20170708154548-ce4a1a486c03 h1:Wdi9nwnhFNAlseAOekn6B5G/+GMtks9UKbvRU/CMM/o=
|
github.com/renier/xmlrpc v0.0.0-20170708154548-ce4a1a486c03 h1:Wdi9nwnhFNAlseAOekn6B5G/+GMtks9UKbvRU/CMM/o=
|
||||||
github.com/renier/xmlrpc v0.0.0-20170708154548-ce4a1a486c03/go.mod h1:gRAiPF5C5Nd0eyyRdqIu9qTiFSoZzpTq727b5B8fkkU=
|
github.com/renier/xmlrpc v0.0.0-20170708154548-ce4a1a486c03/go.mod h1:gRAiPF5C5Nd0eyyRdqIu9qTiFSoZzpTq727b5B8fkkU=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||||
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
|
|||||||
Reference in New Issue
Block a user