From 9e38a88883597dd69c5d31f50b0fc3d9237200f9 Mon Sep 17 00:00:00 2001 From: vinay-gopalan <86625824+vinay-gopalan@users.noreply.github.com> Date: Tue, 11 Feb 2025 12:09:26 -0800 Subject: [PATCH] Add automated root rotation support to DB Secrets (#29557) --- builtin/credential/aws/path_config_client.go | 15 +- builtin/logical/aws/path_config_root.go | 15 +- builtin/logical/database/backend.go | 22 ++ builtin/logical/database/backend_test.go | 20 ++ .../database/path_config_connection.go | 54 ++++- .../database/path_rotate_credentials.go | 220 +++++++++--------- changelog/29497.txt | 12 +- changelog/29557.txt | 5 + sdk/helper/automatedrotationutil/fields.go | 4 +- .../automatedrotationutil/fields_test.go | 10 + sdk/rotation/rotation_job.go | 5 + 11 files changed, 258 insertions(+), 124 deletions(-) create mode 100644 changelog/29557.txt diff --git a/builtin/credential/aws/path_config_client.go b/builtin/credential/aws/path_config_client.go index d6125bfa3d..fdaaf5c0a3 100644 --- a/builtin/credential/aws/path_config_client.go +++ b/builtin/credential/aws/path_config_client.go @@ -6,6 +6,7 @@ package awsauth import ( "context" "errors" + "fmt" "net/http" "net/textproto" "net/url" @@ -394,7 +395,7 @@ func (b *backend) pathConfigClientCreateUpdate(ctx context.Context, req *logical var performedRotationManagerOpern string if configEntry.ShouldDeregisterRotationJob() { - performedRotationManagerOpern = "deregistration" + performedRotationManagerOpern = rotation.PerformedDeregistration // Disable Automated Rotation and Deregister credentials if required deregisterReq := &rotation.RotationJobDeregisterRequest{ MountPoint: req.MountPoint, @@ -406,7 +407,7 @@ func (b *backend) pathConfigClientCreateUpdate(ctx context.Context, req *logical return logical.ErrorResponse("error deregistering rotation job: %s", err), nil } } else if configEntry.ShouldRegisterRotationJob() { - performedRotationManagerOpern = "registration" + performedRotationManagerOpern = rotation.PerformedRegistration // Register the rotation job if it's required. cfgReq := &rotation.RotationJobConfigureRequest{ MountPoint: req.MountPoint, @@ -434,11 +435,15 @@ func (b *backend) pathConfigClientCreateUpdate(ctx context.Context, req *logical if changedCreds || changedOtherConfig || req.Operation == logical.CreateOperation { if err := req.Storage.Put(ctx, entry); err != nil { + b.Logger().Error("write to storage failed but the rotation manager still succeeded.", + "operation", performedRotationManagerOpern, "mount", req.MountPoint, "path", req.Path) + + wrappedError := err if performedRotationManagerOpern != "" { - b.Logger().Error("write to storage failed but the rotation manager still succeeded.", - "operation", performedRotationManagerOpern, "mount", req.MountPoint, "path", req.Path) + wrappedError = fmt.Errorf("write to storage failed but the rotation manager still succeeded; "+ + "operation=%s, mount=%s, path=%s, storageError=%s", performedRotationManagerOpern, req.MountPoint, req.Path, err) } - return nil, err + return nil, wrappedError } } diff --git a/builtin/logical/aws/path_config_root.go b/builtin/logical/aws/path_config_root.go index c2d57936ea..6ac9693a4c 100644 --- a/builtin/logical/aws/path_config_root.go +++ b/builtin/logical/aws/path_config_root.go @@ -6,6 +6,7 @@ package aws import ( "context" "errors" + "fmt" "github.com/aws/aws-sdk-go/aws" "github.com/hashicorp/vault/sdk/framework" @@ -252,7 +253,7 @@ func (b *backend) pathConfigRootWrite(ctx context.Context, req *logical.Request, var performedRotationManagerOpern string if rc.ShouldDeregisterRotationJob() { - performedRotationManagerOpern = "deregistration" + performedRotationManagerOpern = rotation.PerformedDeregistration // Disable Automated Rotation and Deregister credentials if required deregisterReq := &rotation.RotationJobDeregisterRequest{ MountPoint: req.MountPoint, @@ -264,7 +265,7 @@ func (b *backend) pathConfigRootWrite(ctx context.Context, req *logical.Request, return logical.ErrorResponse("error deregistering rotation job: %s", err), nil } } else if rc.ShouldRegisterRotationJob() { - performedRotationManagerOpern = "registration" + performedRotationManagerOpern = rotation.PerformedRegistration // Register the rotation job if it's required. cfgReq := &rotation.RotationJobConfigureRequest{ MountPoint: req.MountPoint, @@ -282,11 +283,15 @@ func (b *backend) pathConfigRootWrite(ctx context.Context, req *logical.Request, // Save the config if err := putConfigToStorage(ctx, req, rc); err != nil { + b.Logger().Error("write to storage failed but the rotation manager still succeeded.", + "operation", performedRotationManagerOpern, "mount", req.MountPoint, "path", req.Path) + + wrappedError := err if performedRotationManagerOpern != "" { - b.Logger().Error("write to storage failed but the rotation manager still succeeded.", - "operation", performedRotationManagerOpern, "mount", req.MountPoint, "path", req.Path) + wrappedError = fmt.Errorf("write to storage failed but the rotation manager still succeeded; "+ + "operation=%s, mount=%s, path=%s, storageError=%s", performedRotationManagerOpern, req.MountPoint, req.Path, err) } - return nil, err + return nil, wrappedError } // clear possible cached IAM / STS clients after successfully updating diff --git a/builtin/logical/database/backend.go b/builtin/logical/database/backend.go index 8237ce9b53..f79d812728 100644 --- a/builtin/logical/database/backend.go +++ b/builtin/logical/database/backend.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "net/rpc" + "regexp" "strconv" "strings" "sync" @@ -39,6 +40,8 @@ const ( minRootCredRollbackAge = 1 * time.Minute ) +var databaseConfigNameFromRotationIDRegex = regexp.MustCompile("^.+/config/(.+$)") + type dbPluginInstance struct { sync.RWMutex database databaseVersionWrapper @@ -127,6 +130,14 @@ func Backend(conf *logical.BackendConfig) *databaseBackend { WALRollback: b.walRollback, WALRollbackMinAge: minRootCredRollbackAge, BackendType: logical.TypeLogical, + RotateCredential: func(ctx context.Context, request *logical.Request) error { + name, err := b.getDatabaseConfigNameFromRotationID(request.RotationID) + if err != nil { + return err + } + _, err = b.rotateRootCredentials(ctx, request, name) + return err + }, } b.logger = conf.Logger @@ -483,6 +494,17 @@ func (b *databaseBackend) dbEvent(ctx context.Context, } } +func (b *databaseBackend) getDatabaseConfigNameFromRotationID(path string) (string, error) { + if !databaseConfigNameFromRotationIDRegex.MatchString(path) { + return "", fmt.Errorf("no name found from rotation ID") + } + res := databaseConfigNameFromRotationIDRegex.FindStringSubmatch(path) + if len(res) != 2 { + return "", fmt.Errorf("unexpected number of matches (%d) for name in rotation ID", len(res)) + } + return res[1], nil +} + const backendHelp = ` The database backend supports using many different databases as secret backends, including but not limited to: diff --git a/builtin/logical/database/backend_test.go b/builtin/logical/database/backend_test.go index ed39f2221b..24f71c8d39 100644 --- a/builtin/logical/database/backend_test.go +++ b/builtin/logical/database/backend_test.go @@ -213,6 +213,10 @@ func TestBackend_config_connection(t *testing.T) { "plugin_version": "", "verify_connection": false, "skip_static_role_import_rotation": false, + "rotation_schedule": "", + "rotation_period": 0, + "rotation_window": 0, + "disable_automated_rotation": false, } configReq.Operation = logical.ReadOperation resp, err = b.HandleRequest(namespace.RootContext(nil), configReq) @@ -221,6 +225,7 @@ func TestBackend_config_connection(t *testing.T) { } delete(resp.Data["connection_details"].(map[string]interface{}), "name") + delete(resp.Data, "AutomatedRotationParams") if !reflect.DeepEqual(expected, resp.Data) { t.Fatalf("bad: expected:%#v\nactual:%#v\n", expected, resp.Data) } @@ -269,6 +274,10 @@ func TestBackend_config_connection(t *testing.T) { "plugin_version": "", "verify_connection": false, "skip_static_role_import_rotation": false, + "rotation_schedule": "", + "rotation_period": 0, + "rotation_window": 0, + "disable_automated_rotation": false, } configReq.Operation = logical.ReadOperation resp, err = b.HandleRequest(namespace.RootContext(nil), configReq) @@ -277,6 +286,7 @@ func TestBackend_config_connection(t *testing.T) { } delete(resp.Data["connection_details"].(map[string]interface{}), "name") + delete(resp.Data, "AutomatedRotationParams") if !reflect.DeepEqual(expected, resp.Data) { t.Fatalf("bad: expected:%#v\nactual:%#v\n", expected, resp.Data) } @@ -314,6 +324,10 @@ func TestBackend_config_connection(t *testing.T) { "plugin_version": "", "verify_connection": false, "skip_static_role_import_rotation": false, + "rotation_schedule": "", + "rotation_period": 0, + "rotation_window": 0, + "disable_automated_rotation": false, } configReq.Operation = logical.ReadOperation resp, err = b.HandleRequest(namespace.RootContext(nil), configReq) @@ -322,6 +336,7 @@ func TestBackend_config_connection(t *testing.T) { } delete(resp.Data["connection_details"].(map[string]interface{}), "name") + delete(resp.Data, "AutomatedRotationParams") if !reflect.DeepEqual(expected, resp.Data) { t.Fatalf("bad: expected:%#v\nactual:%#v\n", expected, resp.Data) } @@ -773,6 +788,10 @@ func TestBackend_connectionCrud(t *testing.T) { "plugin_version": "", "verify_connection": false, "skip_static_role_import_rotation": false, + "rotation_schedule": "", + "rotation_period": json.Number("0"), + "rotation_window": json.Number("0"), + "disable_automated_rotation": false, } resp, err = client.Read("database/config/plugin-test") if err != nil { @@ -780,6 +799,7 @@ func TestBackend_connectionCrud(t *testing.T) { } delete(resp.Data["connection_details"].(map[string]interface{}), "name") + delete(resp.Data, "AutomatedRotationParams") if diff := deep.Equal(resp.Data, expected); diff != nil { t.Fatal(strings.Join(diff, "\n")) } diff --git a/builtin/logical/database/path_config_connection.go b/builtin/logical/database/path_config_connection.go index 283d8f054a..cc3faf3e36 100644 --- a/builtin/logical/database/path_config_connection.go +++ b/builtin/logical/database/path_config_connection.go @@ -17,9 +17,11 @@ import ( "github.com/hashicorp/vault/helper/versions" v5 "github.com/hashicorp/vault/sdk/database/dbplugin/v5" "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/helper/automatedrotationutil" "github.com/hashicorp/vault/sdk/helper/consts" "github.com/hashicorp/vault/sdk/helper/pluginutil" "github.com/hashicorp/vault/sdk/logical" + "github.com/hashicorp/vault/sdk/rotation" ) var ( @@ -49,6 +51,8 @@ type DatabaseConfig struct { // role-level by the role's skip_import_rotation field. The default is // false. Enterprise only. SkipStaticRoleImportRotation bool `json:"skip_static_role_import_rotation" structs:"skip_static_role_import_rotation" mapstructure:"skip_static_role_import_rotation"` + + automatedrotationutil.AutomatedRotationParams } // ConnectionDetails represents the DatabaseConfig.ConnectionDetails map as a @@ -263,6 +267,7 @@ func pathConfigurePluginConnection(b *databaseBackend) *framework.Path { }, } AddConnectionFieldsEnt(fields) + automatedrotationutil.AddAutomatedRotationFields(fields) return &framework.Path{ Pattern: fmt.Sprintf("config/%s", framework.GenericNameRegex("name")), @@ -409,6 +414,7 @@ func (b *databaseBackend) connectionReadHandler() framework.OperationFunc { } resp.Data = structs.New(config).Map() + config.PopulateAutomatedRotationData(resp.Data) return resp, nil } } @@ -500,6 +506,10 @@ func (b *databaseBackend) connectionWriteHandler() framework.OperationFunc { config.SkipStaticRoleImportRotation = skipImportRotationRaw.(bool) } + if err := config.ParseAutomatedRotationFields(data); err != nil { + return logical.ErrorResponse(err.Error()), nil + } + // Remove these entries from the data before we store it keyed under // ConnectionDetails. delete(data.Raw, "name") @@ -510,6 +520,10 @@ func (b *databaseBackend) connectionWriteHandler() framework.OperationFunc { delete(data.Raw, "root_rotation_statements") delete(data.Raw, "password_policy") delete(data.Raw, "skip_static_role_import_rotation") + delete(data.Raw, "rotation_schedule") + delete(data.Raw, "rotation_window") + delete(data.Raw, "rotation_ttl") + delete(data.Raw, "disable_automated_rotation") id, err := uuid.GenerateUUID() if err != nil { @@ -560,6 +574,36 @@ func (b *databaseBackend) connectionWriteHandler() framework.OperationFunc { oldConn.Close() } + var performedRotationManagerOpern string + if config.ShouldDeregisterRotationJob() { + performedRotationManagerOpern = rotation.PerformedDeregistration + // Disable Automated Rotation and Deregister credentials if required + deregisterReq := &rotation.RotationJobDeregisterRequest{ + MountPoint: req.MountPoint, + ReqPath: req.Path, + } + + b.Logger().Debug("Deregistering rotation job", "mount", req.MountPoint+req.Path) + if err := b.System().DeregisterRotationJob(ctx, deregisterReq); err != nil { + return logical.ErrorResponse("error deregistering rotation job: %s", err), nil + } + } else if config.ShouldRegisterRotationJob() { + performedRotationManagerOpern = rotation.PerformedRegistration + // Register the rotation job if it's required. + cfgReq := &rotation.RotationJobConfigureRequest{ + MountPoint: req.MountPoint, + ReqPath: req.Path, + RotationSchedule: config.RotationSchedule, + RotationWindow: config.RotationWindow, + RotationPeriod: config.RotationPeriod, + } + + b.Logger().Debug("Registering rotation job", "mount", req.MountPoint+req.Path) + if _, err = b.System().RegisterRotationJob(ctx, cfgReq); err != nil { + return logical.ErrorResponse("error registering rotation job: %s", err), nil + } + } + // 1.12.0 and 1.12.1 stored builtin plugins in storage, but 1.12.2 reverted // that, so clean up any pre-existing stored builtin versions on write. if versions.IsBuiltinVersion(config.PluginVersion) { @@ -567,7 +611,15 @@ func (b *databaseBackend) connectionWriteHandler() framework.OperationFunc { } err = storeConfig(ctx, req.Storage, name, config) if err != nil { - return nil, err + b.Logger().Error("write to storage failed but the rotation manager still succeeded.", + "operation", performedRotationManagerOpern, "mount", req.MountPoint, "path", req.Path) + + wrappedError := err + if performedRotationManagerOpern != "" { + wrappedError = fmt.Errorf("write to storage failed but the rotation manager still succeeded; "+ + "operation=%s, mount=%s, path=%s, storageError=%s", performedRotationManagerOpern, req.MountPoint, req.Path, err) + } + return nil, wrappedError } resp := &logical.Response{} diff --git a/builtin/logical/database/path_rotate_credentials.go b/builtin/logical/database/path_rotate_credentials.go index d5faf3b3fe..43d131333c 100644 --- a/builtin/logical/database/path_rotate_credentials.go +++ b/builtin/logical/database/path_rotate_credentials.go @@ -77,117 +77,121 @@ func pathRotateRootCredentials(b *databaseBackend) []*framework.Path { func (b *databaseBackend) pathRotateRootCredentialsUpdate() framework.OperationFunc { return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (resp *logical.Response, err error) { name := data.Get("name").(string) - modified := false - defer func() { - if err == nil { - b.dbEvent(ctx, "rotate-root", req.Path, name, modified) - } else { - b.dbEvent(ctx, "rotate-root-fail", req.Path, name, modified) - } - }() - - if name == "" { - return logical.ErrorResponse(respErrEmptyName), nil - } - - config, err := b.DatabaseConfig(ctx, req.Storage, name) - if err != nil { - return nil, err - } - - rootUsername, ok := config.ConnectionDetails["username"].(string) - if !ok || rootUsername == "" { - return nil, fmt.Errorf("unable to rotate root credentials: no username in configuration") - } - - rootPassword, ok := config.ConnectionDetails["password"].(string) - if !ok || rootPassword == "" { - return nil, fmt.Errorf("unable to rotate root credentials: no password in configuration") - } - - dbi, err := b.GetConnection(ctx, req.Storage, name) - if err != nil { - return nil, err - } - - // Take the write lock on the instance - dbi.Lock() - defer func() { - dbi.Unlock() - // Even on error, still remove the connection - b.ClearConnectionId(name, dbi.id) - }() - defer func() { - // Close the plugin - dbi.closed = true - if err := dbi.database.Close(); err != nil { - b.Logger().Error("error closing the database plugin connection", "err", err) - } - }() - - generator, err := newPasswordGenerator(nil) - if err != nil { - return nil, fmt.Errorf("failed to construct credential generator: %s", err) - } - generator.PasswordPolicy = config.PasswordPolicy - - // Generate new credentials - oldPassword := config.ConnectionDetails["password"].(string) - newPassword, err := generator.generate(ctx, b, dbi.database) - if err != nil { - b.CloseIfShutdown(dbi, err) - return nil, fmt.Errorf("failed to generate password: %s", err) - } - config.ConnectionDetails["password"] = newPassword - - // Write a WAL entry - walID, err := framework.PutWAL(ctx, req.Storage, rotateRootWALKey, &rotateRootCredentialsWAL{ - ConnectionName: name, - UserName: rootUsername, - OldPassword: oldPassword, - NewPassword: newPassword, - }) - if err != nil { - return nil, err - } - - updateReq := v5.UpdateUserRequest{ - Username: rootUsername, - CredentialType: v5.CredentialTypePassword, - Password: &v5.ChangePassword{ - NewPassword: newPassword, - Statements: v5.Statements{ - Commands: config.RootCredentialsRotateStatements, - }, - }, - } - newConfigDetails, err := dbi.database.UpdateUser(ctx, updateReq, true) - if err != nil { - return nil, fmt.Errorf("failed to update user: %w", err) - } - if newConfigDetails != nil { - config.ConnectionDetails = newConfigDetails - } - modified = true - - // 1.12.0 and 1.12.1 stored builtin plugins in storage, but 1.12.2 reverted - // that, so clean up any pre-existing stored builtin versions on write. - if versions.IsBuiltinVersion(config.PluginVersion) { - config.PluginVersion = "" - } - err = storeConfig(ctx, req.Storage, name, config) - if err != nil { - return nil, err - } - - err = framework.DeleteWAL(ctx, req.Storage, walID) - if err != nil { - b.Logger().Warn("unable to delete WAL", "error", err, "WAL ID", walID) - } - return nil, nil + return b.rotateRootCredentials(ctx, req, name) } } +func (b *databaseBackend) rotateRootCredentials(ctx context.Context, req *logical.Request, name string) (resp *logical.Response, err error) { + if name == "" { + return logical.ErrorResponse(respErrEmptyName), nil + } + + modified := false + defer func() { + if err == nil { + b.dbEvent(ctx, "rotate-root", req.Path, name, modified) + } else { + b.dbEvent(ctx, "rotate-root-fail", req.Path, name, modified) + } + }() + + config, err := b.DatabaseConfig(ctx, req.Storage, name) + if err != nil { + return nil, err + } + + rootUsername, ok := config.ConnectionDetails["username"].(string) + if !ok || rootUsername == "" { + return nil, fmt.Errorf("unable to rotate root credentials: no username in configuration") + } + + rootPassword, ok := config.ConnectionDetails["password"].(string) + if !ok || rootPassword == "" { + return nil, fmt.Errorf("unable to rotate root credentials: no password in configuration") + } + + dbi, err := b.GetConnection(ctx, req.Storage, name) + if err != nil { + return nil, err + } + + // Take the write lock on the instance + dbi.Lock() + defer func() { + dbi.Unlock() + // Even on error, still remove the connection + b.ClearConnectionId(name, dbi.id) + }() + defer func() { + // Close the plugin + dbi.closed = true + if err := dbi.database.Close(); err != nil { + b.Logger().Error("error closing the database plugin connection", "err", err) + } + }() + + generator, err := newPasswordGenerator(nil) + if err != nil { + return nil, fmt.Errorf("failed to construct credential generator: %s", err) + } + generator.PasswordPolicy = config.PasswordPolicy + + // Generate new credentials + oldPassword := config.ConnectionDetails["password"].(string) + newPassword, err := generator.generate(ctx, b, dbi.database) + if err != nil { + b.CloseIfShutdown(dbi, err) + return nil, fmt.Errorf("failed to generate password: %s", err) + } + config.ConnectionDetails["password"] = newPassword + + // Write a WAL entry + walID, err := framework.PutWAL(ctx, req.Storage, rotateRootWALKey, &rotateRootCredentialsWAL{ + ConnectionName: name, + UserName: rootUsername, + OldPassword: oldPassword, + NewPassword: newPassword, + }) + if err != nil { + return nil, err + } + + updateReq := v5.UpdateUserRequest{ + Username: rootUsername, + CredentialType: v5.CredentialTypePassword, + Password: &v5.ChangePassword{ + NewPassword: newPassword, + Statements: v5.Statements{ + Commands: config.RootCredentialsRotateStatements, + }, + }, + } + newConfigDetails, err := dbi.database.UpdateUser(ctx, updateReq, true) + if err != nil { + return nil, fmt.Errorf("failed to update user: %w", err) + } + if newConfigDetails != nil { + config.ConnectionDetails = newConfigDetails + } + modified = true + + // 1.12.0 and 1.12.1 stored builtin plugins in storage, but 1.12.2 reverted + // that, so clean up any pre-existing stored builtin versions on write. + if versions.IsBuiltinVersion(config.PluginVersion) { + config.PluginVersion = "" + } + err = storeConfig(ctx, req.Storage, name, config) + if err != nil { + return nil, err + } + + err = framework.DeleteWAL(ctx, req.Storage, walID) + if err != nil { + b.Logger().Warn("unable to delete WAL", "error", err, "WAL ID", walID) + } + return nil, nil +} + func (b *databaseBackend) pathRotateRoleCredentialsUpdate() framework.OperationFunc { return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (_ *logical.Response, err error) { name := data.Get("name").(string) diff --git a/changelog/29497.txt b/changelog/29497.txt index d6f2a321e8..52d91535f3 100644 --- a/changelog/29497.txt +++ b/changelog/29497.txt @@ -1,5 +1,11 @@ ```release-note:feature -**Automated Root Rotation**: Adds Automated Root Rotation capabilities to the AWS Auth, AWS Secrets -and DB Secrets plugins. This allows plugin users to automate their root credential rotations based -on configurable schedules/periods via the Rotation Manager. Note: Enterprise only. +**Automated Root Rotation**: Adds Automated Root Rotation capabilities to the AWS Auth and AWS Secrets +plugins. This allows plugin users to automate their root credential rotations based on configurable +schedules/periods via the Rotation Manager. Note: Enterprise only. +``` +```release-note:change +secrets/aws: The AWS Secrets engine now persists entries to storage between writes. This enables users +to not have to pass every required field on each write and to make individual updates as necessary. +Note: in order to zero out a value that is previously configured, users must now explicitly set the +field to its zero value on an update. ``` \ No newline at end of file diff --git a/changelog/29557.txt b/changelog/29557.txt new file mode 100644 index 0000000000..70fa0be32a --- /dev/null +++ b/changelog/29557.txt @@ -0,0 +1,5 @@ +```release-note:feature +**Automated Root Rotation**: Adds Automated Root Rotation capabilities to the DB Secrets plugin. +This allows plugin users to automate their root credential rotations based on configurable +schedules/periods via the Rotation Manager. Note: Enterprise only. +``` \ No newline at end of file diff --git a/sdk/helper/automatedrotationutil/fields.go b/sdk/helper/automatedrotationutil/fields.go index 8bd1fce8cd..4874772ecf 100644 --- a/sdk/helper/automatedrotationutil/fields.go +++ b/sdk/helper/automatedrotationutil/fields.go @@ -63,8 +63,8 @@ func (p *AutomatedRotationParams) ParseAutomatedRotationFields(d *framework.Fiel p.RotationPeriod = rotationPeriodRaw.(int) } - if (scheduleOk && !windowOk) || (windowOk && !scheduleOk) { - return fmt.Errorf("must include both schedule and window") + if windowOk && !scheduleOk { + return fmt.Errorf("cannot use rotation_window without rotation_schedule") } p.DisableAutomatedRotation = d.Get("disable_automated_rotation").(bool) diff --git a/sdk/helper/automatedrotationutil/fields_test.go b/sdk/helper/automatedrotationutil/fields_test.go index 8e05ef9b80..55ce29feb5 100644 --- a/sdk/helper/automatedrotationutil/fields_test.go +++ b/sdk/helper/automatedrotationutil/fields_test.go @@ -86,6 +86,16 @@ func TestParseAutomatedRotationFields(t *testing.T) { }, expectedError: "rotation_window does not apply to period", }, + { + name: "window-without-schedule", + data: &framework.FieldData{ + Raw: map[string]interface{}{ + "rotation_window": 60, + }, + Schema: schemaMap, + }, + expectedError: "cannot use rotation_window without rotation_schedule", + }, } for _, tt := range tests { diff --git a/sdk/rotation/rotation_job.go b/sdk/rotation/rotation_job.go index 35ba234e06..1a71f87165 100644 --- a/sdk/rotation/rotation_job.go +++ b/sdk/rotation/rotation_job.go @@ -10,6 +10,11 @@ import ( "github.com/robfig/cron/v3" ) +const ( + PerformedRegistration = "registration" + PerformedDeregistration = "deregistration" +) + // RotationOptions is an embeddable struct to capture common rotation // settings between a Secret and Auth type RotationOptions struct {