mirror of
https://github.com/hashicorp/vault.git
synced 2026-02-03 20:40:45 -05:00
Quota Inheritance for Rate Limit (OSS) (#22452)
* Quota Inheritance for Rate Limit (OSS) * Added changelog * Fix lease quota inheritance issue * Edit formatting
This commit is contained in:
parent
8250fba0e6
commit
98523a6874
7 changed files with 104 additions and 20 deletions
3
changelog/22452.txt
Normal file
3
changelog/22452.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
core : Add field that allows rate-limit namespace quotas to be inherited by child namespaces.
|
||||
```
|
||||
|
|
@ -124,6 +124,10 @@ namespace1/auth/userpass adds a quota to userpass in namespace1.`,
|
|||
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.`,
|
||||
},
|
||||
"inheritable": {
|
||||
Type: framework.TypeBool,
|
||||
Description: `Whether all child namespaces can inherit this namespace quota.`,
|
||||
},
|
||||
"rate": {
|
||||
Type: framework.TypeFloat,
|
||||
Description: `The maximum number of requests in a given interval to be allowed by the quota rule.
|
||||
|
|
@ -188,6 +192,10 @@ from any further requests until after the 'block_interval' has elapsed.`,
|
|||
Type: framework.TypeInt,
|
||||
Required: true,
|
||||
},
|
||||
"inheritable": {
|
||||
Type: framework.TypeBool,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
|
|
@ -327,6 +335,29 @@ func (b *SystemBackend) handleRateLimitQuotasUpdate() framework.OperationFunc {
|
|||
}
|
||||
}
|
||||
|
||||
var inheritable bool
|
||||
// All global quotas should be inherited by default
|
||||
if ns.Path == "" {
|
||||
inheritable = true
|
||||
}
|
||||
|
||||
if inheritableRaw, ok := d.GetOk("inheritable"); ok {
|
||||
inheritable = inheritableRaw.(bool)
|
||||
if inheritable {
|
||||
if pathSuffix != "" || role != "" || mountPath != "" {
|
||||
return logical.ErrorResponse("only namespace quotas can be configured as inheritable"), nil
|
||||
}
|
||||
} else if ns.Path == "" {
|
||||
// User should not try to configure a global quota that cannot be inherited
|
||||
return logical.ErrorResponse("all global quotas must be inheritable"), nil
|
||||
}
|
||||
}
|
||||
|
||||
// User should not try to configure a global quota to be uninheritable
|
||||
if ns.Path == "" && !inheritable {
|
||||
return logical.ErrorResponse("all global quotas must be inheritable"), nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
|
@ -345,7 +376,7 @@ func (b *SystemBackend) handleRateLimitQuotasUpdate() framework.OperationFunc {
|
|||
|
||||
switch {
|
||||
case quota == nil:
|
||||
quota = quotas.NewRateLimitQuota(name, ns.Path, mountPath, pathSuffix, role, rate, interval, blockInterval)
|
||||
quota = quotas.NewRateLimitQuota(name, ns.Path, mountPath, pathSuffix, role, inheritable, interval, blockInterval, rate)
|
||||
default:
|
||||
// Re-inserting the already indexed object in memdb might cause problems.
|
||||
// So, clone the object. See https://github.com/hashicorp/go-memdb/issues/76.
|
||||
|
|
@ -355,6 +386,7 @@ func (b *SystemBackend) handleRateLimitQuotasUpdate() framework.OperationFunc {
|
|||
rlq.MountPath = mountPath
|
||||
rlq.PathSuffix = pathSuffix
|
||||
rlq.Rate = rate
|
||||
rlq.Inheritable = inheritable
|
||||
rlq.Interval = interval
|
||||
rlq.BlockInterval = blockInterval
|
||||
quota = rlq
|
||||
|
|
@ -403,6 +435,7 @@ func (b *SystemBackend) handleRateLimitQuotasRead() framework.OperationFunc {
|
|||
"path": nsPath + rlq.MountPath + rlq.PathSuffix,
|
||||
"role": rlq.Role,
|
||||
"rate": rlq.Rate,
|
||||
"inheritable": rlq.Inheritable,
|
||||
"interval": int(rlq.Interval.Seconds()),
|
||||
"block_interval": int(rlq.BlockInterval.Seconds()),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -211,6 +211,9 @@ type Quota interface {
|
|||
// Clone creates a clone of the calling quota
|
||||
Clone() Quota
|
||||
|
||||
// Inheritable indicates if this quota can be applied to child namespaces
|
||||
IsInheritable() bool
|
||||
|
||||
// handleRemount updates the mount and namesapce paths of the quota
|
||||
handleRemount(string, string)
|
||||
}
|
||||
|
|
@ -580,10 +583,24 @@ func (m *Manager) queryQuota(txn *memdb.Txn, req *Request) (Quota, error) {
|
|||
return quota, nil
|
||||
}
|
||||
|
||||
// Fetch parent ns quotas
|
||||
curNsSplitPath := strings.SplitAfter(namespace.Canonicalize(req.NamespacePath), "/")
|
||||
for len(curNsSplitPath) > 2 {
|
||||
parentNs := strings.Join(curNsSplitPath[0:len(curNsSplitPath)-2], "")
|
||||
parentQuota, err := quotaFetchFunc(indexNamespace, parentNs, false, false, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if parentQuota != nil && parentQuota.IsInheritable() {
|
||||
return parentQuota, nil
|
||||
}
|
||||
curNsSplitPath = strings.SplitAfter(parentNs, "/")
|
||||
}
|
||||
|
||||
// If the request belongs to "root" namespace, then we have already looked at
|
||||
// global quotas when fetching namespace specific quota rule. When the request
|
||||
// belongs to a non-root namespace, and when there are no namespace specific
|
||||
// quota rules present, we fallback on the global quotas.
|
||||
// quota rules present, we fall back on the global quotas.
|
||||
if req.NamespacePath == "root" {
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,6 +64,9 @@ type RateLimitQuota struct {
|
|||
// PathSuffix is the path suffix to which this quota is applicable
|
||||
PathSuffix string `json:"path_suffix"`
|
||||
|
||||
// Inheritable indicates whether the quota will be inherited by child namespaces
|
||||
Inheritable bool `json:"inheritable"`
|
||||
|
||||
// Rate defines the number of requests allowed per Interval.
|
||||
Rate float64 `json:"rate"`
|
||||
|
||||
|
|
@ -91,7 +94,7 @@ type RateLimitQuota struct {
|
|||
// provided, which will default to 1s when initialized. An optional block
|
||||
// duration may be provided, where if set, when a client reaches the rate limit,
|
||||
// subsequent requests will fail until the block duration has passed.
|
||||
func NewRateLimitQuota(name, nsPath, mountPath, pathSuffix, role string, rate float64, interval, block time.Duration) *RateLimitQuota {
|
||||
func NewRateLimitQuota(name, nsPath, mountPath, pathSuffix, role string, inheritable bool, interval, block time.Duration, rate float64) *RateLimitQuota {
|
||||
id, err := uuid.GenerateUUID()
|
||||
if err != nil {
|
||||
// Fall back to generating with a hash of the name, later in initialize
|
||||
|
|
@ -105,6 +108,7 @@ func NewRateLimitQuota(name, nsPath, mountPath, pathSuffix, role string, rate fl
|
|||
MountPath: mountPath,
|
||||
Role: role,
|
||||
PathSuffix: pathSuffix,
|
||||
Inheritable: inheritable,
|
||||
Rate: rate,
|
||||
Interval: interval,
|
||||
BlockInterval: block,
|
||||
|
|
@ -119,6 +123,7 @@ func (q *RateLimitQuota) Clone() Quota {
|
|||
Name: q.Name,
|
||||
MountPath: q.MountPath,
|
||||
Role: q.Role,
|
||||
Inheritable: q.Inheritable,
|
||||
Type: q.Type,
|
||||
NamespacePath: q.NamespacePath,
|
||||
PathSuffix: q.PathSuffix,
|
||||
|
|
@ -129,6 +134,10 @@ func (q *RateLimitQuota) Clone() Quota {
|
|||
return rlq
|
||||
}
|
||||
|
||||
func (q *RateLimitQuota) IsInheritable() bool {
|
||||
return q.Inheritable
|
||||
}
|
||||
|
||||
// initialize ensures the namespace and max requests are initialized, sets the ID
|
||||
// if it's currently empty, sets the purge interval and stale age to default
|
||||
// values, and finally starts the client purge go routine if it has been started
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ func TestNewRateLimitQuota(t *testing.T) {
|
|||
rlq *RateLimitQuota
|
||||
expectErr bool
|
||||
}{
|
||||
{"valid rate", NewRateLimitQuota("test-rate-limiter", "qa", "/foo/bar", "", "", 16.7, time.Second, 0), false},
|
||||
{"valid rate", NewRateLimitQuota("test-rate-limiter", "qa", "/foo/bar", "", "", false, time.Second, 0, 16.7), false},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
|
@ -47,7 +47,7 @@ func TestNewRateLimitQuota(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRateLimitQuota_Close(t *testing.T) {
|
||||
rlq := NewRateLimitQuota("test-rate-limiter", "qa", "/foo/bar", "", "", 16.7, time.Second, time.Minute)
|
||||
rlq := NewRateLimitQuota("test-rate-limiter", "qa", "/foo/bar", "", "", false, time.Second, time.Minute, 16.7)
|
||||
require.NoError(t, rlq.initialize(logging.NewVaultLogger(log.Trace), metricsutil.BlackholeSink()))
|
||||
require.NoError(t, rlq.close(context.Background()))
|
||||
|
||||
|
|
@ -221,7 +221,7 @@ func TestRateLimitQuota_Update(t *testing.T) {
|
|||
qm, err := NewManager(logging.NewVaultLogger(log.Trace), nil, metricsutil.BlackholeSink())
|
||||
require.NoError(t, err)
|
||||
|
||||
quota := NewRateLimitQuota("quota1", "", "", "", "", 10, time.Second, 0)
|
||||
quota := NewRateLimitQuota("quota1", "", "", "", "", false, time.Second, 0, 10)
|
||||
require.NoError(t, qm.SetQuota(context.Background(), TypeRateLimit.String(), quota, true))
|
||||
require.NoError(t, qm.SetQuota(context.Background(), TypeRateLimit.String(), quota, true))
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ func TestQuotas_MountPathOverwrite(t *testing.T) {
|
|||
qm, err := NewManager(logging.NewVaultLogger(log.Trace), nil, metricsutil.BlackholeSink())
|
||||
require.NoError(t, err)
|
||||
|
||||
quota := NewRateLimitQuota("tq", "", "kv1/", "", "", 10, time.Second, 0)
|
||||
quota := NewRateLimitQuota("tq", "", "kv1/", "", "", false, time.Second, 0, 10)
|
||||
require.NoError(t, qm.SetQuota(context.Background(), TypeRateLimit.String(), quota, false))
|
||||
quota = quota.Clone().(*RateLimitQuota)
|
||||
quota.MountPath = "kv2/"
|
||||
|
|
@ -46,9 +46,9 @@ func TestQuotas_Precedence(t *testing.T) {
|
|||
qm, err := NewManager(logging.NewVaultLogger(log.Trace), nil, metricsutil.BlackholeSink())
|
||||
require.NoError(t, err)
|
||||
|
||||
setQuotaFunc := func(t *testing.T, name, nsPath, mountPath, pathSuffix, role string) Quota {
|
||||
setQuotaFunc := func(t *testing.T, name, nsPath, mountPath, pathSuffix, role string, inheritable bool) Quota {
|
||||
t.Helper()
|
||||
quota := NewRateLimitQuota(name, nsPath, mountPath, pathSuffix, role, 10, time.Second, 0)
|
||||
quota := NewRateLimitQuota(name, nsPath, mountPath, pathSuffix, role, inheritable, time.Second, 0, 10)
|
||||
require.NoError(t, qm.SetQuota(context.Background(), TypeRateLimit.String(), quota, true))
|
||||
return quota
|
||||
}
|
||||
|
|
@ -73,47 +73,65 @@ func TestQuotas_Precedence(t *testing.T) {
|
|||
checkQuotaFunc(t, "", "", "", "", nil)
|
||||
|
||||
// Define global quota and expect that to be returned.
|
||||
rateLimitGlobalQuota := setQuotaFunc(t, "rateLimitGlobalQuota", "", "", "", "")
|
||||
rateLimitGlobalQuota := setQuotaFunc(t, "rateLimitGlobalQuota", "", "", "", "", true)
|
||||
checkQuotaFunc(t, "", "", "", "", rateLimitGlobalQuota)
|
||||
|
||||
// Define a global mount specific quota and expect that to be returned.
|
||||
rateLimitGlobalMountQuota := setQuotaFunc(t, "rateLimitGlobalMountQuota", "", "testmount/", "", "")
|
||||
rateLimitGlobalMountQuota := setQuotaFunc(t, "rateLimitGlobalMountQuota", "", "testmount/", "", "", false)
|
||||
checkQuotaFunc(t, "", "testmount/", "", "", rateLimitGlobalMountQuota)
|
||||
|
||||
// Define a global mount + path specific quota and expect that to be returned.
|
||||
rateLimitGlobalMountPathQuota := setQuotaFunc(t, "rateLimitGlobalMountPathQuota", "", "testmount/", "testpath", "")
|
||||
rateLimitGlobalMountPathQuota := setQuotaFunc(t, "rateLimitGlobalMountPathQuota", "", "testmount/", "testpath", "", false)
|
||||
checkQuotaFunc(t, "", "testmount/", "testpath", "", rateLimitGlobalMountPathQuota)
|
||||
|
||||
// Define a namespace quota and expect that to be returned.
|
||||
rateLimitNSQuota := setQuotaFunc(t, "rateLimitNSQuota", "testns/", "", "", "")
|
||||
rateLimitNSQuota := setQuotaFunc(t, "rateLimitNSQuota", "testns/", "", "", "", false)
|
||||
checkQuotaFunc(t, "testns/", "", "", "", rateLimitNSQuota)
|
||||
|
||||
// Define a namespace mount specific quota and expect that to be returned.
|
||||
rateLimitNSMountQuota := setQuotaFunc(t, "rateLimitNSMountQuota", "testns/", "testmount/", "", "")
|
||||
rateLimitNSMountQuota := setQuotaFunc(t, "rateLimitNSMountQuota", "testns/", "testmount/", "", "", false)
|
||||
checkQuotaFunc(t, "testns/", "testmount/", "testpath", "", rateLimitNSMountQuota)
|
||||
|
||||
// Define a namespace mount + glob and expect that to be returned.
|
||||
rateLimitNSMountGlob := setQuotaFunc(t, "rateLimitNSMountGlob", "testns/", "testmount/", "*", "")
|
||||
rateLimitNSMountGlob := setQuotaFunc(t, "rateLimitNSMountGlob", "testns/", "testmount/", "*", "", false)
|
||||
checkQuotaFunc(t, "testns/", "testmount/", "testpath", "", rateLimitNSMountGlob)
|
||||
|
||||
// Define a namespace mount + path specific quota with a glob and expect that to be returned.
|
||||
rateLimitNSMountPathSuffixGlob := setQuotaFunc(t, "rateLimitNSMountPathSuffixGlob", "testns/", "testmount/", "test*", "")
|
||||
rateLimitNSMountPathSuffixGlob := setQuotaFunc(t, "rateLimitNSMountPathSuffixGlob", "testns/", "testmount/", "test*", "", false)
|
||||
checkQuotaFunc(t, "testns/", "testmount/", "testpath", "", rateLimitNSMountPathSuffixGlob)
|
||||
|
||||
// Define a namespace mount + path specific quota with a glob at the end of the path and expect that to be returned.
|
||||
rateLimitNSMountPathSuffixGlobAfterPath := setQuotaFunc(t, "rateLimitNSMountPathSuffixGlobAfterPath", "testns/", "testmount/", "testpath*", "")
|
||||
rateLimitNSMountPathSuffixGlobAfterPath := setQuotaFunc(t, "rateLimitNSMountPathSuffixGlobAfterPath", "testns/", "testmount/", "testpath*", "", false)
|
||||
checkQuotaFunc(t, "testns/", "testmount/", "testpath", "", rateLimitNSMountPathSuffixGlobAfterPath)
|
||||
|
||||
// Define a namespace mount + path specific quota and expect that to be returned.
|
||||
rateLimitNSMountPathQuota := setQuotaFunc(t, "rateLimitNSMountPathQuota", "testns/", "testmount/", "testpath", "")
|
||||
rateLimitNSMountPathQuota := setQuotaFunc(t, "rateLimitNSMountPathQuota", "testns/", "testmount/", "testpath", "", false)
|
||||
checkQuotaFunc(t, "testns/", "testmount/", "testpath", "", rateLimitNSMountPathQuota)
|
||||
|
||||
// Define a namespace mount + role specific quota and expect that to be returned.
|
||||
rateLimitNSMountRoleQuota := setQuotaFunc(t, "rateLimitNSMountPathQuota", "testns/", "testmount/", "", "role")
|
||||
rateLimitNSMountRoleQuota := setQuotaFunc(t, "rateLimitNSMountPathQuota", "testns/", "testmount/", "", "role", false)
|
||||
checkQuotaFunc(t, "testns/", "testmount/", "", "role", rateLimitNSMountRoleQuota)
|
||||
|
||||
// Create an inheritable namespace quota and expect that to be returned on a child namespace
|
||||
rateLimitNSInheritableQuota := setQuotaFunc(t, "rateLimitNSInheritableNSQuota", "testns/nested2/", "", "", "", true)
|
||||
checkQuotaFunc(t, "testns/nested2/nested3/", "testmount/", "", "", rateLimitNSInheritableQuota)
|
||||
checkQuotaFunc(t, "testns/nested2/nested3/nested4/", "testmount/", "", "", rateLimitNSInheritableQuota)
|
||||
|
||||
// Create a non-namespace quota on a nested namespace and make sure it takes precedence over the inherited quota
|
||||
rateLimitNonNSNestedQuota := setQuotaFunc(t, "rateLimitNonNSNestedQuota", "testns/nested2/nested3/", "testmount/", "", "", false)
|
||||
checkQuotaFunc(t, "testns/nested2/nested3/", "testmount/", "", "", rateLimitNonNSNestedQuota)
|
||||
|
||||
// Create a non-namespace quota on a nested namespace and make sure it takes precedence over the inherited quota
|
||||
rateLimitMultiNestedNsInheritableQuota := setQuotaFunc(t, "rateLimitNSInheritableNSQuota", "testns/nested2/nested3/", "", "", "", true)
|
||||
checkQuotaFunc(t, "testns/nested2/nested3/nested4/", "testmount/", "", "", rateLimitMultiNestedNsInheritableQuota)
|
||||
|
||||
// Now that many quota types are defined, verify that the most specific
|
||||
// matches are returned per namespace.
|
||||
checkQuotaFunc(t, "", "", "", "", rateLimitGlobalQuota)
|
||||
checkQuotaFunc(t, "testns/", "", "", "", rateLimitNSQuota)
|
||||
checkQuotaFunc(t, "testns/nested1/", "", "", "", rateLimitGlobalQuota)
|
||||
checkQuotaFunc(t, "testns/nested2/", "", "", "", rateLimitNSInheritableQuota)
|
||||
checkQuotaFunc(t, "testns/nested2/nested6/", "", "", "", rateLimitNSInheritableQuota)
|
||||
checkQuotaFunc(t, "testns/nested2/nested3/", "", "", "", rateLimitMultiNestedNsInheritableQuota)
|
||||
checkQuotaFunc(t, "testns/nested2/nested3/nested4/nested5", "", "", "", rateLimitMultiNestedNsInheritableQuota)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import (
|
|||
log "github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/vault/helper/metricsutil"
|
||||
|
||||
memdb "github.com/hashicorp/go-memdb"
|
||||
"github.com/hashicorp/go-memdb"
|
||||
)
|
||||
|
||||
func quotaTypes() []string {
|
||||
|
|
@ -43,6 +43,10 @@ func (*entManager) Reset() error {
|
|||
|
||||
type LeaseCountQuota struct{}
|
||||
|
||||
func (l LeaseCountQuota) IsInheritable() bool {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (l LeaseCountQuota) allow(_ context.Context, _ *Request) (Response, error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue