2023-03-15 16:00:52 +00:00
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
2020-06-26 21:13:16 +00:00
package vault
import (
"context"
2023-02-15 20:02:21 +00:00
"net/http"
2020-06-26 21:13:16 +00:00
"strings"
2020-07-29 19:15:05 +00:00
"time"
2020-06-26 21:13:16 +00:00
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
2021-02-09 10:46:09 +00:00
"github.com/hashicorp/vault/helper/namespace"
2020-06-26 21:13:16 +00:00
"github.com/hashicorp/vault/vault/quotas"
)
// quotasPaths returns paths that enable quota management
func ( b * SystemBackend ) quotasPaths ( ) [ ] * framework . Path {
return [ ] * framework . Path {
{
Pattern : "quotas/config$" ,
Fields : map [ string ] * framework . FieldSchema {
2020-10-16 18:58:19 +00:00
"rate_limit_exempt_paths" : {
Type : framework . TypeStringSlice ,
Description : "Specifies the list of exempt paths from all rate limit quotas. If empty no paths will be exempt." ,
} ,
2020-06-26 21:13:16 +00:00
"enable_rate_limit_audit_logging" : {
Type : framework . TypeBool ,
Description : "If set, starts audit logging of requests that get rejected due to rate limit quota rule violations." ,
} ,
2020-07-29 19:15:05 +00:00
"enable_rate_limit_response_headers" : {
Type : framework . TypeBool ,
Description : "If set, additional rate limit quota HTTP headers will be added to responses." ,
} ,
2020-06-26 21:13:16 +00:00
} ,
Operations : map [ logical . Operation ] framework . OperationHandler {
logical . UpdateOperation : & framework . PathOperation {
Callback : b . handleQuotasConfigUpdate ( ) ,
2023-02-15 20:02:21 +00:00
Responses : map [ int ] [ ] framework . Response {
http . StatusNoContent : { {
Description : "OK" ,
} } ,
} ,
2020-06-26 21:13:16 +00:00
} ,
logical . ReadOperation : & framework . PathOperation {
Callback : b . handleQuotasConfigRead ( ) ,
2023-02-15 20:02:21 +00:00
Responses : map [ int ] [ ] framework . Response {
http . StatusOK : { {
Description : "OK" ,
Fields : map [ string ] * framework . FieldSchema {
"enable_rate_limit_audit_logging" : {
Type : framework . TypeBool ,
Required : true ,
} ,
"enable_rate_limit_response_headers" : {
Type : framework . TypeBool ,
Required : true ,
} ,
"rate_limit_exempt_paths" : {
Type : framework . TypeStringSlice ,
Required : true ,
} ,
} ,
} } ,
} ,
2020-06-26 21:13:16 +00:00
} ,
} ,
HelpSynopsis : strings . TrimSpace ( quotasHelp [ "quotas-config" ] [ 0 ] ) ,
HelpDescription : strings . TrimSpace ( quotasHelp [ "quotas-config" ] [ 1 ] ) ,
} ,
{
Pattern : "quotas/rate-limit/?$" ,
Operations : map [ logical . Operation ] framework . OperationHandler {
logical . ListOperation : & framework . PathOperation {
Callback : b . handleRateLimitQuotasList ( ) ,
2023-02-15 20:02:21 +00:00
Responses : map [ int ] [ ] framework . Response {
http . StatusOK : { {
Description : "OK" ,
Fields : map [ string ] * framework . FieldSchema {
"keys" : {
Type : framework . TypeStringSlice ,
Required : true ,
} ,
} ,
} } ,
} ,
2020-06-26 21:13:16 +00:00
} ,
} ,
HelpSynopsis : strings . TrimSpace ( quotasHelp [ "rate-limit-list" ] [ 0 ] ) ,
HelpDescription : strings . TrimSpace ( quotasHelp [ "rate-limit-list" ] [ 1 ] ) ,
} ,
{
Pattern : "quotas/rate-limit/" + framework . GenericNameRegex ( "name" ) ,
Fields : map [ string ] * framework . FieldSchema {
"type" : {
Type : framework . TypeString ,
Description : "Type of the quota rule." ,
} ,
"name" : {
Type : framework . TypeString ,
Description : "Name of the quota rule." ,
} ,
"path" : {
Type : framework . TypeString ,
Description : ` Path of the mount or namespace to apply the quota . A blank path configures a
global quota . For example namespace1 / adds a quota to a full namespace ,
namespace1 / auth / userpass adds a quota to userpass in namespace1 . ` ,
2022-06-21 13:31:36 +00:00
} ,
"role" : {
Type : framework . TypeString ,
Description : ` Login role to apply this quota to . Note that when set , path must be configured
to a valid auth method with a concept of roles . ` ,
2020-06-26 21:13:16 +00:00
} ,
"rate" : {
Type : framework . TypeFloat ,
2020-07-29 19:15:05 +00:00
Description : ` The maximum number of requests in a given interval to be allowed by the quota rule .
2020-07-16 18:34:43 +00:00
The ' rate ' must be positive . ` ,
2020-06-26 21:13:16 +00:00
} ,
2020-07-29 19:15:05 +00:00
"interval" : {
Type : framework . TypeDurationSecond ,
Description : "The duration to enforce rate limiting for (default '1s')." ,
} ,
2020-08-17 02:09:18 +00:00
"block_interval" : {
Type : framework . TypeDurationSecond ,
Description : ` If set , when a client reaches a rate limit threshold , the client will be prohibited
from any further requests until after the ' block_interval ' has elapsed . ` ,
} ,
2020-06-26 21:13:16 +00:00
} ,
Operations : map [ logical . Operation ] framework . OperationHandler {
logical . UpdateOperation : & framework . PathOperation {
Callback : b . handleRateLimitQuotasUpdate ( ) ,
2023-02-15 20:02:21 +00:00
Responses : map [ int ] [ ] framework . Response {
http . StatusNoContent : { {
Description : http . StatusText ( http . StatusNoContent ) ,
} } ,
} ,
2020-06-26 21:13:16 +00:00
} ,
logical . ReadOperation : & framework . PathOperation {
Callback : b . handleRateLimitQuotasRead ( ) ,
2023-02-15 20:02:21 +00:00
Responses : map [ int ] [ ] framework . Response {
http . StatusOK : { {
Description : "OK" ,
Fields : map [ string ] * framework . FieldSchema {
"type" : {
Type : framework . TypeString ,
Required : true ,
} ,
"name" : {
Type : framework . TypeString ,
Required : true ,
} ,
"path" : {
Type : framework . TypeString ,
Required : true ,
} ,
"role" : {
Type : framework . TypeString ,
Required : true ,
} ,
"rate" : {
Type : framework . TypeFloat ,
Required : true ,
} ,
"interval" : {
Type : framework . TypeInt ,
Required : true ,
} ,
"block_interval" : {
Type : framework . TypeInt ,
Required : true ,
} ,
} ,
} } ,
} ,
2020-06-26 21:13:16 +00:00
} ,
logical . DeleteOperation : & framework . PathOperation {
Callback : b . handleRateLimitQuotasDelete ( ) ,
2023-02-15 20:02:21 +00:00
Responses : map [ int ] [ ] framework . Response {
http . StatusNoContent : { {
Description : "OK" ,
} } ,
} ,
2020-06-26 21:13:16 +00:00
} ,
} ,
HelpSynopsis : strings . TrimSpace ( quotasHelp [ "rate-limit" ] [ 0 ] ) ,
HelpDescription : strings . TrimSpace ( quotasHelp [ "rate-limit" ] [ 1 ] ) ,
} ,
}
}
func ( b * SystemBackend ) handleQuotasConfigUpdate ( ) framework . OperationFunc {
return func ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( * logical . Response , error ) {
config , err := quotas . LoadConfig ( ctx , b . Core . systemBarrierView )
if err != nil {
return nil , err
}
2020-07-29 19:15:05 +00:00
2020-06-26 21:13:16 +00:00
config . EnableRateLimitAuditLogging = d . Get ( "enable_rate_limit_audit_logging" ) . ( bool )
2020-07-29 19:15:05 +00:00
config . EnableRateLimitResponseHeaders = d . Get ( "enable_rate_limit_response_headers" ) . ( bool )
2020-10-16 18:58:19 +00:00
config . RateLimitExemptPaths = d . Get ( "rate_limit_exempt_paths" ) . ( [ ] string )
2020-06-26 21:13:16 +00:00
entry , err := logical . StorageEntryJSON ( quotas . ConfigPath , config )
if err != nil {
return nil , err
}
if err := req . Storage . Put ( ctx , entry ) ; err != nil {
return nil , err
}
2020-10-16 18:58:19 +00:00
entry , err = logical . StorageEntryJSON ( quotas . DefaultRateLimitExemptPathsToggle , true )
if err != nil {
return nil , err
}
if err := req . Storage . Put ( ctx , entry ) ; err != nil {
return nil , err
}
2020-06-26 21:13:16 +00:00
b . Core . quotaManager . SetEnableRateLimitAuditLogging ( config . EnableRateLimitAuditLogging )
2020-07-29 19:15:05 +00:00
b . Core . quotaManager . SetEnableRateLimitResponseHeaders ( config . EnableRateLimitResponseHeaders )
2020-10-16 18:58:19 +00:00
b . Core . quotaManager . SetRateLimitExemptPaths ( config . RateLimitExemptPaths )
2020-07-29 19:15:05 +00:00
2020-06-26 21:13:16 +00:00
return nil , nil
}
}
func ( b * SystemBackend ) handleQuotasConfigRead ( ) framework . OperationFunc {
return func ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( * logical . Response , error ) {
config := b . Core . quotaManager . Config ( )
return & logical . Response {
Data : map [ string ] interface { } {
2020-07-29 19:15:05 +00:00
"enable_rate_limit_audit_logging" : config . EnableRateLimitAuditLogging ,
"enable_rate_limit_response_headers" : config . EnableRateLimitResponseHeaders ,
2020-10-16 18:58:19 +00:00
"rate_limit_exempt_paths" : config . RateLimitExemptPaths ,
2020-06-26 21:13:16 +00:00
} ,
} , nil
}
}
func ( b * SystemBackend ) handleRateLimitQuotasList ( ) framework . OperationFunc {
return func ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( * logical . Response , error ) {
names , err := b . Core . quotaManager . QuotaNames ( quotas . TypeRateLimit )
if err != nil {
return nil , err
}
return logical . ListResponse ( names ) , nil
}
}
func ( b * SystemBackend ) handleRateLimitQuotasUpdate ( ) framework . OperationFunc {
return func ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( * logical . Response , error ) {
name := d . Get ( "name" ) . ( string )
qType := quotas . TypeRateLimit . String ( )
rate := d . Get ( "rate" ) . ( float64 )
if rate <= 0 {
return logical . ErrorResponse ( "'rate' is invalid" ) , nil
}
2020-07-29 19:15:05 +00:00
interval := time . Second * time . Duration ( d . Get ( "interval" ) . ( int ) )
if interval == 0 {
interval = time . Second
}
2020-08-17 02:09:18 +00:00
blockInterval := time . Second * time . Duration ( d . Get ( "block_interval" ) . ( int ) )
if blockInterval < 0 {
return logical . ErrorResponse ( "'block' is invalid" ) , nil
}
2020-06-26 21:13:16 +00:00
mountPath := sanitizePath ( d . Get ( "path" ) . ( string ) )
ns := b . Core . namespaceByPath ( mountPath )
if ns . ID != namespace . RootNamespaceID {
mountPath = strings . TrimPrefix ( mountPath , ns . Path )
}
2022-06-16 17:23:02 +00:00
var pathSuffix string
2020-06-26 21:13:16 +00:00
if mountPath != "" {
2022-06-16 17:23:02 +00:00
me := b . Core . router . MatchingMountEntry ( namespace . ContextWithNamespace ( ctx , ns ) , mountPath )
if me == nil {
2020-06-26 21:13:16 +00:00
return logical . ErrorResponse ( "invalid mount path %q" , mountPath ) , nil
}
2022-06-16 17:23:02 +00:00
2022-06-17 12:52:43 +00:00
mountAPIPath := me . APIPathNoNamespace ( )
pathSuffix = strings . TrimSuffix ( strings . TrimPrefix ( mountPath , mountAPIPath ) , "/" )
mountPath = mountAPIPath
2020-06-26 21:13:16 +00:00
}
2020-11-05 16:18:07 +00:00
2022-06-21 13:31:36 +00:00
role := d . Get ( "role" ) . ( string )
// If this is a quota with a role, ensure the backend supports role resolution
if role != "" {
if pathSuffix != "" {
return logical . ErrorResponse ( "Quotas cannot contain both a path suffix and a role. If a role is provided, path must be a valid auth mount with a concept of roles" ) , nil
}
authBackend := b . Core . router . MatchingBackend ( namespace . ContextWithNamespace ( ctx , ns ) , mountPath )
if authBackend == nil || authBackend . Type ( ) != logical . TypeCredential {
2022-07-05 17:02:00 +00:00
return logical . ErrorResponse ( "Mount path %q is not a valid auth method and therefore unsuitable for use with role-based quotas" , mountPath ) , nil
2022-06-21 13:31:36 +00:00
}
// We will always error as we aren't supplying real data, but we're looking for "unsupported operation" in particular
_ , err := authBackend . HandleRequest ( ctx , & logical . Request {
Path : "login" ,
Operation : logical . ResolveRoleOperation ,
} )
if err != nil && ( err == logical . ErrUnsupportedOperation || err == logical . ErrUnsupportedPath ) {
2022-07-05 17:02:00 +00:00
return logical . ErrorResponse ( "Mount path %q does not support use with role-based quotas" , mountPath ) , nil
2022-06-21 13:31:36 +00:00
}
}
2022-06-24 12:58:02 +00:00
// Disallow creation of new quota that has properties similar to an
// existing quota.
quotaByFactors , err := b . Core . quotaManager . QuotaByFactors ( ctx , qType , ns . Path , mountPath , pathSuffix , role )
if err != nil {
return nil , err
}
if quotaByFactors != nil && quotaByFactors . QuotaName ( ) != name {
return logical . ErrorResponse ( "quota rule with similar properties exists under the name %q" , quotaByFactors . QuotaName ( ) ) , nil
}
2020-07-15 17:25:00 +00:00
// If a quota already exists, fetch and update it.
quota , err := b . Core . quotaManager . QuotaByName ( qType , name )
2020-06-26 21:13:16 +00:00
if err != nil {
return nil , err
}
switch {
case quota == nil :
2022-06-24 12:58:02 +00:00
quota = quotas . NewRateLimitQuota ( name , ns . Path , mountPath , pathSuffix , role , rate , interval , blockInterval )
2020-06-26 21:13:16 +00:00
default :
2021-02-09 10:46:09 +00:00
// Re-inserting the already indexed object in memdb might cause problems.
// So, clone the object. See https://github.com/hashicorp/go-memdb/issues/76.
2022-02-17 20:17:59 +00:00
clonedQuota := quota . Clone ( )
rlq := clonedQuota . ( * quotas . RateLimitQuota )
2020-06-26 21:13:16 +00:00
rlq . NamespacePath = ns . Path
rlq . MountPath = mountPath
2022-06-16 17:23:02 +00:00
rlq . PathSuffix = pathSuffix
2020-06-26 21:13:16 +00:00
rlq . Rate = rate
2020-07-29 19:15:05 +00:00
rlq . Interval = interval
2020-08-17 02:09:18 +00:00
rlq . BlockInterval = blockInterval
2021-02-09 10:46:09 +00:00
quota = rlq
2020-06-26 21:13:16 +00:00
}
entry , err := logical . StorageEntryJSON ( quotas . QuotaStoragePath ( qType , name ) , quota )
if err != nil {
return nil , err
}
if err := req . Storage . Put ( ctx , entry ) ; err != nil {
return nil , err
}
if err := b . Core . quotaManager . SetQuota ( ctx , qType , quota , false ) ; err != nil {
return nil , err
}
return nil , nil
}
}
func ( b * SystemBackend ) handleRateLimitQuotasRead ( ) framework . OperationFunc {
return func ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( * logical . Response , error ) {
name := d . Get ( "name" ) . ( string )
qType := quotas . TypeRateLimit . String ( )
quota , err := b . Core . quotaManager . QuotaByName ( qType , name )
if err != nil {
return nil , err
}
if quota == nil {
return nil , nil
}
rlq := quota . ( * quotas . RateLimitQuota )
nsPath := rlq . NamespacePath
if rlq . NamespacePath == "root" {
nsPath = ""
}
data := map [ string ] interface { } {
2020-08-17 02:09:18 +00:00
"type" : qType ,
"name" : rlq . Name ,
2022-06-16 17:23:02 +00:00
"path" : nsPath + rlq . MountPath + rlq . PathSuffix ,
2022-06-24 12:58:02 +00:00
"role" : rlq . Role ,
2020-08-17 02:09:18 +00:00
"rate" : rlq . Rate ,
"interval" : int ( rlq . Interval . Seconds ( ) ) ,
"block_interval" : int ( rlq . BlockInterval . Seconds ( ) ) ,
2020-06-26 21:13:16 +00:00
}
return & logical . Response {
Data : data ,
} , nil
}
}
func ( b * SystemBackend ) handleRateLimitQuotasDelete ( ) framework . OperationFunc {
return func ( ctx context . Context , req * logical . Request , d * framework . FieldData ) ( * logical . Response , error ) {
name := d . Get ( "name" ) . ( string )
qType := quotas . TypeRateLimit . String ( )
if err := req . Storage . Delete ( ctx , quotas . QuotaStoragePath ( qType , name ) ) ; err != nil {
return nil , err
}
if err := b . Core . quotaManager . DeleteQuota ( ctx , qType , name ) ; err != nil {
return nil , err
}
return nil , nil
}
}
var quotasHelp = map [ string ] [ 2 ] string {
"quotas-config" : {
"Create, update and read the quota configuration." ,
"" ,
} ,
"rate-limit" : {
` Get , create or update rate limit resource quota for an optional namespace or
mount . ` ,
2020-07-29 19:15:05 +00:00
` A rate limit quota will enforce API rate limiting in a specified interval . A
2020-06-26 21:13:16 +00:00
rate limit quota can be created at the root level or defined on a namespace or
mount by specifying a ' path ' . The rate limiter is applied to each unique client
2020-07-16 18:34:43 +00:00
IP address . ` ,
2020-06-26 21:13:16 +00:00
} ,
"rate-limit-list" : {
"Lists the names of all the rate limit quotas." ,
"This list contains quota definitions from all the namespaces." ,
} ,
}