Skip to content

Commit

Permalink
Applied OSS changes patch (#24040)
Browse files Browse the repository at this point in the history
* Applied OSS changes patch

* Added changelog

* Fix lease count quota issues with stub

* Update quotas_util.go

* Update quotas_util.go

* Edit quota manager to initialize with sync

* Update quotas.go
  • Loading branch information
divyaac authored Nov 6, 2023
1 parent 996e61d commit e8e3061
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 25 deletions.
3 changes: 3 additions & 0 deletions changelog/24040.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:feature
**Quotas in Privileged Namespaces**: Enable creation/update/deletion of quotas from the privileged namespace
```
2 changes: 1 addition & 1 deletion vault/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -3452,7 +3452,7 @@ func (c *Core) ApplyRateLimitQuota(ctx context.Context, req *quotas.Request) (qu

if c.quotaManager != nil {
// skip rate limit checks for paths that are exempt from rate limiting
if c.quotaManager.RateLimitPathExempt(req.Path) {
if c.quotaManager.RateLimitPathExempt(req.Path, req.NamespacePath) {
return resp, nil
}

Expand Down
95 changes: 85 additions & 10 deletions vault/logical_system_quotas.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package vault

import (
"context"
"errors"
"net/http"
"strings"
"time"
Expand All @@ -16,6 +17,13 @@ import (
"github.com/hashicorp/vault/vault/quotas"
)

var (
ErrExemptRateLimitsOnChildNs = errors.New("exempt paths can only be be configured in the root namespace")
ErrInvalidQuotaDeletion = "cannot delete quota configured for a parent namespace"
ErrInvalidQuotaUpdate = "quotas in parent namespaces cannot be updated"
ErrInvalidQuotaOnParentNs = "quotas cannot be configured for parent namespaces"
)

// quotasPaths returns paths that enable quota management
func (b *SystemBackend) quotasPaths() []*framework.Path {
return []*framework.Path{
Expand All @@ -39,6 +47,10 @@ func (b *SystemBackend) quotasPaths() []*framework.Path {
Type: framework.TypeBool,
Description: "If set, additional rate limit quota HTTP headers will be added to responses.",
},
"absolute_rate_limit_exempt_paths": {
Type: framework.TypeStringSlice,
Description: "Specifies the list of exempt global paths from all rate limit quotas. If empty no global paths will be exempt.",
},
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.UpdateOperation: &framework.PathOperation{
Expand Down Expand Up @@ -73,6 +85,10 @@ func (b *SystemBackend) quotasPaths() []*framework.Path {
Type: framework.TypeStringSlice,
Required: true,
},
"absolute_rate_limit_exempt_paths": {
Type: framework.TypeStringSlice,
Required: true,
},
},
}},
},
Expand Down Expand Up @@ -220,33 +236,56 @@ from any further requests until after the 'block_interval' has elapsed.`,

func (b *SystemBackend) handleQuotasConfigUpdate() framework.OperationFunc {
return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
ns, err := namespace.FromContext(ctx)
if err != nil {
return nil, err
}

config, err := quotas.LoadConfig(ctx, b.Core.systemBarrierView)
if err != nil {
return nil, err
}

config.EnableRateLimitAuditLogging = d.Get("enable_rate_limit_audit_logging").(bool)
config.EnableRateLimitResponseHeaders = d.Get("enable_rate_limit_response_headers").(bool)
config.RateLimitExemptPaths = d.Get("rate_limit_exempt_paths").([]string)

_, ok := d.GetOk("absolute_rate_limit_exempt_paths")
// Global rate limit exempt paths can only be defined in the root namespace
if ns.ID != namespace.RootNamespaceID && ok {
return nil, ErrExemptRateLimitsOnChildNs
}

_, ok = d.GetOk("rate_limit_exempt_paths")
// Relative rate limit exempt paths can only be defined in the root namespace
if ns.ID != namespace.RootNamespaceID && ok {
return nil, ErrExemptRateLimitsOnChildNs
}

// Set rate limit exempt paths to correct configuration fields only if in root namespace
if ns.ID == namespace.RootNamespaceID {
config.RateLimitExemptPaths = d.Get("rate_limit_exempt_paths").([]string)
config.AbsoluteRateLimitExemptPaths = d.Get("absolute_rate_limit_exempt_paths").([]string)
}

entry, err := logical.StorageEntryJSON(quotas.ConfigPath, config)
if err != nil {
return nil, err
}
if err := req.Storage.Put(ctx, entry); err != nil {
if err := b.Core.systemBarrierView.Put(ctx, entry); err != nil {
return nil, err
}

entry, err = logical.StorageEntryJSON(quotas.DefaultRateLimitExemptPathsToggle, true)
if err != nil {
return nil, err
}
if err := req.Storage.Put(ctx, entry); err != nil {
if err := b.Core.systemBarrierView.Put(ctx, entry); err != nil {
return nil, err
}

b.Core.quotaManager.SetEnableRateLimitAuditLogging(config.EnableRateLimitAuditLogging)
b.Core.quotaManager.SetEnableRateLimitResponseHeaders(config.EnableRateLimitResponseHeaders)
b.Core.quotaManager.SetGlobalRateLimitExemptPaths(config.AbsoluteRateLimitExemptPaths)
b.Core.quotaManager.SetRateLimitExemptPaths(config.RateLimitExemptPaths)

return nil, nil
Expand All @@ -261,6 +300,7 @@ func (b *SystemBackend) handleQuotasConfigRead() framework.OperationFunc {
"enable_rate_limit_audit_logging": config.EnableRateLimitAuditLogging,
"enable_rate_limit_response_headers": config.EnableRateLimitResponseHeaders,
"rate_limit_exempt_paths": config.RateLimitExemptPaths,
"absolute_rate_limit_exempt_paths": config.AbsoluteRateLimitExemptPaths,
},
}, nil
}
Expand Down Expand Up @@ -297,7 +337,29 @@ func (b *SystemBackend) handleRateLimitQuotasUpdate() framework.OperationFunc {
return logical.ErrorResponse("'block' is invalid"), nil
}

mountPath := sanitizePath(d.Get("path").(string))
rawPath := sanitizePath(d.Get("path").(string))
mountPath := rawPath

// If the quota creation endpoint is being called from the privileged namespace, we want to prepend the namespace to the path
currentNamespace, err := namespace.FromContext(ctx)
if err != nil {
return logical.ErrorResponse(err.Error()), nil
}
if currentNamespace.ID != namespace.RootNamespaceID && !strings.HasPrefix(mountPath, currentNamespace.Path) {
return logical.ErrorResponse(ErrInvalidQuotaOnParentNs), nil
}

// If there is a quota by the same name that was configured on a parent namespace, prohibit updating this quota
if currentNamespace.ID != namespace.RootNamespaceID {
quota, err := b.Core.quotaManager.QuotaByName(qType, name)
if err != nil {
return nil, err
}
if quota != nil && !strings.HasPrefix(quota.GetNamespacePath(), currentNamespace.Path) {
return logical.ErrorResponse(ErrInvalidQuotaUpdate), nil
}
}

ns := b.Core.namespaceByPath(mountPath)
if ns.ID != namespace.RootNamespaceID {
mountPath = strings.TrimPrefix(mountPath, ns.Path)
Expand Down Expand Up @@ -337,7 +399,7 @@ func (b *SystemBackend) handleRateLimitQuotasUpdate() framework.OperationFunc {

var inheritable bool
// All global quotas should be inherited by default
if ns.Path == "" {
if rawPath == "" {
inheritable = true
}

Expand All @@ -347,14 +409,14 @@ func (b *SystemBackend) handleRateLimitQuotasUpdate() framework.OperationFunc {
if pathSuffix != "" || role != "" || mountPath != "" {
return logical.ErrorResponse("only namespace quotas can be configured as inheritable"), nil
}
} else if ns.Path == "" {
} else if rawPath == "" {
// 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 {
if rawPath == "" && !inheritable {
return logical.ErrorResponse("all global quotas must be inheritable"), nil
}

Expand Down Expand Up @@ -391,13 +453,12 @@ func (b *SystemBackend) handleRateLimitQuotasUpdate() framework.OperationFunc {
rlq.BlockInterval = blockInterval
quota = rlq
}

entry, err := logical.StorageEntryJSON(quotas.QuotaStoragePath(qType, name), quota)
if err != nil {
return nil, err
}

if err := req.Storage.Put(ctx, entry); err != nil {
if err := b.Core.systemBarrierView.storage.Put(ctx, entry); err != nil {
return nil, err
}

Expand Down Expand Up @@ -451,7 +512,21 @@ func (b *SystemBackend) handleRateLimitQuotasDelete() framework.OperationFunc {
name := d.Get("name").(string)
qType := quotas.TypeRateLimit.String()

if err := req.Storage.Delete(ctx, quotas.QuotaStoragePath(qType, name)); err != nil {
ns, err := namespace.FromContext(ctx)
if err != nil {
return nil, err
}
if ns.ID != namespace.RootNamespaceID {
quota, err := b.Core.quotaManager.QuotaByName(qType, name)
if err != nil {
return nil, err
}
if quota != nil && !strings.HasPrefix(quota.GetNamespacePath(), ns.Path) {
return logical.ErrorResponse(ErrInvalidQuotaDeletion), nil
}
}

if err := b.Core.systemBarrierView.Delete(ctx, quotas.QuotaStoragePath(qType, name)); err != nil {
return nil, err
}

Expand Down
62 changes: 48 additions & 14 deletions vault/quotas/quotas.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,8 @@ type Manager struct {
// config containing operator preferences and quota behaviors
config *Config

rateLimitPathManager *pathmanager.PathManager
rateLimitPathManager *pathmanager.PathManager
globalRateLimitPathManager *pathmanager.PathManager

storage logical.Storage
ctx context.Context
Expand Down Expand Up @@ -214,6 +215,9 @@ type Quota interface {
// Inheritable indicates if this quota can be applied to child namespaces
IsInheritable() bool

// GetNamespacePath gets the namespace path of the quota
GetNamespacePath() string

// handleRemount updates the mount and namesapce paths of the quota
handleRemount(string, string)
}
Expand Down Expand Up @@ -247,7 +251,14 @@ type Config struct {
// RateLimitExemptPaths defines the set of exempt paths used for all rate limit
// quotas. Any request path that exists in this set is exempt from rate limiting.
// If the set is empty, no paths are exempt.
// The paths specified here are relative and are appended to every namespace during search.
RateLimitExemptPaths []string `json:"rate_limit_exempt_paths"`

// AbsoluteRateLimitExemptPaths defines the set of exempt paths used for all rate limit
// quotas. Any request path that exists in this set is exempt from rate limiting.
// If it is empty, no paths are exempt.
// The paths specified here are absolute paths, and can only be set from the root namespace
AbsoluteRateLimitExemptPaths []string `json:"absolute_rate_limit_exempt_paths"`
}

// Request contains information required by the quota manager to query and
Expand Down Expand Up @@ -282,14 +293,15 @@ func NewManager(logger log.Logger, walkFunc leaseWalkFunc, ms *metricsutil.Clust
}

manager := &Manager{
db: db,
logger: logger,
metricSink: ms,
rateLimitPathManager: pathmanager.New(),
config: new(Config),
quotaLock: &locking.SyncRWMutex{},
quotaConfigLock: &locking.SyncRWMutex{},
dbAndCacheLock: &locking.SyncRWMutex{},
db: db,
logger: logger,
metricSink: ms,
rateLimitPathManager: pathmanager.New(),
globalRateLimitPathManager: pathmanager.New(),
config: new(Config),
quotaLock: &locking.SyncRWMutex{},
quotaConfigLock: &locking.SyncRWMutex{},
dbAndCacheLock: &locking.SyncRWMutex{},
}

if detectDeadlocks {
Expand Down Expand Up @@ -764,6 +776,25 @@ func (m *Manager) setRateLimitExemptPathsLocked(vals []string) {
m.rateLimitPathManager.AddPaths(vals)
}

// SetGlobalRateLimitExemptPaths updates the global rate limit exempt paths in the Manager's
// configuration in addition to updating the path manager. Every call to
// SetGlobalRateLimitExemptPaths will wipe out the existing path manager and set the
// paths based on the provided argument.
func (m *Manager) SetGlobalRateLimitExemptPaths(vals []string) {
m.quotaConfigLock.Lock()
defer m.quotaConfigLock.Unlock()
m.setGlobalRateLimitExemptPathsLocked(vals)
}

func (m *Manager) setGlobalRateLimitExemptPathsLocked(vals []string) {
if vals == nil {
vals = []string{}
}
m.config.AbsoluteRateLimitExemptPaths = vals
m.globalRateLimitPathManager = pathmanager.New()
m.globalRateLimitPathManager.AddPaths(vals)
}

// RateLimitAuditLoggingEnabled returns if the quota configuration allows audit
// logging of request rejections due to rate limiting quota rule violations.
func (m *Manager) RateLimitAuditLoggingEnabled() bool {
Expand All @@ -785,15 +816,18 @@ func (m *Manager) RateLimitResponseHeadersEnabled() bool {
// RateLimitPathExempt returns a boolean dictating if a given path is exempt from
// any rate limit quota. If not rate limit path manager is defined, false is
// returned.
func (m *Manager) RateLimitPathExempt(path string) bool {
func (m *Manager) RateLimitPathExempt(path string, namespacePath string) bool {
m.quotaConfigLock.RLock()
defer m.quotaConfigLock.RUnlock()

if m.rateLimitPathManager == nil {
return false
}

return m.rateLimitPathManager.HasPath(path)
globalRateLimitPath := path
if namespacePath != "root" {
globalRateLimitPath = strings.Join([]string{namespace.Canonicalize(namespacePath), path}, "")
}
return m.globalRateLimitPathManager.HasPath(globalRateLimitPath) || m.rateLimitPathManager.HasPath(path)
}

// Config returns the operator preferences in the quota manager
Expand Down Expand Up @@ -990,6 +1024,7 @@ func (m *Manager) Invalidate(key string) {

m.SetEnableRateLimitAuditLogging(config.EnableRateLimitAuditLogging)
m.SetEnableRateLimitResponseHeaders(config.EnableRateLimitResponseHeaders)
m.SetGlobalRateLimitExemptPaths(config.AbsoluteRateLimitExemptPaths)
m.SetRateLimitExemptPaths(config.RateLimitExemptPaths)

default:
Expand Down Expand Up @@ -1163,8 +1198,7 @@ func (m *Manager) setupQuotaType(ctx context.Context, storage logical.Storage, q
return nil
}

// QuotaStoragePath returns the storage path suffix for persisting the quota
// rule.
// QuotaStoragePath returns the storage path suffix for persisting the quota rule.
func QuotaStoragePath(quotaType, name string) string {
return path.Join(StoragePrefix+quotaType, name)
}
Expand Down
4 changes: 4 additions & 0 deletions vault/quotas/quotas_rate_limit.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ type RateLimitQuota struct {
closePurgeBlockedCh chan struct{}
}

func (q *RateLimitQuota) GetNamespacePath() string {
return q.NamespacePath
}

// NewRateLimitQuota creates a quota checker for imposing limits on the number
// of requests in a given interval. An interval time duration of zero may be
// provided, which will default to 1s when initialized. An optional block
Expand Down
4 changes: 4 additions & 0 deletions vault/quotas/quotas_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ func (*entManager) Reset() error {

type LeaseCountQuota struct{}

func (l LeaseCountQuota) GetNamespacePath() string {
panic("implement me")
}

func (l LeaseCountQuota) IsInheritable() bool {
panic("implement me")
}
Expand Down

0 comments on commit e8e3061

Please sign in to comment.