Support trimming trailing slashes via a mount tuneable to support CMPv2 (#28752)

* Support trimming trailing slashes via a mount tuneable to support CMPv2

* changelog/

* Perform trimming in handleLoginRequest too

* Eagerly fetch the mount entry so we only test this once

* Add a mount match function that gets path and entry

* Update vault/request_handling.go

Co-authored-by: Steven Clark <steven.clark@hashicorp.com>

* more docs

* Some patches (from ENT) didnt apply

* patch fail

* Update vault/router.go

Co-authored-by: Steven Clark <steven.clark@hashicorp.com>

* PR feedback

* dupe

* another dupe

* Add support for enabling trim_request_trailing_slashes on mount creation

* Fix read mount api returning configuration for trim_request_trailing_slashes

* Fix test assertion

* Switch enable and tune arguments to BoolPtrVal to allow end-users to specify false flag

* Add trim-request-trailing-slashes to the auth enable API and CLI

---------

Co-authored-by: Steven Clark <steven.clark@hashicorp.com>
This commit is contained in:
Scott Miller 2024-10-24 10:47:17 -05:00 committed by GitHub
parent 314874c2b1
commit 415d260995
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 292 additions and 140 deletions

View File

@ -290,23 +290,23 @@ type MountInput struct {
}
type MountConfigInput struct {
Options map[string]string `json:"options" mapstructure:"options"`
DefaultLeaseTTL string `json:"default_lease_ttl" mapstructure:"default_lease_ttl"`
Description *string `json:"description,omitempty" mapstructure:"description"`
MaxLeaseTTL string `json:"max_lease_ttl" mapstructure:"max_lease_ttl"`
ForceNoCache bool `json:"force_no_cache" mapstructure:"force_no_cache"`
AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" mapstructure:"audit_non_hmac_request_keys"`
AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" mapstructure:"audit_non_hmac_response_keys"`
ListingVisibility string `json:"listing_visibility,omitempty" mapstructure:"listing_visibility"`
PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" mapstructure:"passthrough_request_headers"`
AllowedResponseHeaders []string `json:"allowed_response_headers,omitempty" mapstructure:"allowed_response_headers"`
TokenType string `json:"token_type,omitempty" mapstructure:"token_type"`
AllowedManagedKeys []string `json:"allowed_managed_keys,omitempty" mapstructure:"allowed_managed_keys"`
PluginVersion string `json:"plugin_version,omitempty"`
UserLockoutConfig *UserLockoutConfigInput `json:"user_lockout_config,omitempty"`
DelegatedAuthAccessors []string `json:"delegated_auth_accessors,omitempty" mapstructure:"delegated_auth_accessors"`
IdentityTokenKey string `json:"identity_token_key,omitempty" mapstructure:"identity_token_key"`
Options map[string]string `json:"options" mapstructure:"options"`
DefaultLeaseTTL string `json:"default_lease_ttl" mapstructure:"default_lease_ttl"`
Description *string `json:"description,omitempty" mapstructure:"description"`
MaxLeaseTTL string `json:"max_lease_ttl" mapstructure:"max_lease_ttl"`
ForceNoCache bool `json:"force_no_cache" mapstructure:"force_no_cache"`
AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" mapstructure:"audit_non_hmac_request_keys"`
AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" mapstructure:"audit_non_hmac_response_keys"`
ListingVisibility string `json:"listing_visibility,omitempty" mapstructure:"listing_visibility"`
PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" mapstructure:"passthrough_request_headers"`
AllowedResponseHeaders []string `json:"allowed_response_headers,omitempty" mapstructure:"allowed_response_headers"`
TokenType string `json:"token_type,omitempty" mapstructure:"token_type"`
AllowedManagedKeys []string `json:"allowed_managed_keys,omitempty" mapstructure:"allowed_managed_keys"`
PluginVersion string `json:"plugin_version,omitempty"`
UserLockoutConfig *UserLockoutConfigInput `json:"user_lockout_config,omitempty"`
DelegatedAuthAccessors []string `json:"delegated_auth_accessors,omitempty" mapstructure:"delegated_auth_accessors"`
IdentityTokenKey string `json:"identity_token_key,omitempty" mapstructure:"identity_token_key"`
TrimRequestTrailingSlashes *bool `json:"trim_request_trailing_slashes,omitempty" mapstructure:"trim_request_trailing_slashes"`
// Deprecated: This field will always be blank for newer server responses.
PluginName string `json:"plugin_name,omitempty" mapstructure:"plugin_name"`
}
@ -328,19 +328,20 @@ type MountOutput struct {
}
type MountConfigOutput struct {
DefaultLeaseTTL int `json:"default_lease_ttl" mapstructure:"default_lease_ttl"`
MaxLeaseTTL int `json:"max_lease_ttl" mapstructure:"max_lease_ttl"`
ForceNoCache bool `json:"force_no_cache" mapstructure:"force_no_cache"`
AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" mapstructure:"audit_non_hmac_request_keys"`
AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" mapstructure:"audit_non_hmac_response_keys"`
ListingVisibility string `json:"listing_visibility,omitempty" mapstructure:"listing_visibility"`
PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" mapstructure:"passthrough_request_headers"`
AllowedResponseHeaders []string `json:"allowed_response_headers,omitempty" mapstructure:"allowed_response_headers"`
TokenType string `json:"token_type,omitempty" mapstructure:"token_type"`
AllowedManagedKeys []string `json:"allowed_managed_keys,omitempty" mapstructure:"allowed_managed_keys"`
UserLockoutConfig *UserLockoutConfigOutput `json:"user_lockout_config,omitempty"`
DelegatedAuthAccessors []string `json:"delegated_auth_accessors,omitempty" mapstructure:"delegated_auth_accessors"`
IdentityTokenKey string `json:"identity_token_key,omitempty" mapstructure:"identity_token_key"`
DefaultLeaseTTL int `json:"default_lease_ttl" mapstructure:"default_lease_ttl"`
MaxLeaseTTL int `json:"max_lease_ttl" mapstructure:"max_lease_ttl"`
ForceNoCache bool `json:"force_no_cache" mapstructure:"force_no_cache"`
AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" mapstructure:"audit_non_hmac_request_keys"`
AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" mapstructure:"audit_non_hmac_response_keys"`
ListingVisibility string `json:"listing_visibility,omitempty" mapstructure:"listing_visibility"`
PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" mapstructure:"passthrough_request_headers"`
AllowedResponseHeaders []string `json:"allowed_response_headers,omitempty" mapstructure:"allowed_response_headers"`
TokenType string `json:"token_type,omitempty" mapstructure:"token_type"`
AllowedManagedKeys []string `json:"allowed_managed_keys,omitempty" mapstructure:"allowed_managed_keys"`
UserLockoutConfig *UserLockoutConfigOutput `json:"user_lockout_config,omitempty"`
DelegatedAuthAccessors []string `json:"delegated_auth_accessors,omitempty" mapstructure:"delegated_auth_accessors"`
IdentityTokenKey string `json:"identity_token_key,omitempty" mapstructure:"identity_token_key"`
TrimRequestTrailingSlashes bool `json:"trim_request_trailing_slashes,omitempty" mapstructure:"trim_request_trailing_slashes"`
// Deprecated: This field will always be blank for newer server responses.
PluginName string `json:"plugin_name,omitempty" mapstructure:"plugin_name"`

3
changelog/28752.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
core: Add a mount tuneable that trims trailing slashes of request paths during POST. Needed to support CMPv2 in PKI.
```

View File

@ -23,24 +23,25 @@ var (
type AuthEnableCommand struct {
*BaseCommand
flagDescription string
flagPath string
flagDefaultLeaseTTL time.Duration
flagMaxLeaseTTL time.Duration
flagAuditNonHMACRequestKeys []string
flagAuditNonHMACResponseKeys []string
flagListingVisibility string
flagPluginName string
flagPassthroughRequestHeaders []string
flagAllowedResponseHeaders []string
flagOptions map[string]string
flagLocal bool
flagSealWrap bool
flagExternalEntropyAccess bool
flagTokenType string
flagVersion int
flagPluginVersion string
flagIdentityTokenKey string
flagDescription string
flagPath string
flagDefaultLeaseTTL time.Duration
flagMaxLeaseTTL time.Duration
flagAuditNonHMACRequestKeys []string
flagAuditNonHMACResponseKeys []string
flagListingVisibility string
flagPluginName string
flagPassthroughRequestHeaders []string
flagAllowedResponseHeaders []string
flagOptions map[string]string
flagLocal bool
flagSealWrap bool
flagExternalEntropyAccess bool
flagTokenType string
flagVersion int
flagPluginVersion string
flagIdentityTokenKey string
flagTrimRequestTrailingSlashes BoolPtr
}
func (c *AuthEnableCommand) Synopsis() string {
@ -217,6 +218,12 @@ func (c *AuthEnableCommand) Flags() *FlagSets {
Usage: "Select the key used to sign plugin identity tokens.",
})
f.BoolPtrVar(&BoolPtrVar{
Name: flagNameTrimRequestTrailingSlashes,
Target: &c.flagTrimRequestTrailingSlashes,
Usage: "Whether to trim trailing slashes for incoming requests to this mount",
})
return set
}
@ -324,6 +331,11 @@ func (c *AuthEnableCommand) Run(args []string) int {
if fl.Name == flagNameIdentityTokenKey {
authOpts.Config.IdentityTokenKey = c.flagIdentityTokenKey
}
if fl.Name == flagNameTrimRequestTrailingSlashes && c.flagTrimRequestTrailingSlashes.IsSet() {
val := c.flagTrimRequestTrailingSlashes.Get()
authOpts.Config.TrimRequestTrailingSlashes = &val
}
})
if err := client.Sys().EnableAuthWithOptions(authPath, authOpts); err != nil {

View File

@ -100,6 +100,7 @@ func TestAuthEnableCommand_Run(t *testing.T) {
"-allowed-response-headers", "authorization",
"-listing-visibility", "unauth",
"-identity-token-key", "default",
"-trim-request-trailing-slashes=true",
"userpass",
})
if exp := 0; code != exp {
@ -127,6 +128,9 @@ func TestAuthEnableCommand_Run(t *testing.T) {
if exp := "The best kind of test"; authInfo.Description != exp {
t.Errorf("expected %q to be %q", authInfo.Description, exp)
}
if !authInfo.Config.TrimRequestTrailingSlashes {
t.Errorf("expected trim_request_trailing_slashes to be enabled")
}
if diff := deep.Equal([]string{"authorization,authentication", "www-authentication"}, authInfo.Config.PassthroughRequestHeaders); len(diff) > 0 {
t.Errorf("Failed to find expected values in PassthroughRequestHeaders. Difference is: %v", diff)
}

View File

@ -40,6 +40,7 @@ type AuthTuneCommand struct {
flagUserLockoutCounterResetDuration time.Duration
flagUserLockoutDisable bool
flagIdentityTokenKey string
flagTrimRequestTrailingSlashes BoolPtr
}
func (c *AuthTuneCommand) Synopsis() string {
@ -195,6 +196,11 @@ func (c *AuthTuneCommand) Flags() *FlagSets {
Usage: "Select the semantic version of the plugin to run. The new version must be registered in " +
"the plugin catalog, and will not start running until the plugin is reloaded.",
})
f.BoolPtrVar(&BoolPtrVar{
Name: flagNameTrimRequestTrailingSlashes,
Target: &c.flagTrimRequestTrailingSlashes,
Usage: "Whether to trim trailing slashes for incoming requests to this mount",
})
f.StringVar(&StringVar{
Name: flagNameIdentityTokenKey,
@ -306,6 +312,11 @@ func (c *AuthTuneCommand) Run(args []string) int {
if fl.Name == flagNameIdentityTokenKey {
mountConfigInput.IdentityTokenKey = c.flagIdentityTokenKey
}
if fl.Name == flagNameTrimRequestTrailingSlashes && c.flagTrimRequestTrailingSlashes.IsSet() {
val := c.flagTrimRequestTrailingSlashes.Get()
mountConfigInput.TrimRequestTrailingSlashes = &val
}
})
// Append /auth (since that's where auths live) and a trailing slash to

View File

@ -120,6 +120,7 @@ func TestAuthTuneCommand_Run(t *testing.T) {
"-listing-visibility", "unauth",
"-plugin-version", version,
"-identity-token-key", "default",
"-trim-request-trailing-slashes=true",
"my-auth/",
})
if exp := 0; code != exp {
@ -156,6 +157,9 @@ func TestAuthTuneCommand_Run(t *testing.T) {
if exp := 3600; mountInfo.Config.MaxLeaseTTL != exp {
t.Errorf("expected %d to be %d", mountInfo.Config.MaxLeaseTTL, exp)
}
if !mountInfo.Config.TrimRequestTrailingSlashes {
t.Errorf("expected trim_request_trailing_slashes to be enabled")
}
if diff := deep.Equal([]string{"authorization", "www-authentication"}, mountInfo.Config.PassthroughRequestHeaders); len(diff) > 0 {
t.Errorf("Failed to find expected values in PassthroughRequestHeaders. Difference is: %v", diff)
}

View File

@ -97,6 +97,8 @@ const (
flagNamePluginVersion = "plugin-version"
// flagNameIdentityTokenKey selects the key used to sign plugin identity tokens
flagNameIdentityTokenKey = "identity-token-key"
// flagNameTrimRequestTrailingSlashes selects the key used to determine whether to trim trailing slashes
flagNameTrimRequestTrailingSlashes = "trim-request-trailing-slashes"
// flagNameUserLockoutThreshold is the flag name used for tuning the auth mount lockout threshold parameter
flagNameUserLockoutThreshold = "user-lockout-threshold"
// flagNameUserLockoutDuration is the flag name used for tuning the auth mount lockout duration parameter

View File

@ -23,26 +23,27 @@ var (
type SecretsEnableCommand struct {
*BaseCommand
flagDescription string
flagPath string
flagDefaultLeaseTTL time.Duration
flagMaxLeaseTTL time.Duration
flagAuditNonHMACRequestKeys []string
flagAuditNonHMACResponseKeys []string
flagListingVisibility string
flagPassthroughRequestHeaders []string
flagAllowedResponseHeaders []string
flagForceNoCache bool
flagPluginName string
flagPluginVersion string
flagOptions map[string]string
flagLocal bool
flagSealWrap bool
flagExternalEntropyAccess bool
flagVersion int
flagAllowedManagedKeys []string
flagDelegatedAuthAccessors []string
flagIdentityTokenKey string
flagDescription string
flagPath string
flagDefaultLeaseTTL time.Duration
flagMaxLeaseTTL time.Duration
flagAuditNonHMACRequestKeys []string
flagAuditNonHMACResponseKeys []string
flagListingVisibility string
flagPassthroughRequestHeaders []string
flagAllowedResponseHeaders []string
flagForceNoCache bool
flagPluginName string
flagPluginVersion string
flagOptions map[string]string
flagLocal bool
flagSealWrap bool
flagExternalEntropyAccess bool
flagVersion int
flagAllowedManagedKeys []string
flagDelegatedAuthAccessors []string
flagIdentityTokenKey string
flagTrimRequestTrailingSlashes BoolPtr
}
func (c *SecretsEnableCommand) Synopsis() string {
@ -245,6 +246,12 @@ func (c *SecretsEnableCommand) Flags() *FlagSets {
Usage: "Select the key used to sign plugin identity tokens.",
})
f.BoolPtrVar(&BoolPtrVar{
Name: flagNameTrimRequestTrailingSlashes,
Target: &c.flagTrimRequestTrailingSlashes,
Usage: "Whether to trim trailing slashes for incoming requests to this mount",
})
return set
}
@ -359,6 +366,11 @@ func (c *SecretsEnableCommand) Run(args []string) int {
if fl.Name == flagNameIdentityTokenKey {
mountInput.Config.IdentityTokenKey = c.flagIdentityTokenKey
}
if fl.Name == flagNameTrimRequestTrailingSlashes && c.flagTrimRequestTrailingSlashes.IsSet() {
val := c.flagTrimRequestTrailingSlashes.Get()
mountInput.Config.TrimRequestTrailingSlashes = &val
}
})
if err := client.Sys().Mount(mountPath, mountInput); err != nil {

View File

@ -120,6 +120,7 @@ func TestSecretsEnableCommand_Run(t *testing.T) {
"-allowed-managed-keys", "key1,key2",
"-identity-token-key", "default",
"-delegated-auth-accessors", "authAcc1,authAcc2",
"-trim-request-trailing-slashes=true",
"-force-no-cache",
"pki",
})
@ -157,6 +158,9 @@ func TestSecretsEnableCommand_Run(t *testing.T) {
if exp := true; mountInfo.Config.ForceNoCache != exp {
t.Errorf("expected %t to be %t", mountInfo.Config.ForceNoCache, exp)
}
if !mountInfo.Config.TrimRequestTrailingSlashes {
t.Errorf("expected trim_request_trailing_slashes to be enabled")
}
if diff := deep.Equal([]string{"authorization,authentication", "www-authentication"}, mountInfo.Config.PassthroughRequestHeaders); len(diff) > 0 {
t.Errorf("Failed to find expected values in PassthroughRequestHeaders. Difference is: %v", diff)
}

View File

@ -23,20 +23,21 @@ var (
type SecretsTuneCommand struct {
*BaseCommand
flagAuditNonHMACRequestKeys []string
flagAuditNonHMACResponseKeys []string
flagDefaultLeaseTTL time.Duration
flagDescription string
flagListingVisibility string
flagMaxLeaseTTL time.Duration
flagPassthroughRequestHeaders []string
flagAllowedResponseHeaders []string
flagOptions map[string]string
flagVersion int
flagPluginVersion string
flagAllowedManagedKeys []string
flagDelegatedAuthAccessors []string
flagIdentityTokenKey string
flagAuditNonHMACRequestKeys []string
flagAuditNonHMACResponseKeys []string
flagDefaultLeaseTTL time.Duration
flagDescription string
flagListingVisibility string
flagMaxLeaseTTL time.Duration
flagPassthroughRequestHeaders []string
flagAllowedResponseHeaders []string
flagOptions map[string]string
flagVersion int
flagPluginVersion string
flagAllowedManagedKeys []string
flagDelegatedAuthAccessors []string
flagIdentityTokenKey string
flagTrimRequestTrailingSlashes BoolPtr
}
func (c *SecretsTuneCommand) Synopsis() string {
@ -175,6 +176,12 @@ func (c *SecretsTuneCommand) Flags() *FlagSets {
Usage: "Select the key used to sign plugin identity tokens.",
})
f.BoolPtrVar(&BoolPtrVar{
Name: flagNameTrimRequestTrailingSlashes,
Target: &c.flagTrimRequestTrailingSlashes,
Usage: "Whether to trim trailing slashes for incoming requests to this mount",
})
return set
}
@ -267,6 +274,10 @@ func (c *SecretsTuneCommand) Run(args []string) int {
if fl.Name == flagNameIdentityTokenKey {
mountConfigInput.IdentityTokenKey = c.flagIdentityTokenKey
}
if fl.Name == flagNameTrimRequestTrailingSlashes && c.flagTrimRequestTrailingSlashes.IsSet() {
val := c.flagTrimRequestTrailingSlashes.Get()
mountConfigInput.TrimRequestTrailingSlashes = &val
}
})
if err := client.Sys().TuneMount(mountPath, mountConfigInput); err != nil {

View File

@ -196,6 +196,7 @@ func TestSecretsTuneCommand_Run(t *testing.T) {
"-listing-visibility", "unauth",
"-plugin-version", version,
"-delegated-auth-accessors", "authAcc1,authAcc2",
"-trim-request-trailing-slashes=true",
"mount_tune_integration/",
})
if exp := 0; code != exp {
@ -232,6 +233,9 @@ func TestSecretsTuneCommand_Run(t *testing.T) {
if exp := 3600; mountInfo.Config.MaxLeaseTTL != exp {
t.Errorf("expected %d to be %d", mountInfo.Config.MaxLeaseTTL, exp)
}
if !mountInfo.Config.TrimRequestTrailingSlashes {
t.Errorf("expected trim_request_trailing_slashes to be enabled")
}
if diff := deep.Equal([]string{"authorization", "www-authentication"}, mountInfo.Config.PassthroughRequestHeaders); len(diff) > 0 {
t.Errorf("Failed to find expected values for PassthroughRequestHeaders. Difference is: %v", diff)
}

View File

@ -1389,7 +1389,9 @@ func (b *SystemBackend) mountInfo(ctx context.Context, entry *MountEntry, legacy
entryConfig["max_lease_ttl"] = coreMaxTTL
}
}
if entry.Config.TrimRequestTrailingSlashes {
entryConfig["trim_request_trailing_slashes"] = true
}
if rawVal, ok := entry.synthesizedConfigCache.Load("audit_non_hmac_request_keys"); ok {
entryConfig["audit_non_hmac_request_keys"] = rawVal.([]string)
}
@ -1613,6 +1615,10 @@ func (b *SystemBackend) handleMount(ctx context.Context, req *logical.Request, d
config.ForceNoCache = true
}
if apiConfig.TrimRequestTrailingSlashes {
config.TrimRequestTrailingSlashes = true
}
if err := checkListingVisibility(apiConfig.ListingVisibility); err != nil {
return logical.ErrorResponse(fmt.Sprintf("invalid listing_visibility %s", apiConfig.ListingVisibility)), nil
}
@ -2149,6 +2155,10 @@ func (b *SystemBackend) handleTuneReadCommon(ctx context.Context, path string) (
resp.Data["identity_token_key"] = rawVal.(string)
}
if mountEntry.Config.TrimRequestTrailingSlashes {
resp.Data["trim_request_trailing_slashes"] = mountEntry.Config.TrimRequestTrailingSlashes
}
if mountEntry.Config.UserLockoutConfig != nil {
resp.Data["user_lockout_counter_reset_duration"] = int64(mountEntry.Config.UserLockoutConfig.LockoutCounterReset.Seconds())
resp.Data["user_lockout_threshold"] = mountEntry.Config.UserLockoutConfig.LockoutThreshold
@ -2753,6 +2763,30 @@ func (b *SystemBackend) handleTuneWriteCommon(ctx context.Context, path string,
}
}
if rawVal, ok := data.GetOk("trim_request_trailing_slashes"); ok {
trimRequestTrailingSlashes := rawVal.(bool)
oldVal := mountEntry.Config.TrimRequestTrailingSlashes
mountEntry.Config.TrimRequestTrailingSlashes = trimRequestTrailingSlashes
// Update the mount table
var err error
switch {
case strings.HasPrefix(path, "auth/"):
err = b.Core.persistAuth(ctx, b.Core.auth, &mountEntry.Local)
default:
err = b.Core.persistMounts(ctx, b.Core.mounts, &mountEntry.Local)
}
if err != nil {
mountEntry.Config.TrimRequestTrailingSlashes = oldVal
return handleError(err)
}
if b.Core.logger.IsInfo() {
b.Core.logger.Info("mount tuning of trim_request_trailing_slashes successful", "path", path)
}
}
var err error
var resp *logical.Response
var options map[string]string
@ -3269,6 +3303,7 @@ func (b *SystemBackend) handleEnableAuth(ctx context.Context, req *logical.Reque
return logical.ErrorResponse(fmt.Sprintf("invalid listing_visibility %s", apiConfig.ListingVisibility)), nil
}
config.ListingVisibility = apiConfig.ListingVisibility
config.TrimRequestTrailingSlashes = apiConfig.TrimRequestTrailingSlashes
if len(apiConfig.AuditNonHMACRequestKeys) > 0 {
config.AuditNonHMACRequestKeys = apiConfig.AuditNonHMACRequestKeys
@ -7156,4 +7191,8 @@ This path responds to the following HTTP methods.
`The label representing a path-prefix within the /.well-known/ path`,
"",
},
"trim_request_trailing_slashes": {
`Whether to trim a trailing slash on incoming requests to this mount`,
"",
},
}

View File

@ -3826,6 +3826,10 @@ func (b *SystemBackend) authPaths() []*framework.Path {
Description: strings.TrimSpace(sysHelp["identity_token_key"][0]),
Required: false,
},
"trim_request_trailing_slashes": {
Type: framework.TypeBool,
Required: false,
},
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
@ -3916,6 +3920,10 @@ func (b *SystemBackend) authPaths() []*framework.Path {
Type: framework.TypeString,
Required: false,
},
"trim_request_trailing_slashes": {
Type: framework.TypeBool,
Required: false,
},
},
}},
},
@ -4686,6 +4694,10 @@ func (b *SystemBackend) mountPaths() []*framework.Path {
Type: framework.TypeString,
Description: strings.TrimSpace(sysHelp["identity_token_key"][0]),
},
"trim_request_trailing_slashes": {
Type: framework.TypeBool,
Description: strings.TrimSpace(sysHelp["trim_request_trailing_slashes"][0]),
},
},
Operations: map[logical.Operation]framework.OperationHandler{
@ -4788,6 +4800,10 @@ func (b *SystemBackend) mountPaths() []*framework.Path {
Type: framework.TypeString,
Required: false,
},
"trim_request_trailing_slashes": {
Type: framework.TypeBool,
Required: false,
},
},
}},
},

View File

@ -349,19 +349,20 @@ type MountEntry struct {
// MountConfig is used to hold settable options
type MountConfig struct {
DefaultLeaseTTL time.Duration `json:"default_lease_ttl,omitempty" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` // Override for global default
MaxLeaseTTL time.Duration `json:"max_lease_ttl,omitempty" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` // Override for global default
ForceNoCache bool `json:"force_no_cache,omitempty" structs:"force_no_cache" mapstructure:"force_no_cache"` // Override for global default
AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" structs:"audit_non_hmac_request_keys" mapstructure:"audit_non_hmac_request_keys"`
AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" structs:"audit_non_hmac_response_keys" mapstructure:"audit_non_hmac_response_keys"`
ListingVisibility ListingVisibilityType `json:"listing_visibility,omitempty" structs:"listing_visibility" mapstructure:"listing_visibility"`
PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" structs:"passthrough_request_headers" mapstructure:"passthrough_request_headers"`
AllowedResponseHeaders []string `json:"allowed_response_headers,omitempty" structs:"allowed_response_headers" mapstructure:"allowed_response_headers"`
TokenType logical.TokenType `json:"token_type,omitempty" structs:"token_type" mapstructure:"token_type"`
AllowedManagedKeys []string `json:"allowed_managed_keys,omitempty" mapstructure:"allowed_managed_keys"`
UserLockoutConfig *UserLockoutConfig `json:"user_lockout_config,omitempty" mapstructure:"user_lockout_config"`
DelegatedAuthAccessors []string `json:"delegated_auth_accessors,omitempty" mapstructure:"delegated_auth_accessors"`
IdentityTokenKey string `json:"identity_token_key,omitempty" mapstructure:"identity_token_key"`
DefaultLeaseTTL time.Duration `json:"default_lease_ttl,omitempty" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` // Override for global default
MaxLeaseTTL time.Duration `json:"max_lease_ttl,omitempty" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` // Override for global default
ForceNoCache bool `json:"force_no_cache,omitempty" structs:"force_no_cache" mapstructure:"force_no_cache"` // Override for global default
AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" structs:"audit_non_hmac_request_keys" mapstructure:"audit_non_hmac_request_keys"`
AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" structs:"audit_non_hmac_response_keys" mapstructure:"audit_non_hmac_response_keys"`
ListingVisibility ListingVisibilityType `json:"listing_visibility,omitempty" structs:"listing_visibility" mapstructure:"listing_visibility"`
PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" structs:"passthrough_request_headers" mapstructure:"passthrough_request_headers"`
AllowedResponseHeaders []string `json:"allowed_response_headers,omitempty" structs:"allowed_response_headers" mapstructure:"allowed_response_headers"`
TokenType logical.TokenType `json:"token_type,omitempty" structs:"token_type" mapstructure:"token_type"`
AllowedManagedKeys []string `json:"allowed_managed_keys,omitempty" mapstructure:"allowed_managed_keys"`
UserLockoutConfig *UserLockoutConfig `json:"user_lockout_config,omitempty" mapstructure:"user_lockout_config"`
DelegatedAuthAccessors []string `json:"delegated_auth_accessors,omitempty" mapstructure:"delegated_auth_accessors"`
IdentityTokenKey string `json:"identity_token_key,omitempty" mapstructure:"identity_token_key"`
TrimRequestTrailingSlashes bool `json:"trim_request_trailing_slashes,omitempty" mapstructure:"trim_request_trailing_slashes"` // If requests to this mount should have trailing slashes trimmed
// PluginName is the name of the plugin registered in the catalog.
//
@ -389,20 +390,21 @@ type APIUserLockoutConfig struct {
// APIMountConfig is an embedded struct of api.MountConfigInput
type APIMountConfig struct {
DefaultLeaseTTL string `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"`
MaxLeaseTTL string `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"`
ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"`
AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" structs:"audit_non_hmac_request_keys" mapstructure:"audit_non_hmac_request_keys"`
AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" structs:"audit_non_hmac_response_keys" mapstructure:"audit_non_hmac_response_keys"`
ListingVisibility ListingVisibilityType `json:"listing_visibility,omitempty" structs:"listing_visibility" mapstructure:"listing_visibility"`
PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" structs:"passthrough_request_headers" mapstructure:"passthrough_request_headers"`
AllowedResponseHeaders []string `json:"allowed_response_headers,omitempty" structs:"allowed_response_headers" mapstructure:"allowed_response_headers"`
TokenType string `json:"token_type" structs:"token_type" mapstructure:"token_type"`
AllowedManagedKeys []string `json:"allowed_managed_keys,omitempty" mapstructure:"allowed_managed_keys"`
UserLockoutConfig *UserLockoutConfig `json:"user_lockout_config,omitempty" mapstructure:"user_lockout_config"`
PluginVersion string `json:"plugin_version,omitempty" mapstructure:"plugin_version"`
DelegatedAuthAccessors []string `json:"delegated_auth_accessors,omitempty" mapstructure:"delegated_auth_accessors"`
IdentityTokenKey string `json:"identity_token_key,omitempty" mapstructure:"identity_token_key"`
DefaultLeaseTTL string `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"`
MaxLeaseTTL string `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"`
ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"`
AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" structs:"audit_non_hmac_request_keys" mapstructure:"audit_non_hmac_request_keys"`
AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" structs:"audit_non_hmac_response_keys" mapstructure:"audit_non_hmac_response_keys"`
ListingVisibility ListingVisibilityType `json:"listing_visibility,omitempty" structs:"listing_visibility" mapstructure:"listing_visibility"`
PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" structs:"passthrough_request_headers" mapstructure:"passthrough_request_headers"`
AllowedResponseHeaders []string `json:"allowed_response_headers,omitempty" structs:"allowed_response_headers" mapstructure:"allowed_response_headers"`
TokenType string `json:"token_type" structs:"token_type" mapstructure:"token_type"`
AllowedManagedKeys []string `json:"allowed_managed_keys,omitempty" mapstructure:"allowed_managed_keys"`
UserLockoutConfig *UserLockoutConfig `json:"user_lockout_config,omitempty" mapstructure:"user_lockout_config"`
PluginVersion string `json:"plugin_version,omitempty" mapstructure:"plugin_version"`
DelegatedAuthAccessors []string `json:"delegated_auth_accessors,omitempty" mapstructure:"delegated_auth_accessors"`
IdentityTokenKey string `json:"identity_token_key,omitempty" mapstructure:"identity_token_key"`
TrimRequestTrailingSlashes bool `json:"trim_request_trailing_slashes,omitempty" mapstructure:"trim_request_trailing_slashes"` // If requests to this mount should have trailing slashes trimmed
// PluginName is the name of the plugin registered in the catalog.
//

View File

@ -612,6 +612,25 @@ func (c *Core) switchedLockHandleRequest(httpCtx context.Context, req *logical.R
}
func (c *Core) handleCancelableRequest(ctx context.Context, req *logical.Request) (resp *logical.Response, err error) {
waitGroup, err := waitForReplicationState(ctx, c, req)
if err != nil {
return nil, err
}
// Decrement the wait group when our request is done
if waitGroup != nil {
defer waitGroup.Done()
}
if c.MissingRequiredState(req.RequiredState(), c.perfStandby) {
return nil, logical.ErrMissingRequiredState
}
// Ensure the req contains a MountPoint as it is depended on by some
// functionality (e.g. quotas)
var entry *MountEntry
req.MountPoint, entry = c.router.MatchingMountAndEntry(ctx, req.Path)
// Allowing writing to a path ending in / makes it extremely difficult to
// understand user intent for the filesystem-like backends (kv,
// cubbyhole) -- did they want a key named foo/ or did they want to write
@ -622,24 +641,11 @@ func (c *Core) handleCancelableRequest(ctx context.Context, req *logical.Request
(req.Operation == logical.UpdateOperation ||
req.Operation == logical.CreateOperation ||
req.Operation == logical.PatchOperation) {
return logical.ErrorResponse("cannot write to a path ending in '/'"), nil
}
waitGroup, err := waitForReplicationState(ctx, c, req)
if err != nil {
return nil, err
}
// MountPoint will not always be set at this point, so we ensure the req contains it
// as it is depended on by some functionality (e.g. quotas)
req.MountPoint = c.router.MatchingMount(ctx, req.Path)
// Decrement the wait group when our request is done
if waitGroup != nil {
defer waitGroup.Done()
}
if c.MissingRequiredState(req.RequiredState(), c.perfStandby) {
return nil, logical.ErrMissingRequiredState
if entry == nil || !entry.Config.TrimRequestTrailingSlashes {
return logical.ErrorResponse("cannot write to a path ending in '/'"), nil
} else {
req.Path = strings.TrimSuffix(req.Path, "/")
}
}
err = c.PopulateTokenEntry(ctx, req)
@ -892,7 +898,6 @@ func (c *Core) handleCancelableRequest(ctx context.Context, req *logical.Request
var nonHMACReqDataKeys []string
var nonHMACRespDataKeys []string
entry := c.router.MatchingMountEntry(ctx, req.Path)
if entry != nil {
// Get and set ignored HMAC'd value. Reset those back to empty afterwards.
if rawVals, ok := entry.synthesizedConfigCache.Load("audit_non_hmac_request_keys"); ok {

View File

@ -373,23 +373,28 @@ func (r *Router) MatchingMountByAccessor(mountAccessor string) *MountEntry {
// MatchingMount returns the mount prefix that would be used for a path
func (r *Router) MatchingMount(ctx context.Context, path string) string {
r.l.RLock()
mount := r.matchingMountInternal(ctx, path)
mount, _ := r.matchingMountInternal(ctx, path)
r.l.RUnlock()
return mount
}
func (r *Router) matchingMountInternal(ctx context.Context, path string) string {
// MatchingMountAndEntry returns the MountEntry used for a path and it's router path
func (r *Router) MatchingMountAndEntry(ctx context.Context, path string) (string, *MountEntry) {
return r.matchingMountInternal(ctx, path)
}
func (r *Router) matchingMountInternal(ctx context.Context, path string) (string, *MountEntry) {
ns, err := namespace.FromContext(ctx)
if err != nil {
return ""
return "", nil
}
path = ns.Path + path
mount, _, ok := r.root.LongestPrefix(path)
mount, raw, ok := r.root.LongestPrefix(path)
if !ok {
return ""
return "", nil
}
return mount
return mount, raw.(*routeEntry).mountEntry
}
// matchingPrefixInternal returns a mount prefix that a path may be a part of
@ -416,7 +421,7 @@ func (r *Router) matchingPrefixInternal(ctx context.Context, path string) string
func (r *Router) MountConflict(ctx context.Context, path string) string {
r.l.RLock()
defer r.l.RUnlock()
if exactMatch := r.matchingMountInternal(ctx, path); exactMatch != "" {
if exactMatch, _ := r.matchingMountInternal(ctx, path); exactMatch != "" {
return exactMatch
}
if prefixMatch := r.matchingPrefixInternal(ctx, path); prefixMatch != "" {

View File

@ -89,6 +89,10 @@ flags](/vault/docs/commands) included on all commands.
- `-token-type` `(string: "")` - Specifies the type of tokens that should be
returned by the auth method.
- `-trim-request-trailing-slashes` `(bool: false)` - If true, requests to
this mount with trailing slashes will have those slashes trimmed.
Necessary for some standards based APIs handled by Vault.
- `-plugin-version` `(string: "")` - Configures the semantic version of the plugin
to use. If unspecified, implies the built-in or any matching unversioned plugin
that may have been registered.

View File

@ -168,6 +168,10 @@ flags](/vault/docs/commands) included on all commands.
- `-token-type` `(string: "")` - Specifies the type of tokens that should be
returned by the auth method.
- `-trim-request-trailing-slashes` `(bool: false)` - If true, requests to
this mount with trailing slashes will have those slashes trimmed.
Necessary for some standards based APIs handled by Vault.
- `-plugin-version` `(string: "")` - Configures the semantic version of the plugin
to use. The new version will not start running until the mount is
[reloaded](/vault/docs/commands/plugin/reload).

View File

@ -111,6 +111,10 @@ flags](/vault/docs/commands) included on all commands.
backend can delegate authentication to. To allow multiple accessors, provide
the `delegated-auth-accessors` multiple times, each time with 1 accessor.
- `-trim-request-trailing-slashes` `(bool: false)` - If true, requests to
this mount with trailing slashes will have those slashes trimmed.
Necessary for some standards based APIs handled by Vault.
- `-plugin-version` `(string: "")` - Configures the semantic version of the plugin
to use. If unspecified, implies the built-in or any matching unversioned plugin
that may have been registered.

View File

@ -97,6 +97,10 @@ flags](/vault/docs/commands) included on all commands.
backend can delegate authentication to. To allow multiple accessors, provide
the `delegated-auth-accessors` multiple times, each time with 1 accessor.
- `-trim-request-trailing-slashes` `(bool: false)` - If true, requests to
this mount with trailing slashes will have those slashes trimmed.
Necessary for some standards based APIs handled by Vault.
- `-plugin-version` `(string: "")` - Configures the semantic version of the plugin
to use. The new version will not start running until the mount is
[reloaded](/vault/docs/commands/plugin/reload).

View File

@ -87,10 +87,10 @@ To get an authentication mount's accessor field, the following command can be us
$ vault read -field=accessor sys/auth/auth/cert
```
For CMP to work within certain clients, a few response headers need to be explicitly allowed
along with configuring the list of accessors the mount can delegate authentication towards.
The following will grant the required response headers, you will need to replace the values for the `delegated-auth-accessors`
to match your values.
For CMP to work within certain clients, a few response headers need to be explicitly
allowed, trailing slashes must be trimmed, and the list of accessors the mount can delegate authentication towards
must be configured. The following will grant the required response headers, you will need to replace the values for
the `delegated-auth-accessors` to match your values.
```shell-session
$ vault secrets tune \
@ -98,6 +98,7 @@ $ vault secrets tune \
-allowed-response-headers="Content-Length" \
-allowed-response-headers="WWW-Authenticate" \
-delegated-auth-accessors="auth_cert_4088ac2d" \
-trim-request-trailing-slashes="true" \
pki
```