diff --git a/internal/cmd/config/config.go b/internal/cmd/config/config.go index 945e48d86..391f0747d 100644 --- a/internal/cmd/config/config.go +++ b/internal/cmd/config/config.go @@ -22,6 +22,7 @@ import ( "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/event" + "github.com/hashicorp/boundary/internal/ratelimit" "github.com/hashicorp/boundary/internal/util" kms_plugin_assets "github.com/hashicorp/boundary/plugins/kms" "github.com/hashicorp/boundary/sdk/wrapper" @@ -214,6 +215,9 @@ type Controller struct { // TODO: This field is currently internal. SchedulerRunJobInterval time.Duration `hcl:"-"` + ApiRateLimits ratelimit.Configs `hcl:"-"` + ApiRateLimiterMaxEntries int `hcl:"api_rate_limit_max_entries"` + // License is the license used by HCP builds License string `hcl:"license"` } @@ -682,6 +686,15 @@ func Parse(d string) (*Config, error) { } } + + result.Controller.ApiRateLimits, err = parseApiRateLimits(obj.Node) + if err != nil { + return nil, err + } + + if result.Controller.ApiRateLimiterMaxEntries <= 0 { + result.Controller.ApiRateLimiterMaxEntries = ratelimit.DefaultLimiterMaxEntries + } } // Parse worker tags @@ -944,6 +957,38 @@ func supportControllersRawConfig(initialUpstreamsRaw, controllersRaw any) (any, return initialUpstreamsRaw, nil } +func parseApiRateLimits(node ast.Node) (ratelimit.Configs, error) { + list, ok := node.(*ast.ObjectList) + if !ok { + return nil, fmt.Errorf("error parsing: file doesn't contain a root object") + } + controllerList := list.Filter("controller") + + configs := make(ratelimit.Configs, 0) + for _, item := range controllerList.Items { + controller, ok := item.Val.(*ast.ObjectType) + if !ok { + return nil, fmt.Errorf("error parsing: file doesn't contain controller object") + } + apiRateLimitsList := controller.List.Filter("api_rate_limit") + + var err error + for i, item := range apiRateLimitsList.Items { + var a ratelimit.Config + if err := hcl.DecodeObject(&a, item.Val); err != nil { + return nil, fmt.Errorf("error decoding controller api_rate_limit entry %d", i) + } + a.Period, err = parseutil.ParseDurationSecond(a.PeriodHCL) + if err != nil { + return nil, fmt.Errorf("error decoding controller api_rate_limit period for entry %d", i) + } + configs = append(configs, &a) + } + } + + return configs, nil +} + func parseWorkerUpstreams(c *Config) ([]string, error) { if c == nil || c.Worker == nil { return nil, fmt.Errorf("config or worker field is nil") diff --git a/internal/cmd/config/config_load_test.go b/internal/cmd/config/config_load_test.go index 392174cc2..614489c35 100644 --- a/internal/cmd/config/config_load_test.go +++ b/internal/cmd/config/config_load_test.go @@ -9,9 +9,11 @@ import ( "io/ioutil" "net/http" "testing" + "time" "github.com/hashicorp/boundary/internal/cmd/config" "github.com/hashicorp/boundary/internal/event" + "github.com/hashicorp/boundary/internal/ratelimit" configutil "github.com/hashicorp/go-secure-stdlib/configutil/v2" "github.com/hashicorp/go-secure-stdlib/listenerutil" "github.com/stretchr/testify/require" @@ -416,6 +418,8 @@ func TestLoad(t *testing.T) { GracefulShutdownWaitDuration: 0, WorkerStatusGracePeriodDuration: 0, LivenessTimeToStaleDuration: 0, + ApiRateLimits: make(ratelimit.Configs, 0), + ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries, }, DevController: false, DevUiPassthroughDir: "", @@ -839,6 +843,8 @@ func TestLoad(t *testing.T) { GracefulShutdownWaitDuration: 0, WorkerStatusGracePeriodDuration: 0, LivenessTimeToStaleDuration: 0, + ApiRateLimits: make(ratelimit.Configs, 0), + ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries, }, DevController: false, DevUiPassthroughDir: "", @@ -1257,6 +1263,27 @@ func TestLoad(t *testing.T) { GracefulShutdownWaitDuration: 0, WorkerStatusGracePeriodDuration: 0, LivenessTimeToStaleDuration: 0, + ApiRateLimits: ratelimit.Configs{ + { + Resources: []string{"*"}, + Actions: []string{"*"}, + Per: "total", + Limit: 50, + PeriodHCL: "1m", + Period: time.Minute, + Unlimited: false, + }, + { + Resources: []string{"*"}, + Actions: []string{"list"}, + Per: "total", + Limit: 20, + PeriodHCL: "1m", + Period: time.Minute, + Unlimited: false, + }, + }, + ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries, }, DevController: false, DevUiPassthroughDir: "", @@ -1675,6 +1702,8 @@ func TestLoad(t *testing.T) { GracefulShutdownWaitDuration: 0, WorkerStatusGracePeriodDuration: 0, LivenessTimeToStaleDuration: 0, + ApiRateLimits: make(ratelimit.Configs, 0), + ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries, }, DevController: false, DevUiPassthroughDir: "", @@ -1764,6 +1793,8 @@ func TestLoad(t *testing.T) { GracefulShutdownWaitDuration: 0, WorkerStatusGracePeriodDuration: 0, LivenessTimeToStaleDuration: 0, + ApiRateLimits: make(ratelimit.Configs, 0), + ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries, }, DevController: false, DevUiPassthroughDir: "", diff --git a/internal/cmd/config/config_test.go b/internal/cmd/config/config_test.go index 581b4c111..466cc7166 100644 --- a/internal/cmd/config/config_test.go +++ b/internal/cmd/config/config_test.go @@ -12,6 +12,7 @@ import ( "time" "github.com/hashicorp/boundary/internal/event" + "github.com/hashicorp/boundary/internal/ratelimit" configutil "github.com/hashicorp/go-secure-stdlib/configutil/v2" "github.com/hashicorp/go-secure-stdlib/listenerutil" "github.com/hashicorp/go-secure-stdlib/parseutil" @@ -109,8 +110,10 @@ func TestDevController(t *testing.T) { }, }, Controller: &Controller{ - Name: "dev-controller", - Description: "A default controller created in dev mode", + Name: "dev-controller", + Description: "A default controller created in dev mode", + ApiRateLimits: make(ratelimit.Configs, 0), + ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries, }, DevController: true, } @@ -482,8 +485,10 @@ func TestDevCombined(t *testing.T) { }, }, Controller: &Controller{ - Name: "dev-controller", - Description: "A default controller created in dev mode", + Name: "dev-controller", + Description: "A default controller created in dev mode", + ApiRateLimits: make(ratelimit.Configs, 0), + ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries, }, DevController: true, Worker: &Worker{ @@ -1549,6 +1554,208 @@ func TestControllerDescription(t *testing.T) { } } +func TestControllerApiRateLimits(t *testing.T) { + tests := []struct { + name string + in string + expLimits ratelimit.Configs + expErr bool + expErrStr string + }{ + { + name: "Single Rate limit", + in: ` + controller { + api_rate_limit { + resources = ["*"] + actions = ["*"] + per = "total" + limit = 50 + period = "1m" + } + }`, + expLimits: ratelimit.Configs{ + { + Resources: []string{"*"}, + Actions: []string{"*"}, + Per: "total", + Limit: 50, + PeriodHCL: "1m", + Period: time.Minute, + Unlimited: false, + }, + }, + expErr: false, + }, + { + name: "Single Rate with name", + in: ` + controller { + api_rate_limit "default" { + resources = ["*"] + actions = ["*"] + per = "total" + limit = 50 + period = "1m" + } + }`, + expLimits: ratelimit.Configs{ + { + Resources: []string{"*"}, + Actions: []string{"*"}, + Per: "total", + Limit: 50, + PeriodHCL: "1m", + Period: time.Minute, + Unlimited: false, + }, + }, + expErr: false, + }, + { + name: "Multiple Rate limit", + in: ` + controller { + api_rate_limit { + resources = ["*"] + actions = ["*"] + per = "total" + limit = 50 + period = "1m" + } + + api_rate_limit { + resources = ["*"] + actions = ["list"] + per = "total" + limit = 20 + period = "1m" + } + }`, + expLimits: ratelimit.Configs{ + { + Resources: []string{"*"}, + Actions: []string{"*"}, + Per: "total", + Limit: 50, + PeriodHCL: "1m", + Period: time.Minute, + Unlimited: false, + }, + { + Resources: []string{"*"}, + Actions: []string{"list"}, + Per: "total", + Limit: 20, + PeriodHCL: "1m", + Period: time.Minute, + Unlimited: false, + }, + }, + expErr: false, + }, + { + name: "Multiple Rate limit with names", + in: ` + controller { + api_rate_limit "default" { + resources = ["*"] + actions = ["*"] + per = "total" + limit = 50 + period = "1m" + } + + api_rate_limit "default-list" { + resources = ["*"] + actions = ["list"] + per = "total" + limit = 20 + period = "1m" + } + }`, + expLimits: ratelimit.Configs{ + { + Resources: []string{"*"}, + Actions: []string{"*"}, + Per: "total", + Limit: 50, + PeriodHCL: "1m", + Period: time.Minute, + Unlimited: false, + }, + { + Resources: []string{"*"}, + Actions: []string{"list"}, + Per: "total", + Limit: 20, + PeriodHCL: "1m", + Period: time.Minute, + Unlimited: false, + }, + }, + expErr: false, + }, + { + name: "Multiple Rate limit with name and no name", + in: ` + controller { + api_rate_limit { + resources = ["*"] + actions = ["*"] + per = "total" + limit = 50 + period = "1m" + } + + api_rate_limit "list" { + resources = ["*"] + actions = ["list"] + per = "total" + limit = 20 + period = "1m" + } + }`, + expLimits: ratelimit.Configs{ + { + Resources: []string{"*"}, + Actions: []string{"*"}, + Per: "total", + Limit: 50, + PeriodHCL: "1m", + Period: time.Minute, + Unlimited: false, + }, + { + Resources: []string{"*"}, + Actions: []string{"list"}, + Per: "total", + Limit: 20, + PeriodHCL: "1m", + Period: time.Minute, + Unlimited: false, + }, + }, + expErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c, err := Parse(tt.in) + if tt.expErr { + require.EqualError(t, err, tt.expErrStr) + require.Nil(t, c) + return + } + + require.NoError(t, err) + require.NotNil(t, c) + require.NotNil(t, c.Controller) + require.Equal(t, tt.expLimits, c.Controller.ApiRateLimits) + }) + } +} + func TestWorkerDescription(t *testing.T) { tests := []struct { name string diff --git a/internal/cmd/config/testdata/TestLoad/MultiFileDuplicateStanza/00-controller.hcl b/internal/cmd/config/testdata/TestLoad/MultiFileDuplicateStanza/00-controller.hcl index 1c571dc61..5796eeb56 100644 --- a/internal/cmd/config/testdata/TestLoad/MultiFileDuplicateStanza/00-controller.hcl +++ b/internal/cmd/config/testdata/TestLoad/MultiFileDuplicateStanza/00-controller.hcl @@ -10,6 +10,14 @@ controller { database { url = "env://LOAD_TEST_BOUNDARY_POSTGRES_URL" } + + api_rate_limit { + resources = ["*"] + actions = ["*"] + per = "total" + limit = 50 + period = "1m" + } } listener "tcp" { diff --git a/internal/cmd/config/testdata/TestLoad/MultiFileDuplicateStanza/01-controller-duplicated.hcl b/internal/cmd/config/testdata/TestLoad/MultiFileDuplicateStanza/01-controller-duplicated.hcl index 342d86e2e..e366964e0 100644 --- a/internal/cmd/config/testdata/TestLoad/MultiFileDuplicateStanza/01-controller-duplicated.hcl +++ b/internal/cmd/config/testdata/TestLoad/MultiFileDuplicateStanza/01-controller-duplicated.hcl @@ -8,4 +8,12 @@ controller { database { url = "env://LOAD_TEST_BOUNDARY_POSTGRES_URL" } + + api_rate_limit { + resources = ["*"] + actions = ["list"] + per = "total" + limit = 20 + period = "1m" + } }