Add snowflake DB API warning (#30327)

* Add API warning based on DB type

* Add deprecation notice

* Add warning to the top of the docs pages

* Update capabilities table

* Filter SQLConnectionProducer fields from unrecognized parameters warning

* Add test case
This commit is contained in:
Robert 2025-04-28 13:05:55 -05:00 committed by GitHub
parent 8a84d13c60
commit bf339bc50d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 134 additions and 42 deletions

View file

@ -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

3
changelog/30327.txt Normal file
View file

@ -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
```

View file

@ -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

View file

@ -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)
}
}

View file

@ -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<name>.+)",
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<name>.+)",
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) {

View file

@ -8,6 +8,12 @@ description: >-
# Snowflake database plugin HTTP API
<Warning title="Password authentication removal">
Snowflake is disabling password authentication for all users in&nbsp;
<a href="https://www.snowflake.com/en/blog/blocking-single-factor-password-authentification">November of 2025.</a>
&nbsp;HashiCorp is working to support key pair authentication in place of passwords.
</Warning>
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.

View file

@ -36,6 +36,8 @@ or raise a ticket with your support team.
@include 'deprecation/ruby-client-library.mdx'
@include 'deprecation/snowflake-password-auth.mdx'
</Tab>
<Tab heading="PENDING REMOVAL">

View file

@ -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

View file

@ -9,6 +9,12 @@ description: >-
# Snowflake database secrets engine
<Warning title="Password authentication removal">
Snowflake is disabling password authentication for all users in&nbsp;
<a href="https://www.snowflake.com/en/blog/blocking-single-factor-password-authentification">November of 2025.</a>
&nbsp;HashiCorp is working to support key pair authentication in place of passwords.
</Warning>
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

View file

@ -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.