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.
672 lines
16 KiB
672 lines
16 KiB
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package controller
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"github.com/hashicorp/boundary/internal/cmd/config"
|
|
"github.com/hashicorp/boundary/internal/event"
|
|
"github.com/hashicorp/boundary/internal/ratelimit"
|
|
"github.com/hashicorp/eventlogger/formatter_filters/cloudevents"
|
|
"github.com/hashicorp/go-hclog"
|
|
"github.com/hashicorp/go-rate"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func testLogger(t *testing.T, testLock hclog.Locker) hclog.Logger {
|
|
t.Helper()
|
|
return hclog.New(&hclog.LoggerOptions{
|
|
Mutex: testLock,
|
|
Name: "test",
|
|
JSONFormat: true,
|
|
})
|
|
}
|
|
|
|
func Test_newRateLimiterConfig(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
var configs ratelimit.Configs
|
|
defaultLimits, err := configs.Limits(ctx)
|
|
require.NoError(t, err)
|
|
|
|
cases := []struct {
|
|
name string
|
|
configs ratelimit.Configs
|
|
maxSize int
|
|
disabled bool
|
|
want *rateLimiterConfig
|
|
wantErr error
|
|
}{
|
|
{
|
|
"disabled",
|
|
nil,
|
|
0,
|
|
true,
|
|
&rateLimiterConfig{disabled: true},
|
|
nil,
|
|
},
|
|
{
|
|
"defaults",
|
|
nil,
|
|
ratelimit.DefaultLimiterMaxQuotas(),
|
|
false,
|
|
&rateLimiterConfig{
|
|
maxSize: 338169,
|
|
configs: nil,
|
|
disabled: false,
|
|
limits: defaultLimits,
|
|
},
|
|
nil,
|
|
},
|
|
{
|
|
"disabledWithConfigs",
|
|
ratelimit.Configs{
|
|
{
|
|
Resources: []string{"*"},
|
|
Actions: []string{"*"},
|
|
Per: "total",
|
|
Limit: 100,
|
|
Period: time.Minute,
|
|
Unlimited: false,
|
|
},
|
|
},
|
|
ratelimit.DefaultLimiterMaxQuotas(),
|
|
true,
|
|
nil,
|
|
fmt.Errorf("controller.newRateLimiterConfig: disabled rate limiter with rate limit configs: configuration issue: error #5000"),
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got, err := newRateLimiterConfig(ctx, tc.configs, tc.maxSize, tc.disabled)
|
|
if tc.wantErr != nil {
|
|
assert.EqualError(t, err, tc.wantErr.Error())
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
assert.Empty(t, cmp.Diff(
|
|
tc.want,
|
|
got,
|
|
cmp.AllowUnexported(rateLimiterConfig{}),
|
|
cmpopts.IgnoreFields(rateLimiterConfig{}, "limits"),
|
|
))
|
|
assert.ElementsMatch(t, got.limits, tc.want.limits)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestController_initializeRateLimiter(t *testing.T) {
|
|
// Disabling eventing so reduce noise.
|
|
event.TestWithoutEventing(t)
|
|
|
|
cases := []struct {
|
|
name string
|
|
conf *config.Config
|
|
wantNopLimiter bool
|
|
wantErr error
|
|
}{
|
|
{
|
|
"disabled",
|
|
&config.Config{
|
|
Controller: &config.Controller{
|
|
ApiRateLimitDisable: true,
|
|
},
|
|
},
|
|
true,
|
|
nil,
|
|
},
|
|
{
|
|
"defaults",
|
|
&config.Config{
|
|
Controller: &config.Controller{
|
|
ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(),
|
|
},
|
|
},
|
|
false,
|
|
nil,
|
|
},
|
|
{
|
|
"invalid",
|
|
&config.Config{
|
|
Controller: &config.Controller{
|
|
ApiRateLimits: ratelimit.Configs{
|
|
{
|
|
Resources: []string{"*"},
|
|
Actions: []string{"*"},
|
|
Per: "total",
|
|
Limit: 100,
|
|
Period: time.Minute,
|
|
Unlimited: false,
|
|
},
|
|
},
|
|
ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(),
|
|
ApiRateLimitDisable: true,
|
|
},
|
|
},
|
|
false,
|
|
fmt.Errorf("controller.newRateLimiterConfig: disabled rate limiter with rate limit configs: configuration issue: error #5000"),
|
|
},
|
|
{
|
|
"nilConfig",
|
|
nil,
|
|
false,
|
|
fmt.Errorf("controller.(Controller).initializeRateLimiter: nil config: parameter violation: error #100"),
|
|
},
|
|
{
|
|
"nilConfigController",
|
|
&config.Config{
|
|
Controller: nil,
|
|
},
|
|
false,
|
|
fmt.Errorf("controller.(Controller).initializeRateLimiter: nil config.Controller: parameter violation: error #100"),
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
c := &Controller{
|
|
baseContext: context.Background(),
|
|
conf: &Config{
|
|
RawConfig: tc.conf,
|
|
},
|
|
}
|
|
err := c.initializeRateLimiter(tc.conf)
|
|
if tc.wantErr != nil {
|
|
assert.EqualError(t, err, tc.wantErr.Error())
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
if tc.wantNopLimiter {
|
|
assert.Equal(t, rate.NopLimiter, c.rateLimiter)
|
|
return
|
|
}
|
|
|
|
_, ok := c.rateLimiter.(*rate.Limiter)
|
|
assert.True(t, ok, "expected rate.Limiter")
|
|
assert.NotNil(t, c.rateLimiter)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestControllerReloadRateLimiter(t *testing.T) {
|
|
// Disabling eventing so reduce noise.
|
|
event.TestWithoutEventing(t)
|
|
|
|
cases := []struct {
|
|
name string
|
|
c *Controller
|
|
conf *config.Config
|
|
wantNewLimiter bool
|
|
wantErr error
|
|
}{
|
|
{
|
|
"newConfigs",
|
|
func() *Controller {
|
|
conf := &config.Config{
|
|
Controller: &config.Controller{
|
|
ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(),
|
|
},
|
|
}
|
|
c := &Controller{
|
|
baseContext: context.Background(),
|
|
conf: &Config{
|
|
RawConfig: conf,
|
|
},
|
|
}
|
|
err := c.initializeRateLimiter(conf)
|
|
require.NoError(t, err)
|
|
return c
|
|
}(),
|
|
&config.Config{
|
|
Controller: &config.Controller{
|
|
ApiRateLimits: ratelimit.Configs{
|
|
{
|
|
Resources: []string{"*"},
|
|
Actions: []string{"*"},
|
|
Per: "total",
|
|
Limit: 100,
|
|
Period: time.Minute,
|
|
Unlimited: false,
|
|
},
|
|
},
|
|
ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(),
|
|
},
|
|
},
|
|
true,
|
|
nil,
|
|
},
|
|
{
|
|
"newMaxSize",
|
|
func() *Controller {
|
|
conf := &config.Config{
|
|
Controller: &config.Controller{
|
|
ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(),
|
|
},
|
|
}
|
|
c := &Controller{
|
|
baseContext: context.Background(),
|
|
conf: &Config{
|
|
RawConfig: conf,
|
|
},
|
|
}
|
|
err := c.initializeRateLimiter(conf)
|
|
require.NoError(t, err)
|
|
return c
|
|
}(),
|
|
&config.Config{
|
|
Controller: &config.Controller{
|
|
ApiRateLimiterMaxQuotas: 3000,
|
|
},
|
|
},
|
|
true,
|
|
nil,
|
|
},
|
|
{
|
|
"newDisabled",
|
|
func() *Controller {
|
|
conf := &config.Config{
|
|
Controller: &config.Controller{
|
|
ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(),
|
|
},
|
|
}
|
|
c := &Controller{
|
|
baseContext: context.Background(),
|
|
conf: &Config{
|
|
RawConfig: conf,
|
|
},
|
|
}
|
|
err := c.initializeRateLimiter(conf)
|
|
require.NoError(t, err)
|
|
return c
|
|
}(),
|
|
&config.Config{
|
|
Controller: &config.Controller{
|
|
ApiRateLimitDisable: true,
|
|
},
|
|
},
|
|
true,
|
|
nil,
|
|
},
|
|
{
|
|
"newEnabled",
|
|
func() *Controller {
|
|
conf := &config.Config{
|
|
Controller: &config.Controller{
|
|
ApiRateLimitDisable: true,
|
|
},
|
|
}
|
|
c := &Controller{
|
|
baseContext: context.Background(),
|
|
conf: &Config{
|
|
RawConfig: conf,
|
|
},
|
|
}
|
|
err := c.initializeRateLimiter(conf)
|
|
require.NoError(t, err)
|
|
return c
|
|
}(),
|
|
&config.Config{
|
|
Controller: &config.Controller{
|
|
ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(),
|
|
},
|
|
},
|
|
true,
|
|
nil,
|
|
},
|
|
{
|
|
"newInvalid",
|
|
func() *Controller {
|
|
conf := &config.Config{
|
|
Controller: &config.Controller{
|
|
ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(),
|
|
},
|
|
}
|
|
c := &Controller{
|
|
baseContext: context.Background(),
|
|
conf: &Config{
|
|
RawConfig: conf,
|
|
},
|
|
}
|
|
err := c.initializeRateLimiter(conf)
|
|
require.NoError(t, err)
|
|
return c
|
|
}(),
|
|
&config.Config{
|
|
Controller: &config.Controller{
|
|
ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(),
|
|
ApiRateLimits: ratelimit.Configs{
|
|
{
|
|
Resources: []string{"*"},
|
|
Actions: []string{"*"},
|
|
Per: "total",
|
|
Limit: 100,
|
|
Period: time.Minute,
|
|
Unlimited: false,
|
|
},
|
|
},
|
|
ApiRateLimitDisable: true,
|
|
},
|
|
},
|
|
false,
|
|
fmt.Errorf("controller.newRateLimiterConfig: disabled rate limiter with rate limit configs: configuration issue: error #5000"),
|
|
},
|
|
{
|
|
"newInvalidMaxSize",
|
|
func() *Controller {
|
|
conf := &config.Config{
|
|
Controller: &config.Controller{
|
|
ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(),
|
|
},
|
|
}
|
|
c := &Controller{
|
|
baseContext: context.Background(),
|
|
conf: &Config{
|
|
RawConfig: conf,
|
|
},
|
|
}
|
|
err := c.initializeRateLimiter(conf)
|
|
require.NoError(t, err)
|
|
return c
|
|
}(),
|
|
&config.Config{
|
|
Controller: &config.Controller{
|
|
ApiRateLimiterMaxQuotas: 0,
|
|
ApiRateLimits: ratelimit.Configs{
|
|
{
|
|
Resources: []string{"*"},
|
|
Actions: []string{"*"},
|
|
Per: "total",
|
|
Limit: 100,
|
|
Period: time.Minute,
|
|
Unlimited: false,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
false,
|
|
fmt.Errorf("controller.(Controller).ReloadRateLimiter: unknown, unknown: error #0: rate.NewLimiter: rate.newExpirableStore: max size must be greater than zero: invalid max size"),
|
|
},
|
|
{
|
|
"newConfigsNoChange",
|
|
func() *Controller {
|
|
conf := &config.Config{
|
|
Controller: &config.Controller{
|
|
ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(),
|
|
},
|
|
}
|
|
c := &Controller{
|
|
baseContext: context.Background(),
|
|
conf: &Config{
|
|
RawConfig: conf,
|
|
},
|
|
}
|
|
err := c.initializeRateLimiter(conf)
|
|
require.NoError(t, err)
|
|
return c
|
|
}(),
|
|
&config.Config{
|
|
Controller: &config.Controller{
|
|
ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(),
|
|
},
|
|
},
|
|
false,
|
|
nil,
|
|
},
|
|
{
|
|
"nilNewConfigController",
|
|
func() *Controller {
|
|
conf := &config.Config{
|
|
Controller: &config.Controller{
|
|
ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(),
|
|
},
|
|
}
|
|
c := &Controller{
|
|
baseContext: context.Background(),
|
|
conf: &Config{
|
|
RawConfig: conf,
|
|
},
|
|
}
|
|
err := c.initializeRateLimiter(conf)
|
|
require.NoError(t, err)
|
|
return c
|
|
}(),
|
|
&config.Config{
|
|
Controller: nil,
|
|
},
|
|
false,
|
|
fmt.Errorf("controller.(Controller).ReloadRateLimiter: nil config.Controller: parameter violation: error #100"),
|
|
},
|
|
{
|
|
"nilNewConfig",
|
|
func() *Controller {
|
|
conf := &config.Config{
|
|
Controller: &config.Controller{
|
|
ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(),
|
|
},
|
|
}
|
|
c := &Controller{
|
|
baseContext: context.Background(),
|
|
conf: &Config{
|
|
RawConfig: conf,
|
|
},
|
|
}
|
|
err := c.initializeRateLimiter(conf)
|
|
require.NoError(t, err)
|
|
return c
|
|
}(),
|
|
nil,
|
|
false,
|
|
fmt.Errorf("controller.(Controller).ReloadRateLimiter: nil config: parameter violation: error #100"),
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
prevLimiter := tc.c.getRateLimiter()
|
|
err := tc.c.ReloadRateLimiter(tc.conf)
|
|
if tc.wantErr != nil {
|
|
assert.EqualError(t, err, tc.wantErr.Error())
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
if tc.wantNewLimiter {
|
|
assert.NotSame(t, prevLimiter, tc.c.getRateLimiter())
|
|
return
|
|
}
|
|
assert.Same(t, prevLimiter, tc.c.getRateLimiter())
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_rateLimiterConfig_writeSysEvent(t *testing.T) {
|
|
c := event.TestEventerConfig(t, t.Name())
|
|
|
|
testLock := &sync.Mutex{}
|
|
testLogger := testLogger(t, testLock)
|
|
e, err := event.NewEventer(testLogger, testLock, t.Name(), c.EventerConfig)
|
|
require.NoError(t, err)
|
|
|
|
info := &event.RequestInfo{Id: "867-5309", EventId: "411"}
|
|
|
|
testCtx, err := event.NewEventerContext(context.Background(), e)
|
|
require.NoError(t, err)
|
|
testCtx, err = event.NewRequestInfoContext(testCtx, info)
|
|
require.NoError(t, err)
|
|
|
|
cases := []struct {
|
|
name string
|
|
setup func(n string) error
|
|
cleanup func()
|
|
sinkFileName string
|
|
configs ratelimit.Configs
|
|
maxSize int
|
|
disabled bool
|
|
}{
|
|
{
|
|
name: "defaults",
|
|
setup: func(n string) error {
|
|
return event.InitSysEventer(testLogger, testLock, n, event.WithEventerConfig(&c.EventerConfig))
|
|
},
|
|
cleanup: func() { event.TestResetSystEventer(t) },
|
|
sinkFileName: c.AllEvents.Name(),
|
|
configs: nil,
|
|
maxSize: ratelimit.DefaultLimiterMaxQuotas(),
|
|
disabled: false,
|
|
},
|
|
{
|
|
name: "override",
|
|
setup: func(n string) error {
|
|
return event.InitSysEventer(testLogger, testLock, n, event.WithEventerConfig(&c.EventerConfig))
|
|
},
|
|
cleanup: func() { event.TestResetSystEventer(t) },
|
|
sinkFileName: c.AllEvents.Name(),
|
|
configs: ratelimit.Configs{
|
|
{
|
|
Resources: []string{"*"},
|
|
Actions: []string{"*"},
|
|
Per: rate.LimitPerTotal.String(),
|
|
Limit: 100,
|
|
Period: time.Minute,
|
|
Unlimited: false,
|
|
},
|
|
{
|
|
Resources: []string{"*"},
|
|
Actions: []string{"*"},
|
|
Per: rate.LimitPerIPAddress.String(),
|
|
Limit: 100,
|
|
Period: time.Minute,
|
|
Unlimited: false,
|
|
},
|
|
{
|
|
Resources: []string{"*"},
|
|
Actions: []string{"*"},
|
|
Per: rate.LimitPerAuthToken.String(),
|
|
Limit: 100,
|
|
Period: time.Minute,
|
|
Unlimited: false,
|
|
},
|
|
},
|
|
maxSize: ratelimit.DefaultLimiterMaxQuotas(),
|
|
disabled: false,
|
|
},
|
|
{
|
|
name: "max_size",
|
|
setup: func(n string) error {
|
|
return event.InitSysEventer(testLogger, testLock, n, event.WithEventerConfig(&c.EventerConfig))
|
|
},
|
|
cleanup: func() { event.TestResetSystEventer(t) },
|
|
sinkFileName: c.AllEvents.Name(),
|
|
configs: nil,
|
|
maxSize: 3000,
|
|
disabled: false,
|
|
},
|
|
{
|
|
name: "disabled",
|
|
setup: func(n string) error {
|
|
return event.InitSysEventer(testLogger, testLock, n, event.WithEventerConfig(&c.EventerConfig))
|
|
},
|
|
cleanup: func() { event.TestResetSystEventer(t) },
|
|
sinkFileName: c.AllEvents.Name(),
|
|
configs: nil,
|
|
maxSize: 0,
|
|
disabled: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if tc.setup != nil {
|
|
require.NoError(t, tc.setup(t.Name()))
|
|
}
|
|
if tc.cleanup != nil {
|
|
defer tc.cleanup()
|
|
}
|
|
|
|
wantFile, err := os.Open(filepath.Join("testdata", t.Name()+".json"))
|
|
require.NoError(t, err)
|
|
defer wantFile.Close()
|
|
want := &cloudevents.Event{}
|
|
wantDecoder := json.NewDecoder(wantFile)
|
|
err = wantDecoder.Decode(want)
|
|
require.NoError(t, err)
|
|
|
|
rlc, err := newRateLimiterConfig(testCtx, tc.configs, tc.maxSize, tc.disabled)
|
|
require.NoError(t, err)
|
|
|
|
rlc.writeSysEvent(testCtx)
|
|
|
|
defer func() { _ = os.WriteFile(tc.sinkFileName, nil, 0o666) }()
|
|
b, err := os.ReadFile(tc.sinkFileName)
|
|
require.NoError(t, err)
|
|
|
|
got := &cloudevents.Event{}
|
|
err = json.Unmarshal(b, got)
|
|
require.NoErrorf(t, err, "json: %s", string(b))
|
|
|
|
assert.Empty(t, cmp.Diff(
|
|
got,
|
|
want,
|
|
cmpopts.IgnoreFields(cloudevents.Event{}, "ID", "Time", "Data"),
|
|
))
|
|
|
|
gotData := got.Data.(map[string]interface{})
|
|
wantData := want.Data.(map[string]interface{})
|
|
assert.Equal(t, len(gotData), len(wantData))
|
|
|
|
for k, v := range wantData {
|
|
switch k {
|
|
case "data":
|
|
wantDataData := v.(map[string]interface{})
|
|
gotDataData := gotData[k].(map[string]interface{})
|
|
assert.Equal(t, len(gotDataData), len(wantDataData))
|
|
for k, v := range wantDataData {
|
|
switch k {
|
|
case "limits":
|
|
wantResources := v.(map[string]interface{})
|
|
gotResources := gotDataData[k].(map[string]interface{})
|
|
for k, v := range wantResources {
|
|
gotv, ok := gotResources[k]
|
|
require.True(t, ok)
|
|
|
|
wantResourceLimits := v.(map[string]interface{})
|
|
gotResourceLimits := gotv.(map[string]interface{})
|
|
require.Equal(t, len(wantResourceLimits), len(gotResourceLimits))
|
|
|
|
for k, v := range wantResourceLimits {
|
|
gotv, ok := gotResourceLimits[k]
|
|
require.True(t, ok)
|
|
gotActionLimits := v.([]interface{})
|
|
wantActionLimits := gotv.([]interface{})
|
|
require.Equal(t, len(gotActionLimits), len(wantActionLimits))
|
|
|
|
assert.ElementsMatch(t, gotActionLimits, wantActionLimits)
|
|
}
|
|
}
|
|
case "max_size", "msg", "disabled":
|
|
assert.Equal(t, v, gotDataData[k])
|
|
default:
|
|
require.Fail(t, "unexpected key %s", k)
|
|
}
|
|
}
|
|
case "op", "version":
|
|
assert.Equal(t, v, gotData[k])
|
|
default:
|
|
require.Fail(t, "unexpected key %s", k)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|