* Add ListResolvableAliases to the user service There are a few non common ways this method uses our ACL and grant system. A user is able to list resolvable aliases for another user if they are granted permission to do so. That means we had to load the grants for the user being listed for and not the requester when determining if an alias is able to resolve to a destination for which the requested user has permission. Similarly, the grant hash used for the list pagination is required to be that of the user being listed and not the requester.
672 lines
16 KiB
Go
672 lines
16 KiB
Go
// 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)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|