Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Applied OSS changes patch #24040

Merged
merged 8 commits into from
Nov 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading