mirror of https://github.com/hashicorp/boundary
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
285 lines
9.8 KiB
285 lines
9.8 KiB
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package ratelimit
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"reflect"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/hashicorp/boundary/internal/errors"
|
|
"github.com/hashicorp/boundary/internal/types/action"
|
|
"github.com/hashicorp/boundary/internal/types/resource"
|
|
"github.com/hashicorp/go-rate"
|
|
|
|
// Imported to register all actions for all resources
|
|
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/accounts"
|
|
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/authmethods"
|
|
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/authtokens"
|
|
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/billing"
|
|
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/credentiallibraries"
|
|
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/credentials"
|
|
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/credentialstores"
|
|
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/groups"
|
|
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/health"
|
|
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/host_catalogs"
|
|
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/host_sets"
|
|
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/hosts"
|
|
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/managed_groups"
|
|
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/policies"
|
|
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/roles"
|
|
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/scopes"
|
|
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/session_recordings"
|
|
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/sessions"
|
|
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/storage_buckets"
|
|
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/targets"
|
|
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/users"
|
|
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/workers"
|
|
)
|
|
|
|
// Defaults used when creating default rate.Limits.
|
|
const (
|
|
DefaultInTotalRequestLimit = 30000
|
|
DefaultIpAddressRequestLimit = 30000
|
|
DefaultAuthTokenRequestLimit = 3000
|
|
DefaultPeriod = time.Second * 30
|
|
DefaultInTotalListRequestLimit = 1500
|
|
DefaultIpAddressListRequestLimit = 1500
|
|
DefaultAuthTokenListRequestLimit = 150
|
|
DefaultListPeriod = time.Second * 30
|
|
)
|
|
|
|
// defaultLimiterMaxQuotas is the default maximum number of quotas that
|
|
// can be tracked by the rate limiter.
|
|
// This is determined at initialization time based on the number of endpoints.
|
|
var defaultLimiterMaxQuotas int
|
|
|
|
var initDefaultLimiterMaxQuotas sync.Once
|
|
|
|
// DefaultLimiterMaxQuotas returns the default maximum number of quotas that
|
|
// can be tracked by the rate limiter.
|
|
func DefaultLimiterMaxQuotas() int {
|
|
initDefaultLimiterMaxQuotas.Do(func() {
|
|
// Calculate the default max number of quotas that the rate limiter can
|
|
// store. This is calculated based on the number of endpoints and a
|
|
// static number of quotas per endpoint. This seems like a reasonable
|
|
// way to determine a sane default value. However, it should be noted
|
|
// that this total is shared across all endpoints, and some endpoints
|
|
// will be used more frequently than others.
|
|
|
|
const quotasPerInTotal = 1
|
|
const quotasPerIpAddress = 1000
|
|
const quotasPerAuthToken = 1000
|
|
|
|
var endpointCount int
|
|
for _, res := range resource.Map {
|
|
switch res {
|
|
case resource.Unknown, resource.All, resource.Controller:
|
|
continue
|
|
}
|
|
|
|
actions, err := action.ActionSetForResource(res)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("No actions registered for resource %q", res.String()))
|
|
}
|
|
endpointCount += len(actions)
|
|
}
|
|
|
|
defaultLimiterMaxQuotas = (endpointCount * quotasPerInTotal) +
|
|
(endpointCount * quotasPerAuthToken) +
|
|
(endpointCount * quotasPerIpAddress)
|
|
})
|
|
return defaultLimiterMaxQuotas
|
|
}
|
|
|
|
// Config is used to configure rate limits. Each config is used to specify
|
|
// the maximum number of requests that can be made in a time period for the
|
|
// corresponding resources and actions.
|
|
type Config struct {
|
|
Resources []string `hcl:"resources"`
|
|
Actions []string `hcl:"actions"`
|
|
Per string `hcl:"per"`
|
|
Limit int `hcl:"limit"`
|
|
PeriodHCL string `hcl:"period"`
|
|
Period time.Duration `hcl:"-"`
|
|
Unlimited bool `hcl:"unlimited"`
|
|
}
|
|
|
|
// Configs is an ordered set of Config.
|
|
type Configs []*Config
|
|
|
|
// Equal checks if a set of Configs is equal to another set of Configs.
|
|
func (c Configs) Equal(o Configs) bool {
|
|
return reflect.DeepEqual(c, o)
|
|
}
|
|
|
|
// Limits creates a slice of rate.Limit from the Configs. This will enumerate
|
|
// every combination of resource+action, defining a Limit for each.
|
|
func (c Configs) Limits(ctx context.Context) ([]rate.Limit, error) {
|
|
const op = "ratelimit.(Configs).Limits"
|
|
|
|
defaults := make(map[string]rate.Limit, len(resource.Map)*len(action.Map))
|
|
|
|
allResources := make([]resource.Type, 0, len(resource.Map))
|
|
for _, res := range resource.Map {
|
|
switch res {
|
|
case resource.Unknown, resource.All, resource.Controller:
|
|
continue
|
|
}
|
|
allResources = append(allResources, res)
|
|
}
|
|
|
|
for _, res := range allResources {
|
|
validActions, err := action.ActionSetForResource(res)
|
|
if err != nil {
|
|
// This shouldn't be possible, since we should have encountered
|
|
// this error during init and panicked already. If for some reason
|
|
// that was not the case, it seems like a good idea to panic here.
|
|
panic(fmt.Sprintf("No actions registered for resource %q", res.String()))
|
|
}
|
|
|
|
for a := range validActions {
|
|
inTotalKey := fmt.Sprintf("%s:%s:%s", res.String(), a.String(), rate.LimitPerTotal)
|
|
authTokenKey := fmt.Sprintf("%s:%s:%s", res.String(), a.String(), rate.LimitPerAuthToken)
|
|
ipAddressKey := fmt.Sprintf("%s:%s:%s", res.String(), a.String(), rate.LimitPerIPAddress)
|
|
switch a {
|
|
case action.List:
|
|
defaults[inTotalKey] = &rate.Limited{
|
|
Resource: res.String(),
|
|
Action: a.String(),
|
|
Per: rate.LimitPerTotal,
|
|
MaxRequests: DefaultInTotalListRequestLimit,
|
|
Period: DefaultListPeriod,
|
|
}
|
|
defaults[authTokenKey] = &rate.Limited{
|
|
Resource: res.String(),
|
|
Action: a.String(),
|
|
Per: rate.LimitPerAuthToken,
|
|
MaxRequests: DefaultAuthTokenListRequestLimit,
|
|
Period: DefaultListPeriod,
|
|
}
|
|
defaults[ipAddressKey] = &rate.Limited{
|
|
Resource: res.String(),
|
|
Action: a.String(),
|
|
Per: rate.LimitPerIPAddress,
|
|
MaxRequests: DefaultIpAddressListRequestLimit,
|
|
Period: DefaultListPeriod,
|
|
}
|
|
default:
|
|
defaults[inTotalKey] = &rate.Limited{
|
|
Resource: res.String(),
|
|
Action: a.String(),
|
|
Per: rate.LimitPerTotal,
|
|
MaxRequests: DefaultInTotalRequestLimit,
|
|
Period: DefaultPeriod,
|
|
}
|
|
defaults[authTokenKey] = &rate.Limited{
|
|
Resource: res.String(),
|
|
Action: a.String(),
|
|
Per: rate.LimitPerAuthToken,
|
|
MaxRequests: DefaultAuthTokenRequestLimit,
|
|
Period: DefaultPeriod,
|
|
}
|
|
defaults[ipAddressKey] = &rate.Limited{
|
|
Resource: res.String(),
|
|
Action: a.String(),
|
|
Per: rate.LimitPerIPAddress,
|
|
MaxRequests: DefaultIpAddressRequestLimit,
|
|
Period: DefaultPeriod,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, cc := range c {
|
|
var resourceSet []resource.Type
|
|
switch {
|
|
case len(cc.Resources) == 1 && cc.Resources[0] == resource.All.String():
|
|
resourceSet = allResources
|
|
default:
|
|
for _, r := range cc.Resources {
|
|
rr, ok := resource.Map[r]
|
|
if !ok {
|
|
return nil, errors.New(ctx, errors.InvalidConfiguration, op, "", errors.WithMsg("unknown resource %s", r))
|
|
}
|
|
resourceSet = append(resourceSet, rr)
|
|
}
|
|
}
|
|
|
|
switch {
|
|
case len(cc.Actions) == 1 && cc.Actions[0] == action.All.String():
|
|
for _, res := range resourceSet {
|
|
validActions, err := action.ActionSetForResource(res)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for a := range validActions {
|
|
key := fmt.Sprintf("%s:%s:%s", res.String(), a.String(), rate.LimitPer(cc.Per))
|
|
|
|
switch {
|
|
case cc.Unlimited:
|
|
defaults[key] = &rate.Unlimited{
|
|
Resource: res.String(),
|
|
Action: a.String(),
|
|
Per: rate.LimitPer(cc.Per),
|
|
}
|
|
default:
|
|
defaults[key] = &rate.Limited{
|
|
Resource: res.String(),
|
|
Action: a.String(),
|
|
Per: rate.LimitPer(cc.Per),
|
|
MaxRequests: uint64(cc.Limit),
|
|
Period: cc.Period,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
default:
|
|
for _, res := range resourceSet {
|
|
validActions, err := action.ActionSetForResource(res)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
validActionMap := make(map[string]action.Type, len(validActions))
|
|
for a := range validActions {
|
|
validActionMap[a.String()] = a
|
|
}
|
|
|
|
for _, aStr := range cc.Actions {
|
|
a, ok := validActionMap[aStr]
|
|
if !ok {
|
|
return nil, errors.New(ctx, errors.InvalidConfiguration, op, "", errors.WithMsg("action %s not valid for resource %s", aStr, res.String()))
|
|
}
|
|
key := fmt.Sprintf("%s:%s:%s", res.String(), a.String(), rate.LimitPer(cc.Per))
|
|
|
|
switch {
|
|
case cc.Unlimited:
|
|
defaults[key] = &rate.Unlimited{
|
|
Resource: res.String(),
|
|
Action: a.String(),
|
|
Per: rate.LimitPer(cc.Per),
|
|
}
|
|
default:
|
|
defaults[key] = &rate.Limited{
|
|
Resource: res.String(),
|
|
Action: a.String(),
|
|
Per: rate.LimitPer(cc.Per),
|
|
MaxRequests: uint64(cc.Limit),
|
|
Period: cc.Period,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
limits := make([]rate.Limit, 0, len(defaults))
|
|
for _, v := range defaults {
|
|
limits = append(limits, v)
|
|
}
|
|
return limits, nil
|
|
}
|