diff --git a/builtin/logical/database/path_config_connection.go b/builtin/logical/database/path_config_connection.go index d06f2a9efe..b737c0a8b7 100644 --- a/builtin/logical/database/path_config_connection.go +++ b/builtin/logical/database/path_config_connection.go @@ -648,6 +648,15 @@ func (b *databaseBackend) connectionWriteHandler() framework.OperationFunc { "Vault (or the sdk if using a custom plugin) to gain password policy support", config.PluginName)) } + // We can ignore the error at this point since we're simply adding a warning. + dbType, _ := dbw.Type() + if dbType == "snowflake" && config.ConnectionDetails["password"] != nil { + resp.AddWarning(`[DEPRECATED] Single-factor password authentication is deprecated in Snowflake and will +be removed by November 2025. Key pair authentication will be required after this date. Please +see the Vault documentation for details on the removal of this feature. More information is +available at https://www.snowflake.com/en/blog/blocking-single-factor-password-authentification`) + } + b.dbEvent(ctx, "config-write", req.Path, name, true) if len(resp.Warnings) == 0 { return nil, nil diff --git a/changelog/30327.txt b/changelog/30327.txt new file mode 100644 index 0000000000..800b8527f5 --- /dev/null +++ b/changelog/30327.txt @@ -0,0 +1,3 @@ +```release-note:bug +database: no longer incorrectly add an "unrecognized parameters" warning for certain SQL database secrets config operations when another warning is returned +``` diff --git a/sdk/database/helper/connutil/sql.go b/sdk/database/helper/connutil/sql.go index 489becf0c1..3df692dcec 100644 --- a/sdk/database/helper/connutil/sql.go +++ b/sdk/database/helper/connutil/sql.go @@ -10,6 +10,7 @@ import ( "fmt" "net/url" "os" + "reflect" "strings" "sync" "time" @@ -79,6 +80,19 @@ type SQLConnectionProducer struct { sync.Mutex } +// This provides the field names for SQLConnectionProducer for field validation in the framework handler. +func SQLConnectionProducerFieldNames() map[string]any { + scp := &SQLConnectionProducer{} + rType := reflect.TypeOf(scp).Elem() + + fieldNames := make(map[string]any, rType.NumField()) + for i := range rType.NumField() { + fieldNames[rType.Field(i).Tag.Get("json")] = 1 + } + + return fieldNames +} + func (c *SQLConnectionProducer) Initialize(ctx context.Context, conf map[string]interface{}, verifyConnection bool) error { _, err := c.Init(ctx, conf, verifyConnection) return err diff --git a/sdk/framework/backend.go b/sdk/framework/backend.go index d3951d585c..3ec32f71bf 100644 --- a/sdk/framework/backend.go +++ b/sdk/framework/backend.go @@ -24,6 +24,7 @@ import ( "github.com/hashicorp/go-kms-wrapping/entropy/v2" "github.com/hashicorp/go-multierror" "github.com/hashicorp/go-secure-stdlib/parseutil" + "github.com/hashicorp/vault/sdk/database/helper/connutil" "github.com/hashicorp/vault/sdk/helper/consts" "github.com/hashicorp/vault/sdk/helper/errutil" "github.com/hashicorp/vault/sdk/helper/license" @@ -247,13 +248,18 @@ func (b *Backend) HandleRequest(ctx context.Context, req *logical.Request) (*log } } + // We need to check SQLConnectionProducer fields separately since they are not top-level Path fields. + var sqlFields map[string]any + if req.MountType == "database" && strings.HasPrefix(req.Path, "config") { + sqlFields = connutil.SQLConnectionProducerFieldNames() + } // Build up the data for the route, with the URL taking priority // for the fields over the PUT data. raw := make(map[string]interface{}, len(path.Fields)) var ignored []string for k, v := range req.Data { raw[k] = v - if !path.TakesArbitraryInput && path.Fields[k] == nil { + if !path.TakesArbitraryInput && path.Fields[k] == nil && sqlFields[k] == nil { ignored = append(ignored, k) } } diff --git a/sdk/framework/backend_test.go b/sdk/framework/backend_test.go index dba5b0c0a3..8718469a2a 100644 --- a/sdk/framework/backend_test.go +++ b/sdk/framework/backend_test.go @@ -52,49 +52,86 @@ func TestBackend_impl(t *testing.T) { } func TestBackendHandleRequestFieldWarnings(t *testing.T) { - handler := func(ctx context.Context, req *logical.Request, data *FieldData) (*logical.Response, error) { - return &logical.Response{ - Data: map[string]interface{}{ - "an_int": data.Get("an_int"), - "a_string": data.Get("a_string"), - "name": data.Get("name"), - }, - }, nil - } + t.Run("check replaced and ignored endpoints", func(t *testing.T) { + handler := func(ctx context.Context, req *logical.Request, data *FieldData) (*logical.Response, error) { + return &logical.Response{ + Data: map[string]interface{}{ + "an_int": data.Get("an_int"), + "a_string": data.Get("a_string"), + "name": data.Get("name"), + }, + }, nil + } - backend := &Backend{ - Paths: []*Path{ - { - Pattern: "foo/bar/(?P.+)", - Fields: map[string]*FieldSchema{ - "an_int": {Type: TypeInt}, - "a_string": {Type: TypeString}, - "name": {Type: TypeString}, - }, - Operations: map[logical.Operation]OperationHandler{ - logical.UpdateOperation: &PathOperation{Callback: handler}, + backend := &Backend{ + Paths: []*Path{ + { + Pattern: "foo/bar/(?P.+)", + Fields: map[string]*FieldSchema{ + "an_int": {Type: TypeInt}, + "a_string": {Type: TypeString}, + "name": {Type: TypeString}, + }, + Operations: map[logical.Operation]OperationHandler{ + logical.UpdateOperation: &PathOperation{Callback: handler}, + }, }, }, - }, - } - ctx := context.Background() - resp, err := backend.HandleRequest(ctx, &logical.Request{ - Operation: logical.UpdateOperation, - Path: "foo/bar/baz", - Data: map[string]interface{}{ - "an_int": 10, - "a_string": "accepted", - "unrecognized1": "unrecognized", - "unrecognized2": 20.2, - "name": "noop", - }, + } + ctx := context.Background() + resp, err := backend.HandleRequest(ctx, &logical.Request{ + Operation: logical.UpdateOperation, + Path: "foo/bar/baz", + Data: map[string]interface{}{ + "an_int": 10, + "a_string": "accepted", + "unrecognized1": "unrecognized", + "unrecognized2": 20.2, + "name": "noop", + }, + }) + require.NoError(t, err) + require.NotNil(t, resp) + require.Len(t, resp.Warnings, 2) + require.True(t, strutil.StrListContains(resp.Warnings, "Endpoint ignored these unrecognized parameters: [unrecognized1 unrecognized2]")) + require.True(t, strutil.StrListContains(resp.Warnings, "Endpoint replaced the value of these parameters with the values captured from the endpoint's path: [name]")) + }) + + t.Run("check ignored DB secrets config ignored fields", func(t *testing.T) { + handler := func(ctx context.Context, req *logical.Request, data *FieldData) (*logical.Response, error) { + return &logical.Response{ + Data: map[string]interface{}{}, + }, nil + } + + backend := &Backend{ + Paths: []*Path{ + { + Pattern: "config/sqldb", + Fields: map[string]*FieldSchema{}, + Operations: map[logical.Operation]OperationHandler{ + logical.UpdateOperation: &PathOperation{Callback: handler}, + }, + }, + }, + } + ctx := context.Background() + resp, err := backend.HandleRequest(ctx, &logical.Request{ + Operation: logical.UpdateOperation, + Path: "config/sqldb", + MountType: "database", + Data: map[string]interface{}{ + "connection_url": "localhost", + "username": "user", + "password": "pass", + "unrecognized1": "unrecognized", + }, + }) + require.NoError(t, err) + require.NotNil(t, resp) + require.Len(t, resp.Warnings, 1) + require.Equal(t, resp.Warnings[0], "Endpoint ignored these unrecognized parameters: [unrecognized1]") }) - require.NoError(t, err) - require.NotNil(t, resp) - t.Log(resp.Warnings) - require.Len(t, resp.Warnings, 2) - require.True(t, strutil.StrListContains(resp.Warnings, "Endpoint ignored these unrecognized parameters: [unrecognized1 unrecognized2]")) - require.True(t, strutil.StrListContains(resp.Warnings, "Endpoint replaced the value of these parameters with the values captured from the endpoint's path: [name]")) } func TestBackendHandleRequest(t *testing.T) { diff --git a/website/content/api-docs/secret/databases/snowflake.mdx b/website/content/api-docs/secret/databases/snowflake.mdx index 2d58f39184..2bb895aa3c 100644 --- a/website/content/api-docs/secret/databases/snowflake.mdx +++ b/website/content/api-docs/secret/databases/snowflake.mdx @@ -8,6 +8,12 @@ description: >- # Snowflake database plugin HTTP API + + Snowflake is disabling password authentication for all users in  + November of 2025. +  HashiCorp is working to support key pair authentication in place of passwords. + + The Snowflake database plugin is one of the supported plugins for the database secrets engine. This plugin generates database credentials dynamically based on configured roles for the Snowflake database. diff --git a/website/content/docs/deprecation/index.mdx b/website/content/docs/deprecation/index.mdx index 4704365549..42ab021dcc 100644 --- a/website/content/docs/deprecation/index.mdx +++ b/website/content/docs/deprecation/index.mdx @@ -36,6 +36,8 @@ or raise a ticket with your support team. @include 'deprecation/ruby-client-library.mdx' +@include 'deprecation/snowflake-password-auth.mdx' + diff --git a/website/content/docs/secrets/databases/index.mdx b/website/content/docs/secrets/databases/index.mdx index ce8890d3a3..a8436706ed 100644 --- a/website/content/docs/secrets/databases/index.mdx +++ b/website/content/docs/secrets/databases/index.mdx @@ -218,7 +218,7 @@ and private key pair to authenticate. | [Redis](/vault/docs/secrets/databases/redis) | No | Yes | Yes | Yes | No | password | | [Redis ElastiCache](/vault/docs/secrets/databases/rediselasticache) | No | No | No | Yes | No | password | | [Redshift](/vault/docs/secrets/databases/redshift) | No | Yes | Yes | Yes | Yes (1.8+) | password | -| [Snowflake](/vault/docs/secrets/databases/snowflake) | No | Yes | Yes | Yes | Yes (1.8+) | password, rsa_private_key | +| [Snowflake](/vault/docs/secrets/databases/snowflake) | No | Yes | Yes | Yes | Yes (1.8+) | password(deprecated), rsa_private_key | ## Custom plugins diff --git a/website/content/docs/secrets/databases/snowflake.mdx b/website/content/docs/secrets/databases/snowflake.mdx index 86325b7101..427f952762 100644 --- a/website/content/docs/secrets/databases/snowflake.mdx +++ b/website/content/docs/secrets/databases/snowflake.mdx @@ -9,6 +9,12 @@ description: >- # Snowflake database secrets engine + + Snowflake is disabling password authentication for all users in  + November of 2025. +  HashiCorp is working to support key pair authentication in place of passwords. + + Snowflake is one of the supported plugins for the database secrets engine. This plugin generates database credentials dynamically based on configured roles for Snowflake-hosted databases and supports [Static Roles](/vault/docs/secrets/databases#static-roles). @@ -23,7 +29,7 @@ The Snowflake database secrets engine uses | Plugin Name | Root Credential Rotation | Dynamic Roles | Static Roles | Username Customization | Credential Types | | --------------------------- | ------------------------ | ------------- | ------------ | ---------------------- |---------------------------| -| `snowflake-database-plugin` | Yes | Yes | Yes | Yes (1.8+) | password, rsa_private_key | +| `snowflake-database-plugin` | Yes | Yes | Yes | Yes (1.8+) | password(deprecated), rsa_private_key | ## Setup diff --git a/website/content/partials/deprecation/snowflake-password-auth.mdx b/website/content/partials/deprecation/snowflake-password-auth.mdx new file mode 100644 index 0000000000..f197003f50 --- /dev/null +++ b/website/content/partials/deprecation/snowflake-password-auth.mdx @@ -0,0 +1,9 @@ +## Snowflake DB password authentication ((#snowflake-db-password-auth)) + +| Announced | Expected end of support | Expected removal | +| :-------: | :---------------------: | :--------------: | +| APR 2025 | NOV 2025 | N/A + +Snowflake is disabling password authentication for all users in [November of 2025](https://www.snowflake.com/en/blog/blocking-single-factor-password-authentification). +HashiCorp is working to support key pair authentication in place of passwords +for this database secrets engine.