From 0fb77358e5380d0c2d1c47e620bc2e27a71d2026 Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Mon, 2 Oct 2023 20:56:35 +0000 Subject: [PATCH 01/26] feat(ratelimit): Define initial ratelimit package and Config --- internal/ratelimit/config.go | 30 ++++++++++++++++++++++++++++++ internal/ratelimit/doc.go | 6 ++++++ 2 files changed, 36 insertions(+) create mode 100644 internal/ratelimit/config.go create mode 100644 internal/ratelimit/doc.go diff --git a/internal/ratelimit/config.go b/internal/ratelimit/config.go new file mode 100644 index 000000000..97c520708 --- /dev/null +++ b/internal/ratelimit/config.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package ratelimit + +import ( + "time" +) + +const ( + // DefaultLimiterMaxEntries is the default maximum number of quotas that + // can be tracked by the rate limiter. + DefaultLimiterMaxEntries = 16384 // TODO: pick a meaningful default value +) + +// 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 diff --git a/internal/ratelimit/doc.go b/internal/ratelimit/doc.go new file mode 100644 index 000000000..129b14b66 --- /dev/null +++ b/internal/ratelimit/doc.go @@ -0,0 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Package ratelimit provides the rate limit configuration and http middleware +// for use by the controller's http API. +package ratelimit From bee45dc4862b24378b388e19d02b2749aef35d46 Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Mon, 2 Oct 2023 21:02:17 +0000 Subject: [PATCH 02/26] feat(config): Add fields for controller rate limit configuration --- internal/cmd/config/config.go | 45 ++++ internal/cmd/config/config_load_test.go | 31 +++ internal/cmd/config/config_test.go | 215 +++++++++++++++++- .../00-controller.hcl | 8 + .../01-controller-duplicated.hcl | 8 + 5 files changed, 303 insertions(+), 4 deletions(-) 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" + } } From c52caae34552bba8714c7f5fd1f2f80cb000e5a4 Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Tue, 31 Oct 2023 13:44:17 +0000 Subject: [PATCH 03/26] refact(action): Make ActionSet behave as a set instead of a list This switches the implementation of an ActionSet from a slice to a map to ensure the elements in an ActionSet are unique. This allows for other packages to perform set operations such as a union or difference without needing to account for duplicates. --- internal/auth/additional_verification_test.go | 10 +- internal/daemon/controller/auth/auth.go | 10 +- .../controller/common/scopeids/scope_ids.go | 4 +- .../handlers/accounts/account_service.go | 16 +- .../handlers/accounts/account_service_test.go | 110 +++++++- .../authmethods/authmethod_service.go | 5 +- .../authmethods/authmethod_service_test.go | 40 ++- .../controller/handlers/authmethods/ldap.go | 4 +- .../handlers/authmethods/ldap_test.go | 11 +- .../controller/handlers/authmethods/oidc.go | 4 +- .../handlers/authmethods/oidc_test.go | 49 +++- .../handlers/authmethods/password.go | 4 +- .../handlers/authmethods/password_test.go | 11 +- .../handlers/authtokens/authtoken_service.go | 8 +- .../authtokens/authtoken_service_test.go | 24 +- .../credentiallibrary_service.go | 8 +- .../credentiallibrary_service_test.go | 66 ++++- .../credentials/credential_service.go | 8 +- .../credentials/credential_service_test.go | 43 ++- .../credentialstore_service.go | 8 +- .../credentialstore_service_test.go | 66 ++++- .../handlers/groups/group_service.go | 8 +- .../handlers/groups/group_service_test.go | 37 ++- .../host_catalogs/host_catalog_service.go | 12 +- .../host_catalog_service_test.go | 84 +++++- .../handlers/host_sets/host_set_service.go | 12 +- .../host_sets/host_set_service_test.go | 82 +++++- .../controller/handlers/hosts/host_service.go | 12 +- .../handlers/hosts/host_service_test.go | 64 ++++- .../managed_groups/managed_group_service.go | 12 +- .../managed_group_service_test.go | 64 ++++- .../controller/handlers/roles/role_service.go | 8 +- .../handlers/roles/role_service_test.go | 37 ++- .../handlers/scopes/scope_service.go | 21 +- .../handlers/scopes/scope_service_test.go | 94 ++++++- .../session_recording_service.go | 8 +- .../handlers/sessions/session_service.go | 8 +- .../handlers/sessions/session_service_test.go | 37 ++- .../storage_buckets/storage_bucket_service.go | 8 +- .../handlers/targets/target_service.go | 8 +- .../targets/tcp/target_service_test.go | 74 +++++- .../controller/handlers/users/user_service.go | 8 +- .../handlers/users/user_service_test.go | 37 ++- .../handlers/workers/worker_service.go | 8 +- .../handlers/workers/worker_service_test.go | 28 +- internal/perms/acl.go | 6 +- internal/perms/acl_test.go | 38 +-- .../tests/api/authmethods/authmethod_test.go | 30 ++- internal/tests/api/hosts/host_test.go | 14 +- internal/tests/api/hostsets/host_set_test.go | 6 + internal/types/action/action.go | 56 +++- internal/types/action/action_test.go | 245 +++++++++++++++++- 52 files changed, 1401 insertions(+), 274 deletions(-) diff --git a/internal/auth/additional_verification_test.go b/internal/auth/additional_verification_test.go index 9de3e08c2..c4a4ebe72 100644 --- a/internal/auth/additional_verification_test.go +++ b/internal/auth/additional_verification_test.go @@ -67,13 +67,13 @@ func TestFetchActionSetForId(t *testing.T) { { name: "no match", id: "zip", - avail: action.ActionSet{action.Read, action.Update}, + avail: action.NewActionSet(action.Read, action.Update), }, { name: "disjoint match", id: "ttcp_bar", - avail: action.ActionSet{action.Delete, action.AddGrants, action.Read, action.RemoveHostSets}, - allowed: action.ActionSet{action.Delete, action.Read}, + avail: action.NewActionSet(action.Delete, action.AddGrants, action.Read, action.RemoveHostSets), + allowed: action.NewActionSet(action.Delete, action.Read), }, { name: "different type", @@ -84,8 +84,8 @@ func TestFetchActionSetForId(t *testing.T) { name: "type match", id: "anything", typeOverride: resource.Role, - avail: action.ActionSet{action.Read, action.AddGrants}, - allowed: action.ActionSet{action.AddGrants}, + avail: action.NewActionSet(action.Read, action.AddGrants), + allowed: action.NewActionSet(action.AddGrants), }, } for _, tt := range cases { diff --git a/internal/daemon/controller/auth/auth.go b/internal/daemon/controller/auth/auth.go index 0fc8b5920..af2f642a8 100644 --- a/internal/daemon/controller/auth/auth.go +++ b/internal/daemon/controller/auth/auth.go @@ -722,10 +722,10 @@ func (r *VerifyResults) fetchActions(id string, typ resource.Type, availableActi res.Type = typ } - ret := make(action.ActionSet, 0, len(availableActions)) - for _, act := range availableActions { + ret := make(action.ActionSet, len(availableActions)) + for act := range availableActions { if r.v.acl.Allowed(*res, act, *r.UserData.User.Id).Authorized { - ret = append(ret, act) + ret.Add(act) } } if len(ret) == 0 { @@ -854,7 +854,7 @@ func (r *VerifyResults) ScopesAuthorizedForList(ctx context.Context, rootScopeId scpId := scp.GetPublicId() aSet := r.FetchActionSetForType(ctx, resource.Unknown, // This is overridden by `WithResource` option. - action.ActionSet{action.List}, + action.NewActionSet(action.List), WithResource(&perms.Resource{Type: resourceType, ScopeId: scpId}), ) @@ -867,7 +867,7 @@ func (r *VerifyResults) ScopesAuthorizedForList(ctx context.Context, rootScopeId // lookup might fail. deferredScopes = append(deferredScopes, scp) case len(aSet) == 1 || r.UserId == globals.RecoveryUserId: - if aSet[0] != action.List { + if !aSet.HasAction(action.List) { return nil, errors.New(ctx, errors.Internal, op, "unexpected action in set") } if scopeResourceMap[scpId] == nil { diff --git a/internal/daemon/controller/common/scopeids/scope_ids.go b/internal/daemon/controller/common/scopeids/scope_ids.go index 12bf7a5ad..ea0c6a01e 100644 --- a/internal/daemon/controller/common/scopeids/scope_ids.go +++ b/internal/daemon/controller/common/scopeids/scope_ids.go @@ -153,7 +153,7 @@ func GetListingResourceInformation( aSet := input.AuthResults.FetchActionSetForType(ctx, // This is overridden by WithResource resource.Unknown, - action.ActionSet{action.List}, + action.NewActionSet(action.List), auth.WithResource(&res), ) switch len(aSet) { @@ -163,7 +163,7 @@ func GetListingResourceInformation( // lookup might fail. deferredScopes = append(deferredScopes, scp) case 1: - if aSet[0] != action.List { + if !aSet.HasAction(action.List) { return nil, errors.New(ctx, errors.Internal, op, "unexpected action in set") } if output.ScopeResourceMap[scpId] == nil { diff --git a/internal/daemon/controller/handlers/accounts/account_service.go b/internal/daemon/controller/handlers/accounts/account_service.go index d537a4f7f..35051d5cb 100644 --- a/internal/daemon/controller/handlers/accounts/account_service.go +++ b/internal/daemon/controller/handlers/accounts/account_service.go @@ -70,34 +70,34 @@ var ( // IdActions contains the set of actions that can be performed on // individual resources IdActions = map[globals.Subtype]action.ActionSet{ - password.Subtype: { + password.Subtype: action.NewActionSet( action.NoOp, action.Read, action.Update, action.Delete, action.SetPassword, action.ChangePassword, - }, - oidc.Subtype: { + ), + oidc.Subtype: action.NewActionSet( action.NoOp, action.Read, action.Update, action.Delete, - }, - ldap.Subtype: { + ), + ldap.Subtype: action.NewActionSet( action.NoOp, action.Read, action.Update, action.Delete, - }, + ), } // CollectionActions contains the set of actions that can be performed on // this collection - CollectionActions = action.ActionSet{ + CollectionActions = action.NewActionSet( action.Create, action.List, - } + ) ) func init() { diff --git a/internal/daemon/controller/handlers/accounts/account_service_test.go b/internal/daemon/controller/handlers/accounts/account_service_test.go index cafc57c1e..0cfd2ab5c 100644 --- a/internal/daemon/controller/handlers/accounts/account_service_test.go +++ b/internal/daemon/controller/handlers/accounts/account_service_test.go @@ -12,6 +12,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/auth/ldap" "github.com/hashicorp/boundary/internal/auth/oidc" @@ -297,7 +298,14 @@ func TestGet(t *testing.T) { return } require.NoError(gErr) - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "GetAccount(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "GetAccount(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } } @@ -424,7 +432,15 @@ func TestListPassword(t *testing.T) { } else { require.NoError(gErr) } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform(), protocmp.SortRepeatedFields(got)), "ListAccounts() with scope %q got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + protocmp.SortRepeatedFields(got), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "ListAccounts() with scope %q got response %q, wanted %q", tc.req, got, tc.res) // Now test with anon if tc.skipAnon { @@ -584,7 +600,14 @@ func TestListOidc(t *testing.T) { return strings.Compare(got.Items[i].GetOidcAccountAttributes().Subject, got.Items[j].GetOidcAccountAttributes().Subject) < 0 }) - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListAccounts() with scope %q got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "ListAccounts() with scope %q got response %q, wanted %q", tc.req, got, tc.res) // Now test with anon if tc.skipAnon { @@ -739,7 +762,14 @@ func TestListLdap(t *testing.T) { return strings.Compare(got.Items[i].GetLdapAccountAttributes().LoginName, got.Items[j].GetLdapAccountAttributes().LoginName) < 0 }) - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListAccounts() with scope %q got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "ListAccounts() with scope %q got response %q, wanted %q", tc.req, got, tc.res) // Now test with anon if tc.skipAnon { @@ -1158,7 +1188,14 @@ func TestCreatePassword(t *testing.T) { got.Item.Id, tc.res.Item.Id = "", "" got.Item.CreatedTime, got.Item.UpdatedTime, tc.res.Item.CreatedTime, tc.res.Item.UpdatedTime = nil, nil, nil, nil } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "CreateAccount(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "CreateAccount(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } } @@ -1395,7 +1432,14 @@ func TestCreateOidc(t *testing.T) { got.Item.Id, tc.res.Item.Id = "", "" got.Item.CreatedTime, got.Item.UpdatedTime, tc.res.Item.CreatedTime, tc.res.Item.UpdatedTime = nil, nil, nil, nil } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "CreateAccount(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "CreateAccount(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } } @@ -1674,7 +1718,14 @@ func TestCreateLdap(t *testing.T) { got.Item.Id, tc.res.Item.Id = "", "" got.Item.CreatedTime, got.Item.UpdatedTime, tc.res.Item.CreatedTime, tc.res.Item.UpdatedTime = nil, nil, nil, nil } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "CreateAccount(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "CreateAccount(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } } @@ -2064,7 +2115,14 @@ func TestUpdatePassword(t *testing.T) { assert.EqualValues(2, got.Item.Version) tc.res.Item.Version = 2 } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "UpdateAccount(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "UpdateAccount(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } } @@ -2454,7 +2512,14 @@ func TestUpdateOidc(t *testing.T) { assert.EqualValues(2, got.Item.Version) tc.res.Item.Version = 2 } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "UpdateAccount(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "UpdateAccount(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } } @@ -2828,7 +2893,14 @@ func TestUpdateLdap(t *testing.T) { assert.EqualValues(2, got.Item.Version) tc.res.Item.Version = 2 } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "UpdateAccount(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "UpdateAccount(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } } @@ -2917,7 +2989,14 @@ func TestSetPassword(t *testing.T) { acct.Version, setResp.GetItem().Version = 0, 0 acct.UpdatedTime, setResp.GetItem().UpdatedTime = nil, nil - assert.Empty(cmp.Diff(acct, setResp.GetItem(), protocmp.Transform())) + assert.Empty(cmp.Diff( + acct, + setResp.GetItem(), + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + )) }) } @@ -3035,7 +3114,14 @@ func TestChangePassword(t *testing.T) { acct.Version, changeResp.GetItem().Version = 0, 0 acct.UpdatedTime, changeResp.GetItem().UpdatedTime = nil, nil - assert.Empty(cmp.Diff(acct, changeResp.GetItem(), protocmp.Transform())) + assert.Empty(cmp.Diff( + acct, + changeResp.GetItem(), + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + )) }) t.Run("unauthenticated update", func(t *testing.T) { diff --git a/internal/daemon/controller/handlers/authmethods/authmethod_service.go b/internal/daemon/controller/handlers/authmethods/authmethod_service.go index 7f2b26a63..aae2d8ab2 100644 --- a/internal/daemon/controller/handlers/authmethods/authmethod_service.go +++ b/internal/daemon/controller/handlers/authmethods/authmethod_service.go @@ -64,14 +64,15 @@ const ( var ( // IdActions contains the set of actions that can be performed on // individual resources + // This will be set in init functions in files for each subtype in this package. IdActions = make(map[globals.Subtype]action.ActionSet) // CollectionActions contains the set of actions that can be performed on // this collection - CollectionActions = action.ActionSet{ + CollectionActions = action.NewActionSet( action.Create, action.List, - } + ) collectionTypeMap = map[resource.Type]action.ActionSet{ resource.Account: accounts.CollectionActions, diff --git a/internal/daemon/controller/handlers/authmethods/authmethod_service_test.go b/internal/daemon/controller/handlers/authmethods/authmethod_service_test.go index dfce01e40..03542ee95 100644 --- a/internal/daemon/controller/handlers/authmethods/authmethod_service_test.go +++ b/internal/daemon/controller/handlers/authmethods/authmethod_service_test.go @@ -17,6 +17,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/auth/ldap" "github.com/hashicorp/boundary/internal/auth/oidc" @@ -269,7 +270,18 @@ func TestGet(t *testing.T) { if oidcAttrs := got.Item.GetOidcAuthMethodsAttributes(); oidcAttrs != nil { assert.NotEqual("secret", oidcAttrs.ClientSecretHmac) } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform(), protocmp.IgnoreFields(&pb.OidcAuthMethodAttributes{}, "client_secret_hmac")), "GetAuthMethod(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + protocmp.IgnoreFields(&pb.OidcAuthMethodAttributes{}, "client_secret_hmac"), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), + ), "GetAuthMethod(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } } @@ -489,10 +501,19 @@ func TestList(t *testing.T) { } slices.SortFunc(got.Items, sorterFn) - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform(), + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), protocmp.IgnoreFields(&pb.OidcAuthMethodAttributes{}, "client_secret_hmac"), - protocmp.IgnoreFields(&pb.LdapAuthMethodAttributes{}, "bind_password_hmac", "client_certificate_key_hmac")), - "ListAuthMethods() for scope %q got response %q, wanted %q", tc.req.GetScopeId(), got, tc.res) + protocmp.IgnoreFields(&pb.LdapAuthMethodAttributes{}, "bind_password_hmac", "client_certificate_key_hmac"), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), + ), "ListAuthMethods() for scope %q got response %q, wanted %q", tc.req.GetScopeId(), got, tc.res) // Now check with anonymous user got, gErr = s.ListAuthMethods(requestauth.DisabledAuthTestContext(iamRepoFn, tc.req.GetScopeId(), requestauth.WithUserId(globals.AnonymousUserId)), tc.req) @@ -1460,7 +1481,15 @@ func TestCreate(t *testing.T) { if tc.res == nil { require.Nil(got) } - cmpOptions := []cmp.Option{protocmp.Transform()} + cmpOptions := []cmp.Option{ + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), + } if got != nil { assert.Contains(got.GetUri(), tc.res.Uri) assert.True(strings.HasPrefix(got.GetItem().GetId(), tc.idPrefix)) @@ -1486,7 +1515,6 @@ func TestCreate(t *testing.T) { assert.True(matches, "%q doesn't match %q", gVal, exp) cmpOptions = append( cmpOptions, - protocmp.SortRepeatedFields(&pb.OidcAuthMethodAttributes{}, "account_claim_maps"), protocmp.IgnoreFields(&pb.OidcAuthMethodAttributes{}, "client_secret_hmac", "callback_url"), ) } diff --git a/internal/daemon/controller/handlers/authmethods/ldap.go b/internal/daemon/controller/handlers/authmethods/ldap.go index 86aefa275..72f12f338 100644 --- a/internal/daemon/controller/handlers/authmethods/ldap.go +++ b/internal/daemon/controller/handlers/authmethods/ldap.go @@ -36,13 +36,13 @@ func init() { panic(err) } - IdActions[ldap.Subtype] = action.ActionSet{ + IdActions[ldap.Subtype] = action.NewActionSet( action.NoOp, action.Read, action.Update, action.Delete, action.Authenticate, - } + ) } const ( diff --git a/internal/daemon/controller/handlers/authmethods/ldap_test.go b/internal/daemon/controller/handlers/authmethods/ldap_test.go index 8129cfd28..032c4a8cc 100644 --- a/internal/daemon/controller/handlers/authmethods/ldap_test.go +++ b/internal/daemon/controller/handlers/authmethods/ldap_test.go @@ -14,6 +14,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/auth/ldap" "github.com/hashicorp/boundary/internal/auth/oidc" @@ -850,7 +851,15 @@ func Test_UpdateLdap(t *testing.T) { if tc.res == nil { require.Nil(got) } - cmpOptions := []cmp.Option{protocmp.Transform()} + cmpOptions := []cmp.Option{ + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), + } if got != nil { assert.NotNilf(tc.res, "Expected UpdateAuthMethod response to be nil, but was %v", got) gotUpdateTime := got.GetItem().GetUpdatedTime().AsTime() diff --git a/internal/daemon/controller/handlers/authmethods/oidc.go b/internal/daemon/controller/handlers/authmethods/oidc.go index df7d4abaa..7bb3c9df5 100644 --- a/internal/daemon/controller/handlers/authmethods/oidc.go +++ b/internal/daemon/controller/handlers/authmethods/oidc.go @@ -57,14 +57,14 @@ func init() { panic(err) } - IdActions[oidc.Subtype] = action.ActionSet{ + IdActions[oidc.Subtype] = action.NewActionSet( action.NoOp, action.Read, action.Update, action.Delete, action.ChangeState, action.Authenticate, - } + ) } type oidcState uint diff --git a/internal/daemon/controller/handlers/authmethods/oidc_test.go b/internal/daemon/controller/handlers/authmethods/oidc_test.go index a60275cf5..938fc3d66 100644 --- a/internal/daemon/controller/handlers/authmethods/oidc_test.go +++ b/internal/daemon/controller/handlers/authmethods/oidc_test.go @@ -16,6 +16,7 @@ import ( "time" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/boundary/globals" authpb "github.com/hashicorp/boundary/internal/gen/controller/auth" @@ -1076,7 +1077,15 @@ func TestUpdate_OIDC(t *testing.T) { if tc.res == nil { require.Nil(got) } - cmpOptions := []cmp.Option{protocmp.Transform()} + cmpOptions := []cmp.Option{ + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), + } if got != nil { assert.NotNilf(tc.res, "Expected UpdateAuthMethod response to be nil, but was %v", got) gotUpdateTime := got.GetItem().GetUpdatedTime().AsTime() @@ -1279,11 +1288,32 @@ func TestUpdate_OIDCDryRun(t *testing.T) { return } require.NoError(t, err) - assert.Empty(t, cmp.Diff(dryUpdated.GetItem(), tc.want, protocmp.Transform(), protocmp.SortRepeatedFields(dryUpdated))) + assert.Empty(t, cmp.Diff( + dryUpdated.GetItem(), + tc.want, + protocmp.Transform(), + protocmp.SortRepeatedFields(dryUpdated), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), + )) got, err := tested.GetAuthMethod(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), &pbs.GetAuthMethodRequest{Id: tc.id}) require.NoError(t, err) - assert.Empty(t, cmp.Diff(got.GetItem(), wireAuthMethod, protocmp.Transform())) + assert.Empty(t, cmp.Diff( + got.GetItem(), + wireAuthMethod, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), + )) }) } } @@ -1543,7 +1573,18 @@ func TestChangeState_OIDC(t *testing.T) { got.Item.UpdatedTime, tc.res.Item.UpdatedTime = nil, nil assert.Empty( - cmp.Diff(got, tc.res, protocmp.Transform(), protocmp.IgnoreFields(&pb.OidcAuthMethodAttributes{}, "client_secret_hmac", "callback_url")), + cmp.Diff( + got, + tc.res, + protocmp.Transform(), + protocmp.IgnoreFields(&pb.OidcAuthMethodAttributes{}, "client_secret_hmac", "callback_url"), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), + ), "ChangeState() got response %q, wanted %q", got, tc.res, ) }) diff --git a/internal/daemon/controller/handlers/authmethods/password.go b/internal/daemon/controller/handlers/authmethods/password.go index 40d921507..38fda9178 100644 --- a/internal/daemon/controller/handlers/authmethods/password.go +++ b/internal/daemon/controller/handlers/authmethods/password.go @@ -36,13 +36,13 @@ func init() { panic(err) } - IdActions[password.Subtype] = action.ActionSet{ + IdActions[password.Subtype] = action.NewActionSet( action.NoOp, action.Read, action.Update, action.Delete, action.Authenticate, - } + ) } // createPwInRepo creates a password auth method in a repo and returns the result. diff --git a/internal/daemon/controller/handlers/authmethods/password_test.go b/internal/daemon/controller/handlers/authmethods/password_test.go index 40f83fc02..c4051f79a 100644 --- a/internal/daemon/controller/handlers/authmethods/password_test.go +++ b/internal/daemon/controller/handlers/authmethods/password_test.go @@ -12,6 +12,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/auth/ldap" "github.com/hashicorp/boundary/internal/auth/oidc" @@ -458,7 +459,15 @@ func TestUpdate_Password(t *testing.T) { require.Nil(got) } - cmpOptions := []cmp.Option{protocmp.Transform()} + cmpOptions := []cmp.Option{ + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), + } if got != nil { assert.NotNilf(tc.res, "Expected UpdateAuthMethod response to be nil, but was %v", got) diff --git a/internal/daemon/controller/handlers/authtokens/authtoken_service.go b/internal/daemon/controller/handlers/authtokens/authtoken_service.go index 435524adc..1a87df8bc 100644 --- a/internal/daemon/controller/handlers/authtokens/authtoken_service.go +++ b/internal/daemon/controller/handlers/authtokens/authtoken_service.go @@ -27,19 +27,19 @@ import ( var ( // IdActions contains the set of actions that can be performed on // individual resources - IdActions = action.ActionSet{ + IdActions = action.NewActionSet( action.NoOp, action.Read, action.ReadSelf, action.Delete, action.DeleteSelf, - } + ) // CollectionActions contains the set of actions that can be performed on // this collection - CollectionActions = action.ActionSet{ + CollectionActions = action.NewActionSet( action.List, - } + ) ) // Service handles request as described by the pbs.AuthTokenServiceServer interface. diff --git a/internal/daemon/controller/handlers/authtokens/authtoken_service_test.go b/internal/daemon/controller/handlers/authtokens/authtoken_service_test.go index 0091a13ba..393a2223a 100644 --- a/internal/daemon/controller/handlers/authtokens/authtoken_service_test.go +++ b/internal/daemon/controller/handlers/authtokens/authtoken_service_test.go @@ -11,6 +11,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/authtoken" "github.com/hashicorp/boundary/internal/daemon/controller/auth" @@ -113,7 +114,7 @@ func TestGetSelf(t *testing.T) { require.NotNil(got) assert.Equal(tc.token.GetPublicId(), got.GetItem().GetId()) // Ensure we didn't simply have e.g. read on all tokens - assert.Equal([]string{"read:self", "delete:self"}, got.Item.GetAuthorizedActions()) + assert.ElementsMatch([]string{"read:self", "delete:self"}, got.Item.GetAuthorizedActions()) }) } } @@ -189,7 +190,14 @@ func TestGet(t *testing.T) { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "GetAuthToken(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "GetAuthToken(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "GetAuthToken(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } } @@ -258,7 +266,7 @@ func TestList_Self(t *testing.T) { require.Len(got.Items, 1) assert.Equal(got.Items[0].GetId(), tc.requester.GetPublicId()) // Ensure we didn't simply have e.g. read on all tokens - assert.Equal(got.Items[0].GetAuthorizedActions(), []string{"read:self", "delete:self"}) + assert.ElementsMatch(got.Items[0].GetAuthorizedActions(), []string{"read:self", "delete:self"}) }) } } @@ -403,7 +411,15 @@ func TestList(t *testing.T) { } else { require.NoError(gErr) } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform(), protocmp.SortRepeatedFields(got)), "ListAuthTokens() with scope %q got response %q, wanted %q", tc.req.GetScopeId(), got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + protocmp.SortRepeatedFields(got), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "ListAuthTokens() with scope %q got response %q, wanted %q", tc.req.GetScopeId(), got, tc.res) // Now check anon listing got, gErr = s.ListAuthTokens(auth.DisabledAuthTestContext(iamRepoFn, tc.req.GetScopeId(), auth.WithUserId(globals.AnonymousUserId)), tc.req) diff --git a/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service.go b/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service.go index 1313604b9..a72dfbdf5 100644 --- a/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service.go +++ b/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service.go @@ -60,19 +60,19 @@ var ( // IdActions contains the set of actions that can be performed on // individual resources - IdActions = action.ActionSet{ + IdActions = action.NewActionSet( action.NoOp, action.Read, action.Update, action.Delete, - } + ) // CollectionActions contains the set of actions that can be performed on // this collection - CollectionActions = action.ActionSet{ + CollectionActions = action.NewActionSet( action.Create, action.List, - } + ) validCredentialTypesVaultGeneric = []globals.CredentialType{ globals.UsernamePasswordCredentialType, diff --git a/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service_test.go b/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service_test.go index f00212920..4f93068fe 100644 --- a/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service_test.go +++ b/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service_test.go @@ -12,6 +12,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/credential/vault" "github.com/hashicorp/boundary/internal/daemon/controller/auth" @@ -155,7 +156,14 @@ func TestList(t *testing.T) { sort.Slice(want.Items, func(i, j int) bool { return want.Items[i].GetId() < want.Items[j].GetId() }) - assert.Empty(t, cmp.Diff(got, want, protocmp.Transform())) + assert.Empty(t, cmp.Diff( + got, + want, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + )) // Test anonymous listing got, gErr = s.ListCredentialLibraries(auth.DisabledAuthTestContext(iamRepoFn, prj.GetPublicId(), auth.WithUserId(globals.AnonymousUserId)), tc.req) @@ -269,7 +277,14 @@ func TestList_Attributes(t *testing.T) { sort.Slice(want.Items, func(i, j int) bool { return want.Items[i].GetId() < want.Items[j].GetId() }) - assert.Empty(t, cmp.Diff(got, want, protocmp.Transform())) + assert.Empty(t, cmp.Diff( + got, + want, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + )) // Test anonymous listing got, gErr = s.ListCredentialLibraries(auth.DisabledAuthTestContext(iamRepoFn, prj.GetPublicId(), auth.WithUserId(globals.AnonymousUserId)), tc.req) @@ -843,7 +858,15 @@ func TestCreate(t *testing.T) { got.Item.Id, tc.res.Item.Id = "", "" got.Item.CreatedTime, got.Item.UpdatedTime, tc.res.Item.CreatedTime, tc.res.Item.UpdatedTime = nil, nil, nil, nil } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform(), protocmp.SortRepeatedFields(got)), "CreateCredentialLibrary(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + protocmp.SortRepeatedFields(got), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "CreateCredentialLibrary(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } } @@ -1041,7 +1064,14 @@ func TestGet(t *testing.T) { return } require.NoError(t, gErr) - assert.Empty(t, cmp.Diff(got, tc.res, protocmp.Transform())) + assert.Empty(t, cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + )) // Test anonymous get got, gErr = s.GetCredentialLibrary(auth.DisabledAuthTestContext(iamRepoFn, prj.GetPublicId(), auth.WithUserId(globals.AnonymousUserId)), req) @@ -1750,7 +1780,14 @@ func TestUpdate(t *testing.T) { assert.EqualValues(2, got.Item.Version) want.Item.Version = 2 - assert.Empty(cmp.Diff(got, want, protocmp.Transform())) + assert.Empty(cmp.Diff( + got, + want, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + )) }) } @@ -2210,7 +2247,15 @@ func TestCreate_SSHCertificateCredentialLibrary(t *testing.T) { got.Item.Id, tc.res.Item.Id = "", "" got.Item.CreatedTime, got.Item.UpdatedTime, tc.res.Item.CreatedTime, tc.res.Item.UpdatedTime = nil, nil, nil, nil } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform(), protocmp.SortRepeatedFields(got)), "CreateCredentialLibrary(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + protocmp.SortRepeatedFields(got), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "CreateCredentialLibrary(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } } @@ -2468,7 +2513,14 @@ func TestUpdate_SSHCertificateCredentialLibrary(t *testing.T) { assert.EqualValues(2, got.Item.Version) want.Item.Version = 2 - assert.Empty(cmp.Diff(got, want, protocmp.Transform())) + assert.Empty(cmp.Diff( + got, + want, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + )) }) } diff --git a/internal/daemon/controller/handlers/credentials/credential_service.go b/internal/daemon/controller/handlers/credentials/credential_service.go index 769787f7c..c6e824baa 100644 --- a/internal/daemon/controller/handlers/credentials/credential_service.go +++ b/internal/daemon/controller/handlers/credentials/credential_service.go @@ -48,19 +48,19 @@ var ( // IdActions contains the set of actions that can be performed on // individual resources - IdActions = action.ActionSet{ + IdActions = action.NewActionSet( action.NoOp, action.Read, action.Update, action.Delete, - } + ) // CollectionActions contains the set of actions that can be performed on // this collection - CollectionActions = action.ActionSet{ + CollectionActions = action.NewActionSet( action.Create, action.List, - } + ) ) func init() { diff --git a/internal/daemon/controller/handlers/credentials/credential_service_test.go b/internal/daemon/controller/handlers/credentials/credential_service_test.go index 11821edaa..e46c1db98 100644 --- a/internal/daemon/controller/handlers/credentials/credential_service_test.go +++ b/internal/daemon/controller/handlers/credentials/credential_service_test.go @@ -12,6 +12,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/credential" "github.com/hashicorp/boundary/internal/credential/static" @@ -186,9 +187,17 @@ func TestList(t *testing.T) { return } require.NoError(t, gErr) - assert.Empty(t, cmp.Diff(got, tc.res, protocmp.Transform(), protocmp.SortRepeated(func(x, y *pb.Credential) bool { - return x.Id < y.Id - }))) + assert.Empty(t, cmp.Diff( + got, + tc.res, + protocmp.Transform(), + protocmp.SortRepeated(func(x, y *pb.Credential) bool { + return x.Id < y.Id + }), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + )) // Test anonymous listing got, gErr = s.ListCredentials(auth.DisabledAuthTestContext(iamRepoFn, prj.GetPublicId(), auth.WithUserId(globals.AnonymousUserId)), tc.req) @@ -388,7 +397,14 @@ func TestGet(t *testing.T) { return } require.NoError(t, gErr) - assert.Empty(t, cmp.Diff(got, tc.res, protocmp.Transform())) + assert.Empty(t, cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + )) // Test anonymous get got, gErr = s.GetCredential(auth.DisabledAuthTestContext(iamRepoFn, prj.GetPublicId(), auth.WithUserId(globals.AnonymousUserId)), req) @@ -829,7 +845,15 @@ func TestCreate(t *testing.T) { got.Item.Id, tc.res.Item.Id = "", "" got.Item.CreatedTime, got.Item.UpdatedTime, tc.res.Item.CreatedTime, tc.res.Item.UpdatedTime = nil, nil, nil, nil } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform(), protocmp.SortRepeatedFields(got)), "CreateCredential(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + protocmp.SortRepeatedFields(got), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "CreateCredential(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } } @@ -1276,7 +1300,14 @@ func TestUpdate(t *testing.T) { assert.EqualValues(2, got.Item.Version) want.Item.Version = 2 - assert.Empty(cmp.Diff(got, want, protocmp.Transform())) + assert.Empty(cmp.Diff( + got, + want, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + )) }) } diff --git a/internal/daemon/controller/handlers/credentialstores/credentialstore_service.go b/internal/daemon/controller/handlers/credentialstores/credentialstore_service.go index 2c87588fc..813fb05e8 100644 --- a/internal/daemon/controller/handlers/credentialstores/credentialstore_service.go +++ b/internal/daemon/controller/handlers/credentialstores/credentialstore_service.go @@ -49,19 +49,19 @@ var ( // IdActions contains the set of actions that can be performed on // individual resources - IdActions = action.ActionSet{ + IdActions = action.NewActionSet( action.NoOp, action.Read, action.Update, action.Delete, - } + ) // CollectionActions contains the set of actions that can be performed on // this collection - CollectionActions = action.ActionSet{ + CollectionActions = action.NewActionSet( action.Create, action.List, - } + ) vaultCollectionTypeMap = map[resource.Type]action.ActionSet{ resource.CredentialLibrary: credentiallibraries.CollectionActions, diff --git a/internal/daemon/controller/handlers/credentialstores/credentialstore_service_test.go b/internal/daemon/controller/handlers/credentialstores/credentialstore_service_test.go index 5ec481988..8fd327137 100644 --- a/internal/daemon/controller/handlers/credentialstores/credentialstore_service_test.go +++ b/internal/daemon/controller/handlers/credentialstores/credentialstore_service_test.go @@ -14,6 +14,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/boundary/globals" credstatic "github.com/hashicorp/boundary/internal/credential/static" "github.com/hashicorp/boundary/internal/credential/vault" @@ -184,9 +185,20 @@ func TestList(t *testing.T) { return } require.NoError(t, gErr) - assert.Empty(t, cmp.Diff(got, tc.res, protocmp.Transform(), protocmp.SortRepeated(func(x, y *pb.CredentialStore) bool { - return x.Id < y.Id - }))) + assert.Empty(t, cmp.Diff( + got, + tc.res, + protocmp.Transform(), + protocmp.SortRepeated(func(x, y *pb.CredentialStore) bool { + return x.Id < y.Id + }), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), + )) // Test anonymous listing got, gErr = s.ListCredentialStores(auth.DisabledAuthTestContext(iamRepoFn, tc.req.GetScopeId(), auth.WithUserId(globals.AnonymousUserId)), tc.req) @@ -551,6 +563,12 @@ func TestCreateVault(t *testing.T) { cmpOptions := []cmp.Option{ protocmp.Transform(), protocmp.SortRepeatedFields(got), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), } if got != nil { assert.Contains(got.GetUri(), tc.res.Uri) @@ -728,6 +746,12 @@ func TestCreateStatic(t *testing.T) { cmpOptions := []cmp.Option{ protocmp.Transform(), protocmp.SortRepeatedFields(got), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), } if got != nil { assert.Contains(got.GetUri(), tc.res.Uri) @@ -870,7 +894,17 @@ func TestGet(t *testing.T) { return } require.NoError(t, gErr) - assert.Empty(t, cmp.Diff(got, tc.res, protocmp.Transform())) + assert.Empty(t, cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), + )) // Test anonymous get got, gErr = s.GetCredentialStore(auth.DisabledAuthTestContext(iamRepoFn, prj.GetPublicId(), auth.WithUserId(globals.AnonymousUserId)), req) @@ -1221,7 +1255,17 @@ func TestUpdateVault(t *testing.T) { got.Item.GetVaultCredentialStoreAttributes().ClientCertificateKeyHmac = "" } - assert.Empty(cmp.Diff(got, want, protocmp.Transform())) + assert.Empty(cmp.Diff( + got, + want, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), + )) }) } @@ -1412,7 +1456,17 @@ func TestUpdateStatic(t *testing.T) { assert.EqualValues(2, got.Item.Version) want.Item.Version = 2 - assert.Empty(cmp.Diff(got, want, protocmp.Transform())) + assert.Empty(cmp.Diff( + got, + want, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), + )) }) } diff --git a/internal/daemon/controller/handlers/groups/group_service.go b/internal/daemon/controller/handlers/groups/group_service.go index 95fb1bfca..f67d24064 100644 --- a/internal/daemon/controller/handlers/groups/group_service.go +++ b/internal/daemon/controller/handlers/groups/group_service.go @@ -32,7 +32,7 @@ var ( // IdActions contains the set of actions that can be performed on // individual resources - IdActions = action.ActionSet{ + IdActions = action.NewActionSet( action.NoOp, action.Read, action.Update, @@ -40,14 +40,14 @@ var ( action.AddMembers, action.SetMembers, action.RemoveMembers, - } + ) // CollectionActions contains the set of actions that can be performed on // this collection - CollectionActions = action.ActionSet{ + CollectionActions = action.NewActionSet( action.Create, action.List, - } + ) ) func init() { diff --git a/internal/daemon/controller/handlers/groups/group_service_test.go b/internal/daemon/controller/handlers/groups/group_service_test.go index 1372eeec4..cee910614 100644 --- a/internal/daemon/controller/handlers/groups/group_service_test.go +++ b/internal/daemon/controller/handlers/groups/group_service_test.go @@ -11,6 +11,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/daemon/controller/auth" "github.com/hashicorp/boundary/internal/daemon/controller/handlers" @@ -208,7 +209,14 @@ func TestGet(t *testing.T) { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "GetGroup(%+v) got error %v, wanted %v", req, gErr, tc.err) } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "GetGroup(%q) got response\n%q, wanted\n%q", req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "GetGroup(%q) got response\n%q, wanted\n%q", req, got, tc.res) }) } } @@ -333,7 +341,14 @@ func TestList(t *testing.T) { return } require.NoError(gErr) - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListGroups(%q) got response %q, wanted %q", tc.req.GetScopeId(), got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "ListGroups(%q) got response %q, wanted %q", tc.req.GetScopeId(), got, tc.res) // Test the anon case got, gErr = s.ListGroups(auth.DisabledAuthTestContext(repoFn, tc.req.GetScopeId(), auth.WithUserId(globals.AnonymousUserId)), tc.req) @@ -566,7 +581,14 @@ func TestCreate(t *testing.T) { got.Item.Id, tc.res.Item.Id = "", "" got.Item.CreatedTime, got.Item.UpdatedTime, tc.res.Item.CreatedTime, tc.res.Item.UpdatedTime = nil, nil, nil, nil } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "CreateGroup(%q) got response %q, wanted %q", req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "CreateGroup(%q) got response %q, wanted %q", req, got, tc.res) }) } } @@ -973,7 +995,14 @@ func TestUpdate(t *testing.T) { assert.Equal(ver+1, got.GetItem().GetVersion()) tc.res.Item.Version = ver + 1 } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "UpdateGroup(%q) got response %q, wanted %q", req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "UpdateGroup(%q) got response %q, wanted %q", req, got, tc.res) }) } } diff --git a/internal/daemon/controller/handlers/host_catalogs/host_catalog_service.go b/internal/daemon/controller/handlers/host_catalogs/host_catalog_service.go index b5cbd8ab3..b0070dd3e 100644 --- a/internal/daemon/controller/handlers/host_catalogs/host_catalog_service.go +++ b/internal/daemon/controller/handlers/host_catalogs/host_catalog_service.go @@ -43,19 +43,19 @@ var ( // IdActions contains the set of actions that can be performed on // individual resources - IdActions = action.ActionSet{ + IdActions = action.NewActionSet( action.NoOp, action.Read, action.Update, action.Delete, - } + ) // CollectionActions contains the set of actions that can be performed on // this collection - CollectionActions = action.ActionSet{ + CollectionActions = action.NewActionSet( action.Create, action.List, - } + ) collectionTypeMap = map[globals.Subtype]map[resource.Type]action.ActionSet{ static.Subtype: { @@ -64,9 +64,9 @@ var ( }, hostplugin.Subtype: { resource.HostSet: host_sets.CollectionActions, - resource.Host: action.ActionSet{ + resource.Host: action.NewActionSet( action.List, - }, + ), }, } ) diff --git a/internal/daemon/controller/handlers/host_catalogs/host_catalog_service_test.go b/internal/daemon/controller/handlers/host_catalogs/host_catalog_service_test.go index f6c8c8a8a..a33ef8f5a 100644 --- a/internal/daemon/controller/handlers/host_catalogs/host_catalog_service_test.go +++ b/internal/daemon/controller/handlers/host_catalogs/host_catalog_service_test.go @@ -163,7 +163,17 @@ func TestGet_Static(t *testing.T) { if tc.res != nil { tc.res.Item.Version = 1 } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "GetHostCatalog(%q) got response %q, wanted %q", req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), + ), "GetHostCatalog(%q) got response %q, wanted %q", req, got, tc.res) }) } } @@ -279,7 +289,17 @@ func TestGet_Plugin(t *testing.T) { if tc.res != nil { tc.res.Item.Version = 1 } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "GetHostCatalog(%q) got response %q, wanted %q", req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), + ), "GetHostCatalog(%q) got response %q, wanted %q", req, got, tc.res) }) } } @@ -473,7 +493,17 @@ func TestList(t *testing.T) { return tc.res.Items[i].GetId() < tc.res.Items[j].GetId() }) - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListHostCatalogs() for scope %q got response %q, wanted %q", tc.req.GetScopeId(), got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), + ), "ListHostCatalogs() for scope %q got response %q, wanted %q", tc.req.GetScopeId(), got, tc.res) // Test with anon user got, gErr = s.ListHostCatalogs(auth.DisabledAuthTestContext(iamRepoFn, tc.req.GetScopeId(), auth.WithUserId(globals.AnonymousUserId)), tc.req) @@ -817,7 +847,17 @@ func TestCreate_Static(t *testing.T) { if tc.res != nil { tc.res.Item.Version = 1 } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "CreateHostCatalog(%q) got response %q, wanted %q", req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), + ), "CreateHostCatalog(%q) got response %q, wanted %q", req, got, tc.res) }) } } @@ -995,7 +1035,17 @@ func TestCreate_Plugin(t *testing.T) { if tc.res != nil { tc.res.Item.Version = 1 } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "CreateHostCatalog(%q) got response %q, wanted %q", req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), + ), "CreateHostCatalog(%q) got response %q, wanted %q", req, got, tc.res) }) } } @@ -1364,7 +1414,17 @@ func TestUpdate_Static(t *testing.T) { if tc.res != nil { tc.res.Item.Version = version + 1 } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "UpdateHostCatalog(%q) got response %q, wanted %q", req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), + ), "UpdateHostCatalog(%q) got response %q, wanted %q", req, got, tc.res) }) } } @@ -1481,7 +1541,17 @@ func TestUpdate_Plugin(t *testing.T) { check: func(t *testing.T, in *pb.HostCatalog) { assert.Equal(t, "new", in.Name.GetValue()) assert.Equal(t, "desc", in.Description.GetValue()) - assert.Empty(t, cmp.Diff(authorizedCollectionActions[hostplugin.Subtype], in.GetAuthorizedCollectionActions(), cmpopts.IgnoreUnexported(structpb.ListValue{}, structpb.Value{}))) + assert.Empty(t, cmp.Diff( + authorizedCollectionActions[hostplugin.Subtype], + in.GetAuthorizedCollectionActions(), + cmpopts.IgnoreUnexported(structpb.ListValue{}, structpb.Value{}), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), + )) }, }, { diff --git a/internal/daemon/controller/handlers/host_sets/host_set_service.go b/internal/daemon/controller/handlers/host_sets/host_set_service.go index 533ceca18..8f3dadc11 100644 --- a/internal/daemon/controller/handlers/host_sets/host_set_service.go +++ b/internal/daemon/controller/handlers/host_sets/host_set_service.go @@ -40,7 +40,7 @@ var ( // IdActions contains the set of actions that can be performed on // individual resources idActionsTypeMap = map[globals.Subtype]action.ActionSet{ - static.Subtype: { + static.Subtype: action.NewActionSet( action.NoOp, action.Read, action.Update, @@ -48,21 +48,21 @@ var ( action.AddHosts, action.SetHosts, action.RemoveHosts, - }, - hostplugin.Subtype: { + ), + hostplugin.Subtype: action.NewActionSet( action.NoOp, action.Read, action.Update, action.Delete, - }, + ), } // CollectionActions contains the set of actions that can be performed on // this collection - CollectionActions = action.ActionSet{ + CollectionActions = action.NewActionSet( action.Create, action.List, - } + ) ) const domain = "host" diff --git a/internal/daemon/controller/handlers/host_sets/host_set_service_test.go b/internal/daemon/controller/handlers/host_sets/host_set_service_test.go index 3184a12ac..89f161e27 100644 --- a/internal/daemon/controller/handlers/host_sets/host_set_service_test.go +++ b/internal/daemon/controller/handlers/host_sets/host_set_service_test.go @@ -13,6 +13,7 @@ import ( "github.com/golang/protobuf/ptypes/wrappers" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/daemon/controller/auth" "github.com/hashicorp/boundary/internal/daemon/controller/handlers" @@ -139,7 +140,14 @@ func TestGet_Static(t *testing.T) { if tc.res != nil { tc.res.Item.Version = 1 } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "GetHostSet(%q) got response %q, wanted %q", req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "GetHostSet(%q) got response %q, wanted %q", req, got, tc.res) }) } } @@ -258,7 +266,14 @@ func TestGet_Plugin(t *testing.T) { if tc.res != nil { tc.res.Item.Version = 1 } - assert.Empty(cmp.Diff(got.GetItem(), tc.res.GetItem(), protocmp.Transform()), "GetHostSet(%q) got response %q, wanted %q", req, got, tc.res) + assert.Empty(cmp.Diff( + got.GetItem(), + tc.res.GetItem(), + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "GetHostSet(%q) got response %q, wanted %q", req, got, tc.res) }) } } @@ -347,7 +362,14 @@ func TestList_Static(t *testing.T) { return } require.NoError(gErr) - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListHostSets(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "ListHostSets(%q) got response %q, wanted %q", tc.req, got, tc.res) // Test with anon user got, gErr = s.ListHostSets(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId(), auth.WithUserId(globals.AnonymousUserId)), tc.req) @@ -461,7 +483,14 @@ func TestList_Plugin(t *testing.T) { return } require.NoError(gErr) - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListHostSets(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "ListHostSets(%q) got response %q, wanted %q", tc.req, got, tc.res) // Test with anon user got, gErr = s.ListHostSets(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId(), auth.WithUserId(globals.AnonymousUserId)), tc.req) @@ -544,7 +573,14 @@ func TestDelete_Static(t *testing.T) { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "DeleteHostSet(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) } - assert.Empty(cmp.Diff(tc.res, got, protocmp.Transform()), "DeleteHostSet(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + tc.res, + got, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "DeleteHostSet(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } } @@ -622,7 +658,14 @@ func TestDelete_Plugin(t *testing.T) { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "DeleteHostSet(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) } - assert.Empty(cmp.Diff(tc.res, got, protocmp.Transform()), "DeleteHostSet(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + tc.res, + got, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "DeleteHostSet(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } } @@ -818,7 +861,14 @@ func TestCreate_Static(t *testing.T) { if tc.res != nil { tc.res.Item.Version = 1 } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "CreateHostSet(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "CreateHostSet(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } } @@ -1086,7 +1136,14 @@ func TestCreate_Plugin(t *testing.T) { if tc.res != nil { tc.res.Item.Version = 1 } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "CreateHostSet(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "CreateHostSet(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } } @@ -1468,7 +1525,14 @@ func TestUpdate_Static(t *testing.T) { }) } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "UpdateHostSet(%q) got response %q, wanted %q", req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "UpdateHostSet(%q) got response %q, wanted %q", req, got, tc.res) }) } } diff --git a/internal/daemon/controller/handlers/hosts/host_service.go b/internal/daemon/controller/handlers/hosts/host_service.go index 545a2e600..8eeef2617 100644 --- a/internal/daemon/controller/handlers/hosts/host_service.go +++ b/internal/daemon/controller/handlers/hosts/host_service.go @@ -37,24 +37,24 @@ var ( // IdActions contains the set of actions that can be performed on // individual resources idActionsTypeMap = map[globals.Subtype]action.ActionSet{ - static.Subtype: { + static.Subtype: action.NewActionSet( action.NoOp, action.Read, action.Update, action.Delete, - }, - hostplugin.Subtype: { + ), + hostplugin.Subtype: action.NewActionSet( action.NoOp, action.Read, - }, + ), } // CollectionActions contains the set of actions that can be performed on // this collection - CollectionActions = action.ActionSet{ + CollectionActions = action.NewActionSet( action.Create, action.List, - } + ) ) const domain = "host" diff --git a/internal/daemon/controller/handlers/hosts/host_service_test.go b/internal/daemon/controller/handlers/hosts/host_service_test.go index 13dd0189c..8c2a02123 100644 --- a/internal/daemon/controller/handlers/hosts/host_service_test.go +++ b/internal/daemon/controller/handlers/hosts/host_service_test.go @@ -13,6 +13,7 @@ import ( "github.com/golang/protobuf/ptypes/wrappers" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/daemon/controller/auth" "github.com/hashicorp/boundary/internal/daemon/controller/handlers" @@ -134,7 +135,14 @@ func TestGet_Static(t *testing.T) { if tc.res != nil { tc.res.Item.Version = 1 } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "GetHost(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "GetHost(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } } @@ -253,7 +261,14 @@ func TestGet_Plugin(t *testing.T) { if tc.res != nil { tc.res.Item.Version = 1 } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "GetHost(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "GetHost(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } } @@ -359,7 +374,14 @@ func TestList_Static(t *testing.T) { return } require.NoError(gErr) - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListHosts(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "ListHosts(%q) got response %q, wanted %q", tc.req, got, tc.res) // Test anonymous listing got, gErr = s.ListHosts(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId(), auth.WithUserId(globals.AnonymousUserId)), tc.req) @@ -486,7 +508,14 @@ func TestList_Plugin(t *testing.T) { sort.Slice(got.Items, func(i, j int) bool { return got.Items[i].GetId() < got.Items[j].GetId() }) - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListHosts(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "ListHosts(%q) got response %q, wanted %q", tc.req, got, tc.res) // Test anonymous listing got, gErr = s.ListHosts(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId(), auth.WithUserId(globals.AnonymousUserId)), tc.req) @@ -580,7 +609,14 @@ func TestDelete(t *testing.T) { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "DeleteHost(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) } - assert.Empty(cmp.Diff(tc.res, got, protocmp.Transform()), "DeleteHost(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + tc.res, + got, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "DeleteHost(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } } @@ -856,7 +892,14 @@ func TestCreate(t *testing.T) { if tc.res != nil { tc.res.Item.Version = 1 } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "CreateHost(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "CreateHost(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } } @@ -1324,7 +1367,14 @@ func TestUpdate_Static(t *testing.T) { // Clear all values which are hard to compare against. got.Item.UpdatedTime, tc.res.Item.UpdatedTime = nil, nil tc.res.Item.Version = version + 1 - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "UpdateHost(%q) got response %q, wanted %q", req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "UpdateHost(%q) got response %q, wanted %q", req, got, tc.res) }) } } diff --git a/internal/daemon/controller/handlers/managed_groups/managed_group_service.go b/internal/daemon/controller/handlers/managed_groups/managed_group_service.go index b4bd7b2be..55e9466fc 100644 --- a/internal/daemon/controller/handlers/managed_groups/managed_group_service.go +++ b/internal/daemon/controller/handlers/managed_groups/managed_group_service.go @@ -45,26 +45,26 @@ var ( // IdActions contains the set of actions that can be performed on // individual resources IdActions = map[globals.Subtype]action.ActionSet{ - oidc.Subtype: { + oidc.Subtype: action.NewActionSet( action.NoOp, action.Read, action.Update, action.Delete, - }, - ldap.Subtype: { + ), + ldap.Subtype: action.NewActionSet( action.NoOp, action.Read, action.Update, action.Delete, - }, + ), } // CollectionActions contains the set of actions that can be performed on // this collection - CollectionActions = action.ActionSet{ + CollectionActions = action.NewActionSet( action.Create, action.List, - } + ) ) func init() { diff --git a/internal/daemon/controller/handlers/managed_groups/managed_group_service_test.go b/internal/daemon/controller/handlers/managed_groups/managed_group_service_test.go index 6c5e306b9..293b60937 100644 --- a/internal/daemon/controller/handlers/managed_groups/managed_group_service_test.go +++ b/internal/daemon/controller/handlers/managed_groups/managed_group_service_test.go @@ -13,6 +13,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/auth/ldap" "github.com/hashicorp/boundary/internal/auth/oidc" @@ -252,7 +253,14 @@ func TestGet(t *testing.T) { return } require.NoError(gErr) - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "GetManagedGroup(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "GetManagedGroup(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } } @@ -393,7 +401,14 @@ func TestListOidc(t *testing.T) { return strings.Compare(got.Items[i].GetName().GetValue(), got.Items[j].GetName().GetValue()) < 0 }) - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListManagedGroups() with scope %q got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "ListManagedGroups() with scope %q got response %q, wanted %q", tc.req, got, tc.res) // Now test with anon if tc.skipAnon { @@ -551,7 +566,14 @@ func TestListLdap(t *testing.T) { return strings.Compare(got.Items[i].GetName().GetValue(), got.Items[j].GetName().GetValue()) < 0 }) - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListManagedGroups() with scope %q got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "ListManagedGroups() with scope %q got response %q, wanted %q", tc.req, got, tc.res) // Now test with anon if tc.skipAnon { @@ -909,7 +931,14 @@ func TestCreateOidc(t *testing.T) { got.Item.Id, tc.res.Item.Id = "", "" got.Item.CreatedTime, got.Item.UpdatedTime, tc.res.Item.CreatedTime, tc.res.Item.UpdatedTime = nil, nil, nil, nil } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "CreateManagedGroup(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "CreateManagedGroup(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } } @@ -1115,7 +1144,14 @@ func TestCreateLdap(t *testing.T) { got.Item.Id, tc.res.Item.Id = "", "" got.Item.CreatedTime, got.Item.UpdatedTime, tc.res.Item.CreatedTime, tc.res.Item.UpdatedTime = nil, nil, nil, nil } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "CreateManagedGroup(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "CreateManagedGroup(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } } @@ -1503,7 +1539,14 @@ func TestUpdateOidc(t *testing.T) { assert.EqualValues(2, got.Item.Version) tc.res.Item.Version = 2 } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "UpdateManagedGroup(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "UpdateManagedGroup(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } } @@ -1897,7 +1940,14 @@ func TestUpdateLdap(t *testing.T) { assert.EqualValues(2, got.Item.Version) tc.res.Item.Version = 2 } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "UpdateManagedGroup(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "UpdateManagedGroup(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } } diff --git a/internal/daemon/controller/handlers/roles/role_service.go b/internal/daemon/controller/handlers/roles/role_service.go index 246508648..fb88f43dd 100644 --- a/internal/daemon/controller/handlers/roles/role_service.go +++ b/internal/daemon/controller/handlers/roles/role_service.go @@ -33,7 +33,7 @@ var ( // IdActions contains the set of actions that can be performed on // individual resources - IdActions = action.ActionSet{ + IdActions = action.NewActionSet( action.NoOp, action.Read, action.Update, @@ -44,14 +44,14 @@ var ( action.AddGrants, action.SetGrants, action.RemoveGrants, - } + ) // CollectionActions contains the set of actions that can be performed on // this collection - CollectionActions = action.ActionSet{ + CollectionActions = action.NewActionSet( action.Create, action.List, - } + ) ) func init() { diff --git a/internal/daemon/controller/handlers/roles/role_service_test.go b/internal/daemon/controller/handlers/roles/role_service_test.go index e332fa01e..687efabb4 100644 --- a/internal/daemon/controller/handlers/roles/role_service_test.go +++ b/internal/daemon/controller/handlers/roles/role_service_test.go @@ -11,6 +11,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/auth/ldap" "github.com/hashicorp/boundary/internal/auth/oidc" @@ -185,7 +186,14 @@ func TestGet(t *testing.T) { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "GetRole(%+v) got error %v, wanted %v", req, gErr, tc.err) } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "GetRole(%q) got response\n%q, wanted\n%q", req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "GetRole(%q) got response\n%q, wanted\n%q", req, got, tc.res) }) } } @@ -311,7 +319,14 @@ func TestList(t *testing.T) { return } require.NoError(gErr) - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListRoles(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "ListRoles(%q) got response %q, wanted %q", tc.req, got, tc.res) // Test the anon case got, gErr = s.ListRoles(auth.DisabledAuthTestContext(repoFn, tc.req.GetScopeId(), auth.WithUserId(globals.AnonymousUserId)), tc.req) @@ -566,7 +581,14 @@ func TestCreate(t *testing.T) { got.Item.Id, tc.res.Item.Id = "", "" got.Item.CreatedTime, got.Item.UpdatedTime, tc.res.Item.CreatedTime, tc.res.Item.UpdatedTime = nil, nil, nil, nil } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "CreateRole(%q) got response\n%q, wanted\n%q", req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "CreateRole(%q) got response\n%q, wanted\n%q", req, got, tc.res) }) } } @@ -1009,7 +1031,14 @@ func TestUpdate(t *testing.T) { assert.Equal(ver+1, got.GetItem().GetVersion()) tc.res.Item.Version = ver + 1 } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "UpdateRole(%q) got response\n%q,\nwanted\n%q", req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "UpdateRole(%q) got response\n%q,\nwanted\n%q", req, got, tc.res) }) } } diff --git a/internal/daemon/controller/handlers/scopes/scope_service.go b/internal/daemon/controller/handlers/scopes/scope_service.go index eaa165cf0..c87313090 100644 --- a/internal/daemon/controller/handlers/scopes/scope_service.go +++ b/internal/daemon/controller/handlers/scopes/scope_service.go @@ -49,23 +49,23 @@ var ( // IdActions contains the set of actions that can be performed on // individual resources - IdActions = action.ActionSet{ + IdActions = action.NewActionSet( action.NoOp, action.Read, action.Update, action.Delete, - } + ) // CollectionActions contains the set of actions that can be performed on // this collection - CollectionActions = action.ActionSet{ + CollectionActions = action.NewActionSet( action.Create, action.List, action.ListScopeKeys, action.RotateScopeKeys, action.ListScopeKeyVersionDestructionJobs, action.DestroyScopeKeyVersion, - } + ) scopeCollectionTypeMapMap = map[string]map[resource.Type]action.ActionSet{ scope.Global.String(): { @@ -96,9 +96,14 @@ var ( resource.Group: groups.CollectionActions, resource.HostCatalog: host_catalogs.CollectionActions, resource.Role: roles.CollectionActions, - resource.Scope: CollectionActions[2:], // Only Scope key actions are allowed on the project level - resource.Session: sessions.CollectionActions, - resource.Target: targets.CollectionActions, + resource.Scope: action.NewActionSet( + action.ListScopeKeys, + action.RotateScopeKeys, + action.ListScopeKeyVersionDestructionJobs, + action.DestroyScopeKeyVersion, + ), // Only Scope key actions are allowed on the project level + resource.Session: sessions.CollectionActions, + resource.Target: targets.CollectionActions, }, } ) @@ -252,7 +257,7 @@ func (s Service) GetScope(ctx context.Context, req *pbs.GetScopeRequest) (*pbs.G act := IdActions // Can't delete global so elide it if p.GetPublicId() == scope.Global.String() { - act = act[0:3] + act = action.Difference(act, action.NewActionSet(action.Delete)) } if outputFields.Has(globals.AuthorizedActionsField) { outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authResults.FetchActionSetForId(ctx, p.GetPublicId(), act).Strings())) diff --git a/internal/daemon/controller/handlers/scopes/scope_service_test.go b/internal/daemon/controller/handlers/scopes/scope_service_test.go index 3a070c8ce..b9f67796f 100644 --- a/internal/daemon/controller/handlers/scopes/scope_service_test.go +++ b/internal/daemon/controller/handlers/scopes/scope_service_test.go @@ -13,6 +13,7 @@ import ( "github.com/golang/protobuf/ptypes/wrappers" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/authtoken" @@ -98,11 +99,11 @@ var globalAuthorizedCollectionActions = map[string]*structpb.ListValue{ "scopes": { Values: []*structpb.Value{ structpb.NewStringValue("create"), + structpb.NewStringValue("destroy-key-version"), structpb.NewStringValue("list"), + structpb.NewStringValue("list-key-version-destruction-jobs"), structpb.NewStringValue("list-keys"), structpb.NewStringValue("rotate-keys"), - structpb.NewStringValue("list-key-version-destruction-jobs"), - structpb.NewStringValue("destroy-key-version"), }, }, "session-recordings": { @@ -160,11 +161,11 @@ var orgAuthorizedCollectionActions = map[string]*structpb.ListValue{ "scopes": { Values: []*structpb.Value{ structpb.NewStringValue("create"), + structpb.NewStringValue("destroy-key-version"), structpb.NewStringValue("list"), + structpb.NewStringValue("list-key-version-destruction-jobs"), structpb.NewStringValue("list-keys"), structpb.NewStringValue("rotate-keys"), - structpb.NewStringValue("list-key-version-destruction-jobs"), - structpb.NewStringValue("destroy-key-version"), }, }, "session-recordings": { @@ -218,10 +219,10 @@ var projectAuthorizedCollectionActions = map[string]*structpb.ListValue{ }, "scopes": { Values: []*structpb.Value{ + structpb.NewStringValue("destroy-key-version"), + structpb.NewStringValue("list-key-version-destruction-jobs"), structpb.NewStringValue("list-keys"), structpb.NewStringValue("rotate-keys"), - structpb.NewStringValue("list-key-version-destruction-jobs"), - structpb.NewStringValue("destroy-key-version"), }, }, "targets": { @@ -329,7 +330,17 @@ func TestGet(t *testing.T) { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "GetScope(%+v) got error\n%v, wanted\n%v", req, gErr, tc.err) } - assert.Empty(cmp.Diff(tc.res, got, protocmp.Transform()), "GetScope(%q) got response\n%q, wanted\n%q", req, got, tc.res) + assert.Empty(cmp.Diff( + tc.res, + got, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), + ), "GetScope(%q) got response\n%q, wanted\n%q", req, got, tc.res) }) } } @@ -414,7 +425,17 @@ func TestList(t *testing.T) { assert.True(errors.Is(gErr, tc.err), "ListScopes(%+v) got error\n%v, wanted\n%v", tc.req, gErr, tc.err) return } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListScopes(%q) got response\n%q\nwanted\n%q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), + ), "ListScopes(%q) got response\n%q\nwanted\n%q", tc.req, got, tc.res) // Now test with anonymous listing got, gErr = s.ListScopes(auth.DisabledAuthTestContext(repoFn, tc.scopeId, auth.WithUserId(globals.AnonymousUserId)), tc.req) @@ -529,7 +550,17 @@ func TestList(t *testing.T) { return } require.NoError(gErr) - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListScopes(%q) got response\n%q, wanted\n%q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), + ), "ListScopes(%q) got response\n%q, wanted\n%q", tc.req, got, tc.res) // Now test with anonymous listing got, gErr = s.ListScopes(auth.DisabledAuthTestContext(repoFn, tc.scopeId, auth.WithUserId(globals.AnonymousUserId)), tc.req) @@ -965,7 +996,17 @@ func TestCreate(t *testing.T) { got.Item.Id, tc.res.Item.Id = "", "" got.Item.CreatedTime, got.Item.UpdatedTime, tc.res.Item.CreatedTime, tc.res.Item.UpdatedTime = nil, nil, nil, nil } - assert.Empty(cmp.Diff(tc.res, got, protocmp.Transform()), "CreateScope(%q) got response %q, wanted %q", req, got, tc.res) + assert.Empty(cmp.Diff( + tc.res, + got, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), + ), "CreateScope(%q) got response %q, wanted %q", req, got, tc.res) }) } } @@ -1539,7 +1580,17 @@ func TestUpdate(t *testing.T) { assert.Equal(ver+1, got.GetItem().GetVersion()) tc.res.Item.Version = ver + 1 } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "UpdateScope(%q) got response\n%q, wanted\n%q", req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), + ), "UpdateScope(%q) got response\n%q, wanted\n%q", req, got, tc.res) }) } } @@ -2027,6 +2078,9 @@ func TestListKeys(t *testing.T) { protocmp.SortRepeated(func(i, j *pb.Key) bool { return i.GetPurpose() < j.GetPurpose() }), protocmp.IgnoreFields(&pb.Key{}, "id", "created_time"), protocmp.IgnoreFields(&pb.KeyVersion{}, "id", "created_time"), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), ), "ListKeys(%q) got response\n%q, wanted\n%q", tt.req, got, tt.res, ) @@ -2408,6 +2462,12 @@ func TestListKeyVersionDestructionJobs(t *testing.T) { protocmp.Transform(), protocmp.SortRepeated(func(i, j *pb.KeyVersionDestructionJob) bool { return i.GetTotalCount() < j.GetTotalCount() }), protocmp.IgnoreFields(&pb.KeyVersionDestructionJob{}, "key_version_id", "created_time"), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), ), "ListKeyVersionDestructionJobs(%q) got response\n%q, wanted\n%q", tt.req, got, tt.res, ) @@ -2658,7 +2718,17 @@ func TestDestroyKeyVersion(t *testing.T) { require.NoError(gErr) } - assert.Empty(cmp.Diff(tt.res, got, protocmp.Transform()), "DestroyKeyVersion(%q) got response\n%q, wanted\n%q", tt.req, got, tt.res) + assert.Empty(cmp.Diff( + tt.res, + got, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + cmpopts.SortSlices(func(a, b protocmp.Message) bool { + return a.String() < b.String() + }), + ), "DestroyKeyVersion(%q) got response\n%q, wanted\n%q", tt.req, got, tt.res) }) } } diff --git a/internal/daemon/controller/handlers/session_recordings/session_recording_service.go b/internal/daemon/controller/handlers/session_recordings/session_recording_service.go index 63556f95e..5cff170a3 100644 --- a/internal/daemon/controller/handlers/session_recordings/session_recording_service.go +++ b/internal/daemon/controller/handlers/session_recordings/session_recording_service.go @@ -19,17 +19,17 @@ import ( var ( // IdActions contains the set of actions that can be performed on // individual resources - IdActions = action.ActionSet{ + IdActions = action.NewActionSet( action.NoOp, action.Read, action.Download, - } + ) // CollectionActions contains the set of actions that can be performed on // this collection - CollectionActions = action.ActionSet{ + CollectionActions = action.NewActionSet( action.List, - } + ) ) // NewServiceFn returns a storage bucket service which is not implemented in OSS diff --git a/internal/daemon/controller/handlers/sessions/session_service.go b/internal/daemon/controller/handlers/sessions/session_service.go index 2032234ea..589b4070e 100644 --- a/internal/daemon/controller/handlers/sessions/session_service.go +++ b/internal/daemon/controller/handlers/sessions/session_service.go @@ -29,19 +29,19 @@ import ( var ( // IdActions contains the set of actions that can be performed on // individual resources - IdActions = action.ActionSet{ + IdActions = action.NewActionSet( action.NoOp, action.Read, action.ReadSelf, action.Cancel, action.CancelSelf, - } + ) // CollectionActions contains the set of actions that can be performed on // this collection - CollectionActions = action.ActionSet{ + CollectionActions = action.NewActionSet( action.List, - } + ) ) // Service handles request as described by the pbs.SessionServiceServer interface. diff --git a/internal/daemon/controller/handlers/sessions/session_service_test.go b/internal/daemon/controller/handlers/sessions/session_service_test.go index f67027822..f121285a8 100644 --- a/internal/daemon/controller/handlers/sessions/session_service_test.go +++ b/internal/daemon/controller/handlers/sessions/session_service_test.go @@ -11,6 +11,7 @@ import ( "time" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/authtoken" "github.com/hashicorp/boundary/internal/daemon/controller/auth" @@ -158,7 +159,14 @@ func TestGetSession(t *testing.T) { assert.True(got.GetItem().GetExpirationTime().AsTime().Sub(tc.res.GetItem().GetExpirationTime().AsTime()) < 10*time.Millisecond) tc.res.GetItem().ExpirationTime = got.GetItem().GetExpirationTime() } - assert.Empty(cmp.Diff(tc.res, got, protocmp.Transform()), "GetSession(%q) got response\n%q, wanted\n%q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + tc.res, + got, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "GetSession(%q) got response\n%q, wanted\n%q", tc.req, got, tc.res) }) } } @@ -544,7 +552,14 @@ func TestList(t *testing.T) { wantSess.ExpirationTime = got.GetItems()[i].GetExpirationTime() } } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListSessions(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "ListSessions(%q) got response %q, wanted %q", tc.req, got, tc.res) // Test with other user otherRequestInfo := authpb.RequestInfo{ @@ -760,7 +775,14 @@ func TestCancel(t *testing.T) { EndTime: got.GetItem().GetUpdatedTime(), }, } - assert.Empty(cmp.Diff(got.GetItem().GetStates(), wantState, protocmp.Transform()), "CancelSession(%q) states") + assert.Empty(cmp.Diff( + got.GetItem().GetStates(), + wantState, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "CancelSession(%q) states") got.GetItem().States = nil got.GetItem().UpdatedTime = nil @@ -771,7 +793,14 @@ func TestCancel(t *testing.T) { assert.Equal(got.GetItem().HostId, tc.res.GetItem().HostId) assert.Equal(got.GetItem().HostSetId, tc.res.GetItem().HostSetId) - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "CancelSession(%q) got response\n%q, wanted\n%q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "CancelSession(%q) got response\n%q, wanted\n%q", tc.req, got, tc.res) if tc.req != nil { require.NotNil(got) diff --git a/internal/daemon/controller/handlers/storage_buckets/storage_bucket_service.go b/internal/daemon/controller/handlers/storage_buckets/storage_bucket_service.go index 02544a5bc..62eb333b7 100644 --- a/internal/daemon/controller/handlers/storage_buckets/storage_bucket_service.go +++ b/internal/daemon/controller/handlers/storage_buckets/storage_bucket_service.go @@ -17,19 +17,19 @@ import ( var ( // IdActions contains the set of actions that can be performed on // individual resources - IdActions = action.ActionSet{ + IdActions = action.NewActionSet( action.NoOp, action.Read, action.Update, action.Delete, - } + ) // CollectionActions contains the set of actions that can be performed on // this collection - CollectionActions = action.ActionSet{ + CollectionActions = action.NewActionSet( action.Create, action.List, - } + ) ) // NewServiceFn returns a storage bucket service which is not implemented in OSS diff --git a/internal/daemon/controller/handlers/targets/target_service.go b/internal/daemon/controller/handlers/targets/target_service.go index a1e5a2ae1..fdccef245 100644 --- a/internal/daemon/controller/handlers/targets/target_service.go +++ b/internal/daemon/controller/handlers/targets/target_service.go @@ -74,7 +74,7 @@ var ( // IdActions contains the set of actions that can be performed on // individual resources - IdActions = action.ActionSet{ + IdActions = action.NewActionSet( action.NoOp, action.Read, action.Update, @@ -86,14 +86,14 @@ var ( action.SetCredentialSources, action.RemoveCredentialSources, action.AuthorizeSession, - } + ) // CollectionActions contains the set of actions that can be performed on // this collection - CollectionActions = action.ActionSet{ + CollectionActions = action.NewActionSet( action.Create, action.List, - } + ) validateCredentialSourcesFn = func(context.Context, globals.Subtype, []target.CredentialSource) error { return nil } ValidateIngressWorkerFilterFn = IngressWorkerFilterUnsupported diff --git a/internal/daemon/controller/handlers/targets/tcp/target_service_test.go b/internal/daemon/controller/handlers/targets/tcp/target_service_test.go index 2b41fff62..c64602718 100644 --- a/internal/daemon/controller/handlers/targets/tcp/target_service_test.go +++ b/internal/daemon/controller/handlers/targets/tcp/target_service_test.go @@ -17,6 +17,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/auth/ldap" "github.com/hashicorp/boundary/internal/auth/oidc" @@ -241,7 +242,14 @@ func TestGet(t *testing.T) { if tc.res != nil { tc.res.Item.Version = 1 } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "GetTarget(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "GetTarget(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } } @@ -390,7 +398,14 @@ func TestList(t *testing.T) { for _, t := range got.Items { want, ok := wantById[t.Id] assert.True(ok, "Got unexpected target with id: %s", t.Id) - assert.Empty(cmp.Diff(t, want, protocmp.Transform()), "got %v, wanted %v", t, want) + assert.Empty(cmp.Diff( + t, + want, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "got %v, wanted %v", t, want) } // Test with anon user @@ -490,7 +505,14 @@ func TestDelete(t *testing.T) { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "DeleteTarget(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) } - assert.Empty(cmp.Diff(tc.res, got, protocmp.Transform()), "DeleteTarget(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + tc.res, + got, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "DeleteTarget(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } } @@ -772,7 +794,14 @@ func TestCreate(t *testing.T) { if tc.res != nil { tc.res.Item.Version = 1 } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "CreateTarget(%q)\n got response %q\n, wanted %q\n", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "CreateTarget(%q)\n got response %q\n, wanted %q\n", tc.req, got, tc.res) }) } // Reset worker filter func @@ -1266,7 +1295,14 @@ func TestUpdate(t *testing.T) { if tc.res != nil { tc.res.Item.Version = tc.req.Item.Version + 1 } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "UpdateTarget(%q) got response %q, wanted %q", req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "UpdateTarget(%q) got response %q, wanted %q", req, got, tc.res) }) } // Reset worker filter funcs @@ -2624,7 +2660,14 @@ func TestAuthorizeSession(t *testing.T) { Id: tar.GetPublicId(), }) require.NoError(t, err) - assert.NotEmpty(t, cmp.Diff(asRes1.GetItem().GetCredentials(), asRes2.GetItem().GetCredentials(), protocmp.Transform()), + assert.NotEmpty(t, cmp.Diff( + asRes1.GetItem().GetCredentials(), + asRes2.GetItem().GetCredentials(), + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "the credentials aren't unique per request authorized session") _, err = s.AuthorizeSession(ctx, &pbs.AuthorizeSessionRequest{ @@ -2700,7 +2743,15 @@ func TestAuthorizeSession(t *testing.T) { gotCred.Secret = nil got.AuthorizationToken, got.SessionId, got.CreatedTime = "", "", nil - assert.Empty(t, cmp.Diff(got, want, protocmp.Transform(), protocmp.IgnoreFields(&pb.SessionAuthorization{}, "expiration"))) + assert.Empty(t, cmp.Diff( + got, + want, + protocmp.Transform(), + protocmp.IgnoreFields(&pb.SessionAuthorization{}, "expiration"), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + )) }) } } @@ -3293,7 +3344,14 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) { gotCred.Secret = nil got.AuthorizationToken, got.SessionId, got.CreatedTime = "", "", nil - assert.Empty(t, cmp.Diff(got, want, protocmp.Transform())) + assert.Empty(t, cmp.Diff( + got, + want, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + )) }) } } diff --git a/internal/daemon/controller/handlers/users/user_service.go b/internal/daemon/controller/handlers/users/user_service.go index b7380fb81..56b5d099c 100644 --- a/internal/daemon/controller/handlers/users/user_service.go +++ b/internal/daemon/controller/handlers/users/user_service.go @@ -32,7 +32,7 @@ var ( // IdActions contains the set of actions that can be performed on // individual resources - IdActions = action.ActionSet{ + IdActions = action.NewActionSet( action.NoOp, action.Read, action.Update, @@ -40,14 +40,14 @@ var ( action.AddAccounts, action.SetAccounts, action.RemoveAccounts, - } + ) // CollectionActions contains the set of actions that can be performed on // this collection - CollectionActions = action.ActionSet{ + CollectionActions = action.NewActionSet( action.Create, action.List, - } + ) ) func init() { diff --git a/internal/daemon/controller/handlers/users/user_service_test.go b/internal/daemon/controller/handlers/users/user_service_test.go index 77fa88b2b..45c99df4e 100644 --- a/internal/daemon/controller/handlers/users/user_service_test.go +++ b/internal/daemon/controller/handlers/users/user_service_test.go @@ -12,6 +12,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/auth/ldap" "github.com/hashicorp/boundary/internal/auth/oidc" @@ -152,7 +153,14 @@ func TestGet(t *testing.T) { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "GetUser(%+v) got error %v, wanted %v", req, gErr, tc.err) } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "GetUser(%q) got response %q, wanted %q", req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "GetUser(%q) got response %q, wanted %q", req, got, tc.res) }) } } @@ -304,7 +312,14 @@ func TestList(t *testing.T) { sort.Sort(resUsers) tc.res.Items = resUsers.users } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListUsers(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "ListUsers(%q) got response %q, wanted %q", tc.req, got, tc.res) // Test with anon user got, gErr = s.ListUsers(auth.DisabledAuthTestContext(repoFn, tc.req.GetScopeId(), auth.WithUserId(globals.AnonymousUserId)), tc.req) @@ -493,7 +508,14 @@ func TestCreate(t *testing.T) { got.Item.Id, tc.res.Item.Id = "", "" got.Item.CreatedTime, got.Item.UpdatedTime, tc.res.Item.CreatedTime, tc.res.Item.UpdatedTime = nil, nil, nil, nil } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "CreateUser(%q) got response %q, wanted %q", tc.req, got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "CreateUser(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } } @@ -763,7 +785,14 @@ func TestUpdate(t *testing.T) { assert.Equal(version+1, got.GetItem().GetVersion()) tc.res.Item.Version = version + 1 } - assert.Empty(cmp.Diff(tc.res, got, protocmp.Transform()), "UpdateUser(%q) got response %q, wanted %q", req, got, tc.res) + assert.Empty(cmp.Diff( + tc.res, + got, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "UpdateUser(%q) got response %q, wanted %q", req, got, tc.res) }) } } diff --git a/internal/daemon/controller/handlers/workers/worker_service.go b/internal/daemon/controller/handlers/workers/worker_service.go index cc6118a35..a62f15911 100644 --- a/internal/daemon/controller/handlers/workers/worker_service.go +++ b/internal/daemon/controller/handlers/workers/worker_service.go @@ -47,7 +47,7 @@ var ( // IdActions contains the set of actions that can be performed on // individual resources - IdActions = action.ActionSet{ + IdActions = action.NewActionSet( action.NoOp, action.Read, action.Update, @@ -55,17 +55,17 @@ var ( action.AddWorkerTags, action.SetWorkerTags, action.RemoveWorkerTags, - } + ) // CollectionActions contains the set of actions that can be performed on // this collection - CollectionActions = action.ActionSet{ + CollectionActions = action.NewActionSet( action.CreateControllerLed, action.CreateWorkerLed, action.List, action.ReadCertificateAuthority, action.ReinitializeCertificateAuthority, - } + ) // downstreamWorkers returns a list of worker ids which are directly // connected downstream of the provided worker. downstreamWorkers = emptyDownstreamWorkers diff --git a/internal/daemon/controller/handlers/workers/worker_service_test.go b/internal/daemon/controller/handlers/workers/worker_service_test.go index d47683332..f50af2633 100644 --- a/internal/daemon/controller/handlers/workers/worker_service_test.go +++ b/internal/daemon/controller/handlers/workers/worker_service_test.go @@ -11,6 +11,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/boundary/globals" wl "github.com/hashicorp/boundary/internal/daemon/common" "github.com/hashicorp/boundary/internal/daemon/controller/auth" @@ -294,7 +295,14 @@ func TestGet(t *testing.T) { } else { require.NoError(t, err) } - assert.Empty(t, cmp.Diff(got, tc.res, protocmp.Transform()), "GetWorker(%q) got response\n%q, wanted\n%q", tc.req, got, tc.res) + assert.Empty(t, cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "GetWorker(%q) got response\n%q, wanted\n%q", tc.req, got, tc.res) }) } } @@ -435,7 +443,14 @@ func TestList(t *testing.T) { sort.Slice(tc.res.Items, func(i, j int) bool { return tc.res.Items[i].GetName().GetValue() < tc.res.Items[j].GetName().GetValue() }) - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListWorkers(%q) got response %q, wanted %q", tc.req.GetScopeId(), got, tc.res) + assert.Empty(cmp.Diff( + got, + tc.res, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "ListWorkers(%q) got response %q, wanted %q", tc.req.GetScopeId(), got, tc.res) // Test the anon case got, gErr = s.ListWorkers(auth.DisabledAuthTestContext(iamRepoFn, tc.req.GetScopeId(), auth.WithUserId(globals.AnonymousUserId)), tc.req) @@ -1046,7 +1061,14 @@ func TestUpdate(t *testing.T) { expRes.Item.Version = tc.req.Item.Version + 1 - assert.Empty(t, cmp.Diff(got, expRes, protocmp.Transform()), "UpdateWorker(%q) got response %q, wanted %q", req, got, expRes) + assert.Empty(t, cmp.Diff( + got, + expRes, + protocmp.Transform(), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), "UpdateWorker(%q) got response %q, wanted %q", req, got, expRes) }) } } diff --git a/internal/perms/acl.go b/internal/perms/acl.go index 9974e105e..17cc53e46 100644 --- a/internal/perms/acl.go +++ b/internal/perms/acl.go @@ -316,7 +316,7 @@ func (a ACL) ListPermissions(requestedScopes map[string]*scopes.ScopeInfo, if ok := grant.actions[action.All]; ok { found = true } else { - for _, a := range idActions { + for a := range idActions { if ok := grant.actions[a]; ok { found = true break @@ -328,10 +328,10 @@ func (a ACL) ListPermissions(requestedScopes map[string]*scopes.ScopeInfo, } actions, _ := grant.Actions() - excludeList := make(action.ActionSet, 0, len(actions)) + excludeList := make(action.ActionSet, len(actions)) for _, aa := range actions { if aa != action.List { - excludeList = append(excludeList, aa) + excludeList.Add(aa) } } p.OnlySelf = p.OnlySelf && excludeList.OnlySelf() diff --git a/internal/perms/acl_test.go b/internal/perms/acl_test.go index b1d3e4709..f45c76385 100644 --- a/internal/perms/acl_test.go +++ b/internal/perms/acl_test.go @@ -440,7 +440,7 @@ func TestACL_ListPermissions(t *testing.T) { "o_this_one_too": nil, }, resourceType: resource.Session, - actionSet: action.ActionSet{action.Read}, + actionSet: action.NewActionSet(action.Read), expPermissions: []Permission{}, }, { @@ -453,7 +453,7 @@ func TestACL_ListPermissions(t *testing.T) { }, scopes: map[string]*scopes.ScopeInfo{"o_1": nil}, resourceType: resource.Session, // We're requesting sessions. - actionSet: action.ActionSet{action.Read}, + actionSet: action.NewActionSet(action.Read), expPermissions: []Permission{}, }, { @@ -466,7 +466,7 @@ func TestACL_ListPermissions(t *testing.T) { }, scopes: map[string]*scopes.ScopeInfo{"o_1": nil}, resourceType: resource.Session, - actionSet: action.ActionSet{action.Read}, + actionSet: action.NewActionSet(action.Read), expPermissions: []Permission{}, }, { @@ -479,7 +479,7 @@ func TestACL_ListPermissions(t *testing.T) { }, scopes: map[string]*scopes.ScopeInfo{"o_1": nil}, resourceType: resource.Session, - actionSet: action.ActionSet{action.Read}, + actionSet: action.NewActionSet(action.Read), expPermissions: []Permission{}, skipGrantValidationChecking: true, }, @@ -493,7 +493,7 @@ func TestACL_ListPermissions(t *testing.T) { }, scopes: map[string]*scopes.ScopeInfo{"o_1": nil}, resourceType: resource.Session, - actionSet: action.ActionSet{action.Read}, + actionSet: action.NewActionSet(action.Read), expPermissions: []Permission{ { ScopeId: "o_1", @@ -515,7 +515,7 @@ func TestACL_ListPermissions(t *testing.T) { }, scopes: map[string]*scopes.ScopeInfo{"o_1": nil}, resourceType: resource.Session, - actionSet: action.ActionSet{action.ReadSelf}, + actionSet: action.NewActionSet(action.ReadSelf), expPermissions: []Permission{ { ScopeId: "o_1", @@ -540,7 +540,7 @@ func TestACL_ListPermissions(t *testing.T) { }, scopes: map[string]*scopes.ScopeInfo{"o_1": nil}, resourceType: resource.Session, - actionSet: action.ActionSet{action.Read}, + actionSet: action.NewActionSet(action.Read), expPermissions: []Permission{ { ScopeId: "o_1", @@ -562,7 +562,7 @@ func TestACL_ListPermissions(t *testing.T) { }, scopes: map[string]*scopes.ScopeInfo{"o_1": nil}, resourceType: resource.Session, - actionSet: action.ActionSet{action.ReadSelf}, + actionSet: action.NewActionSet(action.ReadSelf), expPermissions: []Permission{ { ScopeId: "o_1", @@ -584,7 +584,7 @@ func TestACL_ListPermissions(t *testing.T) { }, scopes: map[string]*scopes.ScopeInfo{"o_1": nil}, resourceType: resource.Session, - actionSet: action.ActionSet{action.NoOp}, + actionSet: action.NewActionSet(action.NoOp), expPermissions: []Permission{ { ScopeId: "o_1", @@ -606,7 +606,7 @@ func TestACL_ListPermissions(t *testing.T) { }, scopes: map[string]*scopes.ScopeInfo{"o_1": nil}, resourceType: resource.Session, - actionSet: action.ActionSet{action.Read}, + actionSet: action.NewActionSet(action.Read), expPermissions: []Permission{}, }, { @@ -622,7 +622,7 @@ func TestACL_ListPermissions(t *testing.T) { }, scopes: map[string]*scopes.ScopeInfo{"o_1": nil}, resourceType: resource.Session, - actionSet: action.ActionSet{action.NoOp}, + actionSet: action.NewActionSet(action.NoOp), expPermissions: []Permission{ { ScopeId: "o_1", @@ -644,7 +644,7 @@ func TestACL_ListPermissions(t *testing.T) { }, scopes: map[string]*scopes.ScopeInfo{"o_1": nil}, resourceType: resource.Host, - actionSet: action.ActionSet{action.ReadSelf}, + actionSet: action.NewActionSet(action.ReadSelf), expPermissions: []Permission{ { ScopeId: "o_1", @@ -669,7 +669,7 @@ func TestACL_ListPermissions(t *testing.T) { }, scopes: map[string]*scopes.ScopeInfo{"o_1": nil}, resourceType: resource.Session, - actionSet: action.ActionSet{action.NoOp, action.Read, action.ReadSelf, action.Cancel, action.CancelSelf}, + actionSet: action.NewActionSet(action.NoOp, action.Read, action.ReadSelf, action.Cancel, action.CancelSelf), expPermissions: []Permission{ { ScopeId: "o_1", @@ -691,7 +691,7 @@ func TestACL_ListPermissions(t *testing.T) { }, scopes: map[string]*scopes.ScopeInfo{"o_1": nil}, resourceType: resource.Session, - actionSet: action.ActionSet{action.Read, action.Create, action.Delete}, + actionSet: action.NewActionSet(action.Read, action.Create, action.Delete), expPermissions: []Permission{ { ScopeId: "o_1", @@ -713,7 +713,7 @@ func TestACL_ListPermissions(t *testing.T) { }, scopes: map[string]*scopes.ScopeInfo{"o_1": nil}, resourceType: resource.Host, - actionSet: action.ActionSet{action.Read, action.Create, action.Delete}, + actionSet: action.NewActionSet(action.Read, action.Create, action.Delete), expPermissions: []Permission{ { ScopeId: "o_1", @@ -742,7 +742,7 @@ func TestACL_ListPermissions(t *testing.T) { }, scopes: map[string]*scopes.ScopeInfo{"o_1": nil, "o_2": nil}, resourceType: resource.Session, - actionSet: action.ActionSet{action.Read, action.ReadSelf}, + actionSet: action.NewActionSet(action.Read, action.ReadSelf), expPermissions: []Permission{ { ScopeId: "o_1", @@ -767,7 +767,7 @@ func TestACL_ListPermissions(t *testing.T) { userId: globals.RecoveryUserId, scopes: map[string]*scopes.ScopeInfo{"o_1": nil, "o_2": nil}, resourceType: resource.Session, - actionSet: action.ActionSet{action.Read, action.Create, action.Delete}, + actionSet: action.NewActionSet(action.Read, action.Create, action.Delete), expPermissions: []Permission{ { ScopeId: "o_1", @@ -792,7 +792,7 @@ func TestACL_ListPermissions(t *testing.T) { userId: globals.RecoveryUserId, scopes: map[string]*scopes.ScopeInfo{"o_1": nil, "o_2": nil}, resourceType: resource.Target, - actionSet: action.ActionSet{action.Read, action.Create, action.Delete}, + actionSet: action.NewActionSet(action.Read, action.Create, action.Delete), expPermissions: []Permission{ { ScopeId: "o_1", @@ -816,7 +816,7 @@ func TestACL_ListPermissions(t *testing.T) { name: "separate_type_id_resource_grants", scopes: map[string]*scopes.ScopeInfo{"p_1": nil}, resourceType: resource.Target, - actionSet: action.ActionSet{action.Read, action.Cancel}, + actionSet: action.NewActionSet(action.Read, action.Cancel), aclGrants: []scopeGrant{ { scope: "p_1", diff --git a/internal/tests/api/authmethods/authmethod_test.go b/internal/tests/api/authmethods/authmethod_test.go index 4ac555c1e..dc585dac6 100644 --- a/internal/tests/api/authmethods/authmethod_test.go +++ b/internal/tests/api/authmethods/authmethod_test.go @@ -225,6 +225,9 @@ func TestList(t *testing.T) { cmpopts.SortSlices(func(a, b *authmethods.AuthMethod) bool { return a.Name < b.Name }), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), ), ) @@ -251,6 +254,9 @@ func TestList(t *testing.T) { cmpopts.SortSlices(func(a, b *authmethods.AuthMethod) bool { return a.Name < b.Name }), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), ), ) @@ -274,6 +280,9 @@ func TestList(t *testing.T) { cmpopts.SortSlices(func(a, b *authmethods.AuthMethod) bool { return a.Name < b.Name }), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), ), ) @@ -281,7 +290,14 @@ func TestList(t *testing.T) { authmethods.WithFilter(`"/item/attributes/client_id"=="client-id"`)) require.NoError(err) require.Len(result.Items, 1) - assert.Empty(cmp.Diff(oidcAM.Item, result.Items[0], cmpopts.IgnoreUnexported(authmethods.AuthMethod{}))) + assert.Empty(cmp.Diff( + oidcAM.Item, + result.Items[0], + cmpopts.IgnoreUnexported(authmethods.AuthMethod{}), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + )) result, err = amClient.List(tc.Context(), global, authmethods.WithFilter(`"/item/attributes/min_login_name_length"==3`)) @@ -295,6 +311,9 @@ func TestList(t *testing.T) { cmpopts.SortSlices(func(a, b *authmethods.AuthMethod) bool { return a.Name < b.Name }), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), ), ) @@ -302,7 +321,14 @@ func TestList(t *testing.T) { authmethods.WithFilter(`"/item/attributes/min_password_length"==10`)) require.NoError(err) require.Len(result.Items, 1) - assert.Empty(cmp.Diff(pwAM.Item, result.Items[0], cmpopts.IgnoreUnexported(authmethods.AuthMethod{}))) + assert.Empty(cmp.Diff( + pwAM.Item, + result.Items[0], + cmpopts.IgnoreUnexported(authmethods.AuthMethod{}), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + )) } func TestCustomMethods(t *testing.T) { diff --git a/internal/tests/api/hosts/host_test.go b/internal/tests/api/hosts/host_test.go index 58246fa34..737eb8926 100644 --- a/internal/tests/api/hosts/host_test.go +++ b/internal/tests/api/hosts/host_test.go @@ -9,6 +9,8 @@ import ( "testing" "time" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/boundary/api" "github.com/hashicorp/boundary/api/hostcatalogs" "github.com/hashicorp/boundary/api/hosts" @@ -125,7 +127,17 @@ func TestPluginHosts(t *testing.T) { h, err := hClient.Read(tc.Context(), hl.Items[0].Id) require.NoError(err) - assert.Equal(hl.Items[0], h.Item) + assert.Empty( + cmp.Diff( + hl.Items[0], + h.Item, + cmpopts.IgnoreUnexported(hosts.Host{}), + cmpopts.IgnoreFields(hosts.Host{}, "Version", "UpdatedTime"), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), + ), + ) _, err = hClient.Update(tc.Context(), h.Item.Id, h.Item.Version, hosts.WithName("foo")) require.Error(err) diff --git a/internal/tests/api/hostsets/host_set_test.go b/internal/tests/api/hostsets/host_set_test.go index f4f15fa32..fa22b68f1 100644 --- a/internal/tests/api/hostsets/host_set_test.go +++ b/internal/tests/api/hostsets/host_set_test.go @@ -146,6 +146,9 @@ func TestList_Plugin(t *testing.T) { comparableSetSlice(ul.Items), cmpopts.IgnoreUnexported(hostsets.HostSet{}), cmpopts.IgnoreFields(hostsets.HostSet{}, "Version", "UpdatedTime"), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), ), ) @@ -165,6 +168,9 @@ func TestList_Plugin(t *testing.T) { cmpopts.SortSlices(func(x, y *hostsets.HostSet) bool { return x.Id < y.Id }), + cmpopts.SortSlices(func(a, b string) bool { + return a < b + }), ), ) diff --git a/internal/types/action/action.go b/internal/types/action/action.go index f1dbd10e3..71798add5 100644 --- a/internal/types/action/action.go +++ b/internal/types/action/action.go @@ -215,29 +215,61 @@ func (act Type) IsActionOrParent(suspect Type) bool { return strings.HasPrefix(suspect.String(), fmt.Sprintf("%s:", act.String())) } -// ActionSet stores a slice of action types -type ActionSet []Type +// ActionSet is a set of action.Type. +type ActionSet map[Type]struct{} + +// NewActionSet creates an ActionSet. +func NewActionSet(t ...Type) ActionSet { + a := make(ActionSet, len(t)) + a.Add(t...) + return a +} + +// Add adds the provided Types t to a, any duplicates are ignored. +func (a ActionSet) Add(t ...Type) { + for _, tt := range t { + a[tt] = struct{}{} + } +} + +// Union returns a new ActionSet that is the union of the sets. +func Union(sets ...ActionSet) ActionSet { + a := make(ActionSet) + for _, s := range sets { + for k := range s { + a.Add(k) + } + } + return a +} + +// Difference returns a new ActionSet that is the difference of a-b. +func Difference(a ActionSet, b ActionSet) ActionSet { + c := make(ActionSet) + for t := range a { + if !b.HasAction(t) { + c.Add(t) + } + } + return c +} // Strings converts Actions into a slice of the actions' string equivalents func (a ActionSet) Strings() []string { if a == nil { return nil } - ret := make([]string, len(a)) - for i, act := range a { - ret[i] = act.String() + ret := make([]string, 0, len(a)) + for act := range a { + ret = append(ret, act.String()) } return ret } // HasAction returns whether the action set contains the given action. func (a ActionSet) HasAction(act Type) bool { - for _, v := range a { - if v == act { - return true - } - } - return false + _, ok := a[act] + return ok } // OnlySelf returns true if all actions in the action set are self types. An @@ -247,7 +279,7 @@ func (a ActionSet) OnlySelf() bool { if len(a) == 0 { return false } - for _, v := range a { + for v := range a { if !strings.HasSuffix(v.String(), ":self") { return false } diff --git a/internal/types/action/action_test.go b/internal/types/action/action_test.go index c19e43bbd..8112d1e0e 100644 --- a/internal/types/action/action_test.go +++ b/internal/types/action/action_test.go @@ -155,23 +155,26 @@ func TestActionStrings(t *testing.T) { }{ { name: "basic test", - actions: ActionSet{Read, AuthorizeSession}, + actions: NewActionSet(Read, AuthorizeSession), want: []string{"read", "authorize-session"}, }, { name: "reverse test to check ordering", - actions: ActionSet{AuthorizeSession, Read}, + actions: NewActionSet(AuthorizeSession, Read), want: []string{"authorize-session", "read"}, }, { name: "another test", - actions: ActionSet{Delete, AddGrants}, + actions: NewActionSet(Delete, AddGrants), want: []string{"delete", "add-grants"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.actions.Strings(), tt.want) + // TODO: previously the order seemed to matter for these tests, + // Is this really imporant? And if so, is simply having a stable + // order sufficient? + assert.ElementsMatch(t, tt.actions.Strings(), tt.want) }) } } @@ -185,31 +188,31 @@ func TestHasAction(t *testing.T) { }{ { name: "has 1", - actions: ActionSet{Read, AuthorizeSession}, + actions: NewActionSet(Read, AuthorizeSession), action: Read, want: true, }, { name: "has 2", - actions: ActionSet{Read, AuthorizeSession}, + actions: NewActionSet(Read, AuthorizeSession), action: AuthorizeSession, want: true, }, { name: "empty", - actions: ActionSet{}, + actions: NewActionSet(), action: AuthorizeSession, want: false, }, { name: "does not have 1", - actions: ActionSet{Read, AuthorizeSession}, + actions: NewActionSet(Read, AuthorizeSession), action: ReadSelf, want: false, }, { name: "does not have 2", - actions: ActionSet{Read, AuthorizeSession}, + actions: NewActionSet(Read, AuthorizeSession), action: Delete, want: false, }, @@ -229,12 +232,12 @@ func TestOnlySelf(t *testing.T) { }{ { name: "has only self 1", - actions: ActionSet{ReadSelf, CancelSelf}, + actions: NewActionSet(ReadSelf, CancelSelf), want: true, }, { name: "has only self 2", - actions: ActionSet{ReadSelf}, + actions: NewActionSet(ReadSelf), want: true, }, { @@ -244,12 +247,12 @@ func TestOnlySelf(t *testing.T) { }, { name: "does not have only self 1", - actions: ActionSet{Read, AuthorizeSession}, + actions: NewActionSet(Read, AuthorizeSession), want: false, }, { name: "does not have only self 2", - actions: ActionSet{ReadSelf, AuthorizeSession}, + actions: NewActionSet(ReadSelf, AuthorizeSession), want: false, }, } @@ -296,3 +299,219 @@ func TestIsActionOrParent(t *testing.T) { }) } } + +func TestNewActionSet(t *testing.T) { + tests := []struct { + name string + types []Type + want ActionSet + }{ + { + name: "empty", + types: []Type{}, + want: make(ActionSet), + }, + { + name: "single", + types: []Type{List}, + want: ActionSet{ + List: struct{}{}, + }, + }, + { + name: "multiple", + types: []Type{List, Create, Read, Update, Delete}, + want: ActionSet{ + List: struct{}{}, + Create: struct{}{}, + Read: struct{}{}, + Update: struct{}{}, + Delete: struct{}{}, + }, + }, + { + name: "multiple-duplicates", + types: []Type{List, Read, List, Read, Create, Read, Update, Delete}, + want: ActionSet{ + List: struct{}{}, + Create: struct{}{}, + Read: struct{}{}, + Update: struct{}{}, + Delete: struct{}{}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := NewActionSet(tt.types...) + assert.Equal(t, tt.want, s) + }) + } +} + +func TestActionSetUnion(t *testing.T) { + tests := []struct { + name string + sets []ActionSet + want ActionSet + }{ + { + name: "empty", + sets: []ActionSet{}, + want: ActionSet{}, + }, + { + name: "one", + sets: []ActionSet{ + { + List: struct{}{}, + Create: struct{}{}, + Read: struct{}{}, + }, + }, + want: ActionSet{ + List: struct{}{}, + Create: struct{}{}, + Read: struct{}{}, + }, + }, + { + name: "multiple-no-duplicates", + sets: []ActionSet{ + { + List: struct{}{}, + Create: struct{}{}, + }, + { + Update: struct{}{}, + Delete: struct{}{}, + }, + { + Read: struct{}{}, + }, + }, + want: ActionSet{ + List: struct{}{}, + Create: struct{}{}, + Read: struct{}{}, + Update: struct{}{}, + Delete: struct{}{}, + }, + }, + { + name: "multiple-duplicates", + sets: []ActionSet{ + { + List: struct{}{}, + Create: struct{}{}, + }, + { + List: struct{}{}, + Update: struct{}{}, + Delete: struct{}{}, + }, + { + Update: struct{}{}, + Read: struct{}{}, + }, + }, + want: ActionSet{ + List: struct{}{}, + Create: struct{}{}, + Read: struct{}{}, + Update: struct{}{}, + Delete: struct{}{}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := Union(tt.sets...) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestActionSetDifference(t *testing.T) { + tests := []struct { + name string + a ActionSet + b ActionSet + want ActionSet + }{ + { + name: "same-set", + a: ActionSet{ + List: struct{}{}, + Update: struct{}{}, + }, + b: ActionSet{ + List: struct{}{}, + Update: struct{}{}, + }, + want: ActionSet{}, + }, + { + name: "distinct", + a: ActionSet{ + List: struct{}{}, + Update: struct{}{}, + }, + b: ActionSet{ + Read: struct{}{}, + Delete: struct{}{}, + }, + want: ActionSet{ + List: struct{}{}, + Update: struct{}{}, + }, + }, + { + name: "some-overlap", + a: ActionSet{ + List: struct{}{}, + Update: struct{}{}, + }, + b: ActionSet{ + Read: struct{}{}, + List: struct{}{}, + Delete: struct{}{}, + }, + want: ActionSet{ + Update: struct{}{}, + }, + }, + { + name: "nil-a", + a: nil, + b: ActionSet{ + Read: struct{}{}, + List: struct{}{}, + Delete: struct{}{}, + }, + want: ActionSet{}, + }, + { + name: "nil-b", + a: ActionSet{ + Read: struct{}{}, + List: struct{}{}, + Delete: struct{}{}, + }, + b: nil, + want: ActionSet{ + Read: struct{}{}, + List: struct{}{}, + Delete: struct{}{}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := Difference(tt.a, tt.b) + assert.Equal(t, tt.want, got) + }) + } +} From 8643d8e6c67acf33233da7a69ff3ec2a1f7d91b0 Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Wed, 1 Nov 2023 14:19:09 +0000 Subject: [PATCH 04/26] feat(errors): Add code for invalid configuration --- internal/errors/code.go | 3 +++ internal/errors/code_test.go | 5 +++++ internal/errors/info.go | 4 ++++ internal/errors/kind.go | 24 +++++++++++++----------- internal/errors/kind_test.go | 5 +++++ 5 files changed, 30 insertions(+), 11 deletions(-) diff --git a/internal/errors/code.go b/internal/errors/code.go index c474638c0..62950b4f3 100644 --- a/internal/errors/code.go +++ b/internal/errors/code.go @@ -146,4 +146,7 @@ const ( // OIDC authentication provided errors OidcProviderCallbackError Code = 4000 // OidcProviderCallbackError represents an error that is passed by the OIDC provider to the callback endpoint + + // Configuration error codes + InvalidConfiguration Code = 5000 // InvalidConfiguration represents an error with the configuration file. ) diff --git a/internal/errors/code_test.go b/internal/errors/code_test.go index e98d6fed3..4ad8be5b8 100644 --- a/internal/errors/code_test.go +++ b/internal/errors/code_test.go @@ -410,6 +410,11 @@ func TestCode_Both_String_Info(t *testing.T) { c: ChecksumMismatch, want: ChecksumMismatch, }, + { + name: "InvalidConfiguration", + c: InvalidConfiguration, + want: InvalidConfiguration, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/errors/info.go b/internal/errors/info.go index 6dea00cdf..d1951be66 100644 --- a/internal/errors/info.go +++ b/internal/errors/info.go @@ -343,4 +343,8 @@ var errorCodeInfo = map[Code]Info{ Message: "checksum mismatch", Kind: Integrity, }, + InvalidConfiguration: { + Message: "invalid configuration", + Kind: Configuration, + }, } diff --git a/internal/errors/kind.go b/internal/errors/kind.go index 1f7a6a832..79118d245 100644 --- a/internal/errors/kind.go +++ b/internal/errors/kind.go @@ -18,20 +18,22 @@ const ( State External VaultToken + Configuration ) func (e Kind) String() string { return map[Kind]string{ - Other: "unknown", - Parameter: "parameter violation", - Integrity: "integrity violation", - Search: "search issue", - Password: "password violation", - Transaction: "db transaction issue", - Encryption: "encryption issue", - Encoding: "encoding issue", - State: "state violation", - External: "external system issue", - VaultToken: "vault token issue", + Other: "unknown", + Parameter: "parameter violation", + Integrity: "integrity violation", + Search: "search issue", + Password: "password violation", + Transaction: "db transaction issue", + Encryption: "encryption issue", + Encoding: "encoding issue", + State: "state violation", + External: "external system issue", + VaultToken: "vault token issue", + Configuration: "configuration issue", }[e] } diff --git a/internal/errors/kind_test.go b/internal/errors/kind_test.go index 842fea0b6..726809e5a 100644 --- a/internal/errors/kind_test.go +++ b/internal/errors/kind_test.go @@ -46,6 +46,11 @@ func TestKind_String(t *testing.T) { e: VaultToken, want: "vault token issue", }, + { + name: "Configuration", + e: Configuration, + want: "configuration issue", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 3f4a72354177175eea7d5ef98c3e06d09ebd1bfd Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Mon, 9 Oct 2023 19:36:51 +0000 Subject: [PATCH 05/26] feat(action): Add registrar for valid actions by resource --- internal/types/action/registrar.go | 73 +++++++++++++++++++++ internal/types/action/registrar_ext_test.go | 71 ++++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 internal/types/action/registrar.go create mode 100644 internal/types/action/registrar_ext_test.go diff --git a/internal/types/action/registrar.go b/internal/types/action/registrar.go new file mode 100644 index 000000000..310079f4e --- /dev/null +++ b/internal/types/action/registrar.go @@ -0,0 +1,73 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package action + +import ( + "fmt" + "sync" + + "github.com/hashicorp/boundary/internal/types/resource" +) + +type resourceActionSets struct { + individual ActionSet + collection ActionSet + valid ActionSet +} + +type byResource struct { + mu sync.RWMutex + m map[resource.Type]*resourceActionSets +} + +func (b *byResource) add(r resource.Type, individual ActionSet, collection ActionSet) error { + b.mu.Lock() + defer b.mu.Unlock() + + if b.m == nil { + b.m = make(map[resource.Type]*resourceActionSets) + } + + _, ok := b.m[r] + if ok { + return fmt.Errorf("%s already registered", r) + } + b.m[r] = &resourceActionSets{ + individual: individual, + collection: collection, + valid: Union(individual, collection), + } + + return nil +} + +func (b *byResource) get(r resource.Type) (*resourceActionSets, error) { + b.mu.RLock() + defer b.mu.RUnlock() + a, ok := b.m[r] + if !ok { + return nil, fmt.Errorf("resource not found: %s", r) + } + return a, nil +} + +var byResourceRegistrar = &byResource{} + +// RegisterResource registers ActionSets for r. If RegisterResource is called +// twice with the same resource, it panics. +func RegisterResource(r resource.Type, individual ActionSet, collection ActionSet) { + if err := byResourceRegistrar.add(r, individual, collection); err != nil { + panic(err) + } +} + +// ActionSetForResource returns the ActionSet registered for r +// or an error if r has not been registered. +func ActionSetForResource(r resource.Type) (ActionSet, error) { + a, err := byResourceRegistrar.get(r) + if err != nil { + return nil, err + } + return a.valid, nil +} diff --git a/internal/types/action/registrar_ext_test.go b/internal/types/action/registrar_ext_test.go new file mode 100644 index 000000000..9c42f6a76 --- /dev/null +++ b/internal/types/action/registrar_ext_test.go @@ -0,0 +1,71 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package action_test + +import ( + "errors" + "testing" + + "github.com/hashicorp/boundary/internal/types/action" + "github.com/hashicorp/boundary/internal/types/resource" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRegisterResource(t *testing.T) { + t.Run("Pancis", func(t *testing.T) { + // Ensure the resource is not registered yet + _, err := action.ActionSetForResource(resource.Session) + require.EqualError(t, err, "resource not found: session") + + // Now register the resource + action.RegisterResource(resource.Session, action.NewActionSet(action.Read, action.Create), action.NewActionSet(action.List)) + got, err := action.ActionSetForResource(resource.Session) + require.NoError(t, err) + assert.Equal(t, action.NewActionSet(action.Read, action.Create, action.List), got) + + // Attempting to register the same resource again should panic + require.Panics( + t, + func() { + action.RegisterResource(resource.Session, action.NewActionSet(action.Update, action.Read, action.Create), action.NewActionSet(action.List)) + }, + ) + }) + + // Run these after the Pancis test rely on resource.Session already being registered. + t.Run("ActionSetForResource", func(t *testing.T) { + cases := []struct { + name string + res resource.Type + want action.ActionSet + wantErr error + }{ + { + "NotRegistered", + resource.Target, + nil, + errors.New("resource not found: target"), + }, + { + "Registered", + resource.Session, + action.NewActionSet(action.Read, action.Create, action.List), + nil, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got, err := action.ActionSetForResource(tc.res) + if tc.wantErr != nil { + require.EqualError(t, err, tc.wantErr.Error()) + return + } + + require.NoError(t, err) + assert.Equal(t, tc.want, got) + }) + } + }) +} From 51adde4b2cdec9eed9b375a00d195776bf9b763a Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Wed, 11 Oct 2023 18:26:16 +0000 Subject: [PATCH 06/26] feat(handlers): Register actions for each resource type This registers the ActionSets for each resource when the handler packages are initialized. This then allows other packages to retrieve the ActionSets for a given resource from the action package. The initial consumer of this will be the ratelimit package so that it can programmatically enumerate all of the valid combinations of a resource and action. This leaves a number of TODO to refactor the handler packages to also retrieve the action sets from the registered actions instead of using a package variable. This is left as a TODO since there is some inconsistencies in how these package variables are used between the handlers. For example a number of packages use a map of subtypes to ActionSets which is then used withing the handler implementation. Others use a map, but don't seem to use it at all. And others just use an ActionSet. --- .../handlers/accounts/account_service.go | 4 ++++ .../controller/handlers/authmethods/z_init.go | 20 +++++++++++++++++++ .../handlers/authtokens/authtoken_service.go | 5 +++++ .../credentiallibrary_service.go | 3 +++ .../credentials/credential_service.go | 3 +++ .../credentialstore_service.go | 3 +++ .../handlers/groups/group_service.go | 3 +++ .../host_catalogs/host_catalog_service.go | 3 +++ .../handlers/host_sets/host_set_service.go | 4 ++++ .../controller/handlers/hosts/host_service.go | 4 ++++ .../managed_groups/managed_group_service.go | 4 ++++ .../controller/handlers/roles/role_service.go | 3 +++ .../handlers/scopes/scope_service.go | 4 ++++ .../session_recording_service.go | 6 ++++++ .../handlers/sessions/session_service.go | 5 +++++ .../storage_buckets/storage_bucket_service.go | 6 ++++++ .../handlers/targets/target_service.go | 5 +++++ .../controller/handlers/users/user_service.go | 2 ++ .../handlers/workers/worker_service.go | 3 +++ 19 files changed, 90 insertions(+) create mode 100644 internal/daemon/controller/handlers/authmethods/z_init.go diff --git a/internal/daemon/controller/handlers/accounts/account_service.go b/internal/daemon/controller/handlers/accounts/account_service.go index 35051d5cb..07f5a533a 100644 --- a/internal/daemon/controller/handlers/accounts/account_service.go +++ b/internal/daemon/controller/handlers/accounts/account_service.go @@ -28,6 +28,7 @@ import ( "github.com/hashicorp/boundary/internal/types/resource" "github.com/hashicorp/boundary/internal/types/subtypes" pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/accounts" + "golang.org/x/exp/maps" "google.golang.org/grpc/codes" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/wrapperspb" @@ -108,6 +109,9 @@ func init() { if oidcMaskManager, err = handlers.NewMaskManager(context.Background(), handlers.MaskDestination{&oidcstore.Account{}}, handlers.MaskSource{&pb.Account{}, &pb.OidcAccountAttributes{}}); err != nil { panic(err) } + + // TODO: refactor to remove IdActions and CollectionActions package variables + action.RegisterResource(resource.Account, action.Union(maps.Values(IdActions)...), CollectionActions) } // Service handles request as described by the pbs.AccountServiceServer interface. diff --git a/internal/daemon/controller/handlers/authmethods/z_init.go b/internal/daemon/controller/handlers/authmethods/z_init.go new file mode 100644 index 000000000..0fa1a7f32 --- /dev/null +++ b/internal/daemon/controller/handlers/authmethods/z_init.go @@ -0,0 +1,20 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package authmethods + +import ( + "github.com/hashicorp/boundary/internal/types/action" + "github.com/hashicorp/boundary/internal/types/resource" + "golang.org/x/exp/maps" +) + +// init functions process in alphabetical order of filenames within a package. +// this needs to run last since it used the IdActions variable which gets +// modified in other init functions in this package. +func init() { + // TODO: refactor to remove IdActions and CollectionActions package variables + // Then this registration can happen in authmethod_service.go and we don't + // need to worry about the order of init functions in a package. + action.RegisterResource(resource.AuthMethod, action.Union(maps.Values(IdActions)...), CollectionActions) +} diff --git a/internal/daemon/controller/handlers/authtokens/authtoken_service.go b/internal/daemon/controller/handlers/authtokens/authtoken_service.go index 1a87df8bc..853fc4d79 100644 --- a/internal/daemon/controller/handlers/authtokens/authtoken_service.go +++ b/internal/daemon/controller/handlers/authtokens/authtoken_service.go @@ -42,6 +42,11 @@ var ( ) ) +func init() { + // TODO: refactor to remove IdActionsMap and CollectionActions package variables + action.RegisterResource(resource.AuthToken, IdActions, CollectionActions) +} + // Service handles request as described by the pbs.AuthTokenServiceServer interface. type Service struct { pbs.UnsafeAuthTokenServiceServer diff --git a/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service.go b/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service.go index a72dfbdf5..f58d69ab2 100644 --- a/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service.go +++ b/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service.go @@ -103,6 +103,9 @@ func init() { ); err != nil { panic(err) } + + // TODO: refactor to remove IdActionsMap and CollectionActions package variables + action.RegisterResource(resource.CredentialLibrary, IdActions, CollectionActions) } // Service handles request as described by the pbs.CredentialLibraryServiceServer interface. diff --git a/internal/daemon/controller/handlers/credentials/credential_service.go b/internal/daemon/controller/handlers/credentials/credential_service.go index c6e824baa..beb9c5cba 100644 --- a/internal/daemon/controller/handlers/credentials/credential_service.go +++ b/internal/daemon/controller/handlers/credentials/credential_service.go @@ -86,6 +86,9 @@ func init() { ); err != nil { panic(err) } + + // TODO: refactor to remove IdActionsMap and CollectionActions package variables + action.RegisterResource(resource.Credential, IdActions, CollectionActions) } // Service handles request as described by the pbs.CredentialServiceServer interface. diff --git a/internal/daemon/controller/handlers/credentialstores/credentialstore_service.go b/internal/daemon/controller/handlers/credentialstores/credentialstore_service.go index 813fb05e8..b73a42083 100644 --- a/internal/daemon/controller/handlers/credentialstores/credentialstore_service.go +++ b/internal/daemon/controller/handlers/credentialstores/credentialstore_service.go @@ -86,6 +86,9 @@ func init() { ); err != nil { panic(err) } + + // TODO: refactor to remove IdActionsMap and CollectionActions package variables + action.RegisterResource(resource.CredentialStore, IdActions, CollectionActions) } // Service handles request as described by the pbs.CredentialStoreServiceServer interface. diff --git a/internal/daemon/controller/handlers/groups/group_service.go b/internal/daemon/controller/handlers/groups/group_service.go index f67d24064..cc12412df 100644 --- a/internal/daemon/controller/handlers/groups/group_service.go +++ b/internal/daemon/controller/handlers/groups/group_service.go @@ -59,6 +59,9 @@ func init() { ); err != nil { panic(err) } + + // TODO: refactor to remove IdActionsMap and CollectionActions package variables + action.RegisterResource(resource.Group, IdActions, CollectionActions) } // Service handles request as described by the pbs.GroupServiceServer interface. diff --git a/internal/daemon/controller/handlers/host_catalogs/host_catalog_service.go b/internal/daemon/controller/handlers/host_catalogs/host_catalog_service.go index b0070dd3e..22be14e28 100644 --- a/internal/daemon/controller/handlers/host_catalogs/host_catalog_service.go +++ b/internal/daemon/controller/handlers/host_catalogs/host_catalog_service.go @@ -89,6 +89,9 @@ func init() { ); err != nil { panic(err) } + + // TODO: refactor to remove idActionsMap and CollectionActions package variables + action.RegisterResource(resource.HostCatalog, IdActions, CollectionActions) } type Service struct { diff --git a/internal/daemon/controller/handlers/host_sets/host_set_service.go b/internal/daemon/controller/handlers/host_sets/host_set_service.go index 8f3dadc11..530e568e3 100644 --- a/internal/daemon/controller/handlers/host_sets/host_set_service.go +++ b/internal/daemon/controller/handlers/host_sets/host_set_service.go @@ -28,6 +28,7 @@ import ( pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/plugins" "github.com/hashicorp/go-secure-stdlib/strutil" + "golang.org/x/exp/maps" "google.golang.org/grpc/codes" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/structpb" @@ -83,6 +84,9 @@ func init() { ); err != nil { panic(err) } + + // TODO: refactor to remove idActionsMap and CollectionActions package variables + action.RegisterResource(resource.HostSet, action.Union(maps.Values(idActionsTypeMap)...), CollectionActions) } type Service struct { diff --git a/internal/daemon/controller/handlers/hosts/host_service.go b/internal/daemon/controller/handlers/hosts/host_service.go index 8eeef2617..2617a5e42 100644 --- a/internal/daemon/controller/handlers/hosts/host_service.go +++ b/internal/daemon/controller/handlers/hosts/host_service.go @@ -27,6 +27,7 @@ import ( "github.com/hashicorp/boundary/internal/types/subtypes" pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hosts" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/plugins" + "golang.org/x/exp/maps" "google.golang.org/grpc/codes" "google.golang.org/protobuf/types/known/wrapperspb" ) @@ -68,6 +69,9 @@ func init() { ); err != nil { panic(err) } + + // TODO: refactor to remove idActionsMap and CollectionActions package variables + action.RegisterResource(resource.Host, action.Union(maps.Values(idActionsTypeMap)...), CollectionActions) } type Service struct { diff --git a/internal/daemon/controller/handlers/managed_groups/managed_group_service.go b/internal/daemon/controller/handlers/managed_groups/managed_group_service.go index 55e9466fc..7d9e835eb 100644 --- a/internal/daemon/controller/handlers/managed_groups/managed_group_service.go +++ b/internal/daemon/controller/handlers/managed_groups/managed_group_service.go @@ -26,6 +26,7 @@ import ( "github.com/hashicorp/boundary/internal/types/subtypes" pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/managedgroups" "github.com/hashicorp/go-bexpr" + "golang.org/x/exp/maps" "google.golang.org/grpc/codes" "google.golang.org/protobuf/types/known/wrapperspb" ) @@ -83,6 +84,9 @@ func init() { ); err != nil { panic(err) } + + // TODO: refactor to remove IdActions and CollectionActions package variables + action.RegisterResource(resource.ManagedGroup, action.Union(maps.Values(IdActions)...), CollectionActions) } // Service handles request as described by the pbs.ManagedGroupServiceServer interface. diff --git a/internal/daemon/controller/handlers/roles/role_service.go b/internal/daemon/controller/handlers/roles/role_service.go index fb88f43dd..4a7551abe 100644 --- a/internal/daemon/controller/handlers/roles/role_service.go +++ b/internal/daemon/controller/handlers/roles/role_service.go @@ -63,6 +63,9 @@ func init() { ); err != nil { panic(err) } + + // TODO: refactor to remove IdActions and CollectionActions package variables + action.RegisterResource(resource.Role, IdActions, CollectionActions) } // Service handles request as described by the pbs.RoleServiceServer interface. diff --git a/internal/daemon/controller/handlers/scopes/scope_service.go b/internal/daemon/controller/handlers/scopes/scope_service.go index c87313090..f385fac10 100644 --- a/internal/daemon/controller/handlers/scopes/scope_service.go +++ b/internal/daemon/controller/handlers/scopes/scope_service.go @@ -67,6 +67,7 @@ var ( action.DestroyScopeKeyVersion, ) + // TODO: get this from action registry scopeCollectionTypeMapMap = map[string]map[resource.Type]action.ActionSet{ scope.Global.String(): { resource.AuthMethod: authmethods.CollectionActions, @@ -117,6 +118,9 @@ func init() { ); err != nil { panic(err) } + + // TODO: refactor to remove IdActions and CollectionActions package variables + action.RegisterResource(resource.Scope, IdActions, CollectionActions) } // Service handles requests as described by the pbs.ScopeServiceServer interface. diff --git a/internal/daemon/controller/handlers/session_recordings/session_recording_service.go b/internal/daemon/controller/handlers/session_recordings/session_recording_service.go index 5cff170a3..0741bf955 100644 --- a/internal/daemon/controller/handlers/session_recordings/session_recording_service.go +++ b/internal/daemon/controller/handlers/session_recordings/session_recording_service.go @@ -12,6 +12,7 @@ import ( intglobals "github.com/hashicorp/boundary/internal/globals" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/types/action" + "github.com/hashicorp/boundary/internal/types/resource" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) @@ -32,6 +33,11 @@ var ( ) ) +func init() { + // TODO: refactor to remove IdActions and CollectionActions package variables + action.RegisterResource(resource.SessionRecording, IdActions, CollectionActions) +} + // NewServiceFn returns a storage bucket service which is not implemented in OSS var NewServiceFn = func(ctx context.Context, iamRepoFn common.IamRepoFactory, diff --git a/internal/daemon/controller/handlers/sessions/session_service.go b/internal/daemon/controller/handlers/sessions/session_service.go index 589b4070e..4c24a8878 100644 --- a/internal/daemon/controller/handlers/sessions/session_service.go +++ b/internal/daemon/controller/handlers/sessions/session_service.go @@ -44,6 +44,11 @@ var ( ) ) +func init() { + // TODO: refactor to remove IdActions and CollectionActions package variables + action.RegisterResource(resource.Session, IdActions, CollectionActions) +} + // Service handles request as described by the pbs.SessionServiceServer interface. type Service struct { pbs.UnsafeSessionServiceServer diff --git a/internal/daemon/controller/handlers/storage_buckets/storage_bucket_service.go b/internal/daemon/controller/handlers/storage_buckets/storage_bucket_service.go index 62eb333b7..1061c9897 100644 --- a/internal/daemon/controller/handlers/storage_buckets/storage_bucket_service.go +++ b/internal/daemon/controller/handlers/storage_buckets/storage_bucket_service.go @@ -10,6 +10,7 @@ import ( pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" "github.com/hashicorp/boundary/internal/globals" "github.com/hashicorp/boundary/internal/types/action" + "github.com/hashicorp/boundary/internal/types/resource" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) @@ -32,6 +33,11 @@ var ( ) ) +func init() { + // TODO: refactor to remove IdActions and CollectionActions package variables + action.RegisterResource(resource.StorageBucket, IdActions, CollectionActions) +} + // NewServiceFn returns a storage bucket service which is not implemented in OSS var NewServiceFn = func(ctx context.Context, pluginStorageRepoFn common.PluginStorageBucketRepoFactory, diff --git a/internal/daemon/controller/handlers/targets/target_service.go b/internal/daemon/controller/handlers/targets/target_service.go index fdccef245..2e95e52e1 100644 --- a/internal/daemon/controller/handlers/targets/target_service.go +++ b/internal/daemon/controller/handlers/targets/target_service.go @@ -102,6 +102,11 @@ var ( WorkerFilterDeprecationMessage = fmt.Sprintf("This field is deprecated. Use %s instead.", globals.EgressWorkerFilterField) ) +func init() { + // TODO: refactor to remove IdActions and CollectionActions package variables + action.RegisterResource(resource.Target, IdActions, CollectionActions) +} + func IngressWorkerFilterUnsupported(string) error { return fmt.Errorf("Ingress Worker Filter field is not supported in OSS") } diff --git a/internal/daemon/controller/handlers/users/user_service.go b/internal/daemon/controller/handlers/users/user_service.go index 56b5d099c..f66b175c4 100644 --- a/internal/daemon/controller/handlers/users/user_service.go +++ b/internal/daemon/controller/handlers/users/user_service.go @@ -59,6 +59,8 @@ func init() { ); err != nil { panic(err) } + // TODO: refactor to remove IdActions and CollectionActions package variables + action.RegisterResource(resource.User, IdActions, CollectionActions) } // Service handles request as described by the pbs.UserServiceServer interface. diff --git a/internal/daemon/controller/handlers/workers/worker_service.go b/internal/daemon/controller/handlers/workers/worker_service.go index a62f15911..16a1d154c 100644 --- a/internal/daemon/controller/handlers/workers/worker_service.go +++ b/internal/daemon/controller/handlers/workers/worker_service.go @@ -80,6 +80,9 @@ func init() { ); err != nil { panic(err) } + + // TODO: refactor to remove IdActions and CollectionActions package variables + action.RegisterResource(resource.Worker, IdActions, CollectionActions) } func emptyDownstreamWorkers(context.Context, string, common.Downstreamers) []string { From bc48e58035503efdeb0ceb753ae6b7e0b7119851 Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Mon, 9 Oct 2023 19:40:35 +0000 Subject: [PATCH 07/26] chore: Add go-rate dependency --- go.mod | 1 + go.sum | 2 ++ 2 files changed, 3 insertions(+) diff --git a/go.mod b/go.mod index 07ed60968..be90c9c42 100644 --- a/go.mod +++ b/go.mod @@ -94,6 +94,7 @@ require ( github.com/hashicorp/cap/ldap v0.0.0-20231012003312-273118a6e3b8 github.com/hashicorp/dbassert v0.0.0-20231012105025-1bc1bd88e22b github.com/hashicorp/go-kms-wrapping/extras/kms/v2 v2.0.0-20231124110655-4315424d22d0 + github.com/hashicorp/go-rate v0.0.0-20231106220535-0b8f59b56792 github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/nodeenrollment v0.2.9 github.com/jackc/pgx/v5 v5.4.3 diff --git a/go.sum b/go.sum index 86b7dcd1b..d2f133013 100644 --- a/go.sum +++ b/go.sum @@ -260,6 +260,8 @@ github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9 github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= github.com/hashicorp/go-plugin v1.5.2 h1:aWv8eimFqWlsEiMrYZdPYl+FdHaBJSN4AWwGWfT1G2Y= github.com/hashicorp/go-plugin v1.5.2/go.mod h1:w1sAEES3g3PuV/RzUrgow20W2uErMly84hhD3um1WL4= +github.com/hashicorp/go-rate v0.0.0-20231106220535-0b8f59b56792 h1:YS6tr7P9pfMqfomjrquQEHEj+tEk3aCj1OuYp6u87aQ= +github.com/hashicorp/go-rate v0.0.0-20231106220535-0b8f59b56792/go.mod h1:uTQC66EFZuSKunHYx0hHIfdH3nhRrv7UOOE8Ug0yI0A= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZnpUv3/+BxzFA= github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= From 61fcd857ecd38e5b7486e26d399aed100cd3b818 Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Mon, 9 Oct 2023 19:42:02 +0000 Subject: [PATCH 08/26] feat(resource): Add function to get type from plural string --- internal/types/resource/resource.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/internal/types/resource/resource.go b/internal/types/resource/resource.go index a21d1cdcc..14106c3bc 100644 --- a/internal/types/resource/resource.go +++ b/internal/types/resource/resource.go @@ -3,7 +3,10 @@ package resource -import "encoding/json" +import ( + "encoding/json" + "strings" +) // Type defines the types of resources in the system type Type uint @@ -81,6 +84,16 @@ func (r Type) PluralString() string { } } +func FromPlural(s string) (Type, bool) { + switch s { + case "credential-libraries": + return CredentialLibrary, true + default: + t, ok := Map[strings.TrimSuffix(s, "s")] + return t, ok + } +} + var Map = map[string]Type{ Unknown.String(): Unknown, All.String(): All, From 6da47443fd79df3d14e0b25cb0df8237a7f0a563 Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Mon, 9 Oct 2023 19:39:49 +0000 Subject: [PATCH 09/26] feat(ratelimit): Create rate limits from configs --- internal/cmd/config/config.go | 2 +- internal/cmd/config/config_load_test.go | 10 +- internal/cmd/config/config_test.go | 4 +- internal/daemon/controller/testing.go | 2 + internal/ratelimit/config.go | 204 +++++++++- internal/ratelimit/config_test.go | 473 ++++++++++++++++++++++++ 6 files changed, 684 insertions(+), 11 deletions(-) create mode 100644 internal/ratelimit/config_test.go diff --git a/internal/cmd/config/config.go b/internal/cmd/config/config.go index 391f0747d..b630f992b 100644 --- a/internal/cmd/config/config.go +++ b/internal/cmd/config/config.go @@ -693,7 +693,7 @@ func Parse(d string) (*Config, error) { } if result.Controller.ApiRateLimiterMaxEntries <= 0 { - result.Controller.ApiRateLimiterMaxEntries = ratelimit.DefaultLimiterMaxEntries + result.Controller.ApiRateLimiterMaxEntries = ratelimit.DefaultLimiterMaxEntries() } } diff --git a/internal/cmd/config/config_load_test.go b/internal/cmd/config/config_load_test.go index 614489c35..ac76012dd 100644 --- a/internal/cmd/config/config_load_test.go +++ b/internal/cmd/config/config_load_test.go @@ -419,7 +419,7 @@ func TestLoad(t *testing.T) { WorkerStatusGracePeriodDuration: 0, LivenessTimeToStaleDuration: 0, ApiRateLimits: make(ratelimit.Configs, 0), - ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries, + ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), }, DevController: false, DevUiPassthroughDir: "", @@ -844,7 +844,7 @@ func TestLoad(t *testing.T) { WorkerStatusGracePeriodDuration: 0, LivenessTimeToStaleDuration: 0, ApiRateLimits: make(ratelimit.Configs, 0), - ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries, + ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), }, DevController: false, DevUiPassthroughDir: "", @@ -1283,7 +1283,7 @@ func TestLoad(t *testing.T) { Unlimited: false, }, }, - ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries, + ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), }, DevController: false, DevUiPassthroughDir: "", @@ -1703,7 +1703,7 @@ func TestLoad(t *testing.T) { WorkerStatusGracePeriodDuration: 0, LivenessTimeToStaleDuration: 0, ApiRateLimits: make(ratelimit.Configs, 0), - ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries, + ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), }, DevController: false, DevUiPassthroughDir: "", @@ -1794,7 +1794,7 @@ func TestLoad(t *testing.T) { WorkerStatusGracePeriodDuration: 0, LivenessTimeToStaleDuration: 0, ApiRateLimits: make(ratelimit.Configs, 0), - ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries, + ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), }, DevController: false, DevUiPassthroughDir: "", diff --git a/internal/cmd/config/config_test.go b/internal/cmd/config/config_test.go index 466cc7166..845edeb0f 100644 --- a/internal/cmd/config/config_test.go +++ b/internal/cmd/config/config_test.go @@ -113,7 +113,7 @@ func TestDevController(t *testing.T) { Name: "dev-controller", Description: "A default controller created in dev mode", ApiRateLimits: make(ratelimit.Configs, 0), - ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries, + ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), }, DevController: true, } @@ -488,7 +488,7 @@ func TestDevCombined(t *testing.T) { Name: "dev-controller", Description: "A default controller created in dev mode", ApiRateLimits: make(ratelimit.Configs, 0), - ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries, + ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), }, DevController: true, Worker: &Worker{ diff --git a/internal/daemon/controller/testing.go b/internal/daemon/controller/testing.go index 40c29fee8..ac3bb7d22 100644 --- a/internal/daemon/controller/testing.go +++ b/internal/daemon/controller/testing.go @@ -32,6 +32,7 @@ import ( "github.com/hashicorp/boundary/internal/host/plugin" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/ratelimit" "github.com/hashicorp/boundary/internal/scheduler" "github.com/hashicorp/boundary/internal/server" "github.com/hashicorp/boundary/internal/session" @@ -656,6 +657,7 @@ func TestControllerConfig(t testing.TB, ctx context.Context, tc *TestController, } } opts.Config.Controller.Scheduler.JobRunIntervalDuration = opts.SchedulerRunJobInterval + opts.Config.Controller.ApiRateLimiterMaxEntries = ratelimit.DefaultLimiterMaxEntries() if opts.EnableEventing { opts.Config.Eventing = &event.EventerConfig{ diff --git a/internal/ratelimit/config.go b/internal/ratelimit/config.go index 97c520708..dd8cc1162 100644 --- a/internal/ratelimit/config.go +++ b/internal/ratelimit/config.go @@ -4,15 +4,90 @@ package ratelimit import ( + "context" + "fmt" + "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/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/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 ( - // DefaultLimiterMaxEntries is the default maximum number of quotas that - // can be tracked by the rate limiter. - DefaultLimiterMaxEntries = 16384 // TODO: pick a meaningful default value + DefaultRequestLimit = 3000 + DefaultPeriod = time.Second * 30 + DefaultListRequestLimit = 60 + DefaultListPeriod = time.Second * 30 ) +// defaultLimiterMaxEntries 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 defaultLimiterMaxEntries int + +var initDefaultLimiterMaxEntries sync.Once + +// DefaultLimiterMaxEntries returns the default maximum number of quotas that +// can be tracked by the rate limiter. +func DefaultLimiterMaxEntries() int { + initDefaultLimiterMaxEntries.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) + } + + defaultLimiterMaxEntries = (endpointCount * quotasPerInTotal) + + (endpointCount * quotasPerAuthToken) + + (endpointCount * quotasPerIpAddress) + }) + return defaultLimiterMaxEntries +} + // 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. @@ -28,3 +103,126 @@ type Config struct { // Configs is an ordered set of Config. type Configs []*Config + +// 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 { + key := fmt.Sprintf("%s:%s:%s", res.String(), a.String(), rate.LimitPerTotal) + switch a { + case action.List: + defaults[key] = &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: DefaultListRequestLimit, + Period: DefaultListPeriod, + } + default: + defaults[key] = &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: DefaultRequestLimit, + 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.LimitPerTotal) + + defaults[key] = &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPer(cc.Per), + Unlimited: cc.Unlimited, + 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.LimitPerTotal) + + defaults[key] = &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPer(cc.Per), + Unlimited: cc.Unlimited, + 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 +} diff --git a/internal/ratelimit/config_test.go b/internal/ratelimit/config_test.go new file mode 100644 index 000000000..d5374f6bb --- /dev/null +++ b/internal/ratelimit/config_test.go @@ -0,0 +1,473 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package ratelimit + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/hashicorp/boundary/internal/types/action" + "github.com/hashicorp/boundary/internal/types/resource" + "github.com/hashicorp/go-rate" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestConfigsLimits(t *testing.T) { + ctx := context.Background() + cases := []struct { + name string + configs Configs + want []*rate.Limit + wantErr error + }{ + { + "empty", + Configs{}, + func() []*rate.Limit { + limits := make([]*rate.Limit, 0, len(resource.Map)*len(action.Map)) + for _, res := range resource.Map { + switch res { + case resource.Unknown, resource.All, resource.Controller: + continue + } + validActions, err := action.ActionSetForResource(res) + require.NoError(t, err) + for a := range validActions { + switch a { + case action.Unknown, action.All: + continue + case action.List: + limits = append(limits, &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: DefaultListRequestLimit, + Period: DefaultListPeriod, + }) + default: + limits = append(limits, &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: DefaultRequestLimit, + Period: DefaultPeriod, + }) + } + } + } + return limits + }(), + nil, + }, + { + "single-config-all-resources-all-actions", + Configs{ + { + Resources: []string{"*"}, + Actions: []string{"*"}, + Per: "total", + Limit: 20, + Period: time.Minute * 5, + }, + }, + func() []*rate.Limit { + limits := make([]*rate.Limit, 0, len(resource.Map)*len(action.Map)) + for _, res := range resource.Map { + switch res { + case resource.Unknown, resource.All, resource.Controller: + continue + } + validActions, err := action.ActionSetForResource(res) + require.NoError(t, err) + for a := range validActions { + switch a { + case action.Unknown, action.All: + continue + default: + limits = append(limits, &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: 20, + Period: time.Minute * 5, + }) + } + } + } + return limits + }(), + nil, + }, + { + "broad-config-with-specific-override", + Configs{ + { + Resources: []string{"*"}, + Actions: []string{"*"}, + Per: "total", + Limit: 20, + Period: time.Minute * 5, + }, + { + Resources: []string{"target"}, + Actions: []string{"list"}, + Per: "total", + Limit: 10, + Period: time.Minute, + }, + }, + func() []*rate.Limit { + limits := make([]*rate.Limit, 0, len(resource.Map)*len(action.Map)) + for _, res := range resource.Map { + switch res { + case resource.Unknown, resource.All, resource.Controller: + continue + } + validActions, err := action.ActionSetForResource(res) + require.NoError(t, err) + for a := range validActions { + switch a { + case action.Unknown, action.All: + continue + case action.List: + if res == resource.Target { + limits = append(limits, &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: 10, + Period: time.Minute, + }) + continue + } + fallthrough + default: + limits = append(limits, &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: 20, + Period: time.Minute * 5, + }) + } + } + } + return limits + }(), + nil, + }, + { + "multiple-resources", + Configs{ + { + Resources: []string{"target", "session"}, + Actions: []string{"*"}, + Per: "total", + Limit: 10, + Period: time.Minute, + }, + }, + func() []*rate.Limit { + limits := make([]*rate.Limit, 0, len(resource.Map)*len(action.Map)) + for _, res := range resource.Map { + switch res { + case resource.Unknown, resource.All, resource.Controller: + continue + case resource.Target, resource.Session: + validActions, err := action.ActionSetForResource(res) + require.NoError(t, err) + for a := range validActions { + switch a { + case action.Unknown, action.All: + continue + default: + limits = append(limits, &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: 10, + Period: time.Minute, + }) + } + } + default: + validActions, err := action.ActionSetForResource(res) + require.NoError(t, err) + for a := range validActions { + switch a { + case action.Unknown, action.All: + continue + case action.List: + limits = append(limits, &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: DefaultListRequestLimit, + Period: DefaultListPeriod, + }) + default: + limits = append(limits, &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: DefaultRequestLimit, + Period: DefaultPeriod, + }) + } + } + } + } + return limits + }(), + nil, + }, + { + "multiple-actions", + Configs{ + { + Resources: []string{"target", "session"}, + Actions: []string{"list", "read"}, + Per: "total", + Limit: 10, + Period: time.Minute, + }, + }, + func() []*rate.Limit { + limits := make([]*rate.Limit, 0, len(resource.Map)*len(action.Map)) + for _, res := range resource.Map { + switch res { + case resource.Unknown, resource.All, resource.Controller: + continue + case resource.Target, resource.Session: + validActions, err := action.ActionSetForResource(res) + require.NoError(t, err) + for a := range validActions { + switch a { + case action.Unknown, action.All: + continue + case action.List, action.Read: + limits = append(limits, &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: 10, + Period: time.Minute, + }) + default: + limits = append(limits, &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: DefaultRequestLimit, + Period: DefaultPeriod, + }) + } + } + default: + validActions, err := action.ActionSetForResource(res) + require.NoError(t, err) + for a := range validActions { + switch a { + case action.Unknown, action.All: + continue + case action.List: + limits = append(limits, &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: DefaultListRequestLimit, + Period: DefaultListPeriod, + }) + default: + limits = append(limits, &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: DefaultRequestLimit, + Period: DefaultPeriod, + }) + } + } + } + } + return limits + }(), + nil, + }, + { + "order-matters", + Configs{ + // This one is overridden by the second config that is more broad. + { + Resources: []string{"target", "session"}, + Actions: []string{"*"}, + Per: "total", + Limit: 10, + Period: time.Minute, + }, + { + Resources: []string{"*"}, + Actions: []string{"*"}, + Per: "total", + Limit: 20, + Period: time.Minute * 5, + }, + }, + func() []*rate.Limit { + limits := make([]*rate.Limit, 0, len(resource.Map)*len(action.Map)) + for _, res := range resource.Map { + switch res { + case resource.Unknown, resource.All, resource.Controller: + continue + } + validActions, err := action.ActionSetForResource(res) + require.NoError(t, err) + for a := range validActions { + switch a { + case action.Unknown, action.All: + continue + default: + limits = append(limits, &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: 20, + Period: time.Minute * 5, + }) + } + } + } + return limits + }(), + nil, + }, + { + "no-limits", + Configs{ + { + Resources: []string{"*"}, + Actions: []string{"*"}, + Per: "total", + Unlimited: true, + }, + }, + func() []*rate.Limit { + limits := make([]*rate.Limit, 0, len(resource.Map)*len(action.Map)) + for _, res := range resource.Map { + switch res { + case resource.Unknown, resource.All, resource.Controller: + continue + } + validActions, err := action.ActionSetForResource(res) + require.NoError(t, err) + for a := range validActions { + switch a { + case action.Unknown, action.All: + continue + default: + limits = append(limits, &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + Unlimited: true, + }) + } + } + } + return limits + }(), + nil, + }, + { + "invalid-resource", + Configs{ + { + Resources: []string{"foo"}, + Actions: []string{"*"}, + Per: "total", + Limit: 10, + Period: time.Minute, + }, + }, + nil, + fmt.Errorf("ratelimit.(Configs).Limits: unknown resource foo: configuration issue: error #5000"), + }, + { + "invalid-action-for-resource", + Configs{ + { + Resources: []string{"session"}, + Actions: []string{"authorize-session"}, + Per: "total", + Limit: 10, + Period: time.Minute, + }, + }, + nil, + fmt.Errorf("ratelimit.(Configs).Limits: action authorize-session not valid for resource session: configuration issue: error #5000"), + }, + { + "invalid-action", + Configs{ + { + Resources: []string{"session"}, + Actions: []string{"foo"}, + Per: "total", + Limit: 10, + Period: time.Minute, + }, + }, + nil, + fmt.Errorf("ratelimit.(Configs).Limits: action foo not valid for resource session: configuration issue: error #5000"), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.configs.Limits(ctx) + if tc.wantErr != nil { + require.EqualError(t, err, tc.wantErr.Error()) + return + } + require.NoError(t, err) + assert.ElementsMatch(t, tc.want, got) + }) + } +} + +func TestDefaulLimiterMaxEntries(t *testing.T) { + var want int + + var endpointCount int + for _, res := range resource.Map { + switch res { + case resource.Unknown, resource.All, resource.Controller: + continue + } + + actions, err := action.ActionSetForResource(res) + require.NoError(t, err) + endpointCount += len(actions) + } + want = endpointCount*2000 + endpointCount + + got := DefaultLimiterMaxEntries() + assert.Equal(t, want, got) +} From 2853324479f8e63a4b4d8ac252d61598fe22b617 Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Mon, 9 Oct 2023 19:41:28 +0000 Subject: [PATCH 10/26] feat(ratelimit): Add http middleware handler function --- internal/ratelimit/handler.go | 153 +++++++++++ internal/ratelimit/handler_test.go | 405 +++++++++++++++++++++++++++++ 2 files changed, 558 insertions(+) create mode 100644 internal/ratelimit/handler.go create mode 100644 internal/ratelimit/handler_test.go diff --git a/internal/ratelimit/handler.go b/internal/ratelimit/handler.go new file mode 100644 index 000000000..38bc9fb79 --- /dev/null +++ b/internal/ratelimit/handler.go @@ -0,0 +1,153 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package ratelimit + +import ( + "fmt" + "math" + "net/http" + "regexp" + + "github.com/hashicorp/boundary/internal/types/action" + "github.com/hashicorp/boundary/internal/types/resource" + "github.com/hashicorp/go-rate" +) + +var ( + errUnknownResource = &extractResourceActionErr{http.StatusNotFound, "unknown resource"} + errUnknownAction = &extractResourceActionErr{http.StatusBadRequest, "unknown action"} + errUnsupportedAction = &extractResourceActionErr{http.StatusMethodNotAllowed, "invalid action"} +) + +type extractResourceActionErr struct { + statusCode int + msg string +} + +func (e *extractResourceActionErr) Error() string { + return e.msg +} + +var pathRegex = regexp.MustCompile(`/v1/(?P[\w-]+)((/(?P[\w/]+))?(:(?P[\w-:]+)?)?)?`) + +func extractResourceAction(path, method string) (res, act string, err error) { + var id string + + var r resource.Type + var ok bool + var actionSet action.ActionSet + + // TODO: replace regex with lexer + match := pathRegex.FindStringSubmatch(path) + for i, name := range pathRegex.SubexpNames() { + switch name { + case "resource": + res = match[i] + r, ok = resource.FromPlural(res) + if !ok { + res = resource.Unknown.String() + err = errUnknownResource + return + } + res = r.String() + case "action": + act = match[i] + if act != "" { + actionSet, err = action.ActionSetForResource(r) + if err != nil { + act = action.Unknown.String() + err = errUnknownAction + return + } + at, ok := action.Map[act] + if !ok { + act = action.Unknown.String() + err = errUnknownAction + return + } + if !actionSet.HasAction(at) { + err = errUnsupportedAction + return + } + } + case "id": + id = match[i] + } + } + + switch act { + case "": + switch id { + case "": + switch method { + case http.MethodGet: + act = action.List.String() + case http.MethodPost: + act = action.Create.String() + default: + act = action.Unknown.String() + err = errUnsupportedAction + return + } + default: + switch method { + case http.MethodGet: + act = action.Read.String() + case http.MethodDelete: + act = action.Delete.String() + case http.MethodPatch: + act = action.Update.String() + default: + act = action.Unknown.String() + err = errUnsupportedAction + return + } + } + } + return +} + +// Handler is an http middleware handler that checks if a request is allowed +// using the provided rate limiter. If the request is allowed, the next handler +// is called. Otherwise a 429 is returned with the Retry-After response header +// set to the number of seconds the client should wait to make it's next request. +func Handler(l *rate.Limiter, next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + path := req.URL.Path + method := req.Method + + res, a, err := extractResourceAction(path, method) + if err != nil { + if extractErr, ok := err.(*extractResourceActionErr); ok { + rw.WriteHeader(extractErr.statusCode) + return + } + rw.WriteHeader(http.StatusInternalServerError) + return + } + + allowed, quota, err := l.Allow(res, a) + if err != nil { + if errFull, ok := err.(*rate.ErrLimiterFull); ok { + rw.Header().Add("Retry-After", fmt.Sprintf("%.0f", math.Ceil(errFull.RetryIn.Seconds()))) + rw.WriteHeader(http.StatusServiceUnavailable) + return + } + + // The only other error here should be rate.ErrLimitNotFound, which + // shouldn't be possible given how we initialize the limiter and + // the checks done by extractResourceAction. + rw.WriteHeader(http.StatusInternalServerError) + return + } + + if !allowed { + rw.Header().Add("Retry-After", fmt.Sprintf("%.0f", quota.ResetsIn().Seconds())) + rw.WriteHeader(http.StatusTooManyRequests) + return + } + + next.ServeHTTP(rw, req) + }) +} diff --git a/internal/ratelimit/handler_test.go b/internal/ratelimit/handler_test.go new file mode 100644 index 000000000..e0ab17ec1 --- /dev/null +++ b/internal/ratelimit/handler_test.go @@ -0,0 +1,405 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package ratelimit_test + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/hashicorp/boundary/internal/ratelimit" + "github.com/hashicorp/boundary/internal/types/action" + "github.com/hashicorp/boundary/internal/types/resource" + "github.com/hashicorp/go-rate" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHandler(t *testing.T) { + cases := []struct { + name string + limiter *rate.Limiter + req func(uri string) *http.Request + expectCode int + expectHeaders http.Header + }{ + { + "AllowedList", + func() *rate.Limiter { + r, err := rate.NewLimiter([]*rate.Limit{ + { + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: 10, + Period: time.Minute, + }, + }, 10) + require.NoError(t, err) + return r + }(), + func(uri string) *http.Request { + r, err := http.NewRequest(http.MethodGet, uri+"/v1/targets", nil) + require.NoError(t, err) + return r + }, + http.StatusOK, + http.Header{}, + }, + { + "AllowedCreate", + func() *rate.Limiter { + r, err := rate.NewLimiter([]*rate.Limit{ + { + Resource: resource.Target.String(), + Action: action.Create.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: 10, + Period: time.Minute, + }, + }, 10) + require.NoError(t, err) + return r + }(), + func(uri string) *http.Request { + r, err := http.NewRequest(http.MethodPost, uri+"/v1/targets", nil) + require.NoError(t, err) + return r + }, + http.StatusOK, + http.Header{}, + }, + { + "AllowedRead", + func() *rate.Limiter { + r, err := rate.NewLimiter([]*rate.Limit{ + { + Resource: resource.Target.String(), + Action: action.Read.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: 10, + Period: time.Minute, + }, + }, 10) + require.NoError(t, err) + return r + }(), + func(uri string) *http.Request { + r, err := http.NewRequest(http.MethodGet, uri+"/v1/targets/ttcp_123456789", nil) + require.NoError(t, err) + return r + }, + http.StatusOK, + http.Header{}, + }, + { + "AllowedUpdate", + func() *rate.Limiter { + r, err := rate.NewLimiter([]*rate.Limit{ + { + Resource: resource.Target.String(), + Action: action.Update.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: 10, + Period: time.Minute, + }, + }, 10) + require.NoError(t, err) + return r + }(), + func(uri string) *http.Request { + r, err := http.NewRequest(http.MethodPatch, uri+"/v1/targets/ttcp_123456789", nil) + require.NoError(t, err) + return r + }, + http.StatusOK, + http.Header{}, + }, + { + "AllowedDelete", + func() *rate.Limiter { + r, err := rate.NewLimiter([]*rate.Limit{ + { + Resource: resource.Target.String(), + Action: action.Delete.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: 10, + Period: time.Minute, + }, + }, 10) + require.NoError(t, err) + return r + }(), + func(uri string) *http.Request { + r, err := http.NewRequest(http.MethodDelete, uri+"/v1/targets/ttcp_123456789", nil) + require.NoError(t, err) + return r + }, + http.StatusOK, + http.Header{}, + }, + { + "AllowedAuthorizeSession", + func() *rate.Limiter { + r, err := rate.NewLimiter([]*rate.Limit{ + { + Resource: resource.Target.String(), + Action: action.AuthorizeSession.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: 10, + Period: time.Minute, + }, + }, 10) + require.NoError(t, err) + return r + }(), + func(uri string) *http.Request { + r, err := http.NewRequest(http.MethodDelete, uri+"/v1/targets/ttcp_123456789:authorize-session", nil) + require.NoError(t, err) + return r + }, + http.StatusOK, + http.Header{}, + }, + { + "Limited", + func() *rate.Limiter { + r, err := rate.NewLimiter([]*rate.Limit{ + { + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: 2, + Period: time.Minute, + }, + }, 10) + require.NoError(t, err) + _, q, err := r.Allow("target", "list") + require.NoError(t, err) + + // consume all of the quota + remaining := int(q.Remaining()) + for i := 0; i < remaining; i++ { + q.Consume() + } + return r + }(), + func(uri string) *http.Request { + r, err := http.NewRequest(http.MethodGet, uri+"/v1/targets", nil) + require.NoError(t, err) + return r + }, + http.StatusTooManyRequests, + http.Header{ + "Retry-After": []string{"60"}, + }, + }, + { + "LimiterFull", + func() *rate.Limiter { + r, err := rate.NewLimiter([]*rate.Limit{ + { + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: 2, + Period: time.Minute, + }, + { + Resource: resource.Target.String(), + Action: action.Read.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: 2, + Period: time.Minute, + }, + }, 1) + require.NoError(t, err) + // use up all of the capacity for the limiter + _, _, err = r.Allow("target", "read") + require.NoError(t, err) + return r + }(), + func(uri string) *http.Request { + r, err := http.NewRequest(http.MethodGet, uri+"/v1/targets", nil) + require.NoError(t, err) + return r + }, + http.StatusServiceUnavailable, + http.Header{ + "Retry-After": []string{"1"}, + }, + }, + { + "MissingLimit", + func() *rate.Limiter { + r, err := rate.NewLimiter([]*rate.Limit{ + { + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: 2, + Period: time.Minute, + }, + }, 10) + require.NoError(t, err) + return r + }(), + func(uri string) *http.Request { + r, err := http.NewRequest(http.MethodPost, uri+"/v1/targets", nil) + require.NoError(t, err) + return r + }, + http.StatusInternalServerError, + http.Header{}, + }, + { + "UnknownResource", + func() *rate.Limiter { + r, err := rate.NewLimiter([]*rate.Limit{ + { + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: 2, + Period: time.Minute, + }, + }, 10) + require.NoError(t, err) + return r + }(), + func(uri string) *http.Request { + r, err := http.NewRequest(http.MethodPost, uri+"/v1/foo", nil) + require.NoError(t, err) + return r + }, + http.StatusNotFound, + http.Header{}, + }, + { + "UnknownAction", + func() *rate.Limiter { + r, err := rate.NewLimiter([]*rate.Limit{ + { + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: 2, + Period: time.Minute, + }, + }, 10) + require.NoError(t, err) + return r + }(), + func(uri string) *http.Request { + r, err := http.NewRequest(http.MethodPut, uri+"/v1/target/ttcp_123456789:foo", nil) + require.NoError(t, err) + return r + }, + http.StatusBadRequest, + http.Header{}, + }, + { + "UnknownActionInvalidIdHttpMethod", + func() *rate.Limiter { + r, err := rate.NewLimiter([]*rate.Limit{ + { + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: 2, + Period: time.Minute, + }, + }, 10) + require.NoError(t, err) + return r + }(), + func(uri string) *http.Request { + r, err := http.NewRequest(http.MethodPost, uri+"/v1/target/ttcp_123456789", nil) + require.NoError(t, err) + return r + }, + http.StatusMethodNotAllowed, + http.Header{}, + }, + { + "UnknownActionInvalidHttpMethod", + func() *rate.Limiter { + r, err := rate.NewLimiter([]*rate.Limit{ + { + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: 2, + Period: time.Minute, + }, + }, 10) + require.NoError(t, err) + return r + }(), + func(uri string) *http.Request { + r, err := http.NewRequest(http.MethodDelete, uri+"/v1/target", nil) + require.NoError(t, err) + return r + }, + http.StatusMethodNotAllowed, + http.Header{}, + }, + { + "UnsupportedAction", + func() *rate.Limiter { + r, err := rate.NewLimiter([]*rate.Limit{ + { + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: 2, + Period: time.Minute, + }, + }, 10) + require.NoError(t, err) + return r + }(), + func(uri string) *http.Request { + r, err := http.NewRequest(http.MethodPut, uri+"/v1/host/hst_123456789:authorize-session", nil) + require.NoError(t, err) + return r + }, + http.StatusMethodNotAllowed, + http.Header{}, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + server := httptest.NewServer(ratelimit.Handler(tc.limiter, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + return + }))) + req := tc.req(server.URL) + client := &http.Client{} + res, err := client.Do(req) + require.NoError(t, err) + assert.Equal(t, tc.expectCode, res.StatusCode) + for key, value := range tc.expectHeaders { + assert.Equal(t, value[0], res.Header.Get(key)) + } + }) + } +} From 00bd2899afb4158d1da10fea05702f2fdccd2691 Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Mon, 9 Oct 2023 19:43:25 +0000 Subject: [PATCH 11/26] feat(controller): Add ratelimiter http middleware --- internal/daemon/controller/controller.go | 13 +++++++++++++ internal/daemon/controller/handler.go | 3 ++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/internal/daemon/controller/controller.go b/internal/daemon/controller/controller.go index a56ed92bc..ce25afa11 100644 --- a/internal/daemon/controller/controller.go +++ b/internal/daemon/controller/controller.go @@ -50,6 +50,7 @@ import ( external_plugins "github.com/hashicorp/boundary/sdk/plugins" "github.com/hashicorp/boundary/version" "github.com/hashicorp/go-hclog" + "github.com/hashicorp/go-rate" "github.com/hashicorp/go-secure-stdlib/mlock" "github.com/hashicorp/go-secure-stdlib/pluginutil/v2" "github.com/hashicorp/nodeenrollment" @@ -119,6 +120,8 @@ type Controller struct { apiGrpcServerListener grpcServerListener apiGrpcGatewayTicket string + rateLimiter *rate.Limiter + // Repo factory methods AuthTokenRepoFn common.AuthTokenRepoFactory VaultCredentialRepoFn common.VaultCredentialRepoFactory @@ -245,6 +248,16 @@ func New(ctx context.Context, conf *Config) (*Controller, error) { } c.clusterListener = clusterListeners[0] + rateLimits, err := conf.RawConfig.Controller.ApiRateLimits.Limits(c.baseContext) + if err != nil { + return nil, fmt.Errorf("error parsing rate limit configuration: %w", err) + } + + c.rateLimiter, err = rate.NewLimiter(rateLimits, conf.RawConfig.Controller.ApiRateLimiterMaxEntries) + if err != nil { + return nil, fmt.Errorf("error initializing rate limiter: %w", err) + } + var pluginLogger hclog.Logger for _, enabledPlugin := range c.enabledPlugins { if pluginLogger == nil { diff --git a/internal/daemon/controller/handler.go b/internal/daemon/controller/handler.go index b461c3cb3..9731befd5 100644 --- a/internal/daemon/controller/handler.go +++ b/internal/daemon/controller/handler.go @@ -46,6 +46,7 @@ import ( "github.com/hashicorp/boundary/internal/gen/controller/api/services" authpb "github.com/hashicorp/boundary/internal/gen/controller/auth" opsservices "github.com/hashicorp/boundary/internal/gen/ops/services" + "github.com/hashicorp/boundary/internal/ratelimit" "github.com/hashicorp/go-cleanhttp" "github.com/hashicorp/go-secure-stdlib/listenerutil" "github.com/hashicorp/go-secure-stdlib/strutil" @@ -73,7 +74,7 @@ func createMuxWithEndpoints(c *Controller, props HandlerProperties) (http.Handle } mux := http.NewServeMux() - mux.Handle("/v1/", grpcGwMux) + mux.Handle("/v1/", ratelimit.Handler(c.rateLimiter, grpcGwMux)) mux.Handle(uiPath, handleUi(c)) isUiRequest := func(req *http.Request) bool { From 059d4992dedfa3fd0648b1b220e7143f467a4036 Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Wed, 18 Oct 2023 16:42:42 +0000 Subject: [PATCH 12/26] feat(ratelimit): Add support for limits per ip address and auth token --- go.mod | 2 +- go.sum | 4 +- internal/daemon/controller/handler.go | 2 +- internal/ratelimit/config.go | 60 ++- internal/ratelimit/config_test.go | 592 +++++++++++++++++++++----- internal/ratelimit/handler.go | 27 +- internal/ratelimit/handler_test.go | 410 +++++++++++++++++- 7 files changed, 958 insertions(+), 139 deletions(-) diff --git a/go.mod b/go.mod index be90c9c42..d5ce6854b 100644 --- a/go.mod +++ b/go.mod @@ -94,7 +94,7 @@ require ( github.com/hashicorp/cap/ldap v0.0.0-20231012003312-273118a6e3b8 github.com/hashicorp/dbassert v0.0.0-20231012105025-1bc1bd88e22b github.com/hashicorp/go-kms-wrapping/extras/kms/v2 v2.0.0-20231124110655-4315424d22d0 - github.com/hashicorp/go-rate v0.0.0-20231106220535-0b8f59b56792 + github.com/hashicorp/go-rate v0.0.0-20231129140553-d912bfc00ee2 github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/nodeenrollment v0.2.9 github.com/jackc/pgx/v5 v5.4.3 diff --git a/go.sum b/go.sum index d2f133013..c04601408 100644 --- a/go.sum +++ b/go.sum @@ -260,8 +260,8 @@ github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9 github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= github.com/hashicorp/go-plugin v1.5.2 h1:aWv8eimFqWlsEiMrYZdPYl+FdHaBJSN4AWwGWfT1G2Y= github.com/hashicorp/go-plugin v1.5.2/go.mod h1:w1sAEES3g3PuV/RzUrgow20W2uErMly84hhD3um1WL4= -github.com/hashicorp/go-rate v0.0.0-20231106220535-0b8f59b56792 h1:YS6tr7P9pfMqfomjrquQEHEj+tEk3aCj1OuYp6u87aQ= -github.com/hashicorp/go-rate v0.0.0-20231106220535-0b8f59b56792/go.mod h1:uTQC66EFZuSKunHYx0hHIfdH3nhRrv7UOOE8Ug0yI0A= +github.com/hashicorp/go-rate v0.0.0-20231129140553-d912bfc00ee2 h1:vNHBERW+GdaqHG0a+McDV/ExpdFshheEzie41wb8nts= +github.com/hashicorp/go-rate v0.0.0-20231129140553-d912bfc00ee2/go.mod h1:uTQC66EFZuSKunHYx0hHIfdH3nhRrv7UOOE8Ug0yI0A= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZnpUv3/+BxzFA= github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= diff --git a/internal/daemon/controller/handler.go b/internal/daemon/controller/handler.go index 9731befd5..d390cf1b6 100644 --- a/internal/daemon/controller/handler.go +++ b/internal/daemon/controller/handler.go @@ -74,7 +74,7 @@ func createMuxWithEndpoints(c *Controller, props HandlerProperties) (http.Handle } mux := http.NewServeMux() - mux.Handle("/v1/", ratelimit.Handler(c.rateLimiter, grpcGwMux)) + mux.Handle("/v1/", ratelimit.Handler(c.baseContext, c.rateLimiter, grpcGwMux)) mux.Handle(uiPath, handleUi(c)) isUiRequest := func(req *http.Request) bool { diff --git a/internal/ratelimit/config.go b/internal/ratelimit/config.go index dd8cc1162..a4ea23138 100644 --- a/internal/ratelimit/config.go +++ b/internal/ratelimit/config.go @@ -39,10 +39,14 @@ import ( // Defaults used when creating default rate.Limits. const ( - DefaultRequestLimit = 3000 - DefaultPeriod = time.Second * 30 - DefaultListRequestLimit = 60 - DefaultListPeriod = time.Second * 30 + DefaultInTotalRequestLimit = 3000 + DefaultIpAddressRequestLimit = 3000 + DefaultAuthTokenRequestLimit = 3000 + DefaultPeriod = time.Second * 30 + DefaultInTotalListRequestLimit = 60 + DefaultIpAddressListRequestLimit = 60 + DefaultAuthTokenListRequestLimit = 60 + DefaultListPeriod = time.Second * 30 ) // defaultLimiterMaxEntries is the default maximum number of quotas that @@ -130,24 +134,58 @@ func (c Configs) Limits(ctx context.Context) ([]*rate.Limit, error) { } for a := range validActions { - key := fmt.Sprintf("%s:%s:%s", res.String(), a.String(), rate.LimitPerTotal) + 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[key] = &rate.Limit{ + defaults[inTotalKey] = &rate.Limit{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerTotal, Unlimited: false, - MaxRequests: DefaultListRequestLimit, + MaxRequests: DefaultInTotalListRequestLimit, + Period: DefaultListPeriod, + } + defaults[authTokenKey] = &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerAuthToken, + Unlimited: false, + MaxRequests: DefaultAuthTokenListRequestLimit, + Period: DefaultListPeriod, + } + defaults[ipAddressKey] = &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerIPAddress, + Unlimited: false, + MaxRequests: DefaultIpAddressListRequestLimit, Period: DefaultListPeriod, } default: - defaults[key] = &rate.Limit{ + defaults[inTotalKey] = &rate.Limit{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerTotal, Unlimited: false, - MaxRequests: DefaultRequestLimit, + MaxRequests: DefaultInTotalRequestLimit, + Period: DefaultPeriod, + } + defaults[authTokenKey] = &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerAuthToken, + Unlimited: false, + MaxRequests: DefaultAuthTokenRequestLimit, + Period: DefaultPeriod, + } + defaults[ipAddressKey] = &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerIPAddress, + Unlimited: false, + MaxRequests: DefaultIpAddressRequestLimit, Period: DefaultPeriod, } } @@ -177,7 +215,7 @@ func (c Configs) Limits(ctx context.Context) ([]*rate.Limit, error) { return nil, err } for a := range validActions { - key := fmt.Sprintf("%s:%s:%s", res.String(), a.String(), rate.LimitPerTotal) + key := fmt.Sprintf("%s:%s:%s", res.String(), a.String(), rate.LimitPer(cc.Per)) defaults[key] = &rate.Limit{ Resource: res.String(), @@ -205,7 +243,7 @@ func (c Configs) Limits(ctx context.Context) ([]*rate.Limit, error) { 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.LimitPerTotal) + key := fmt.Sprintf("%s:%s:%s", res.String(), a.String(), rate.LimitPer(cc.Per)) defaults[key] = &rate.Limit{ Resource: res.String(), diff --git a/internal/ratelimit/config_test.go b/internal/ratelimit/config_test.go index d5374f6bb..6c0d83193 100644 --- a/internal/ratelimit/config_test.go +++ b/internal/ratelimit/config_test.go @@ -41,23 +41,61 @@ func TestConfigsLimits(t *testing.T) { case action.Unknown, action.All: continue case action.List: - limits = append(limits, &rate.Limit{ - Resource: res.String(), - Action: a.String(), - Per: rate.LimitPerTotal, - Unlimited: false, - MaxRequests: DefaultListRequestLimit, - Period: DefaultListPeriod, - }) + limits = append( + limits, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: DefaultInTotalListRequestLimit, + Period: DefaultListPeriod, + }, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerIPAddress, + Unlimited: false, + MaxRequests: DefaultIpAddressListRequestLimit, + Period: DefaultListPeriod, + }, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerAuthToken, + Unlimited: false, + MaxRequests: DefaultAuthTokenListRequestLimit, + Period: DefaultListPeriod, + }, + ) default: - limits = append(limits, &rate.Limit{ - Resource: res.String(), - Action: a.String(), - Per: rate.LimitPerTotal, - Unlimited: false, - MaxRequests: DefaultRequestLimit, - Period: DefaultPeriod, - }) + limits = append( + limits, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: DefaultInTotalRequestLimit, + Period: DefaultPeriod, + }, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerIPAddress, + Unlimited: false, + MaxRequests: DefaultIpAddressRequestLimit, + Period: DefaultPeriod, + }, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerAuthToken, + Unlimited: false, + MaxRequests: DefaultAuthTokenRequestLimit, + Period: DefaultPeriod, + }, + ) } } } @@ -75,6 +113,20 @@ func TestConfigsLimits(t *testing.T) { Limit: 20, Period: time.Minute * 5, }, + { + Resources: []string{"*"}, + Actions: []string{"*"}, + Per: "ip-address", + Limit: 10, + Period: time.Minute * 5, + }, + { + Resources: []string{"*"}, + Actions: []string{"*"}, + Per: "auth-token", + Limit: 5, + Period: time.Minute * 5, + }, }, func() []*rate.Limit { limits := make([]*rate.Limit, 0, len(resource.Map)*len(action.Map)) @@ -90,14 +142,33 @@ func TestConfigsLimits(t *testing.T) { case action.Unknown, action.All: continue default: - limits = append(limits, &rate.Limit{ - Resource: res.String(), - Action: a.String(), - Per: rate.LimitPerTotal, - Unlimited: false, - MaxRequests: 20, - Period: time.Minute * 5, - }) + limits = append( + limits, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: 20, + Period: time.Minute * 5, + }, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerIPAddress, + Unlimited: false, + MaxRequests: 10, + Period: time.Minute * 5, + }, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerAuthToken, + Unlimited: false, + MaxRequests: 5, + Period: time.Minute * 5, + }, + ) } } } @@ -115,6 +186,20 @@ func TestConfigsLimits(t *testing.T) { Limit: 20, Period: time.Minute * 5, }, + { + Resources: []string{"*"}, + Actions: []string{"*"}, + Per: "ip-address", + Limit: 20, + Period: time.Minute * 5, + }, + { + Resources: []string{"*"}, + Actions: []string{"*"}, + Per: "auth-token", + Limit: 20, + Period: time.Minute * 5, + }, { Resources: []string{"target"}, Actions: []string{"list"}, @@ -122,6 +207,20 @@ func TestConfigsLimits(t *testing.T) { Limit: 10, Period: time.Minute, }, + { + Resources: []string{"target"}, + Actions: []string{"list"}, + Per: "ip-address", + Limit: 5, + Period: time.Minute, + }, + { + Resources: []string{"target"}, + Actions: []string{"list"}, + Per: "auth-token", + Limit: 1, + Period: time.Minute, + }, }, func() []*rate.Limit { limits := make([]*rate.Limit, 0, len(resource.Map)*len(action.Map)) @@ -138,26 +237,64 @@ func TestConfigsLimits(t *testing.T) { continue case action.List: if res == resource.Target { - limits = append(limits, &rate.Limit{ - Resource: res.String(), - Action: a.String(), - Per: rate.LimitPerTotal, - Unlimited: false, - MaxRequests: 10, - Period: time.Minute, - }) + limits = append( + limits, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: 10, + Period: time.Minute, + }, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerIPAddress, + Unlimited: false, + MaxRequests: 5, + Period: time.Minute, + }, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerAuthToken, + Unlimited: false, + MaxRequests: 1, + Period: time.Minute, + }, + ) continue } fallthrough default: - limits = append(limits, &rate.Limit{ - Resource: res.String(), - Action: a.String(), - Per: rate.LimitPerTotal, - Unlimited: false, - MaxRequests: 20, - Period: time.Minute * 5, - }) + limits = append( + limits, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: 20, + Period: time.Minute * 5, + }, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerIPAddress, + Unlimited: false, + MaxRequests: 20, + Period: time.Minute * 5, + }, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerAuthToken, + Unlimited: false, + MaxRequests: 20, + Period: time.Minute * 5, + }, + ) } } } @@ -175,6 +312,20 @@ func TestConfigsLimits(t *testing.T) { Limit: 10, Period: time.Minute, }, + { + Resources: []string{"target", "session"}, + Actions: []string{"*"}, + Per: "ip-address", + Limit: 5, + Period: time.Minute, + }, + { + Resources: []string{"target", "session"}, + Actions: []string{"*"}, + Per: "auth-token", + Limit: 1, + Period: time.Minute, + }, }, func() []*rate.Limit { limits := make([]*rate.Limit, 0, len(resource.Map)*len(action.Map)) @@ -190,14 +341,33 @@ func TestConfigsLimits(t *testing.T) { case action.Unknown, action.All: continue default: - limits = append(limits, &rate.Limit{ - Resource: res.String(), - Action: a.String(), - Per: rate.LimitPerTotal, - Unlimited: false, - MaxRequests: 10, - Period: time.Minute, - }) + limits = append( + limits, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: 10, + Period: time.Minute, + }, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerIPAddress, + Unlimited: false, + MaxRequests: 5, + Period: time.Minute, + }, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerAuthToken, + Unlimited: false, + MaxRequests: 1, + Period: time.Minute, + }, + ) } } default: @@ -208,23 +378,61 @@ func TestConfigsLimits(t *testing.T) { case action.Unknown, action.All: continue case action.List: - limits = append(limits, &rate.Limit{ - Resource: res.String(), - Action: a.String(), - Per: rate.LimitPerTotal, - Unlimited: false, - MaxRequests: DefaultListRequestLimit, - Period: DefaultListPeriod, - }) + limits = append( + limits, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: DefaultInTotalListRequestLimit, + Period: DefaultListPeriod, + }, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerIPAddress, + Unlimited: false, + MaxRequests: DefaultIpAddressListRequestLimit, + Period: DefaultListPeriod, + }, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerAuthToken, + Unlimited: false, + MaxRequests: DefaultAuthTokenListRequestLimit, + Period: DefaultListPeriod, + }, + ) default: - limits = append(limits, &rate.Limit{ - Resource: res.String(), - Action: a.String(), - Per: rate.LimitPerTotal, - Unlimited: false, - MaxRequests: DefaultRequestLimit, - Period: DefaultPeriod, - }) + limits = append( + limits, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: DefaultInTotalRequestLimit, + Period: DefaultPeriod, + }, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerIPAddress, + Unlimited: false, + MaxRequests: DefaultIpAddressRequestLimit, + Period: DefaultPeriod, + }, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerAuthToken, + Unlimited: false, + MaxRequests: DefaultAuthTokenRequestLimit, + Period: DefaultPeriod, + }, + ) } } } @@ -243,6 +451,20 @@ func TestConfigsLimits(t *testing.T) { Limit: 10, Period: time.Minute, }, + { + Resources: []string{"target", "session"}, + Actions: []string{"list", "read"}, + Per: "ip-address", + Limit: 5, + Period: time.Minute, + }, + { + Resources: []string{"target", "session"}, + Actions: []string{"list", "read"}, + Per: "auth-token", + Limit: 1, + Period: time.Minute, + }, }, func() []*rate.Limit { limits := make([]*rate.Limit, 0, len(resource.Map)*len(action.Map)) @@ -258,23 +480,61 @@ func TestConfigsLimits(t *testing.T) { case action.Unknown, action.All: continue case action.List, action.Read: - limits = append(limits, &rate.Limit{ - Resource: res.String(), - Action: a.String(), - Per: rate.LimitPerTotal, - Unlimited: false, - MaxRequests: 10, - Period: time.Minute, - }) + limits = append( + limits, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: 10, + Period: time.Minute, + }, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerIPAddress, + Unlimited: false, + MaxRequests: 5, + Period: time.Minute, + }, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerAuthToken, + Unlimited: false, + MaxRequests: 1, + Period: time.Minute, + }, + ) default: - limits = append(limits, &rate.Limit{ - Resource: res.String(), - Action: a.String(), - Per: rate.LimitPerTotal, - Unlimited: false, - MaxRequests: DefaultRequestLimit, - Period: DefaultPeriod, - }) + limits = append( + limits, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: DefaultInTotalRequestLimit, + Period: DefaultPeriod, + }, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerIPAddress, + Unlimited: false, + MaxRequests: DefaultIpAddressRequestLimit, + Period: DefaultPeriod, + }, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerAuthToken, + Unlimited: false, + MaxRequests: DefaultAuthTokenRequestLimit, + Period: DefaultPeriod, + }, + ) } } default: @@ -285,23 +545,61 @@ func TestConfigsLimits(t *testing.T) { case action.Unknown, action.All: continue case action.List: - limits = append(limits, &rate.Limit{ - Resource: res.String(), - Action: a.String(), - Per: rate.LimitPerTotal, - Unlimited: false, - MaxRequests: DefaultListRequestLimit, - Period: DefaultListPeriod, - }) + limits = append( + limits, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: DefaultInTotalListRequestLimit, + Period: DefaultListPeriod, + }, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerIPAddress, + Unlimited: false, + MaxRequests: DefaultIpAddressListRequestLimit, + Period: DefaultListPeriod, + }, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerAuthToken, + Unlimited: false, + MaxRequests: DefaultAuthTokenListRequestLimit, + Period: DefaultListPeriod, + }, + ) default: - limits = append(limits, &rate.Limit{ - Resource: res.String(), - Action: a.String(), - Per: rate.LimitPerTotal, - Unlimited: false, - MaxRequests: DefaultRequestLimit, - Period: DefaultPeriod, - }) + limits = append( + limits, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: DefaultInTotalRequestLimit, + Period: DefaultPeriod, + }, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerIPAddress, + Unlimited: false, + MaxRequests: DefaultIpAddressRequestLimit, + Period: DefaultPeriod, + }, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerAuthToken, + Unlimited: false, + MaxRequests: DefaultAuthTokenRequestLimit, + Period: DefaultPeriod, + }, + ) } } } @@ -342,15 +640,62 @@ func TestConfigsLimits(t *testing.T) { switch a { case action.Unknown, action.All: continue + case action.List: + limits = append( + limits, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: 20, + Period: time.Minute * 5, + }, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerIPAddress, + Unlimited: false, + MaxRequests: DefaultIpAddressListRequestLimit, + Period: DefaultListPeriod, + }, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerAuthToken, + Unlimited: false, + MaxRequests: DefaultAuthTokenListRequestLimit, + Period: DefaultListPeriod, + }, + ) default: - limits = append(limits, &rate.Limit{ - Resource: res.String(), - Action: a.String(), - Per: rate.LimitPerTotal, - Unlimited: false, - MaxRequests: 20, - Period: time.Minute * 5, - }) + limits = append( + limits, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: 20, + Period: time.Minute * 5, + }, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerIPAddress, + Unlimited: false, + MaxRequests: DefaultIpAddressRequestLimit, + Period: DefaultPeriod, + }, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerAuthToken, + Unlimited: false, + MaxRequests: DefaultAuthTokenRequestLimit, + Period: DefaultPeriod, + }, + ) } } } @@ -367,6 +712,18 @@ func TestConfigsLimits(t *testing.T) { Per: "total", Unlimited: true, }, + { + Resources: []string{"*"}, + Actions: []string{"*"}, + Per: "ip-address", + Unlimited: true, + }, + { + Resources: []string{"*"}, + Actions: []string{"*"}, + Per: "auth-token", + Unlimited: true, + }, }, func() []*rate.Limit { limits := make([]*rate.Limit, 0, len(resource.Map)*len(action.Map)) @@ -382,12 +739,27 @@ func TestConfigsLimits(t *testing.T) { case action.Unknown, action.All: continue default: - limits = append(limits, &rate.Limit{ - Resource: res.String(), - Action: a.String(), - Per: rate.LimitPerTotal, - Unlimited: true, - }) + limits = append( + limits, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + Unlimited: true, + }, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerIPAddress, + Unlimited: true, + }, + &rate.Limit{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerAuthToken, + Unlimited: true, + }, + ) } } } diff --git a/internal/ratelimit/handler.go b/internal/ratelimit/handler.go index 38bc9fb79..dec2a69de 100644 --- a/internal/ratelimit/handler.go +++ b/internal/ratelimit/handler.go @@ -4,11 +4,14 @@ package ratelimit import ( + "context" "fmt" "math" "net/http" "regexp" + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/event" "github.com/hashicorp/boundary/internal/types/action" "github.com/hashicorp/boundary/internal/types/resource" "github.com/hashicorp/go-rate" @@ -112,12 +115,23 @@ func extractResourceAction(path, method string) (res, act string, err error) { // using the provided rate limiter. If the request is allowed, the next handler // is called. Otherwise a 429 is returned with the Retry-After response header // set to the number of seconds the client should wait to make it's next request. -func Handler(l *rate.Limiter, next http.Handler) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - path := req.URL.Path - method := req.Method +func Handler(ctx context.Context, l *rate.Limiter, next http.Handler) http.Handler { + const op = "ratelimit.Handler" - res, a, err := extractResourceAction(path, method) + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + reqInfo, ok := event.RequestInfoFromContext(req.Context()) + if !ok || reqInfo == nil || reqInfo.ClientIp == "" { + rw.WriteHeader(http.StatusInternalServerError) + return + } + + authtoken, ok := req.Context().Value(globals.ContextAuthTokenPublicIdKey).(string) + if !ok { + rw.WriteHeader(http.StatusInternalServerError) + return + } + + res, a, err := extractResourceAction(req.URL.Path, req.Method) if err != nil { if extractErr, ok := err.(*extractResourceActionErr); ok { rw.WriteHeader(extractErr.statusCode) @@ -127,14 +141,13 @@ func Handler(l *rate.Limiter, next http.Handler) http.Handler { return } - allowed, quota, err := l.Allow(res, a) + allowed, quota, err := l.Allow(res, a, reqInfo.ClientIp, authtoken) if err != nil { if errFull, ok := err.(*rate.ErrLimiterFull); ok { rw.Header().Add("Retry-After", fmt.Sprintf("%.0f", math.Ceil(errFull.RetryIn.Seconds()))) rw.WriteHeader(http.StatusServiceUnavailable) return } - // The only other error here should be rate.ErrLimitNotFound, which // shouldn't be possible given how we initialize the limiter and // the checks done by extractResourceAction. diff --git a/internal/ratelimit/handler_test.go b/internal/ratelimit/handler_test.go index e0ab17ec1..013cabf1c 100644 --- a/internal/ratelimit/handler_test.go +++ b/internal/ratelimit/handler_test.go @@ -4,11 +4,15 @@ package ratelimit_test import ( + "context" "net/http" "net/http/httptest" "testing" "time" + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/daemon/common" + "github.com/hashicorp/boundary/internal/event" "github.com/hashicorp/boundary/internal/ratelimit" "github.com/hashicorp/boundary/internal/types/action" "github.com/hashicorp/boundary/internal/types/resource" @@ -18,10 +22,14 @@ import ( ) func TestHandler(t *testing.T) { + ctx := context.Background() + cases := []struct { name string limiter *rate.Limiter req func(uri string) *http.Request + ip string + authtoken string expectCode int expectHeaders http.Header }{ @@ -37,6 +45,22 @@ func TestHandler(t *testing.T) { MaxRequests: 10, Period: time.Minute, }, + { + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerIPAddress, + Unlimited: false, + MaxRequests: 10, + Period: time.Minute, + }, + { + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerAuthToken, + Unlimited: false, + MaxRequests: 10, + Period: time.Minute, + }, }, 10) require.NoError(t, err) return r @@ -46,6 +70,8 @@ func TestHandler(t *testing.T) { require.NoError(t, err) return r }, + "127.0.0.1", + "authtoken", http.StatusOK, http.Header{}, }, @@ -61,6 +87,22 @@ func TestHandler(t *testing.T) { MaxRequests: 10, Period: time.Minute, }, + { + Resource: resource.Target.String(), + Action: action.Create.String(), + Per: rate.LimitPerIPAddress, + Unlimited: false, + MaxRequests: 10, + Period: time.Minute, + }, + { + Resource: resource.Target.String(), + Action: action.Create.String(), + Per: rate.LimitPerAuthToken, + Unlimited: false, + MaxRequests: 10, + Period: time.Minute, + }, }, 10) require.NoError(t, err) return r @@ -70,6 +112,8 @@ func TestHandler(t *testing.T) { require.NoError(t, err) return r }, + "127.0.0.1", + "authtoken", http.StatusOK, http.Header{}, }, @@ -85,6 +129,22 @@ func TestHandler(t *testing.T) { MaxRequests: 10, Period: time.Minute, }, + { + Resource: resource.Target.String(), + Action: action.Read.String(), + Per: rate.LimitPerIPAddress, + Unlimited: false, + MaxRequests: 10, + Period: time.Minute, + }, + { + Resource: resource.Target.String(), + Action: action.Read.String(), + Per: rate.LimitPerAuthToken, + Unlimited: false, + MaxRequests: 10, + Period: time.Minute, + }, }, 10) require.NoError(t, err) return r @@ -94,6 +154,8 @@ func TestHandler(t *testing.T) { require.NoError(t, err) return r }, + "127.0.0.1", + "authtoken", http.StatusOK, http.Header{}, }, @@ -109,6 +171,22 @@ func TestHandler(t *testing.T) { MaxRequests: 10, Period: time.Minute, }, + { + Resource: resource.Target.String(), + Action: action.Update.String(), + Per: rate.LimitPerIPAddress, + Unlimited: false, + MaxRequests: 10, + Period: time.Minute, + }, + { + Resource: resource.Target.String(), + Action: action.Update.String(), + Per: rate.LimitPerAuthToken, + Unlimited: false, + MaxRequests: 10, + Period: time.Minute, + }, }, 10) require.NoError(t, err) return r @@ -118,6 +196,8 @@ func TestHandler(t *testing.T) { require.NoError(t, err) return r }, + "127.0.0.1", + "authtoken", http.StatusOK, http.Header{}, }, @@ -133,6 +213,22 @@ func TestHandler(t *testing.T) { MaxRequests: 10, Period: time.Minute, }, + { + Resource: resource.Target.String(), + Action: action.Delete.String(), + Per: rate.LimitPerIPAddress, + Unlimited: false, + MaxRequests: 10, + Period: time.Minute, + }, + { + Resource: resource.Target.String(), + Action: action.Delete.String(), + Per: rate.LimitPerAuthToken, + Unlimited: false, + MaxRequests: 10, + Period: time.Minute, + }, }, 10) require.NoError(t, err) return r @@ -142,6 +238,8 @@ func TestHandler(t *testing.T) { require.NoError(t, err) return r }, + "127.0.0.1", + "authtoken", http.StatusOK, http.Header{}, }, @@ -157,6 +255,22 @@ func TestHandler(t *testing.T) { MaxRequests: 10, Period: time.Minute, }, + { + Resource: resource.Target.String(), + Action: action.AuthorizeSession.String(), + Per: rate.LimitPerIPAddress, + Unlimited: false, + MaxRequests: 10, + Period: time.Minute, + }, + { + Resource: resource.Target.String(), + Action: action.AuthorizeSession.String(), + Per: rate.LimitPerAuthToken, + Unlimited: false, + MaxRequests: 10, + Period: time.Minute, + }, }, 10) require.NoError(t, err) return r @@ -166,6 +280,8 @@ func TestHandler(t *testing.T) { require.NoError(t, err) return r }, + "127.0.0.1", + "authtoken", http.StatusOK, http.Header{}, }, @@ -181,9 +297,25 @@ func TestHandler(t *testing.T) { MaxRequests: 2, Period: time.Minute, }, + { + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerIPAddress, + Unlimited: false, + MaxRequests: 2, + Period: time.Minute, + }, + { + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerAuthToken, + Unlimited: false, + MaxRequests: 2, + Period: time.Minute, + }, }, 10) require.NoError(t, err) - _, q, err := r.Allow("target", "list") + _, q, err := r.Allow("target", "list", "", "") require.NoError(t, err) // consume all of the quota @@ -198,6 +330,8 @@ func TestHandler(t *testing.T) { require.NoError(t, err) return r }, + "127.0.0.1", + "authtoken", http.StatusTooManyRequests, http.Header{ "Retry-After": []string{"60"}, @@ -223,10 +357,42 @@ func TestHandler(t *testing.T) { MaxRequests: 2, Period: time.Minute, }, - }, 1) + { + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerIPAddress, + Unlimited: false, + MaxRequests: 2, + Period: time.Minute, + }, + { + Resource: resource.Target.String(), + Action: action.Read.String(), + Per: rate.LimitPerIPAddress, + Unlimited: false, + MaxRequests: 2, + Period: time.Minute, + }, + { + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerAuthToken, + Unlimited: false, + MaxRequests: 2, + Period: time.Minute, + }, + { + Resource: resource.Target.String(), + Action: action.Read.String(), + Per: rate.LimitPerAuthToken, + Unlimited: false, + MaxRequests: 2, + Period: time.Minute, + }, + }, 3) require.NoError(t, err) // use up all of the capacity for the limiter - _, _, err = r.Allow("target", "read") + _, _, err = r.Allow("target", "read", "", "") require.NoError(t, err) return r }(), @@ -235,6 +401,8 @@ func TestHandler(t *testing.T) { require.NoError(t, err) return r }, + "127.0.0.1", + "authtoken", http.StatusServiceUnavailable, http.Header{ "Retry-After": []string{"1"}, @@ -252,6 +420,22 @@ func TestHandler(t *testing.T) { MaxRequests: 2, Period: time.Minute, }, + { + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerIPAddress, + Unlimited: false, + MaxRequests: 2, + Period: time.Minute, + }, + { + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerAuthToken, + Unlimited: false, + MaxRequests: 2, + Period: time.Minute, + }, }, 10) require.NoError(t, err) return r @@ -261,6 +445,8 @@ func TestHandler(t *testing.T) { require.NoError(t, err) return r }, + "127.0.0.1", + "authtoken", http.StatusInternalServerError, http.Header{}, }, @@ -276,6 +462,22 @@ func TestHandler(t *testing.T) { MaxRequests: 2, Period: time.Minute, }, + { + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerIPAddress, + Unlimited: false, + MaxRequests: 2, + Period: time.Minute, + }, + { + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerAuthToken, + Unlimited: false, + MaxRequests: 2, + Period: time.Minute, + }, }, 10) require.NoError(t, err) return r @@ -285,6 +487,8 @@ func TestHandler(t *testing.T) { require.NoError(t, err) return r }, + "127.0.0.1", + "authtoken", http.StatusNotFound, http.Header{}, }, @@ -300,6 +504,22 @@ func TestHandler(t *testing.T) { MaxRequests: 2, Period: time.Minute, }, + { + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerIPAddress, + Unlimited: false, + MaxRequests: 2, + Period: time.Minute, + }, + { + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerAuthToken, + Unlimited: false, + MaxRequests: 2, + Period: time.Minute, + }, }, 10) require.NoError(t, err) return r @@ -309,6 +529,8 @@ func TestHandler(t *testing.T) { require.NoError(t, err) return r }, + "127.0.0.1", + "authtoken", http.StatusBadRequest, http.Header{}, }, @@ -324,6 +546,22 @@ func TestHandler(t *testing.T) { MaxRequests: 2, Period: time.Minute, }, + { + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerIPAddress, + Unlimited: false, + MaxRequests: 2, + Period: time.Minute, + }, + { + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerAuthToken, + Unlimited: false, + MaxRequests: 2, + Period: time.Minute, + }, }, 10) require.NoError(t, err) return r @@ -333,6 +571,8 @@ func TestHandler(t *testing.T) { require.NoError(t, err) return r }, + "127.0.0.1", + "authtoken", http.StatusMethodNotAllowed, http.Header{}, }, @@ -348,6 +588,22 @@ func TestHandler(t *testing.T) { MaxRequests: 2, Period: time.Minute, }, + { + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerIPAddress, + Unlimited: false, + MaxRequests: 2, + Period: time.Minute, + }, + { + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerAuthToken, + Unlimited: false, + MaxRequests: 2, + Period: time.Minute, + }, }, 10) require.NoError(t, err) return r @@ -357,6 +613,8 @@ func TestHandler(t *testing.T) { require.NoError(t, err) return r }, + "127.0.0.1", + "authtoken", http.StatusMethodNotAllowed, http.Header{}, }, @@ -372,6 +630,22 @@ func TestHandler(t *testing.T) { MaxRequests: 2, Period: time.Minute, }, + { + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerIPAddress, + Unlimited: false, + MaxRequests: 2, + Period: time.Minute, + }, + { + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerAuthToken, + Unlimited: false, + MaxRequests: 2, + Period: time.Minute, + }, }, 10) require.NoError(t, err) return r @@ -381,6 +655,8 @@ func TestHandler(t *testing.T) { require.NoError(t, err) return r }, + "127.0.0.1", + "authtoken", http.StatusMethodNotAllowed, http.Header{}, }, @@ -388,10 +664,29 @@ func TestHandler(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - server := httptest.NewServer(ratelimit.Handler(tc.limiter, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - return - }))) + server := httptest.NewServer( + func(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + ctx := req.Context() + id, err := event.NewId(event.IdPrefix) + require.NoError(t, err) + ctx, err = event.NewRequestInfoContext(ctx, &event.RequestInfo{ + Id: id, + EventId: common.GeneratedTraceId(ctx), + ClientIp: tc.ip, + }) + require.NoError(t, err) + ctx = context.WithValue(ctx, globals.ContextAuthTokenPublicIdKey, tc.authtoken) + + req = req.Clone(ctx) + + next.ServeHTTP(rw, req) + }) + }(ratelimit.Handler(ctx, tc.limiter, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + return + }))), + ) req := tc.req(server.URL) client := &http.Client{} res, err := client.Do(req) @@ -403,3 +698,104 @@ func TestHandler(t *testing.T) { }) } } + +func TestHandlerErrors(t *testing.T) { + ctx := context.Background() + + cases := []struct { + name string + ctxFn func(context.Context) context.Context + expectCode int + }{ + { + "NoRequestInfo", + func(ctx context.Context) context.Context { + ctx = context.WithValue(ctx, globals.ContextAuthTokenPublicIdKey, "auth-token") + return ctx + }, + http.StatusInternalServerError, + }, + { + "ClientIpEmptyString", + func(ctx context.Context) context.Context { + id, err := event.NewId(event.IdPrefix) + require.NoError(t, err) + ctx, err = event.NewRequestInfoContext(ctx, &event.RequestInfo{ + Id: id, + EventId: common.GeneratedTraceId(ctx), + ClientIp: "", + }) + require.NoError(t, err) + ctx = context.WithValue(ctx, globals.ContextAuthTokenPublicIdKey, "auth-token") + return ctx + }, + http.StatusInternalServerError, + }, + { + "NoAuthToken", + func(ctx context.Context) context.Context { + id, err := event.NewId(event.IdPrefix) + require.NoError(t, err) + ctx, err = event.NewRequestInfoContext(ctx, &event.RequestInfo{ + Id: id, + EventId: common.GeneratedTraceId(ctx), + ClientIp: "127.0.0.1", + }) + require.NoError(t, err) + return ctx + }, + http.StatusInternalServerError, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + r, err := rate.NewLimiter([]*rate.Limit{ + { + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: 10, + Period: time.Minute, + }, + { + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerIPAddress, + Unlimited: false, + MaxRequests: 10, + Period: time.Minute, + }, + { + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerAuthToken, + Unlimited: false, + MaxRequests: 10, + Period: time.Minute, + }, + }, 10) + require.NoError(t, err) + server := httptest.NewServer( + func(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + ctx := tc.ctxFn(req.Context()) + req = req.Clone(ctx) + + next.ServeHTTP(rw, req) + }) + }(ratelimit.Handler(ctx, r, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + return + }))), + ) + req, err := http.NewRequest(http.MethodGet, server.URL+"/v1/targets", nil) + require.NoError(t, err) + client := &http.Client{} + res, err := client.Do(req) + require.NoError(t, err) + assert.Equal(t, tc.expectCode, res.StatusCode) + }) + } +} From e337cf8f69cc8a2b9d1cf208170ecd80be882e1d Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Thu, 19 Oct 2023 20:57:49 +0000 Subject: [PATCH 13/26] feat(controller): Add authtoken public id to request context This allows for http middleware handler functions that run later in the handler function stack to have access to the extracted auth token public id. --- globals/globals.go | 5 +++++ internal/daemon/controller/handler.go | 1 + 2 files changed, 6 insertions(+) diff --git a/globals/globals.go b/globals/globals.go index e0abb6b27..0f9dcc6ea 100644 --- a/globals/globals.go +++ b/globals/globals.go @@ -17,6 +17,7 @@ const ( type ( ContextMaxRequestSizeType struct{} ContextOriginalRequestPathType struct{} + ContextAuthTokenPublicIdType struct{} ) var ( @@ -30,6 +31,10 @@ var ( // about clashing string identifiers ContextMaxRequestSizeTypeKey ContextMaxRequestSizeType + // ContextAuthTokenPublicIdKey is a value to keep linters from complaining + // about clashing string identifiers + ContextAuthTokenPublicIdKey ContextAuthTokenPublicIdType + // ContextOriginalRequestPathTypeKey is a value to keep linters from complaining // about clashing string identifiers ContextOriginalRequestPathTypeKey ContextOriginalRequestPathType diff --git a/internal/daemon/controller/handler.go b/internal/daemon/controller/handler.go index d390cf1b6..d6f1dbdb4 100644 --- a/internal/daemon/controller/handler.go +++ b/internal/daemon/controller/handler.go @@ -411,6 +411,7 @@ func wrapHandlerWithCommonFuncs(h http.Handler, c *Controller, props HandlerProp } requestInfo.PublicId, requestInfo.EncryptedToken, requestInfo.TokenFormat = auth.GetTokenFromRequest(ctx, c.kms, r) + ctx = context.WithValue(ctx, globals.ContextAuthTokenPublicIdKey, requestInfo.PublicId) if info, ok := event.RequestInfoFromContext(ctx); ok { // piggyback some eventing fields with the auth info proto message From 26854b01a89a1fb88de0f6749f71f249afd49578 Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Tue, 28 Nov 2023 21:42:45 +0000 Subject: [PATCH 14/26] feat(ratelimit): Set RateLimit and RateLimit-Policy headers --- internal/ratelimit/handler.go | 12 +++++++++++ internal/ratelimit/handler_test.go | 34 ++++++++++++++++++++++++------ 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/internal/ratelimit/handler.go b/internal/ratelimit/handler.go index dec2a69de..8db61ccfb 100644 --- a/internal/ratelimit/handler.go +++ b/internal/ratelimit/handler.go @@ -11,6 +11,7 @@ import ( "regexp" "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/event" "github.com/hashicorp/boundary/internal/types/action" "github.com/hashicorp/boundary/internal/types/resource" @@ -155,6 +156,17 @@ func Handler(ctx context.Context, l *rate.Limiter, next http.Handler) http.Handl return } + l.SetUsageHeader(quota, rw.Header()) + if err := l.SetPolicyHeader(res, a, rw.Header()); err != nil { + // Wrap error to emit an error event. An error here would be + // unexpected, since the only possible error would be + // ErrLimitPolicyNotFound which would have been returned by Allow + // and handled above. And even that would be unexpected, given how + // the limiter is initialized and the checks done by + // extractResourceAction. + errors.Wrap(ctx, err, op) + } + if !allowed { rw.Header().Add("Retry-After", fmt.Sprintf("%.0f", quota.ResetsIn().Seconds())) rw.WriteHeader(http.StatusTooManyRequests) diff --git a/internal/ratelimit/handler_test.go b/internal/ratelimit/handler_test.go index 013cabf1c..969cb3979 100644 --- a/internal/ratelimit/handler_test.go +++ b/internal/ratelimit/handler_test.go @@ -73,7 +73,10 @@ func TestHandler(t *testing.T) { "127.0.0.1", "authtoken", http.StatusOK, - http.Header{}, + http.Header{ + "RateLimit-Policy": []string{`10;w=60;comment="total", 10;w=60;comment="ip-address", 10;w=60;comment="auth-token"`}, + "RateLimit": []string{`limit=10, remaining=9, reset=59`}, + }, }, { "AllowedCreate", @@ -115,7 +118,10 @@ func TestHandler(t *testing.T) { "127.0.0.1", "authtoken", http.StatusOK, - http.Header{}, + http.Header{ + "RateLimit-Policy": []string{`10;w=60;comment="total", 10;w=60;comment="ip-address", 10;w=60;comment="auth-token"`}, + "RateLimit": []string{`limit=10, remaining=9, reset=59`}, + }, }, { "AllowedRead", @@ -157,7 +163,10 @@ func TestHandler(t *testing.T) { "127.0.0.1", "authtoken", http.StatusOK, - http.Header{}, + http.Header{ + "RateLimit-Policy": []string{`10;w=60;comment="total", 10;w=60;comment="ip-address", 10;w=60;comment="auth-token"`}, + "RateLimit": []string{`limit=10, remaining=9, reset=59`}, + }, }, { "AllowedUpdate", @@ -199,7 +208,10 @@ func TestHandler(t *testing.T) { "127.0.0.1", "authtoken", http.StatusOK, - http.Header{}, + http.Header{ + "RateLimit-Policy": []string{`10;w=60;comment="total", 10;w=60;comment="ip-address", 10;w=60;comment="auth-token"`}, + "RateLimit": []string{`limit=10, remaining=9, reset=59`}, + }, }, { "AllowedDelete", @@ -241,7 +253,10 @@ func TestHandler(t *testing.T) { "127.0.0.1", "authtoken", http.StatusOK, - http.Header{}, + http.Header{ + "RateLimit-Policy": []string{`10;w=60;comment="total", 10;w=60;comment="ip-address", 10;w=60;comment="auth-token"`}, + "RateLimit": []string{`limit=10, remaining=9, reset=59`}, + }, }, { "AllowedAuthorizeSession", @@ -283,7 +298,10 @@ func TestHandler(t *testing.T) { "127.0.0.1", "authtoken", http.StatusOK, - http.Header{}, + http.Header{ + "RateLimit-Policy": []string{`10;w=60;comment="total", 10;w=60;comment="ip-address", 10;w=60;comment="auth-token"`}, + "RateLimit": []string{`limit=10, remaining=9, reset=59`}, + }, }, { "Limited", @@ -334,7 +352,9 @@ func TestHandler(t *testing.T) { "authtoken", http.StatusTooManyRequests, http.Header{ - "Retry-After": []string{"60"}, + "Retry-After": []string{"60"}, + "RateLimit-Policy": []string{`2;w=60;comment="total", 2;w=60;comment="ip-address", 2;w=60;comment="auth-token"`}, + "RateLimit": []string{`limit=2, remaining=0, reset=59`}, }, }, { From f54f2c0fc4256d80ba75116d87c38568290c2b6b Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Thu, 30 Nov 2023 15:13:03 +0000 Subject: [PATCH 15/26] fix(ratelimit): Correctly extract ids from paths with punctuation --- internal/ratelimit/handler.go | 2 +- internal/ratelimit/handler_test.go | 47 +++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/internal/ratelimit/handler.go b/internal/ratelimit/handler.go index 8db61ccfb..02c766812 100644 --- a/internal/ratelimit/handler.go +++ b/internal/ratelimit/handler.go @@ -33,7 +33,7 @@ func (e *extractResourceActionErr) Error() string { return e.msg } -var pathRegex = regexp.MustCompile(`/v1/(?P[\w-]+)((/(?P[\w/]+))?(:(?P[\w-:]+)?)?)?`) +var pathRegex = regexp.MustCompile(`/v1/(?P[\w-]+)((/(?P[^:]+))?(:(?P[\w-:]+)?)?)?`) func extractResourceAction(path, method string) (res, act string, err error) { var id string diff --git a/internal/ratelimit/handler_test.go b/internal/ratelimit/handler_test.go index 969cb3979..9bd0749bc 100644 --- a/internal/ratelimit/handler_test.go +++ b/internal/ratelimit/handler_test.go @@ -291,7 +291,52 @@ func TestHandler(t *testing.T) { return r }(), func(uri string) *http.Request { - r, err := http.NewRequest(http.MethodDelete, uri+"/v1/targets/ttcp_123456789:authorize-session", nil) + r, err := http.NewRequest(http.MethodPut, uri+"/v1/targets/ttcp_123456789:authorize-session", nil) + require.NoError(t, err) + return r + }, + "127.0.0.1", + "authtoken", + http.StatusOK, + http.Header{ + "RateLimit-Policy": []string{`10;w=60;comment="total", 10;w=60;comment="ip-address", 10;w=60;comment="auth-token"`}, + "RateLimit": []string{`limit=10, remaining=9, reset=59`}, + }, + }, + { + "AllowedAuthorizeSessionTargetNameSlash", + func() *rate.Limiter { + r, err := rate.NewLimiter([]*rate.Limit{ + { + Resource: resource.Target.String(), + Action: action.AuthorizeSession.String(), + Per: rate.LimitPerTotal, + Unlimited: false, + MaxRequests: 10, + Period: time.Minute, + }, + { + Resource: resource.Target.String(), + Action: action.AuthorizeSession.String(), + Per: rate.LimitPerIPAddress, + Unlimited: false, + MaxRequests: 10, + Period: time.Minute, + }, + { + Resource: resource.Target.String(), + Action: action.AuthorizeSession.String(), + Per: rate.LimitPerAuthToken, + Unlimited: false, + MaxRequests: 10, + Period: time.Minute, + }, + }, 10) + require.NoError(t, err) + return r + }(), + func(uri string) *http.Request { + r, err := http.NewRequest(http.MethodPut, uri+`/v1/targets/E2E/Test-Target-With\Name:authorize-session`, nil) require.NoError(t, err) return r }, From 0bc423ebd81bac56223b896626ddfbb2989ec12e Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Tue, 24 Oct 2023 14:52:25 +0000 Subject: [PATCH 16/26] feat(controller): Reload api rate limits on SIGHUP --- .../controller_ratelimit_reload_test.go | 327 ++++++++++++++++++ internal/cmd/commands/server/server.go | 13 + internal/daemon/controller/config.go | 6 + internal/daemon/controller/controller.go | 41 ++- internal/daemon/controller/handler.go | 2 +- internal/ratelimit/config.go | 6 + internal/ratelimit/handler.go | 10 +- internal/ratelimit/handler_test.go | 4 +- 8 files changed, 401 insertions(+), 8 deletions(-) create mode 100644 internal/cmd/commands/server/controller_ratelimit_reload_test.go diff --git a/internal/cmd/commands/server/controller_ratelimit_reload_test.go b/internal/cmd/commands/server/controller_ratelimit_reload_test.go new file mode 100644 index 000000000..61f4c7967 --- /dev/null +++ b/internal/cmd/commands/server/controller_ratelimit_reload_test.go @@ -0,0 +1,327 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package server + +import ( + "fmt" + "net/http" + "os" + "sync" + "testing" + "time" + + "github.com/hashicorp/boundary/internal/cmd/config" + "github.com/mitchellh/cli" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + ratelimitConfig = ` +disable_mlock = true + +telemetry { + prometheus_retention_time = "24h" + disable_hostname = true +} + +controller { + name = "test-controller" + description = "A default controller created for tests" + database { + url = "%s" + } + + api_rate_limit { + resources = ["*"] + actions = ["*"] + per = "total" + limit = 2 + period = "1m" + } +} + +kms "aead" { + purpose = "root" + aead_type = "aes-gcm" + key = "%s" + key_id = "global_root" +} + +kms "aead" { + purpose = "worker-auth" + aead_type = "aes-gcm" + key = "%s" + key_id = "global_worker-auth" +} + +kms "aead" { + purpose = "recovery" + aead_type = "aes-gcm" + key = "%s" + key_id = "global_recovery" +} + +listener "tcp" { + purpose = "api" + address = "127.0.0.1:9500" + tls_disable = true +} + +listener "tcp" { + address = "127.0.0.1:9501" + purpose = "cluster" +} +` + + ratelimitConfigReload = ` +disable_mlock = true + +telemetry { + prometheus_retention_time = "24h" + disable_hostname = true +} + +controller { + name = "test-controller" + description = "A default controller created for tests" + database { + url = "%s" + } + + api_rate_limit { + resources = ["*"] + actions = ["*"] + per = "total" + limit = 5 + period = "1m" + } +} + +kms "aead" { + purpose = "root" + aead_type = "aes-gcm" + key = "%s" + key_id = "global_root" +} + +kms "aead" { + purpose = "worker-auth" + aead_type = "aes-gcm" + key = "%s" + key_id = "global_worker-auth" +} + +kms "aead" { + purpose = "recovery" + aead_type = "aes-gcm" + key = "%s" + key_id = "global_recovery" +} + +listener "tcp" { + purpose = "api" + address = "127.0.0.1:9500" + tls_disable = true +} + +listener "tcp" { + address = "127.0.0.1:9501" + purpose = "cluster" +} +` +) + +func TestRealodControllerRateLimits(t *testing.T) { + td := t.TempDir() + + controllerKey := config.DevKeyGeneration() + + closeDB, url, _, err := getInitDatabase(t, controllerKey) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, closeDB()) }) + + cmd := testServerCommand(t, testServerCommandOpts{}) + + workerAuthKey := config.DevKeyGeneration() + recoveryKey := config.DevKeyGeneration() + cfgHcl := fmt.Sprintf(ratelimitConfig, url, controllerKey, workerAuthKey, recoveryKey) + require.NoError(t, os.WriteFile(td+"/config.hcl", []byte(cfgHcl), 0o644)) + + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + + args := []string{"-config", td + "/config.hcl"} + exitCode := cmd.Run(args) + if exitCode != 0 { + output := cmd.UI.(*cli.MockUi).ErrorWriter.String() + cmd.UI.(*cli.MockUi).OutputWriter.String() + t.Errorf("got a non-zero exit status: %s", output) + } + }() + + // Wait until things are up and running (or timeout). + select { + case <-cmd.startedCh: + case <-time.After(15 * time.Second): + t.Fatal("timeout") + } + + // Change config so it is read for reloading + cfgHcl = fmt.Sprintf(ratelimitConfigReload, url, controllerKey, workerAuthKey, recoveryKey) + require.NoError(t, os.WriteFile(td+"/config.hcl", []byte(cfgHcl), 0o644)) + + c := http.Client{} + r, err := c.Do(func() *http.Request { + r, err := http.NewRequest(http.MethodGet, `http://127.0.0.1:9500/v1/targets`, nil) + require.NoError(t, err) + return r + }()) + require.NoError(t, err) + // unauthed request, so we expect a 400 + assert.Equal(t, http.StatusBadRequest, r.StatusCode) + assert.Equal(t, `limit=2, remaining=1, reset=59`, r.Header.Get("Ratelimit")) + assert.Equal(t, `2;w=60;comment="total", 60;w=30;comment="ip-address", 60;w=30;comment="auth-token"`, r.Header.Get("Ratelimit-Policy")) + + r, err = c.Do(func() *http.Request { + r, err := http.NewRequest(http.MethodGet, `http://127.0.0.1:9500/v1/targets`, nil) + require.NoError(t, err) + return r + }()) + require.NoError(t, err) + // unauthed request, so we expect a 400 + assert.Equal(t, http.StatusBadRequest, r.StatusCode) + assert.Equal(t, `limit=2, remaining=0, reset=59`, r.Header.Get("Ratelimit")) + assert.Equal(t, `2;w=60;comment="total", 60;w=30;comment="ip-address", 60;w=30;comment="auth-token"`, r.Header.Get("Ratelimit-Policy")) + + r, err = c.Do(func() *http.Request { + r, err := http.NewRequest(http.MethodGet, `http://127.0.0.1:9500/v1/targets`, nil) + require.NoError(t, err) + return r + }()) + require.NoError(t, err) + // out of quota, so we expect a 429 + assert.Equal(t, http.StatusTooManyRequests, r.StatusCode) + assert.Equal(t, `limit=2, remaining=0, reset=59`, r.Header.Get("Ratelimit")) + assert.Equal(t, `2;w=60;comment="total", 60;w=30;comment="ip-address", 60;w=30;comment="auth-token"`, r.Header.Get("Ratelimit-Policy")) + + cmd.SighupCh <- struct{}{} + select { + case <-cmd.reloadedCh: + case <-time.After(15 * time.Second): + t.Fatal("timeout") + } + + // Make another request, the limit should have reset and the new limit + // should get reported via the headers. + r, err = c.Do(func() *http.Request { + r, err := http.NewRequest(http.MethodGet, `http://127.0.0.1:9500/v1/targets`, nil) + require.NoError(t, err) + return r + }()) + require.NoError(t, err) + // unauthed request, so we expect a 400 + assert.Equal(t, http.StatusBadRequest, r.StatusCode) + assert.Equal(t, `limit=5, remaining=4, reset=59`, r.Header.Get("Ratelimit")) + assert.Equal(t, `5;w=60;comment="total", 60;w=30;comment="ip-address", 60;w=30;comment="auth-token"`, r.Header.Get("Ratelimit-Policy")) + + cmd.ShutdownCh <- struct{}{} + wg.Wait() +} + +func TestRealodControllerRateLimitsSameConfig(t *testing.T) { + td := t.TempDir() + + // Create and migrate database A and B. + controllerKey := config.DevKeyGeneration() + + closeDB, url, _, err := getInitDatabase(t, controllerKey) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, closeDB()) }) + + cmd := testServerCommand(t, testServerCommandOpts{}) + + workerAuthKey := config.DevKeyGeneration() + recoveryKey := config.DevKeyGeneration() + cfgHcl := fmt.Sprintf(ratelimitConfig, url, controllerKey, workerAuthKey, recoveryKey) + require.NoError(t, os.WriteFile(td+"/config.hcl", []byte(cfgHcl), 0o644)) + + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + + args := []string{"-config", td + "/config.hcl"} + exitCode := cmd.Run(args) + if exitCode != 0 { + output := cmd.UI.(*cli.MockUi).ErrorWriter.String() + cmd.UI.(*cli.MockUi).OutputWriter.String() + t.Errorf("got a non-zero exit status: %s", output) + } + }() + + // Wait until things are up and running (or timeout). + select { + case <-cmd.startedCh: + case <-time.After(15 * time.Second): + t.Fatal("timeout") + } + + c := http.Client{} + r, err := c.Do(func() *http.Request { + r, err := http.NewRequest(http.MethodGet, `http://127.0.0.1:9500/v1/targets`, nil) + require.NoError(t, err) + return r + }()) + require.NoError(t, err) + // unauthed request, so we expect a 400 + assert.Equal(t, http.StatusBadRequest, r.StatusCode) + assert.Equal(t, `limit=2, remaining=1, reset=59`, r.Header.Get("Ratelimit")) + assert.Equal(t, `2;w=60;comment="total", 60;w=30;comment="ip-address", 60;w=30;comment="auth-token"`, r.Header.Get("Ratelimit-Policy")) + + r, err = c.Do(func() *http.Request { + r, err := http.NewRequest(http.MethodGet, `http://127.0.0.1:9500/v1/targets`, nil) + require.NoError(t, err) + return r + }()) + require.NoError(t, err) + // unauthed request, so we expect a 400 + assert.Equal(t, http.StatusBadRequest, r.StatusCode) + assert.Equal(t, `limit=2, remaining=0, reset=59`, r.Header.Get("Ratelimit")) + assert.Equal(t, `2;w=60;comment="total", 60;w=30;comment="ip-address", 60;w=30;comment="auth-token"`, r.Header.Get("Ratelimit-Policy")) + + r, err = c.Do(func() *http.Request { + r, err := http.NewRequest(http.MethodGet, `http://127.0.0.1:9500/v1/targets`, nil) + require.NoError(t, err) + return r + }()) + require.NoError(t, err) + // out of quota, so we expect a 429 + assert.Equal(t, http.StatusTooManyRequests, r.StatusCode) + assert.Equal(t, `limit=2, remaining=0, reset=59`, r.Header.Get("Ratelimit")) + assert.Equal(t, `2;w=60;comment="total", 60;w=30;comment="ip-address", 60;w=30;comment="auth-token"`, r.Header.Get("Ratelimit-Policy")) + + cmd.SighupCh <- struct{}{} + select { + case <-cmd.reloadedCh: + case <-time.After(15 * time.Second): + t.Fatal("timeout") + } + + // Make another request, the limit should not have reset, and the current + // quota should still apply. + r, err = c.Do(func() *http.Request { + r, err := http.NewRequest(http.MethodGet, `http://127.0.0.1:9500/v1/targets`, nil) + require.NoError(t, err) + return r + }()) + require.NoError(t, err) + // should still be rate limited, so 429 + assert.Equal(t, http.StatusTooManyRequests, r.StatusCode) + assert.Equal(t, `limit=2, remaining=0, reset=59`, r.Header.Get("Ratelimit")) + assert.Equal(t, `2;w=60;comment="total", 60;w=30;comment="ip-address", 60;w=30;comment="auth-token"`, r.Header.Get("Ratelimit-Policy")) + + cmd.ShutdownCh <- struct{}{} + wg.Wait() +} diff --git a/internal/cmd/commands/server/server.go b/internal/cmd/commands/server/server.go index 19187e993..dec946830 100644 --- a/internal/cmd/commands/server/server.go +++ b/internal/cmd/commands/server/server.go @@ -622,6 +622,8 @@ func (c *Command) StartController(ctx context.Context) error { RawConfig: c.Config, Server: c.Server, TestOverrideWorkerAuthCaCertificateLifetime: c.flagWorkerAuthCaCertificateLifetime, + ApiRateLimits: c.Config.Controller.ApiRateLimits, + ApiRateLimiterMaxEntries: c.Config.Controller.ApiRateLimiterMaxEntries, } var err error @@ -841,6 +843,10 @@ func (c *Command) Reload(newConf *config.Config) error { reloadErrors = stderrors.Join(reloadErrors, fmt.Errorf("failed to reload controller database: %w", err)) } + if err := c.reloadControllerRateLimits(newConf); err != nil { + reloadErrors = stderrors.Join(reloadErrors, fmt.Errorf("failed to reload controller api rate limits: %w", err)) + } + if newConf != nil && c.worker != nil { workerReloadErr := func() error { if newConf.Controller != nil { @@ -952,6 +958,13 @@ func (c *Command) reloadControllerDatabase(newConfig *config.Config) error { return nil } +func (c *Command) reloadControllerRateLimits(newConfig *config.Config) error { + if c.controller == nil || newConfig == nil || newConfig.Controller == nil { + return nil + } + return c.controller.ReloadRateLimiter(newConfig.Controller.ApiRateLimits, newConfig.Controller.ApiRateLimiterMaxEntries) +} + // acquireSchemaManager returns a schema manager and generally acquires a shared lock on // the database. This is done as a mechanism to disallow running migration commands // while the database is in use. diff --git a/internal/daemon/controller/config.go b/internal/daemon/controller/config.go index 2e742f4a2..915ea5099 100644 --- a/internal/daemon/controller/config.go +++ b/internal/daemon/controller/config.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/boundary/internal/cmd/base" "github.com/hashicorp/boundary/internal/cmd/config" + "github.com/hashicorp/boundary/internal/ratelimit" ) type Config struct { @@ -21,4 +22,9 @@ type Config struct { DisableAuthorizationFailures bool // Override worker auth CA certificate lifetime for testing TestOverrideWorkerAuthCaCertificateLifetime time.Duration + + // These are duplicated from the config.Config to track the state if these + // change via SIGHUP reloads. + ApiRateLimits ratelimit.Configs + ApiRateLimiterMaxEntries int } diff --git a/internal/daemon/controller/controller.go b/internal/daemon/controller/controller.go index ce25afa11..f6d6c2802 100644 --- a/internal/daemon/controller/controller.go +++ b/internal/daemon/controller/controller.go @@ -35,6 +35,7 @@ import ( kmsjob "github.com/hashicorp/boundary/internal/kms/job" "github.com/hashicorp/boundary/internal/plugin" "github.com/hashicorp/boundary/internal/plugin/loopback" + "github.com/hashicorp/boundary/internal/ratelimit" "github.com/hashicorp/boundary/internal/scheduler" "github.com/hashicorp/boundary/internal/scheduler/cleaner" "github.com/hashicorp/boundary/internal/scheduler/job" @@ -120,7 +121,7 @@ type Controller struct { apiGrpcServerListener grpcServerListener apiGrpcGatewayTicket string - rateLimiter *rate.Limiter + rateLimiter *atomic.Pointer[rate.Limiter] // Repo factory methods AuthTokenRepoFn common.AuthTokenRepoFactory @@ -172,6 +173,7 @@ func New(ctx context.Context, conf *Config) (*Controller, error) { pkiConnManager: cluster.NewDownstreamManager(), workerStatusGracePeriod: new(atomic.Int64), livenessTimeToStale: new(atomic.Int64), + rateLimiter: new(atomic.Pointer[rate.Limiter]), } if downstreamReceiverFactory != nil { @@ -253,10 +255,11 @@ func New(ctx context.Context, conf *Config) (*Controller, error) { return nil, fmt.Errorf("error parsing rate limit configuration: %w", err) } - c.rateLimiter, err = rate.NewLimiter(rateLimits, conf.RawConfig.Controller.ApiRateLimiterMaxEntries) + rateLimiter, err := rate.NewLimiter(rateLimits, conf.RawConfig.Controller.ApiRateLimiterMaxEntries) if err != nil { return nil, fmt.Errorf("error initializing rate limiter: %w", err) } + c.rateLimiter.Store(rateLimiter) var pluginLogger hclog.Logger for _, enabledPlugin := range c.enabledPlugins { @@ -631,3 +634,37 @@ func (c *Controller) Shutdown() error { func (c *Controller) WorkerStatusUpdateTimes() *sync.Map { return c.workerStatusUpdateTimes } + +func (c *Controller) getRateLimiter() *rate.Limiter { + return c.rateLimiter.Load() +} + +// ReloadRateLimiter replaces the Controller's rate.Limiter with a new rate.Limiter +// using the supplied ratelimit.Configs and max entries. If the configs and max +// entries match the current values, the rate.Limiter is not replaced and no +// error is returned. Otherwise the rate.Limiter is replaced, and the old rate.Limiter +// is shutdown. This means that a client's quota is effectively reset if the +// configuration changes. +func (c *Controller) ReloadRateLimiter(newLimitConfigs ratelimit.Configs, newMaxEntries int) error { + // Config has not changed, no need to reload. + if c.conf.ApiRateLimits.Equal(newLimitConfigs) && c.conf.ApiRateLimiterMaxEntries == newMaxEntries { + return nil + } + + ratelimits, err := newLimitConfigs.Limits(c.baseContext) + if err != nil { + return fmt.Errorf("error parsing rate limit configuration: %w", err) + } + + limiter, err := rate.NewLimiter(ratelimits, newMaxEntries) + if err != nil { + return err + } + + old := c.rateLimiter.Swap(limiter) + + c.conf.ApiRateLimits = newLimitConfigs + c.conf.ApiRateLimiterMaxEntries = newMaxEntries + + return old.Shutdown() +} diff --git a/internal/daemon/controller/handler.go b/internal/daemon/controller/handler.go index d6f1dbdb4..7ef1d4a08 100644 --- a/internal/daemon/controller/handler.go +++ b/internal/daemon/controller/handler.go @@ -74,7 +74,7 @@ func createMuxWithEndpoints(c *Controller, props HandlerProperties) (http.Handle } mux := http.NewServeMux() - mux.Handle("/v1/", ratelimit.Handler(c.baseContext, c.rateLimiter, grpcGwMux)) + mux.Handle("/v1/", ratelimit.Handler(c.baseContext, c.getRateLimiter, grpcGwMux)) mux.Handle(uiPath, handleUi(c)) isUiRequest := func(req *http.Request) bool { diff --git a/internal/ratelimit/config.go b/internal/ratelimit/config.go index a4ea23138..ade0571fe 100644 --- a/internal/ratelimit/config.go +++ b/internal/ratelimit/config.go @@ -6,6 +6,7 @@ package ratelimit import ( "context" "fmt" + "reflect" "sync" "time" @@ -108,6 +109,11 @@ type Config struct { // 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) { diff --git a/internal/ratelimit/handler.go b/internal/ratelimit/handler.go index 02c766812..5498fbaba 100644 --- a/internal/ratelimit/handler.go +++ b/internal/ratelimit/handler.go @@ -112,14 +112,18 @@ func extractResourceAction(path, method string) (res, act string, err error) { return } +// LimiterFunc returns a rate.Limiter +type LimiterFunc func() *rate.Limiter + // Handler is an http middleware handler that checks if a request is allowed -// using the provided rate limiter. If the request is allowed, the next handler +// using the rate limiter returned by f. If the request is allowed, the next handler // is called. Otherwise a 429 is returned with the Retry-After response header // set to the number of seconds the client should wait to make it's next request. -func Handler(ctx context.Context, l *rate.Limiter, next http.Handler) http.Handler { +func Handler(ctx context.Context, f LimiterFunc, next http.Handler) http.Handler { const op = "ratelimit.Handler" - return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + l := f() + reqInfo, ok := event.RequestInfoFromContext(req.Context()) if !ok || reqInfo == nil || reqInfo.ClientIp == "" { rw.WriteHeader(http.StatusInternalServerError) diff --git a/internal/ratelimit/handler_test.go b/internal/ratelimit/handler_test.go index 9bd0749bc..fd4883487 100644 --- a/internal/ratelimit/handler_test.go +++ b/internal/ratelimit/handler_test.go @@ -747,7 +747,7 @@ func TestHandler(t *testing.T) { next.ServeHTTP(rw, req) }) - }(ratelimit.Handler(ctx, tc.limiter, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + }(ratelimit.Handler(ctx, func() *rate.Limiter { return tc.limiter }, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) return }))), @@ -850,7 +850,7 @@ func TestHandlerErrors(t *testing.T) { next.ServeHTTP(rw, req) }) - }(ratelimit.Handler(ctx, r, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + }(ratelimit.Handler(ctx, func() *rate.Limiter { return r }, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) return }))), From 3c2bed92b03ddb12925eb1c685ea0f03300c9ecc Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Wed, 29 Nov 2023 14:17:16 +0000 Subject: [PATCH 17/26] feat(ratelimit): Expose metrics on quota usage This provides two prometheus metrics to monitor the rate limiter: boundary_controller_api_ratelimiter_quota_storage_capacity: This is a gague that shows the total number of quotas that can be stored at a given moment by the rate limiter. boundary_controller_api_ratelimiter_quota_storage_usage: This is a gague that shows the number of quotas currently being stored by the limiter. If this becomes equal to the capacity, requests that would result in a new quota being created will get limited and receive a 429 status code. --- internal/daemon/controller/controller.go | 8 +++-- internal/ratelimit/limiter.go | 16 +++++++++ internal/ratelimit/metrics.go | 43 ++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 internal/ratelimit/limiter.go create mode 100644 internal/ratelimit/metrics.go diff --git a/internal/daemon/controller/controller.go b/internal/daemon/controller/controller.go index f6d6c2802..ab66136b9 100644 --- a/internal/daemon/controller/controller.go +++ b/internal/daemon/controller/controller.go @@ -160,6 +160,7 @@ type Controller struct { func New(ctx context.Context, conf *Config) (*Controller, error) { const op = "controller.New" metric.InitializeApiCollectors(conf.PrometheusRegisterer) + ratelimit.InitializeMetrics(conf.PrometheusRegisterer) c := &Controller{ conf: conf, logger: conf.Logger.Named("controller"), @@ -255,7 +256,10 @@ func New(ctx context.Context, conf *Config) (*Controller, error) { return nil, fmt.Errorf("error parsing rate limit configuration: %w", err) } - rateLimiter, err := rate.NewLimiter(rateLimits, conf.RawConfig.Controller.ApiRateLimiterMaxEntries) + rateLimiter, err := ratelimit.NewLimiter( + rateLimits, + conf.RawConfig.Controller.ApiRateLimiterMaxEntries, + ) if err != nil { return nil, fmt.Errorf("error initializing rate limiter: %w", err) } @@ -656,7 +660,7 @@ func (c *Controller) ReloadRateLimiter(newLimitConfigs ratelimit.Configs, newMax return fmt.Errorf("error parsing rate limit configuration: %w", err) } - limiter, err := rate.NewLimiter(ratelimits, newMaxEntries) + limiter, err := ratelimit.NewLimiter(ratelimits, newMaxEntries) if err != nil { return err } diff --git a/internal/ratelimit/limiter.go b/internal/ratelimit/limiter.go new file mode 100644 index 000000000..069df5b4c --- /dev/null +++ b/internal/ratelimit/limiter.go @@ -0,0 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package ratelimit + +import "github.com/hashicorp/go-rate" + +// NewLimiter creates a rate.Limiter. +func NewLimiter(limits []*rate.Limit, maxEntries int) (*rate.Limiter, error) { + return rate.NewLimiter( + limits, + maxEntries, + rate.WithQuotaStorageUsageMetric(rateLimitQuotaUsage), + rate.WithQuotaStorageCapacityMetric(rateLimitQuotaStorageCapacity), + ) +} diff --git a/internal/ratelimit/metrics.go b/internal/ratelimit/metrics.go new file mode 100644 index 000000000..63c3aa131 --- /dev/null +++ b/internal/ratelimit/metrics.go @@ -0,0 +1,43 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package ratelimit + +import ( + "github.com/hashicorp/boundary/globals" + "github.com/prometheus/client_golang/prometheus" +) + +const ( + subsystem = "controller_api_ratelimiter" +) + +var ( + rateLimitQuotaStorageCapacity prometheus.Gauge = prometheus.NewGauge( + prometheus.GaugeOpts{ + Namespace: globals.MetricNamespace, + Subsystem: subsystem, + Name: "quota_storage_capacity", + Help: "Guague of the number if quotas that can be stored by the rate limiter", + }, + ) + rateLimitQuotaUsage prometheus.Gauge = prometheus.NewGauge( + prometheus.GaugeOpts{ + Namespace: globals.MetricNamespace, + Subsystem: subsystem, + Name: "quota_storage_usage", + Help: "Guague of the number if quotas that are currently being stored by the rate limiter", + }, + ) +) + +// InitializeMetrics initializes the metrics for visibility into the rate limiter. +func InitializeMetrics(r prometheus.Registerer) { + if r == nil { + return + } + r.MustRegister( + rateLimitQuotaStorageCapacity, + rateLimitQuotaUsage, + ) +} From d0c3da5f1d352f3476d6d6886d002611b62a8ace Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Fri, 1 Dec 2023 22:01:49 +0000 Subject: [PATCH 18/26] refact(ratelimiter): Updates due to refactor in go-rate --- go.mod | 2 +- go.sum | 4 +- internal/ratelimit/config.go | 68 ++++++----- internal/ratelimit/config_test.go | 183 +++++++++++----------------- internal/ratelimit/handler.go | 2 +- internal/ratelimit/handler_test.go | 185 +++++++++++------------------ internal/ratelimit/limiter.go | 2 +- 7 files changed, 180 insertions(+), 266 deletions(-) diff --git a/go.mod b/go.mod index d5ce6854b..2306fad94 100644 --- a/go.mod +++ b/go.mod @@ -94,7 +94,7 @@ require ( github.com/hashicorp/cap/ldap v0.0.0-20231012003312-273118a6e3b8 github.com/hashicorp/dbassert v0.0.0-20231012105025-1bc1bd88e22b github.com/hashicorp/go-kms-wrapping/extras/kms/v2 v2.0.0-20231124110655-4315424d22d0 - github.com/hashicorp/go-rate v0.0.0-20231129140553-d912bfc00ee2 + github.com/hashicorp/go-rate v0.0.0-20231130213406-dd41f530f095 github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/nodeenrollment v0.2.9 github.com/jackc/pgx/v5 v5.4.3 diff --git a/go.sum b/go.sum index c04601408..a7fcad51d 100644 --- a/go.sum +++ b/go.sum @@ -260,8 +260,8 @@ github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9 github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= github.com/hashicorp/go-plugin v1.5.2 h1:aWv8eimFqWlsEiMrYZdPYl+FdHaBJSN4AWwGWfT1G2Y= github.com/hashicorp/go-plugin v1.5.2/go.mod h1:w1sAEES3g3PuV/RzUrgow20W2uErMly84hhD3um1WL4= -github.com/hashicorp/go-rate v0.0.0-20231129140553-d912bfc00ee2 h1:vNHBERW+GdaqHG0a+McDV/ExpdFshheEzie41wb8nts= -github.com/hashicorp/go-rate v0.0.0-20231129140553-d912bfc00ee2/go.mod h1:uTQC66EFZuSKunHYx0hHIfdH3nhRrv7UOOE8Ug0yI0A= +github.com/hashicorp/go-rate v0.0.0-20231130213406-dd41f530f095 h1:l/scdJ8Uy823fhxFEdwlDR5NkDRdoOOaH6g36Uqz1AM= +github.com/hashicorp/go-rate v0.0.0-20231130213406-dd41f530f095/go.mod h1:uTQC66EFZuSKunHYx0hHIfdH3nhRrv7UOOE8Ug0yI0A= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZnpUv3/+BxzFA= github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= diff --git a/internal/ratelimit/config.go b/internal/ratelimit/config.go index ade0571fe..9162d5c2f 100644 --- a/internal/ratelimit/config.go +++ b/internal/ratelimit/config.go @@ -116,10 +116,10 @@ func (c Configs) Equal(o Configs) bool { // 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) { +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)) + 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 { @@ -145,52 +145,46 @@ func (c Configs) Limits(ctx context.Context) ([]*rate.Limit, error) { ipAddressKey := fmt.Sprintf("%s:%s:%s", res.String(), a.String(), rate.LimitPerIPAddress) switch a { case action.List: - defaults[inTotalKey] = &rate.Limit{ + defaults[inTotalKey] = &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerTotal, - Unlimited: false, MaxRequests: DefaultInTotalListRequestLimit, Period: DefaultListPeriod, } - defaults[authTokenKey] = &rate.Limit{ + defaults[authTokenKey] = &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerAuthToken, - Unlimited: false, MaxRequests: DefaultAuthTokenListRequestLimit, Period: DefaultListPeriod, } - defaults[ipAddressKey] = &rate.Limit{ + defaults[ipAddressKey] = &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerIPAddress, - Unlimited: false, MaxRequests: DefaultIpAddressListRequestLimit, Period: DefaultListPeriod, } default: - defaults[inTotalKey] = &rate.Limit{ + defaults[inTotalKey] = &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerTotal, - Unlimited: false, MaxRequests: DefaultInTotalRequestLimit, Period: DefaultPeriod, } - defaults[authTokenKey] = &rate.Limit{ + defaults[authTokenKey] = &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerAuthToken, - Unlimited: false, MaxRequests: DefaultAuthTokenRequestLimit, Period: DefaultPeriod, } - defaults[ipAddressKey] = &rate.Limit{ + defaults[ipAddressKey] = &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerIPAddress, - Unlimited: false, MaxRequests: DefaultIpAddressRequestLimit, Period: DefaultPeriod, } @@ -223,13 +217,21 @@ func (c Configs) Limits(ctx context.Context) ([]*rate.Limit, error) { for a := range validActions { key := fmt.Sprintf("%s:%s:%s", res.String(), a.String(), rate.LimitPer(cc.Per)) - defaults[key] = &rate.Limit{ - Resource: res.String(), - Action: a.String(), - Per: rate.LimitPer(cc.Per), - Unlimited: cc.Unlimited, - MaxRequests: uint64(cc.Limit), - Period: cc.Period, + 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, + } } } } @@ -251,20 +253,28 @@ func (c Configs) Limits(ctx context.Context) ([]*rate.Limit, error) { } key := fmt.Sprintf("%s:%s:%s", res.String(), a.String(), rate.LimitPer(cc.Per)) - defaults[key] = &rate.Limit{ - Resource: res.String(), - Action: a.String(), - Per: rate.LimitPer(cc.Per), - Unlimited: cc.Unlimited, - MaxRequests: uint64(cc.Limit), - Period: cc.Period, + 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)) + limits := make([]rate.Limit, 0, len(defaults)) for _, v := range defaults { limits = append(limits, v) } diff --git a/internal/ratelimit/config_test.go b/internal/ratelimit/config_test.go index 6c0d83193..54c4f20b3 100644 --- a/internal/ratelimit/config_test.go +++ b/internal/ratelimit/config_test.go @@ -21,14 +21,14 @@ func TestConfigsLimits(t *testing.T) { cases := []struct { name string configs Configs - want []*rate.Limit + want []rate.Limit wantErr error }{ { "empty", Configs{}, - func() []*rate.Limit { - limits := make([]*rate.Limit, 0, len(resource.Map)*len(action.Map)) + func() []rate.Limit { + limits := make([]rate.Limit, 0, len(resource.Map)*len(action.Map)) for _, res := range resource.Map { switch res { case resource.Unknown, resource.All, resource.Controller: @@ -43,27 +43,24 @@ func TestConfigsLimits(t *testing.T) { case action.List: limits = append( limits, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerTotal, - Unlimited: false, MaxRequests: DefaultInTotalListRequestLimit, Period: DefaultListPeriod, }, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerIPAddress, - Unlimited: false, MaxRequests: DefaultIpAddressListRequestLimit, Period: DefaultListPeriod, }, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerAuthToken, - Unlimited: false, MaxRequests: DefaultAuthTokenListRequestLimit, Period: DefaultListPeriod, }, @@ -71,27 +68,24 @@ func TestConfigsLimits(t *testing.T) { default: limits = append( limits, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerTotal, - Unlimited: false, MaxRequests: DefaultInTotalRequestLimit, Period: DefaultPeriod, }, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerIPAddress, - Unlimited: false, MaxRequests: DefaultIpAddressRequestLimit, Period: DefaultPeriod, }, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerAuthToken, - Unlimited: false, MaxRequests: DefaultAuthTokenRequestLimit, Period: DefaultPeriod, }, @@ -128,8 +122,8 @@ func TestConfigsLimits(t *testing.T) { Period: time.Minute * 5, }, }, - func() []*rate.Limit { - limits := make([]*rate.Limit, 0, len(resource.Map)*len(action.Map)) + func() []rate.Limit { + limits := make([]rate.Limit, 0, len(resource.Map)*len(action.Map)) for _, res := range resource.Map { switch res { case resource.Unknown, resource.All, resource.Controller: @@ -144,27 +138,24 @@ func TestConfigsLimits(t *testing.T) { default: limits = append( limits, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerTotal, - Unlimited: false, MaxRequests: 20, Period: time.Minute * 5, }, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerIPAddress, - Unlimited: false, MaxRequests: 10, Period: time.Minute * 5, }, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerAuthToken, - Unlimited: false, MaxRequests: 5, Period: time.Minute * 5, }, @@ -222,8 +213,8 @@ func TestConfigsLimits(t *testing.T) { Period: time.Minute, }, }, - func() []*rate.Limit { - limits := make([]*rate.Limit, 0, len(resource.Map)*len(action.Map)) + func() []rate.Limit { + limits := make([]rate.Limit, 0, len(resource.Map)*len(action.Map)) for _, res := range resource.Map { switch res { case resource.Unknown, resource.All, resource.Controller: @@ -239,27 +230,24 @@ func TestConfigsLimits(t *testing.T) { if res == resource.Target { limits = append( limits, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerTotal, - Unlimited: false, MaxRequests: 10, Period: time.Minute, }, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerIPAddress, - Unlimited: false, MaxRequests: 5, Period: time.Minute, }, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerAuthToken, - Unlimited: false, MaxRequests: 1, Period: time.Minute, }, @@ -270,27 +258,24 @@ func TestConfigsLimits(t *testing.T) { default: limits = append( limits, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerTotal, - Unlimited: false, MaxRequests: 20, Period: time.Minute * 5, }, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerIPAddress, - Unlimited: false, MaxRequests: 20, Period: time.Minute * 5, }, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerAuthToken, - Unlimited: false, MaxRequests: 20, Period: time.Minute * 5, }, @@ -327,8 +312,8 @@ func TestConfigsLimits(t *testing.T) { Period: time.Minute, }, }, - func() []*rate.Limit { - limits := make([]*rate.Limit, 0, len(resource.Map)*len(action.Map)) + func() []rate.Limit { + limits := make([]rate.Limit, 0, len(resource.Map)*len(action.Map)) for _, res := range resource.Map { switch res { case resource.Unknown, resource.All, resource.Controller: @@ -343,27 +328,24 @@ func TestConfigsLimits(t *testing.T) { default: limits = append( limits, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerTotal, - Unlimited: false, MaxRequests: 10, Period: time.Minute, }, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerIPAddress, - Unlimited: false, MaxRequests: 5, Period: time.Minute, }, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerAuthToken, - Unlimited: false, MaxRequests: 1, Period: time.Minute, }, @@ -380,27 +362,24 @@ func TestConfigsLimits(t *testing.T) { case action.List: limits = append( limits, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerTotal, - Unlimited: false, MaxRequests: DefaultInTotalListRequestLimit, Period: DefaultListPeriod, }, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerIPAddress, - Unlimited: false, MaxRequests: DefaultIpAddressListRequestLimit, Period: DefaultListPeriod, }, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerAuthToken, - Unlimited: false, MaxRequests: DefaultAuthTokenListRequestLimit, Period: DefaultListPeriod, }, @@ -408,27 +387,24 @@ func TestConfigsLimits(t *testing.T) { default: limits = append( limits, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerTotal, - Unlimited: false, MaxRequests: DefaultInTotalRequestLimit, Period: DefaultPeriod, }, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerIPAddress, - Unlimited: false, MaxRequests: DefaultIpAddressRequestLimit, Period: DefaultPeriod, }, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerAuthToken, - Unlimited: false, MaxRequests: DefaultAuthTokenRequestLimit, Period: DefaultPeriod, }, @@ -466,8 +442,8 @@ func TestConfigsLimits(t *testing.T) { Period: time.Minute, }, }, - func() []*rate.Limit { - limits := make([]*rate.Limit, 0, len(resource.Map)*len(action.Map)) + func() []rate.Limit { + limits := make([]rate.Limit, 0, len(resource.Map)*len(action.Map)) for _, res := range resource.Map { switch res { case resource.Unknown, resource.All, resource.Controller: @@ -482,27 +458,24 @@ func TestConfigsLimits(t *testing.T) { case action.List, action.Read: limits = append( limits, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerTotal, - Unlimited: false, MaxRequests: 10, Period: time.Minute, }, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerIPAddress, - Unlimited: false, MaxRequests: 5, Period: time.Minute, }, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerAuthToken, - Unlimited: false, MaxRequests: 1, Period: time.Minute, }, @@ -510,27 +483,24 @@ func TestConfigsLimits(t *testing.T) { default: limits = append( limits, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerTotal, - Unlimited: false, MaxRequests: DefaultInTotalRequestLimit, Period: DefaultPeriod, }, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerIPAddress, - Unlimited: false, MaxRequests: DefaultIpAddressRequestLimit, Period: DefaultPeriod, }, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerAuthToken, - Unlimited: false, MaxRequests: DefaultAuthTokenRequestLimit, Period: DefaultPeriod, }, @@ -547,27 +517,24 @@ func TestConfigsLimits(t *testing.T) { case action.List: limits = append( limits, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerTotal, - Unlimited: false, MaxRequests: DefaultInTotalListRequestLimit, Period: DefaultListPeriod, }, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerIPAddress, - Unlimited: false, MaxRequests: DefaultIpAddressListRequestLimit, Period: DefaultListPeriod, }, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerAuthToken, - Unlimited: false, MaxRequests: DefaultAuthTokenListRequestLimit, Period: DefaultListPeriod, }, @@ -575,27 +542,24 @@ func TestConfigsLimits(t *testing.T) { default: limits = append( limits, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerTotal, - Unlimited: false, MaxRequests: DefaultInTotalRequestLimit, Period: DefaultPeriod, }, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerIPAddress, - Unlimited: false, MaxRequests: DefaultIpAddressRequestLimit, Period: DefaultPeriod, }, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerAuthToken, - Unlimited: false, MaxRequests: DefaultAuthTokenRequestLimit, Period: DefaultPeriod, }, @@ -627,8 +591,8 @@ func TestConfigsLimits(t *testing.T) { Period: time.Minute * 5, }, }, - func() []*rate.Limit { - limits := make([]*rate.Limit, 0, len(resource.Map)*len(action.Map)) + func() []rate.Limit { + limits := make([]rate.Limit, 0, len(resource.Map)*len(action.Map)) for _, res := range resource.Map { switch res { case resource.Unknown, resource.All, resource.Controller: @@ -643,27 +607,24 @@ func TestConfigsLimits(t *testing.T) { case action.List: limits = append( limits, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerTotal, - Unlimited: false, MaxRequests: 20, Period: time.Minute * 5, }, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerIPAddress, - Unlimited: false, MaxRequests: DefaultIpAddressListRequestLimit, Period: DefaultListPeriod, }, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerAuthToken, - Unlimited: false, MaxRequests: DefaultAuthTokenListRequestLimit, Period: DefaultListPeriod, }, @@ -671,27 +632,24 @@ func TestConfigsLimits(t *testing.T) { default: limits = append( limits, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerTotal, - Unlimited: false, MaxRequests: 20, Period: time.Minute * 5, }, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerIPAddress, - Unlimited: false, MaxRequests: DefaultIpAddressRequestLimit, Period: DefaultPeriod, }, - &rate.Limit{ + &rate.Limited{ Resource: res.String(), Action: a.String(), Per: rate.LimitPerAuthToken, - Unlimited: false, MaxRequests: DefaultAuthTokenRequestLimit, Period: DefaultPeriod, }, @@ -725,8 +683,8 @@ func TestConfigsLimits(t *testing.T) { Unlimited: true, }, }, - func() []*rate.Limit { - limits := make([]*rate.Limit, 0, len(resource.Map)*len(action.Map)) + func() []rate.Limit { + limits := make([]rate.Limit, 0, len(resource.Map)*len(action.Map)) for _, res := range resource.Map { switch res { case resource.Unknown, resource.All, resource.Controller: @@ -741,23 +699,20 @@ func TestConfigsLimits(t *testing.T) { default: limits = append( limits, - &rate.Limit{ - Resource: res.String(), - Action: a.String(), - Per: rate.LimitPerTotal, - Unlimited: true, + &rate.Unlimited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, }, - &rate.Limit{ - Resource: res.String(), - Action: a.String(), - Per: rate.LimitPerIPAddress, - Unlimited: true, + &rate.Unlimited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerIPAddress, }, - &rate.Limit{ - Resource: res.String(), - Action: a.String(), - Per: rate.LimitPerAuthToken, - Unlimited: true, + &rate.Unlimited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerAuthToken, }, ) } diff --git a/internal/ratelimit/handler.go b/internal/ratelimit/handler.go index 5498fbaba..626daa221 100644 --- a/internal/ratelimit/handler.go +++ b/internal/ratelimit/handler.go @@ -113,7 +113,7 @@ func extractResourceAction(path, method string) (res, act string, err error) { } // LimiterFunc returns a rate.Limiter -type LimiterFunc func() *rate.Limiter +type LimiterFunc func() Limiter // Handler is an http middleware handler that checks if a request is allowed // using the rate limiter returned by f. If the request is allowed, the next handler diff --git a/internal/ratelimit/handler_test.go b/internal/ratelimit/handler_test.go index fd4883487..dd30ad506 100644 --- a/internal/ratelimit/handler_test.go +++ b/internal/ratelimit/handler_test.go @@ -36,28 +36,25 @@ func TestHandler(t *testing.T) { { "AllowedList", func() *rate.Limiter { - r, err := rate.NewLimiter([]*rate.Limit{ - { + r, err := rate.NewLimiter([]rate.Limit{ + &rate.Limited{ Resource: resource.Target.String(), Action: action.List.String(), Per: rate.LimitPerTotal, - Unlimited: false, MaxRequests: 10, Period: time.Minute, }, - { + &rate.Limited{ Resource: resource.Target.String(), Action: action.List.String(), Per: rate.LimitPerIPAddress, - Unlimited: false, MaxRequests: 10, Period: time.Minute, }, - { + &rate.Limited{ Resource: resource.Target.String(), Action: action.List.String(), Per: rate.LimitPerAuthToken, - Unlimited: false, MaxRequests: 10, Period: time.Minute, }, @@ -81,28 +78,25 @@ func TestHandler(t *testing.T) { { "AllowedCreate", func() *rate.Limiter { - r, err := rate.NewLimiter([]*rate.Limit{ - { + r, err := rate.NewLimiter([]rate.Limit{ + &rate.Limited{ Resource: resource.Target.String(), Action: action.Create.String(), Per: rate.LimitPerTotal, - Unlimited: false, MaxRequests: 10, Period: time.Minute, }, - { + &rate.Limited{ Resource: resource.Target.String(), Action: action.Create.String(), Per: rate.LimitPerIPAddress, - Unlimited: false, MaxRequests: 10, Period: time.Minute, }, - { + &rate.Limited{ Resource: resource.Target.String(), Action: action.Create.String(), Per: rate.LimitPerAuthToken, - Unlimited: false, MaxRequests: 10, Period: time.Minute, }, @@ -126,28 +120,25 @@ func TestHandler(t *testing.T) { { "AllowedRead", func() *rate.Limiter { - r, err := rate.NewLimiter([]*rate.Limit{ - { + r, err := rate.NewLimiter([]rate.Limit{ + &rate.Limited{ Resource: resource.Target.String(), Action: action.Read.String(), Per: rate.LimitPerTotal, - Unlimited: false, MaxRequests: 10, Period: time.Minute, }, - { + &rate.Limited{ Resource: resource.Target.String(), Action: action.Read.String(), Per: rate.LimitPerIPAddress, - Unlimited: false, MaxRequests: 10, Period: time.Minute, }, - { + &rate.Limited{ Resource: resource.Target.String(), Action: action.Read.String(), Per: rate.LimitPerAuthToken, - Unlimited: false, MaxRequests: 10, Period: time.Minute, }, @@ -171,28 +162,25 @@ func TestHandler(t *testing.T) { { "AllowedUpdate", func() *rate.Limiter { - r, err := rate.NewLimiter([]*rate.Limit{ - { + r, err := rate.NewLimiter([]rate.Limit{ + &rate.Limited{ Resource: resource.Target.String(), Action: action.Update.String(), Per: rate.LimitPerTotal, - Unlimited: false, MaxRequests: 10, Period: time.Minute, }, - { + &rate.Limited{ Resource: resource.Target.String(), Action: action.Update.String(), Per: rate.LimitPerIPAddress, - Unlimited: false, MaxRequests: 10, Period: time.Minute, }, - { + &rate.Limited{ Resource: resource.Target.String(), Action: action.Update.String(), Per: rate.LimitPerAuthToken, - Unlimited: false, MaxRequests: 10, Period: time.Minute, }, @@ -216,28 +204,25 @@ func TestHandler(t *testing.T) { { "AllowedDelete", func() *rate.Limiter { - r, err := rate.NewLimiter([]*rate.Limit{ - { + r, err := rate.NewLimiter([]rate.Limit{ + &rate.Limited{ Resource: resource.Target.String(), Action: action.Delete.String(), Per: rate.LimitPerTotal, - Unlimited: false, MaxRequests: 10, Period: time.Minute, }, - { + &rate.Limited{ Resource: resource.Target.String(), Action: action.Delete.String(), Per: rate.LimitPerIPAddress, - Unlimited: false, MaxRequests: 10, Period: time.Minute, }, - { + &rate.Limited{ Resource: resource.Target.String(), Action: action.Delete.String(), Per: rate.LimitPerAuthToken, - Unlimited: false, MaxRequests: 10, Period: time.Minute, }, @@ -261,28 +246,25 @@ func TestHandler(t *testing.T) { { "AllowedAuthorizeSession", func() *rate.Limiter { - r, err := rate.NewLimiter([]*rate.Limit{ - { + r, err := rate.NewLimiter([]rate.Limit{ + &rate.Limited{ Resource: resource.Target.String(), Action: action.AuthorizeSession.String(), Per: rate.LimitPerTotal, - Unlimited: false, MaxRequests: 10, Period: time.Minute, }, - { + &rate.Limited{ Resource: resource.Target.String(), Action: action.AuthorizeSession.String(), Per: rate.LimitPerIPAddress, - Unlimited: false, MaxRequests: 10, Period: time.Minute, }, - { + &rate.Limited{ Resource: resource.Target.String(), Action: action.AuthorizeSession.String(), Per: rate.LimitPerAuthToken, - Unlimited: false, MaxRequests: 10, Period: time.Minute, }, @@ -306,28 +288,25 @@ func TestHandler(t *testing.T) { { "AllowedAuthorizeSessionTargetNameSlash", func() *rate.Limiter { - r, err := rate.NewLimiter([]*rate.Limit{ - { + r, err := rate.NewLimiter([]rate.Limit{ + &rate.Limited{ Resource: resource.Target.String(), Action: action.AuthorizeSession.String(), Per: rate.LimitPerTotal, - Unlimited: false, MaxRequests: 10, Period: time.Minute, }, - { + &rate.Limited{ Resource: resource.Target.String(), Action: action.AuthorizeSession.String(), Per: rate.LimitPerIPAddress, - Unlimited: false, MaxRequests: 10, Period: time.Minute, }, - { + &rate.Limited{ Resource: resource.Target.String(), Action: action.AuthorizeSession.String(), Per: rate.LimitPerAuthToken, - Unlimited: false, MaxRequests: 10, Period: time.Minute, }, @@ -351,28 +330,25 @@ func TestHandler(t *testing.T) { { "Limited", func() *rate.Limiter { - r, err := rate.NewLimiter([]*rate.Limit{ - { + r, err := rate.NewLimiter([]rate.Limit{ + &rate.Limited{ Resource: resource.Target.String(), Action: action.List.String(), Per: rate.LimitPerTotal, - Unlimited: false, MaxRequests: 2, Period: time.Minute, }, - { + &rate.Limited{ Resource: resource.Target.String(), Action: action.List.String(), Per: rate.LimitPerIPAddress, - Unlimited: false, MaxRequests: 2, Period: time.Minute, }, - { + &rate.Limited{ Resource: resource.Target.String(), Action: action.List.String(), Per: rate.LimitPerAuthToken, - Unlimited: false, MaxRequests: 2, Period: time.Minute, }, @@ -405,52 +381,46 @@ func TestHandler(t *testing.T) { { "LimiterFull", func() *rate.Limiter { - r, err := rate.NewLimiter([]*rate.Limit{ - { + r, err := rate.NewLimiter([]rate.Limit{ + &rate.Limited{ Resource: resource.Target.String(), Action: action.List.String(), Per: rate.LimitPerTotal, - Unlimited: false, MaxRequests: 2, Period: time.Minute, }, - { + &rate.Limited{ Resource: resource.Target.String(), Action: action.Read.String(), Per: rate.LimitPerTotal, - Unlimited: false, MaxRequests: 2, Period: time.Minute, }, - { + &rate.Limited{ Resource: resource.Target.String(), Action: action.List.String(), Per: rate.LimitPerIPAddress, - Unlimited: false, MaxRequests: 2, Period: time.Minute, }, - { + &rate.Limited{ Resource: resource.Target.String(), Action: action.Read.String(), Per: rate.LimitPerIPAddress, - Unlimited: false, MaxRequests: 2, Period: time.Minute, }, - { + &rate.Limited{ Resource: resource.Target.String(), Action: action.List.String(), Per: rate.LimitPerAuthToken, - Unlimited: false, MaxRequests: 2, Period: time.Minute, }, - { + &rate.Limited{ Resource: resource.Target.String(), Action: action.Read.String(), Per: rate.LimitPerAuthToken, - Unlimited: false, MaxRequests: 2, Period: time.Minute, }, @@ -476,28 +446,25 @@ func TestHandler(t *testing.T) { { "MissingLimit", func() *rate.Limiter { - r, err := rate.NewLimiter([]*rate.Limit{ - { + r, err := rate.NewLimiter([]rate.Limit{ + &rate.Limited{ Resource: resource.Target.String(), Action: action.List.String(), Per: rate.LimitPerTotal, - Unlimited: false, MaxRequests: 2, Period: time.Minute, }, - { + &rate.Limited{ Resource: resource.Target.String(), Action: action.List.String(), Per: rate.LimitPerIPAddress, - Unlimited: false, MaxRequests: 2, Period: time.Minute, }, - { + &rate.Limited{ Resource: resource.Target.String(), Action: action.List.String(), Per: rate.LimitPerAuthToken, - Unlimited: false, MaxRequests: 2, Period: time.Minute, }, @@ -518,28 +485,25 @@ func TestHandler(t *testing.T) { { "UnknownResource", func() *rate.Limiter { - r, err := rate.NewLimiter([]*rate.Limit{ - { + r, err := rate.NewLimiter([]rate.Limit{ + &rate.Limited{ Resource: resource.Target.String(), Action: action.List.String(), Per: rate.LimitPerTotal, - Unlimited: false, MaxRequests: 2, Period: time.Minute, }, - { + &rate.Limited{ Resource: resource.Target.String(), Action: action.List.String(), Per: rate.LimitPerIPAddress, - Unlimited: false, MaxRequests: 2, Period: time.Minute, }, - { + &rate.Limited{ Resource: resource.Target.String(), Action: action.List.String(), Per: rate.LimitPerAuthToken, - Unlimited: false, MaxRequests: 2, Period: time.Minute, }, @@ -560,28 +524,25 @@ func TestHandler(t *testing.T) { { "UnknownAction", func() *rate.Limiter { - r, err := rate.NewLimiter([]*rate.Limit{ - { + r, err := rate.NewLimiter([]rate.Limit{ + &rate.Limited{ Resource: resource.Target.String(), Action: action.List.String(), Per: rate.LimitPerTotal, - Unlimited: false, MaxRequests: 2, Period: time.Minute, }, - { + &rate.Limited{ Resource: resource.Target.String(), Action: action.List.String(), Per: rate.LimitPerIPAddress, - Unlimited: false, MaxRequests: 2, Period: time.Minute, }, - { + &rate.Limited{ Resource: resource.Target.String(), Action: action.List.String(), Per: rate.LimitPerAuthToken, - Unlimited: false, MaxRequests: 2, Period: time.Minute, }, @@ -602,28 +563,25 @@ func TestHandler(t *testing.T) { { "UnknownActionInvalidIdHttpMethod", func() *rate.Limiter { - r, err := rate.NewLimiter([]*rate.Limit{ - { + r, err := rate.NewLimiter([]rate.Limit{ + &rate.Limited{ Resource: resource.Target.String(), Action: action.List.String(), Per: rate.LimitPerTotal, - Unlimited: false, MaxRequests: 2, Period: time.Minute, }, - { + &rate.Limited{ Resource: resource.Target.String(), Action: action.List.String(), Per: rate.LimitPerIPAddress, - Unlimited: false, MaxRequests: 2, Period: time.Minute, }, - { + &rate.Limited{ Resource: resource.Target.String(), Action: action.List.String(), Per: rate.LimitPerAuthToken, - Unlimited: false, MaxRequests: 2, Period: time.Minute, }, @@ -644,28 +602,25 @@ func TestHandler(t *testing.T) { { "UnknownActionInvalidHttpMethod", func() *rate.Limiter { - r, err := rate.NewLimiter([]*rate.Limit{ - { + r, err := rate.NewLimiter([]rate.Limit{ + &rate.Limited{ Resource: resource.Target.String(), Action: action.List.String(), Per: rate.LimitPerTotal, - Unlimited: false, MaxRequests: 2, Period: time.Minute, }, - { + &rate.Limited{ Resource: resource.Target.String(), Action: action.List.String(), Per: rate.LimitPerIPAddress, - Unlimited: false, MaxRequests: 2, Period: time.Minute, }, - { + &rate.Limited{ Resource: resource.Target.String(), Action: action.List.String(), Per: rate.LimitPerAuthToken, - Unlimited: false, MaxRequests: 2, Period: time.Minute, }, @@ -686,28 +641,25 @@ func TestHandler(t *testing.T) { { "UnsupportedAction", func() *rate.Limiter { - r, err := rate.NewLimiter([]*rate.Limit{ - { + r, err := rate.NewLimiter([]rate.Limit{ + &rate.Limited{ Resource: resource.Target.String(), Action: action.List.String(), Per: rate.LimitPerTotal, - Unlimited: false, MaxRequests: 2, Period: time.Minute, }, - { + &rate.Limited{ Resource: resource.Target.String(), Action: action.List.String(), Per: rate.LimitPerIPAddress, - Unlimited: false, MaxRequests: 2, Period: time.Minute, }, - { + &rate.Limited{ Resource: resource.Target.String(), Action: action.List.String(), Per: rate.LimitPerAuthToken, - Unlimited: false, MaxRequests: 2, Period: time.Minute, }, @@ -815,28 +767,25 @@ func TestHandlerErrors(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - r, err := rate.NewLimiter([]*rate.Limit{ - { + r, err := rate.NewLimiter([]rate.Limit{ + &rate.Limited{ Resource: resource.Target.String(), Action: action.List.String(), Per: rate.LimitPerTotal, - Unlimited: false, MaxRequests: 10, Period: time.Minute, }, - { + &rate.Limited{ Resource: resource.Target.String(), Action: action.List.String(), Per: rate.LimitPerIPAddress, - Unlimited: false, MaxRequests: 10, Period: time.Minute, }, - { + &rate.Limited{ Resource: resource.Target.String(), Action: action.List.String(), Per: rate.LimitPerAuthToken, - Unlimited: false, MaxRequests: 10, Period: time.Minute, }, diff --git a/internal/ratelimit/limiter.go b/internal/ratelimit/limiter.go index 069df5b4c..1aec78ed5 100644 --- a/internal/ratelimit/limiter.go +++ b/internal/ratelimit/limiter.go @@ -6,7 +6,7 @@ package ratelimit import "github.com/hashicorp/go-rate" // NewLimiter creates a rate.Limiter. -func NewLimiter(limits []*rate.Limit, maxEntries int) (*rate.Limiter, error) { +func NewLimiter(limits []rate.Limit, maxEntries int) (*rate.Limiter, error) { return rate.NewLimiter( limits, maxEntries, From 9438be4506d498dcea0a567eb25882a7d63aabd0 Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Fri, 1 Dec 2023 22:14:49 +0000 Subject: [PATCH 19/26] feat(ratelimier): Support disabling rate limiting --- .../controller_ratelimit_reload_test.go | 250 ++++++++- internal/cmd/commands/server/server.go | 4 +- internal/cmd/config/config.go | 1 + internal/daemon/controller/config.go | 9 +- internal/daemon/controller/controller.go | 51 +- internal/daemon/controller/rate_limiter.go | 141 +++++ .../daemon/controller/rate_limiter_test.go | 481 ++++++++++++++++++ internal/ratelimit/handler_test.go | 4 +- internal/ratelimit/limiter.go | 15 +- 9 files changed, 896 insertions(+), 60 deletions(-) create mode 100644 internal/daemon/controller/rate_limiter.go create mode 100644 internal/daemon/controller/rate_limiter_test.go diff --git a/internal/cmd/commands/server/controller_ratelimit_reload_test.go b/internal/cmd/commands/server/controller_ratelimit_reload_test.go index 61f4c7967..24fd44bdc 100644 --- a/internal/cmd/commands/server/controller_ratelimit_reload_test.go +++ b/internal/cmd/commands/server/controller_ratelimit_reload_test.go @@ -126,6 +126,57 @@ listener "tcp" { tls_disable = true } +listener "tcp" { + address = "127.0.0.1:9501" + purpose = "cluster" +} +` + + ratelimitConfigDisabledReload = ` +disable_mlock = true + +telemetry { + prometheus_retention_time = "24h" + disable_hostname = true +} + +controller { + name = "test-controller" + description = "A default controller created for tests" + database { + url = "%s" + } + + api_rate_limit_disable = true +} + +kms "aead" { + purpose = "root" + aead_type = "aes-gcm" + key = "%s" + key_id = "global_root" +} + +kms "aead" { + purpose = "worker-auth" + aead_type = "aes-gcm" + key = "%s" + key_id = "global_worker-auth" +} + +kms "aead" { + purpose = "recovery" + aead_type = "aes-gcm" + key = "%s" + key_id = "global_recovery" +} + +listener "tcp" { + purpose = "api" + address = "127.0.0.1:9500" + tls_disable = true +} + listener "tcp" { address = "127.0.0.1:9501" purpose = "cluster" @@ -169,7 +220,7 @@ func TestRealodControllerRateLimits(t *testing.T) { t.Fatal("timeout") } - // Change config so it is read for reloading + // Change config so it is ready for reloading cfgHcl = fmt.Sprintf(ratelimitConfigReload, url, controllerKey, workerAuthKey, recoveryKey) require.NoError(t, os.WriteFile(td+"/config.hcl", []byte(cfgHcl), 0o644)) @@ -325,3 +376,200 @@ func TestRealodControllerRateLimitsSameConfig(t *testing.T) { cmd.ShutdownCh <- struct{}{} wg.Wait() } + +func TestRealodControllerRateLimitsDisable(t *testing.T) { + td := t.TempDir() + + controllerKey := config.DevKeyGeneration() + + closeDB, url, _, err := getInitDatabase(t, controllerKey) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, closeDB()) }) + + cmd := testServerCommand(t, testServerCommandOpts{}) + + workerAuthKey := config.DevKeyGeneration() + recoveryKey := config.DevKeyGeneration() + cfgHcl := fmt.Sprintf(ratelimitConfig, url, controllerKey, workerAuthKey, recoveryKey) + require.NoError(t, os.WriteFile(td+"/config.hcl", []byte(cfgHcl), 0o644)) + + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + + args := []string{"-config", td + "/config.hcl"} + exitCode := cmd.Run(args) + if exitCode != 0 { + output := cmd.UI.(*cli.MockUi).ErrorWriter.String() + cmd.UI.(*cli.MockUi).OutputWriter.String() + t.Errorf("got a non-zero exit status: %s", output) + } + }() + + // Wait until things are up and running (or timeout). + select { + case <-cmd.startedCh: + case <-time.After(15 * time.Second): + t.Fatal("timeout") + } + + // Change config so it is ready for reloading + cfgHcl = fmt.Sprintf(ratelimitConfigDisabledReload, url, controllerKey, workerAuthKey, recoveryKey) + require.NoError(t, os.WriteFile(td+"/config.hcl", []byte(cfgHcl), 0o644)) + + c := http.Client{} + r, err := c.Do(func() *http.Request { + r, err := http.NewRequest(http.MethodGet, `http://127.0.0.1:9500/v1/targets`, nil) + require.NoError(t, err) + return r + }()) + require.NoError(t, err) + // unauthed request, so we expect a 400 + assert.Equal(t, http.StatusBadRequest, r.StatusCode) + assert.Equal(t, `limit=2, remaining=1, reset=59`, r.Header.Get("Ratelimit")) + assert.Equal(t, `2;w=60;comment="total", 60;w=30;comment="ip-address", 60;w=30;comment="auth-token"`, r.Header.Get("Ratelimit-Policy")) + + r, err = c.Do(func() *http.Request { + r, err := http.NewRequest(http.MethodGet, `http://127.0.0.1:9500/v1/targets`, nil) + require.NoError(t, err) + return r + }()) + require.NoError(t, err) + // unauthed request, so we expect a 400 + assert.Equal(t, http.StatusBadRequest, r.StatusCode) + assert.Equal(t, `limit=2, remaining=0, reset=59`, r.Header.Get("Ratelimit")) + assert.Equal(t, `2;w=60;comment="total", 60;w=30;comment="ip-address", 60;w=30;comment="auth-token"`, r.Header.Get("Ratelimit-Policy")) + + r, err = c.Do(func() *http.Request { + r, err := http.NewRequest(http.MethodGet, `http://127.0.0.1:9500/v1/targets`, nil) + require.NoError(t, err) + return r + }()) + require.NoError(t, err) + // out of quota, so we expect a 429 + assert.Equal(t, http.StatusTooManyRequests, r.StatusCode) + assert.Equal(t, `limit=2, remaining=0, reset=59`, r.Header.Get("Ratelimit")) + assert.Equal(t, `2;w=60;comment="total", 60;w=30;comment="ip-address", 60;w=30;comment="auth-token"`, r.Header.Get("Ratelimit-Policy")) + + cmd.SighupCh <- struct{}{} + select { + case <-cmd.reloadedCh: + case <-time.After(15 * time.Second): + t.Fatal("timeout") + } + + // Make another request, the limit should be disabled + // should get reported via the headers. + r, err = c.Do(func() *http.Request { + r, err := http.NewRequest(http.MethodGet, `http://127.0.0.1:9500/v1/targets`, nil) + require.NoError(t, err) + return r + }()) + require.NoError(t, err) + // unauthed request, so we expect a 400 + assert.Equal(t, http.StatusBadRequest, r.StatusCode) + assert.Equal(t, ``, r.Header.Get("Ratelimit")) + assert.Equal(t, ``, r.Header.Get("Ratelimit-Policy")) + + cmd.ShutdownCh <- struct{}{} + wg.Wait() +} + +func TestRealodControllerRateLimitsEnable(t *testing.T) { + td := t.TempDir() + + controllerKey := config.DevKeyGeneration() + + closeDB, url, _, err := getInitDatabase(t, controllerKey) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, closeDB()) }) + + cmd := testServerCommand(t, testServerCommandOpts{}) + + workerAuthKey := config.DevKeyGeneration() + recoveryKey := config.DevKeyGeneration() + // Start with rate limiting diasabled + cfgHcl := fmt.Sprintf(ratelimitConfigDisabledReload, url, controllerKey, workerAuthKey, recoveryKey) + require.NoError(t, os.WriteFile(td+"/config.hcl", []byte(cfgHcl), 0o644)) + + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + + args := []string{"-config", td + "/config.hcl"} + exitCode := cmd.Run(args) + if exitCode != 0 { + output := cmd.UI.(*cli.MockUi).ErrorWriter.String() + cmd.UI.(*cli.MockUi).OutputWriter.String() + t.Errorf("got a non-zero exit status: %s", output) + } + }() + + // Wait until things are up and running (or timeout). + select { + case <-cmd.startedCh: + case <-time.After(15 * time.Second): + t.Fatal("timeout") + } + + // Change config so it is ready for reloading + cfgHcl = fmt.Sprintf(ratelimitConfig, url, controllerKey, workerAuthKey, recoveryKey) + require.NoError(t, os.WriteFile(td+"/config.hcl", []byte(cfgHcl), 0o644)) + + c := http.Client{} + r, err := c.Do(func() *http.Request { + r, err := http.NewRequest(http.MethodGet, `http://127.0.0.1:9500/v1/targets`, nil) + require.NoError(t, err) + return r + }()) + require.NoError(t, err) + // unauthed request, so we expect a 400 + assert.Equal(t, http.StatusBadRequest, r.StatusCode) + assert.Equal(t, ``, r.Header.Get("Ratelimit")) + assert.Equal(t, ``, r.Header.Get("Ratelimit-Policy")) + + cmd.SighupCh <- struct{}{} + select { + case <-cmd.reloadedCh: + case <-time.After(15 * time.Second): + t.Fatal("timeout") + } + + // Make another request, the limit should be enabled + // should get reported via the headers. + r, err = c.Do(func() *http.Request { + r, err := http.NewRequest(http.MethodGet, `http://127.0.0.1:9500/v1/targets`, nil) + require.NoError(t, err) + return r + }()) + require.NoError(t, err) + // unauthed request, so we expect a 400 + assert.Equal(t, http.StatusBadRequest, r.StatusCode) + assert.Equal(t, `limit=2, remaining=1, reset=59`, r.Header.Get("Ratelimit")) + assert.Equal(t, `2;w=60;comment="total", 60;w=30;comment="ip-address", 60;w=30;comment="auth-token"`, r.Header.Get("Ratelimit-Policy")) + + r, err = c.Do(func() *http.Request { + r, err := http.NewRequest(http.MethodGet, `http://127.0.0.1:9500/v1/targets`, nil) + require.NoError(t, err) + return r + }()) + require.NoError(t, err) + // unauthed request, so we expect a 400 + assert.Equal(t, http.StatusBadRequest, r.StatusCode) + assert.Equal(t, `limit=2, remaining=0, reset=59`, r.Header.Get("Ratelimit")) + assert.Equal(t, `2;w=60;comment="total", 60;w=30;comment="ip-address", 60;w=30;comment="auth-token"`, r.Header.Get("Ratelimit-Policy")) + + r, err = c.Do(func() *http.Request { + r, err := http.NewRequest(http.MethodGet, `http://127.0.0.1:9500/v1/targets`, nil) + require.NoError(t, err) + return r + }()) + require.NoError(t, err) + // out of quota, so we expect a 429 + assert.Equal(t, http.StatusTooManyRequests, r.StatusCode) + assert.Equal(t, `limit=2, remaining=0, reset=59`, r.Header.Get("Ratelimit")) + assert.Equal(t, `2;w=60;comment="total", 60;w=30;comment="ip-address", 60;w=30;comment="auth-token"`, r.Header.Get("Ratelimit-Policy")) + + cmd.ShutdownCh <- struct{}{} + wg.Wait() +} diff --git a/internal/cmd/commands/server/server.go b/internal/cmd/commands/server/server.go index dec946830..b1aa44472 100644 --- a/internal/cmd/commands/server/server.go +++ b/internal/cmd/commands/server/server.go @@ -622,8 +622,6 @@ func (c *Command) StartController(ctx context.Context) error { RawConfig: c.Config, Server: c.Server, TestOverrideWorkerAuthCaCertificateLifetime: c.flagWorkerAuthCaCertificateLifetime, - ApiRateLimits: c.Config.Controller.ApiRateLimits, - ApiRateLimiterMaxEntries: c.Config.Controller.ApiRateLimiterMaxEntries, } var err error @@ -962,7 +960,7 @@ func (c *Command) reloadControllerRateLimits(newConfig *config.Config) error { if c.controller == nil || newConfig == nil || newConfig.Controller == nil { return nil } - return c.controller.ReloadRateLimiter(newConfig.Controller.ApiRateLimits, newConfig.Controller.ApiRateLimiterMaxEntries) + return c.controller.ReloadRateLimiter(newConfig) } // acquireSchemaManager returns a schema manager and generally acquires a shared lock on diff --git a/internal/cmd/config/config.go b/internal/cmd/config/config.go index b630f992b..144bb5769 100644 --- a/internal/cmd/config/config.go +++ b/internal/cmd/config/config.go @@ -217,6 +217,7 @@ type Controller struct { ApiRateLimits ratelimit.Configs `hcl:"-"` ApiRateLimiterMaxEntries int `hcl:"api_rate_limit_max_entries"` + ApiRateLimitDisable bool `hcl:"api_rate_limit_disable"` // License is the license used by HCP builds License string `hcl:"license"` diff --git a/internal/daemon/controller/config.go b/internal/daemon/controller/config.go index 915ea5099..337af919d 100644 --- a/internal/daemon/controller/config.go +++ b/internal/daemon/controller/config.go @@ -8,7 +8,6 @@ import ( "github.com/hashicorp/boundary/internal/cmd/base" "github.com/hashicorp/boundary/internal/cmd/config" - "github.com/hashicorp/boundary/internal/ratelimit" ) type Config struct { @@ -23,8 +22,8 @@ type Config struct { // Override worker auth CA certificate lifetime for testing TestOverrideWorkerAuthCaCertificateLifetime time.Duration - // These are duplicated from the config.Config to track the state if these - // change via SIGHUP reloads. - ApiRateLimits ratelimit.Configs - ApiRateLimiterMaxEntries int + // This is derived from the config.Config. It tracks the state of the + // rate limiter's configuration, and is updated if the config changes via a + // SIGHUP. + rateLimiterConfig *rateLimiterConfig } diff --git a/internal/daemon/controller/controller.go b/internal/daemon/controller/controller.go index ab66136b9..e38dc889b 100644 --- a/internal/daemon/controller/controller.go +++ b/internal/daemon/controller/controller.go @@ -51,7 +51,6 @@ import ( external_plugins "github.com/hashicorp/boundary/sdk/plugins" "github.com/hashicorp/boundary/version" "github.com/hashicorp/go-hclog" - "github.com/hashicorp/go-rate" "github.com/hashicorp/go-secure-stdlib/mlock" "github.com/hashicorp/go-secure-stdlib/pluginutil/v2" "github.com/hashicorp/nodeenrollment" @@ -121,7 +120,8 @@ type Controller struct { apiGrpcServerListener grpcServerListener apiGrpcGatewayTicket string - rateLimiter *atomic.Pointer[rate.Limiter] + rateLimiter ratelimit.Limiter + rateLimiterMu sync.RWMutex // Repo factory methods AuthTokenRepoFn common.AuthTokenRepoFactory @@ -174,7 +174,6 @@ func New(ctx context.Context, conf *Config) (*Controller, error) { pkiConnManager: cluster.NewDownstreamManager(), workerStatusGracePeriod: new(atomic.Int64), livenessTimeToStale: new(atomic.Int64), - rateLimiter: new(atomic.Pointer[rate.Limiter]), } if downstreamReceiverFactory != nil { @@ -251,19 +250,9 @@ func New(ctx context.Context, conf *Config) (*Controller, error) { } c.clusterListener = clusterListeners[0] - rateLimits, err := conf.RawConfig.Controller.ApiRateLimits.Limits(c.baseContext) - if err != nil { - return nil, fmt.Errorf("error parsing rate limit configuration: %w", err) - } - - rateLimiter, err := ratelimit.NewLimiter( - rateLimits, - conf.RawConfig.Controller.ApiRateLimiterMaxEntries, - ) - if err != nil { + if err := c.initializeRateLimiter(conf.RawConfig); err != nil { return nil, fmt.Errorf("error initializing rate limiter: %w", err) } - c.rateLimiter.Store(rateLimiter) var pluginLogger hclog.Logger for _, enabledPlugin := range c.enabledPlugins { @@ -638,37 +627,3 @@ func (c *Controller) Shutdown() error { func (c *Controller) WorkerStatusUpdateTimes() *sync.Map { return c.workerStatusUpdateTimes } - -func (c *Controller) getRateLimiter() *rate.Limiter { - return c.rateLimiter.Load() -} - -// ReloadRateLimiter replaces the Controller's rate.Limiter with a new rate.Limiter -// using the supplied ratelimit.Configs and max entries. If the configs and max -// entries match the current values, the rate.Limiter is not replaced and no -// error is returned. Otherwise the rate.Limiter is replaced, and the old rate.Limiter -// is shutdown. This means that a client's quota is effectively reset if the -// configuration changes. -func (c *Controller) ReloadRateLimiter(newLimitConfigs ratelimit.Configs, newMaxEntries int) error { - // Config has not changed, no need to reload. - if c.conf.ApiRateLimits.Equal(newLimitConfigs) && c.conf.ApiRateLimiterMaxEntries == newMaxEntries { - return nil - } - - ratelimits, err := newLimitConfigs.Limits(c.baseContext) - if err != nil { - return fmt.Errorf("error parsing rate limit configuration: %w", err) - } - - limiter, err := ratelimit.NewLimiter(ratelimits, newMaxEntries) - if err != nil { - return err - } - - old := c.rateLimiter.Swap(limiter) - - c.conf.ApiRateLimits = newLimitConfigs - c.conf.ApiRateLimiterMaxEntries = newMaxEntries - - return old.Shutdown() -} diff --git a/internal/daemon/controller/rate_limiter.go b/internal/daemon/controller/rate_limiter.go new file mode 100644 index 000000000..9cffda696 --- /dev/null +++ b/internal/daemon/controller/rate_limiter.go @@ -0,0 +1,141 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package controller + +import ( + "context" + + "github.com/hashicorp/boundary/internal/cmd/config" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/ratelimit" + "github.com/hashicorp/go-rate" +) + +type rateLimiterConfig struct { + maxSize int + disabled bool + configs ratelimit.Configs + + limits []rate.Limit +} + +func newRateLimiterConfig(ctx context.Context, configs ratelimit.Configs, maxSize int, disabled bool) (*rateLimiterConfig, error) { + const op = "controller.newRateLimiterConfig" + + switch { + case disabled && len(configs) != 0: + return nil, errors.New(ctx, errors.InvalidConfiguration, op, "disabled rate limiter with rate limit configs") + } + + var limits []rate.Limit + var err error + if !disabled { + if limits, err = configs.Limits(ctx); err != nil { + return nil, err + } + } + + return &rateLimiterConfig{ + maxSize: maxSize, + disabled: disabled, + configs: configs, + limits: limits, + }, nil +} + +func (c *Controller) initializeRateLimiter(conf *config.Config) error { + const op = "controller.(Controller).initializeRateLimiter" + switch { + case conf == nil: + return errors.New(c.baseContext, errors.InvalidParameter, op, "nil config") + case conf.Controller == nil: + return errors.New(c.baseContext, errors.InvalidParameter, op, "nil config.Controller") + } + + c.rateLimiterMu.Lock() + defer c.rateLimiterMu.Unlock() + + rlConfig, err := newRateLimiterConfig( + c.baseContext, + conf.Controller.ApiRateLimits, + conf.Controller.ApiRateLimiterMaxEntries, + conf.Controller.ApiRateLimitDisable, + ) + if err != nil { + return err + } + + switch { + case rlConfig.disabled: + c.rateLimiter = rate.NopLimiter + default: + c.rateLimiter, err = ratelimit.NewLimiter(rlConfig.limits, rlConfig.maxSize) + if err != nil { + return err + } + } + + c.conf.rateLimiterConfig = rlConfig + + return nil +} + +func (c *Controller) getRateLimiter() ratelimit.Limiter { + c.rateLimiterMu.RLock() + defer c.rateLimiterMu.RUnlock() + return c.rateLimiter +} + +// ReloadRateLimiter replaces the Controller's rate.Limiter with a new rate.Limiter +// using the supplied ratelimit.Configs and max entries. If the configs and max +// entries match the current values, the rate.Limiter is not replaced and no +// error is returned. Otherwise the rate.Limiter is replaced, and the old rate.Limiter +// is shutdown. This means that a client's quota is effectively reset if the +// configuration changes. +func (c *Controller) ReloadRateLimiter(newConfig *config.Config) error { + const op = "controller.(Controller).ReloadRateLimiter" + + switch { + case newConfig == nil: + return errors.New(c.baseContext, errors.InvalidParameter, op, "nil config") + case newConfig.Controller == nil: + return errors.New(c.baseContext, errors.InvalidParameter, op, "nil config.Controller") + } + + rlConfig, err := newRateLimiterConfig( + c.baseContext, + newConfig.Controller.ApiRateLimits, + newConfig.Controller.ApiRateLimiterMaxEntries, + newConfig.Controller.ApiRateLimitDisable, + ) + if err != nil { + return err + } + + // Config has not changed, no need to reload. + if c.conf.rateLimiterConfig.maxSize == rlConfig.maxSize && + c.conf.rateLimiterConfig.disabled == rlConfig.disabled && + c.conf.rateLimiterConfig.configs.Equal(rlConfig.configs) { + return nil + } + + var limiter ratelimit.Limiter + switch { + case rlConfig.disabled: + limiter = rate.NopLimiter + default: + limiter, err = ratelimit.NewLimiter(rlConfig.limits, rlConfig.maxSize) + if err != nil { + return errors.Wrap(c.baseContext, err, op) + } + + } + c.rateLimiterMu.Lock() + old := c.rateLimiter + c.rateLimiter = limiter + c.conf.rateLimiterConfig = rlConfig + c.rateLimiterMu.Unlock() + + return old.Shutdown() +} diff --git a/internal/daemon/controller/rate_limiter_test.go b/internal/daemon/controller/rate_limiter_test.go new file mode 100644 index 000000000..f250b1c20 --- /dev/null +++ b/internal/daemon/controller/rate_limiter_test.go @@ -0,0 +1,481 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package controller + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "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/go-rate" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +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.DefaultLimiterMaxEntries(), + false, + &rateLimiterConfig{ + maxSize: 296148, + configs: nil, + disabled: false, + limits: defaultLimits, + }, + nil, + }, + { + "disabledWithConfigs", + ratelimit.Configs{ + { + Resources: []string{"*"}, + Actions: []string{"*"}, + Per: "total", + Limit: 100, + Period: time.Minute, + Unlimited: false, + }, + }, + ratelimit.DefaultLimiterMaxEntries(), + 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{ + ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), + }, + }, + false, + nil, + }, + { + "invalid", + &config.Config{ + Controller: &config.Controller{ + ApiRateLimits: ratelimit.Configs{ + { + Resources: []string{"*"}, + Actions: []string{"*"}, + Per: "total", + Limit: 100, + Period: time.Minute, + Unlimited: false, + }, + }, + ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), + 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{ + ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), + }, + } + 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, + }, + }, + ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), + }, + }, + true, + nil, + }, + { + "newMaxSize", + func() *Controller { + conf := &config.Config{ + Controller: &config.Controller{ + ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), + }, + } + c := &Controller{ + baseContext: context.Background(), + conf: &Config{ + RawConfig: conf, + }, + } + err := c.initializeRateLimiter(conf) + require.NoError(t, err) + return c + }(), + &config.Config{ + Controller: &config.Controller{ + ApiRateLimiterMaxEntries: 3000, + }, + }, + true, + nil, + }, + { + "newDisabled", + func() *Controller { + conf := &config.Config{ + Controller: &config.Controller{ + ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), + }, + } + 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{ + ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), + }, + }, + true, + nil, + }, + { + "newInvalid", + func() *Controller { + conf := &config.Config{ + Controller: &config.Controller{ + ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), + }, + } + c := &Controller{ + baseContext: context.Background(), + conf: &Config{ + RawConfig: conf, + }, + } + err := c.initializeRateLimiter(conf) + require.NoError(t, err) + return c + }(), + &config.Config{ + Controller: &config.Controller{ + ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), + 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{ + ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), + }, + } + c := &Controller{ + baseContext: context.Background(), + conf: &Config{ + RawConfig: conf, + }, + } + err := c.initializeRateLimiter(conf) + require.NoError(t, err) + return c + }(), + &config.Config{ + Controller: &config.Controller{ + ApiRateLimiterMaxEntries: 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{ + ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), + }, + } + c := &Controller{ + baseContext: context.Background(), + conf: &Config{ + RawConfig: conf, + }, + } + err := c.initializeRateLimiter(conf) + require.NoError(t, err) + return c + }(), + &config.Config{ + Controller: &config.Controller{ + ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), + }, + }, + false, + nil, + }, + { + "nilNewConfigController", + func() *Controller { + conf := &config.Config{ + Controller: &config.Controller{ + ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), + }, + } + 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{ + ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), + }, + } + 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()) + }) + } +} diff --git a/internal/ratelimit/handler_test.go b/internal/ratelimit/handler_test.go index dd30ad506..a4019b7ff 100644 --- a/internal/ratelimit/handler_test.go +++ b/internal/ratelimit/handler_test.go @@ -699,7 +699,7 @@ func TestHandler(t *testing.T) { next.ServeHTTP(rw, req) }) - }(ratelimit.Handler(ctx, func() *rate.Limiter { return tc.limiter }, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + }(ratelimit.Handler(ctx, func() ratelimit.Limiter { return tc.limiter }, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) return }))), @@ -799,7 +799,7 @@ func TestHandlerErrors(t *testing.T) { next.ServeHTTP(rw, req) }) - }(ratelimit.Handler(ctx, func() *rate.Limiter { return r }, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + }(ratelimit.Handler(ctx, func() ratelimit.Limiter { return r }, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) return }))), diff --git a/internal/ratelimit/limiter.go b/internal/ratelimit/limiter.go index 1aec78ed5..78f25ce37 100644 --- a/internal/ratelimit/limiter.go +++ b/internal/ratelimit/limiter.go @@ -3,7 +3,20 @@ package ratelimit -import "github.com/hashicorp/go-rate" +import ( + "net/http" + + "github.com/hashicorp/go-rate" +) + +// Limiter is used to provide rate limiting. This is the interface implemented +// by both rate.Limiter and rate.NopLimiter. +type Limiter interface { + SetPolicyHeader(string, string, http.Header) error + SetUsageHeader(*rate.Quota, http.Header) + Allow(string, string, string, string) (bool, *rate.Quota, error) + Shutdown() error +} // NewLimiter creates a rate.Limiter. func NewLimiter(limits []rate.Limit, maxEntries int) (*rate.Limiter, error) { From a150c588bb3922f1055abb8a4a1d02b86b82ce85 Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Fri, 1 Dec 2023 22:15:54 +0000 Subject: [PATCH 20/26] feat(controller): Emit sys event detailing the rate limit configuration A sys event will be emitted when the controller is constructed, and if the rate limits are changed via a SIGHUP. The event contains the details of the rate limits for each combination of resource and action. This event can be used by operators to confirm the limits are set as they would expect based on the controller config. --- internal/daemon/controller/rate_limiter.go | 84 + .../daemon/controller/rate_limiter_test.go | 192 +- .../defaults.json | 3903 +++++++++++++++++ .../disabled.json | 15 + .../max_size.json | 3903 +++++++++++++++++ .../override.json | 3903 +++++++++++++++++ 6 files changed, 11999 insertions(+), 1 deletion(-) create mode 100644 internal/daemon/controller/testdata/Test_rateLimiterConfig_writeSysEvent/defaults.json create mode 100644 internal/daemon/controller/testdata/Test_rateLimiterConfig_writeSysEvent/disabled.json create mode 100644 internal/daemon/controller/testdata/Test_rateLimiterConfig_writeSysEvent/max_size.json create mode 100644 internal/daemon/controller/testdata/Test_rateLimiterConfig_writeSysEvent/override.json diff --git a/internal/daemon/controller/rate_limiter.go b/internal/daemon/controller/rate_limiter.go index 9cffda696..f32e4b773 100644 --- a/internal/daemon/controller/rate_limiter.go +++ b/internal/daemon/controller/rate_limiter.go @@ -8,10 +8,29 @@ import ( "github.com/hashicorp/boundary/internal/cmd/config" "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/event" "github.com/hashicorp/boundary/internal/ratelimit" "github.com/hashicorp/go-rate" ) +// limit is a representation of a rate.Limit that is used when emitting a sys +// event to report the rate limit configuration. +type limit struct { + Resource string `json:"resource"` + Action string `json:"action"` + Per string `json:"per"` + Unlimited bool `json:"unlimited"` + Limit uint64 `json:"limit"` + Period string `json:"period"` +} + +// Types used for the nested structure of the sys event. +type ( + actionLimits []limit + resourceActions map[string]actionLimits + resources map[string]resourceActions +) + type rateLimiterConfig struct { maxSize int disabled bool @@ -44,6 +63,69 @@ func newRateLimiterConfig(ctx context.Context, configs ratelimit.Configs, maxSiz }, nil } +// writeSysEvent writes a sys event for c +func (c *rateLimiterConfig) writeSysEvent(ctx context.Context) { + const op = "controller.(rateLimiterConfig).writeSysEvent" + + if c.disabled { + event.WriteSysEvent( + ctx, + op, + "controller api rate limiter", + "disabled", + true, + ) + return + } + + e := make(resources) + + for _, l := range c.limits { + var r resourceActions + var a actionLimits + var ok bool + r, ok = e[l.GetResource()] + if !ok { + r = make(resourceActions) + e[l.GetResource()] = r + } + + a, ok = r[l.GetAction()] + if !ok { + a = make(actionLimits, 0, 3) + } + + switch ll := l.(type) { + case *rate.Limited: + a = append(a, limit{ + Resource: ll.Resource, + Action: ll.Action, + Per: ll.Per.String(), + Unlimited: false, + Limit: ll.MaxRequests, + Period: ll.Period.String(), + }) + case *rate.Unlimited: + a = append(a, limit{ + Resource: ll.Resource, + Action: ll.Action, + Per: ll.Per.String(), + Unlimited: true, + }) + } + r[l.GetAction()] = a + } + event.WriteSysEvent( + ctx, + op, + "controller api rate limiter", + "limits", + e, + "max_size", + c.maxSize, + ) +} + func (c *Controller) initializeRateLimiter(conf *config.Config) error { const op = "controller.(Controller).initializeRateLimiter" switch { @@ -78,6 +160,7 @@ func (c *Controller) initializeRateLimiter(conf *config.Config) error { c.conf.rateLimiterConfig = rlConfig + rlConfig.writeSysEvent(c.baseContext) return nil } @@ -136,6 +219,7 @@ func (c *Controller) ReloadRateLimiter(newConfig *config.Config) error { c.rateLimiter = limiter c.conf.rateLimiterConfig = rlConfig c.rateLimiterMu.Unlock() + rlConfig.writeSysEvent(c.baseContext) return old.Shutdown() } diff --git a/internal/daemon/controller/rate_limiter_test.go b/internal/daemon/controller/rate_limiter_test.go index f250b1c20..704ac3976 100644 --- a/internal/daemon/controller/rate_limiter_test.go +++ b/internal/daemon/controller/rate_limiter_test.go @@ -7,7 +7,6 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" "os" "path/filepath" "sync" @@ -19,11 +18,22 @@ import ( "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() @@ -479,3 +489,183 @@ func TestControllerReloadRateLimiter(t *testing.T) { }) } } + +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 + }{ + { + "defaults", + func(n string) error { + return event.InitSysEventer(testLogger, testLock, n, event.WithEventerConfig(&c.EventerConfig)) + }, + func() { event.TestResetSystEventer(t) }, + c.AllEvents.Name(), + nil, + ratelimit.DefaultLimiterMaxEntries(), + false, + }, + { + "override", + func(n string) error { + return event.InitSysEventer(testLogger, testLock, n, event.WithEventerConfig(&c.EventerConfig)) + }, + func() { event.TestResetSystEventer(t) }, + c.AllEvents.Name(), + 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, + }, + }, + ratelimit.DefaultLimiterMaxEntries(), + false, + }, + { + "max_size", + func(n string) error { + return event.InitSysEventer(testLogger, testLock, n, event.WithEventerConfig(&c.EventerConfig)) + }, + func() { event.TestResetSystEventer(t) }, + c.AllEvents.Name(), + nil, + 3000, + false, + }, + { + "disabled", + func(n string) error { + return event.InitSysEventer(testLogger, testLock, n, event.WithEventerConfig(&c.EventerConfig)) + }, + func() { event.TestResetSystEventer(t) }, + c.AllEvents.Name(), + nil, + 0, + 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(gotResourceLimits), len(wantResourceLimits)) + + 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) + } + } + }) + } +} diff --git a/internal/daemon/controller/testdata/Test_rateLimiterConfig_writeSysEvent/defaults.json b/internal/daemon/controller/testdata/Test_rateLimiterConfig_writeSysEvent/defaults.json new file mode 100644 index 000000000..1d9fbe71f --- /dev/null +++ b/internal/daemon/controller/testdata/Test_rateLimiterConfig_writeSysEvent/defaults.json @@ -0,0 +1,3903 @@ +{ + "id": "", + "source": "https://hashicorp.com/boundary/Test_rateLimiterConfig_writeSysEvent", + "specversion": "1.0", + "type": "system", + "data": { + "data": { + "limits": { + "account": { + "change-password": [ + { + "action": "change-password", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "account", + "unlimited": false + }, + { + "action": "change-password", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "account", + "unlimited": false + }, + { + "action": "change-password", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "account", + "unlimited": false + } + ], + "create": [ + { + "action": "create", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "account", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "account", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "account", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "account", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "account", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "account", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "account", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "account", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "account", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "account", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "account", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "account", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "account", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "account", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "account", + "unlimited": false + } + ], + "set-password": [ + { + "action": "set-password", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "account", + "unlimited": false + }, + { + "action": "set-password", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "account", + "unlimited": false + }, + { + "action": "set-password", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "account", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "account", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "account", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "account", + "unlimited": false + } + ] + }, + "auth-method": { + "authenticate": [ + { + "action": "authenticate", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "authenticate", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "authenticate", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "auth-method", + "unlimited": false + } + ], + "change-state": [ + { + "action": "change-state", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "change-state", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "change-state", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "auth-method", + "unlimited": false + } + ], + "create": [ + { + "action": "create", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "auth-method", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "auth-method", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "auth-method", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "auth-method", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "auth-method", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "auth-method", + "unlimited": false + } + ] + }, + "auth-token": { + "delete": [ + { + "action": "delete", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "auth-token", + "unlimited": false + } + ], + "delete:self": [ + { + "action": "delete:self", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "delete:self", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "delete:self", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "auth-token", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "auth-token", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "auth-token", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "auth-token", + "unlimited": false + } + ], + "read:self": [ + { + "action": "read:self", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "read:self", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "read:self", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "auth-token", + "unlimited": false + } + ] + }, + "credential": { + "create": [ + { + "action": "create", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "credential", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "credential", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "credential", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "credential", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "credential", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "credential", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "credential", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "credential", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "credential", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "credential", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "credential", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "credential", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "credential", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "credential", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "credential", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "credential", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "credential", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "credential", + "unlimited": false + } + ] + }, + "credential-library": { + "create": [ + { + "action": "create", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "credential-library", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "credential-library", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "credential-library", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "credential-library", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "credential-library", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "credential-library", + "unlimited": false + } + ] + }, + "credential-store": { + "create": [ + { + "action": "create", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "credential-store", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "credential-store", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "credential-store", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "credential-store", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "credential-store", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "credential-store", + "unlimited": false + } + ] + }, + "group": { + "add-members": [ + { + "action": "add-members", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "add-members", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "add-members", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "group", + "unlimited": false + } + ], + "create": [ + { + "action": "create", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "group", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "group", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "group", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "group", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "group", + "unlimited": false + } + ], + "remove-members": [ + { + "action": "remove-members", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "remove-members", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "remove-members", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "group", + "unlimited": false + } + ], + "set-members": [ + { + "action": "set-members", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "set-members", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "set-members", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "group", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "group", + "unlimited": false + } + ] + }, + "host": { + "create": [ + { + "action": "create", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "host", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "host", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "host", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host", + "unlimited": false + } + ] + }, + "host-catalog": { + "create": [ + { + "action": "create", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + } + ] + }, + "host-set": { + "add-hosts": [ + { + "action": "add-hosts", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "add-hosts", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "add-hosts", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host-set", + "unlimited": false + } + ], + "create": [ + { + "action": "create", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host-set", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host-set", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "host-set", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host-set", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host-set", + "unlimited": false + } + ], + "remove-hosts": [ + { + "action": "remove-hosts", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "remove-hosts", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "remove-hosts", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host-set", + "unlimited": false + } + ], + "set-hosts": [ + { + "action": "set-hosts", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "set-hosts", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "set-hosts", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host-set", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host-set", + "unlimited": false + } + ] + }, + "managed-group": { + "create": [ + { + "action": "create", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "managed-group", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "managed-group", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "managed-group", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "managed-group", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "managed-group", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "managed-group", + "unlimited": false + } + ] + }, + "role": { + "add-grants": [ + { + "action": "add-grants", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "add-grants", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "add-grants", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "role", + "unlimited": false + } + ], + "add-principals": [ + { + "action": "add-principals", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "add-principals", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "add-principals", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "role", + "unlimited": false + } + ], + "create": [ + { + "action": "create", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "role", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "role", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "role", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "role", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "role", + "unlimited": false + } + ], + "remove-grants": [ + { + "action": "remove-grants", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "remove-grants", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "remove-grants", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "role", + "unlimited": false + } + ], + "remove-principals": [ + { + "action": "remove-principals", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "remove-principals", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "remove-principals", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "role", + "unlimited": false + } + ], + "set-grants": [ + { + "action": "set-grants", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "set-grants", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "set-grants", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "role", + "unlimited": false + } + ], + "set-principals": [ + { + "action": "set-principals", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "set-principals", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "set-principals", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "role", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "role", + "unlimited": false + } + ] + }, + "scope": { + "create": [ + { + "action": "create", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "scope", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "scope", + "unlimited": false + } + ], + "destroy-key-version": [ + { + "action": "destroy-key-version", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "destroy-key-version", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "destroy-key-version", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "scope", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "scope", + "unlimited": false + } + ], + "list-key-version-destruction-jobs": [ + { + "action": "list-key-version-destruction-jobs", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "list-key-version-destruction-jobs", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "list-key-version-destruction-jobs", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "scope", + "unlimited": false + } + ], + "list-keys": [ + { + "action": "list-keys", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "list-keys", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "list-keys", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "scope", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "scope", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "scope", + "unlimited": false + } + ], + "rotate-keys": [ + { + "action": "rotate-keys", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "rotate-keys", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "rotate-keys", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "scope", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "scope", + "unlimited": false + } + ] + }, + "session": { + "cancel": [ + { + "action": "cancel", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "session", + "unlimited": false + }, + { + "action": "cancel", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "session", + "unlimited": false + }, + { + "action": "cancel", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "session", + "unlimited": false + } + ], + "cancel:self": [ + { + "action": "cancel:self", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "session", + "unlimited": false + }, + { + "action": "cancel:self", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "session", + "unlimited": false + }, + { + "action": "cancel:self", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "session", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "session", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "session", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "session", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "session", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "session", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "session", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "session", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "session", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "session", + "unlimited": false + } + ], + "read:self": [ + { + "action": "read:self", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "session", + "unlimited": false + }, + { + "action": "read:self", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "session", + "unlimited": false + }, + { + "action": "read:self", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "session", + "unlimited": false + } + ] + }, + "session-recording": { + "download": [ + { + "action": "download", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "session-recording", + "unlimited": false + }, + { + "action": "download", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "session-recording", + "unlimited": false + }, + { + "action": "download", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "session-recording", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "session-recording", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "session-recording", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "session-recording", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "session-recording", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "session-recording", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "session-recording", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "session-recording", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "session-recording", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "session-recording", + "unlimited": false + } + ] + }, + "storage-bucket": { + "create": [ + { + "action": "create", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + } + ] + }, + "target": { + "add-credential-sources": [ + { + "action": "add-credential-sources", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "add-credential-sources", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "add-credential-sources", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "target", + "unlimited": false + } + ], + "add-host-sources": [ + { + "action": "add-host-sources", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "add-host-sources", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "add-host-sources", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "target", + "unlimited": false + } + ], + "authorize-session": [ + { + "action": "authorize-session", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "authorize-session", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "authorize-session", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "target", + "unlimited": false + } + ], + "create": [ + { + "action": "create", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "target", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "target", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "target", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "target", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "target", + "unlimited": false + } + ], + "remove-credential-sources": [ + { + "action": "remove-credential-sources", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "remove-credential-sources", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "remove-credential-sources", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "target", + "unlimited": false + } + ], + "remove-host-sources": [ + { + "action": "remove-host-sources", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "remove-host-sources", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "remove-host-sources", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "target", + "unlimited": false + } + ], + "set-credential-sources": [ + { + "action": "set-credential-sources", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "set-credential-sources", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "set-credential-sources", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "target", + "unlimited": false + } + ], + "set-host-sources": [ + { + "action": "set-host-sources", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "set-host-sources", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "set-host-sources", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "target", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "target", + "unlimited": false + } + ] + }, + "user": { + "add-accounts": [ + { + "action": "add-accounts", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "add-accounts", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "add-accounts", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "user", + "unlimited": false + } + ], + "create": [ + { + "action": "create", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "user", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "user", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "user", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "user", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "user", + "unlimited": false + } + ], + "remove-accounts": [ + { + "action": "remove-accounts", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "remove-accounts", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "remove-accounts", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "user", + "unlimited": false + } + ], + "set-accounts": [ + { + "action": "set-accounts", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "set-accounts", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "set-accounts", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "user", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "user", + "unlimited": false + } + ] + }, + "worker": { + "add-worker-tags": [ + { + "action": "add-worker-tags", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "add-worker-tags", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "add-worker-tags", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "worker", + "unlimited": false + } + ], + "create:controller-led": [ + { + "action": "create:controller-led", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "create:controller-led", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "create:controller-led", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "worker", + "unlimited": false + } + ], + "create:worker-led": [ + { + "action": "create:worker-led", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "create:worker-led", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "create:worker-led", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "worker", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "worker", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "worker", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "worker", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "worker", + "unlimited": false + } + ], + "read-certificate-authority": [ + { + "action": "read-certificate-authority", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "read-certificate-authority", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "read-certificate-authority", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "worker", + "unlimited": false + } + ], + "reinitialize-certificate-authority": [ + { + "action": "reinitialize-certificate-authority", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "reinitialize-certificate-authority", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "reinitialize-certificate-authority", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "worker", + "unlimited": false + } + ], + "remove-worker-tags": [ + { + "action": "remove-worker-tags", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "remove-worker-tags", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "remove-worker-tags", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "worker", + "unlimited": false + } + ], + "set-worker-tags": [ + { + "action": "set-worker-tags", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "set-worker-tags", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "set-worker-tags", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "worker", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "worker", + "unlimited": false + } + ] + } + }, + "max_size": 296148, + "msg": "controller api rate limiter" + }, + "op": "controller.(rateLimiterConfig).writeSysEvent", + "version": "v0.1" + }, + "datacontentype": "application/cloudevents" +} diff --git a/internal/daemon/controller/testdata/Test_rateLimiterConfig_writeSysEvent/disabled.json b/internal/daemon/controller/testdata/Test_rateLimiterConfig_writeSysEvent/disabled.json new file mode 100644 index 000000000..d1e97eb24 --- /dev/null +++ b/internal/daemon/controller/testdata/Test_rateLimiterConfig_writeSysEvent/disabled.json @@ -0,0 +1,15 @@ +{ + "id": "", + "source": "https://hashicorp.com/boundary/Test_rateLimiterConfig_writeSysEvent", + "specversion": "1.0", + "type": "system", + "data": { + "data": { + "disabled": true, + "msg": "controller api rate limiter" + }, + "op": "controller.(rateLimiterConfig).writeSysEvent", + "version": "v0.1" + }, + "datacontentype": "application/cloudevents" +} diff --git a/internal/daemon/controller/testdata/Test_rateLimiterConfig_writeSysEvent/max_size.json b/internal/daemon/controller/testdata/Test_rateLimiterConfig_writeSysEvent/max_size.json new file mode 100644 index 000000000..91216ea14 --- /dev/null +++ b/internal/daemon/controller/testdata/Test_rateLimiterConfig_writeSysEvent/max_size.json @@ -0,0 +1,3903 @@ +{ + "id": "", + "source": "https://hashicorp.com/boundary/Test_rateLimiterConfig_writeSysEvent", + "specversion": "1.0", + "type": "system", + "data": { + "data": { + "limits": { + "account": { + "change-password": [ + { + "action": "change-password", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "account", + "unlimited": false + }, + { + "action": "change-password", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "account", + "unlimited": false + }, + { + "action": "change-password", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "account", + "unlimited": false + } + ], + "create": [ + { + "action": "create", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "account", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "account", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "account", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "account", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "account", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "account", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "account", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "account", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "account", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "account", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "account", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "account", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "account", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "account", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "account", + "unlimited": false + } + ], + "set-password": [ + { + "action": "set-password", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "account", + "unlimited": false + }, + { + "action": "set-password", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "account", + "unlimited": false + }, + { + "action": "set-password", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "account", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "account", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "account", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "account", + "unlimited": false + } + ] + }, + "auth-method": { + "authenticate": [ + { + "action": "authenticate", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "authenticate", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "authenticate", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "auth-method", + "unlimited": false + } + ], + "change-state": [ + { + "action": "change-state", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "change-state", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "change-state", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "auth-method", + "unlimited": false + } + ], + "create": [ + { + "action": "create", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "auth-method", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "auth-method", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "auth-method", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "auth-method", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "auth-method", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "auth-method", + "unlimited": false + } + ] + }, + "auth-token": { + "delete": [ + { + "action": "delete", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "auth-token", + "unlimited": false + } + ], + "delete:self": [ + { + "action": "delete:self", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "delete:self", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "delete:self", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "auth-token", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "auth-token", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "auth-token", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "auth-token", + "unlimited": false + } + ], + "read:self": [ + { + "action": "read:self", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "read:self", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "read:self", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "auth-token", + "unlimited": false + } + ] + }, + "credential": { + "create": [ + { + "action": "create", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "credential", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "credential", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "credential", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "credential", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "credential", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "credential", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "credential", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "credential", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "credential", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "credential", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "credential", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "credential", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "credential", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "credential", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "credential", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "credential", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "credential", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "credential", + "unlimited": false + } + ] + }, + "credential-library": { + "create": [ + { + "action": "create", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "credential-library", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "credential-library", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "credential-library", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "credential-library", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "credential-library", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "credential-library", + "unlimited": false + } + ] + }, + "credential-store": { + "create": [ + { + "action": "create", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "credential-store", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "credential-store", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "credential-store", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "credential-store", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "credential-store", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "credential-store", + "unlimited": false + } + ] + }, + "group": { + "add-members": [ + { + "action": "add-members", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "add-members", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "add-members", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "group", + "unlimited": false + } + ], + "create": [ + { + "action": "create", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "group", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "group", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "group", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "group", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "group", + "unlimited": false + } + ], + "remove-members": [ + { + "action": "remove-members", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "remove-members", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "remove-members", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "group", + "unlimited": false + } + ], + "set-members": [ + { + "action": "set-members", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "set-members", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "set-members", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "group", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "group", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "group", + "unlimited": false + } + ] + }, + "host": { + "create": [ + { + "action": "create", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "host", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "host", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "host", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host", + "unlimited": false + } + ] + }, + "host-catalog": { + "create": [ + { + "action": "create", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host-catalog", + "unlimited": false + } + ] + }, + "host-set": { + "add-hosts": [ + { + "action": "add-hosts", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "add-hosts", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "add-hosts", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host-set", + "unlimited": false + } + ], + "create": [ + { + "action": "create", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host-set", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host-set", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "host-set", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host-set", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host-set", + "unlimited": false + } + ], + "remove-hosts": [ + { + "action": "remove-hosts", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "remove-hosts", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "remove-hosts", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host-set", + "unlimited": false + } + ], + "set-hosts": [ + { + "action": "set-hosts", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "set-hosts", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "set-hosts", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host-set", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "host-set", + "unlimited": false + } + ] + }, + "managed-group": { + "create": [ + { + "action": "create", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "managed-group", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "managed-group", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "managed-group", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "managed-group", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "managed-group", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "managed-group", + "unlimited": false + } + ] + }, + "role": { + "add-grants": [ + { + "action": "add-grants", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "add-grants", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "add-grants", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "role", + "unlimited": false + } + ], + "add-principals": [ + { + "action": "add-principals", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "add-principals", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "add-principals", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "role", + "unlimited": false + } + ], + "create": [ + { + "action": "create", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "role", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "role", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "role", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "role", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "role", + "unlimited": false + } + ], + "remove-grants": [ + { + "action": "remove-grants", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "remove-grants", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "remove-grants", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "role", + "unlimited": false + } + ], + "remove-principals": [ + { + "action": "remove-principals", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "remove-principals", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "remove-principals", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "role", + "unlimited": false + } + ], + "set-grants": [ + { + "action": "set-grants", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "set-grants", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "set-grants", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "role", + "unlimited": false + } + ], + "set-principals": [ + { + "action": "set-principals", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "set-principals", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "set-principals", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "role", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "role", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "role", + "unlimited": false + } + ] + }, + "scope": { + "create": [ + { + "action": "create", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "scope", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "scope", + "unlimited": false + } + ], + "destroy-key-version": [ + { + "action": "destroy-key-version", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "destroy-key-version", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "destroy-key-version", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "scope", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "scope", + "unlimited": false + } + ], + "list-key-version-destruction-jobs": [ + { + "action": "list-key-version-destruction-jobs", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "list-key-version-destruction-jobs", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "list-key-version-destruction-jobs", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "scope", + "unlimited": false + } + ], + "list-keys": [ + { + "action": "list-keys", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "list-keys", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "list-keys", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "scope", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "scope", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "scope", + "unlimited": false + } + ], + "rotate-keys": [ + { + "action": "rotate-keys", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "rotate-keys", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "rotate-keys", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "scope", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "scope", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "scope", + "unlimited": false + } + ] + }, + "session": { + "cancel": [ + { + "action": "cancel", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "session", + "unlimited": false + }, + { + "action": "cancel", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "session", + "unlimited": false + }, + { + "action": "cancel", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "session", + "unlimited": false + } + ], + "cancel:self": [ + { + "action": "cancel:self", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "session", + "unlimited": false + }, + { + "action": "cancel:self", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "session", + "unlimited": false + }, + { + "action": "cancel:self", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "session", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "session", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "session", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "session", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "session", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "session", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "session", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "session", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "session", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "session", + "unlimited": false + } + ], + "read:self": [ + { + "action": "read:self", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "session", + "unlimited": false + }, + { + "action": "read:self", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "session", + "unlimited": false + }, + { + "action": "read:self", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "session", + "unlimited": false + } + ] + }, + "session-recording": { + "download": [ + { + "action": "download", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "session-recording", + "unlimited": false + }, + { + "action": "download", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "session-recording", + "unlimited": false + }, + { + "action": "download", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "session-recording", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "session-recording", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "session-recording", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "session-recording", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "session-recording", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "session-recording", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "session-recording", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "session-recording", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "session-recording", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "session-recording", + "unlimited": false + } + ] + }, + "storage-bucket": { + "create": [ + { + "action": "create", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "storage-bucket", + "unlimited": false + } + ] + }, + "target": { + "add-credential-sources": [ + { + "action": "add-credential-sources", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "add-credential-sources", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "add-credential-sources", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "target", + "unlimited": false + } + ], + "add-host-sources": [ + { + "action": "add-host-sources", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "add-host-sources", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "add-host-sources", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "target", + "unlimited": false + } + ], + "authorize-session": [ + { + "action": "authorize-session", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "authorize-session", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "authorize-session", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "target", + "unlimited": false + } + ], + "create": [ + { + "action": "create", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "target", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "target", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "target", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "target", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "target", + "unlimited": false + } + ], + "remove-credential-sources": [ + { + "action": "remove-credential-sources", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "remove-credential-sources", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "remove-credential-sources", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "target", + "unlimited": false + } + ], + "remove-host-sources": [ + { + "action": "remove-host-sources", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "remove-host-sources", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "remove-host-sources", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "target", + "unlimited": false + } + ], + "set-credential-sources": [ + { + "action": "set-credential-sources", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "set-credential-sources", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "set-credential-sources", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "target", + "unlimited": false + } + ], + "set-host-sources": [ + { + "action": "set-host-sources", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "set-host-sources", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "set-host-sources", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "target", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "target", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "target", + "unlimited": false + } + ] + }, + "user": { + "add-accounts": [ + { + "action": "add-accounts", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "add-accounts", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "add-accounts", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "user", + "unlimited": false + } + ], + "create": [ + { + "action": "create", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "create", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "user", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "user", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "user", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "user", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "user", + "unlimited": false + } + ], + "remove-accounts": [ + { + "action": "remove-accounts", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "remove-accounts", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "remove-accounts", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "user", + "unlimited": false + } + ], + "set-accounts": [ + { + "action": "set-accounts", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "set-accounts", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "set-accounts", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "user", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "user", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "user", + "unlimited": false + } + ] + }, + "worker": { + "add-worker-tags": [ + { + "action": "add-worker-tags", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "add-worker-tags", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "add-worker-tags", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "worker", + "unlimited": false + } + ], + "create:controller-led": [ + { + "action": "create:controller-led", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "create:controller-led", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "create:controller-led", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "worker", + "unlimited": false + } + ], + "create:worker-led": [ + { + "action": "create:worker-led", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "create:worker-led", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "create:worker-led", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "worker", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "delete", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "worker", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 60, + "per": "ip-address", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "total", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "list", + "limit": 60, + "per": "auth-token", + "period": "30s", + "resource": "worker", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "no-op", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "worker", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "read", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "worker", + "unlimited": false + } + ], + "read-certificate-authority": [ + { + "action": "read-certificate-authority", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "read-certificate-authority", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "read-certificate-authority", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "worker", + "unlimited": false + } + ], + "reinitialize-certificate-authority": [ + { + "action": "reinitialize-certificate-authority", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "reinitialize-certificate-authority", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "reinitialize-certificate-authority", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "worker", + "unlimited": false + } + ], + "remove-worker-tags": [ + { + "action": "remove-worker-tags", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "remove-worker-tags", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "remove-worker-tags", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "worker", + "unlimited": false + } + ], + "set-worker-tags": [ + { + "action": "set-worker-tags", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "set-worker-tags", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "set-worker-tags", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "worker", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 3000, + "per": "total", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "auth-token", + "period": "30s", + "resource": "worker", + "unlimited": false + }, + { + "action": "update", + "limit": 3000, + "per": "ip-address", + "period": "30s", + "resource": "worker", + "unlimited": false + } + ] + } + }, + "max_size": 3000, + "msg": "controller api rate limiter" + }, + "op": "controller.(rateLimiterConfig).writeSysEvent", + "version": "v0.1" + }, + "datacontentype": "application/cloudevents" +} diff --git a/internal/daemon/controller/testdata/Test_rateLimiterConfig_writeSysEvent/override.json b/internal/daemon/controller/testdata/Test_rateLimiterConfig_writeSysEvent/override.json new file mode 100644 index 000000000..c3ec2d340 --- /dev/null +++ b/internal/daemon/controller/testdata/Test_rateLimiterConfig_writeSysEvent/override.json @@ -0,0 +1,3903 @@ +{ + "id": "", + "source": "https://hashicorp.com/boundary/Test_rateLimiterConfig_writeSysEvent", + "specversion": "1.0", + "type": "system", + "data": { + "data": { + "limits": { + "account": { + "change-password": [ + { + "action": "change-password", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "account", + "unlimited": false + }, + { + "action": "change-password", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "account", + "unlimited": false + }, + { + "action": "change-password", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "account", + "unlimited": false + } + ], + "create": [ + { + "action": "create", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "account", + "unlimited": false + }, + { + "action": "create", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "account", + "unlimited": false + }, + { + "action": "create", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "account", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "account", + "unlimited": false + }, + { + "action": "delete", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "account", + "unlimited": false + }, + { + "action": "delete", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "account", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "account", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "account", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "account", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "account", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "account", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "account", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "account", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "account", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "account", + "unlimited": false + } + ], + "set-password": [ + { + "action": "set-password", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "account", + "unlimited": false + }, + { + "action": "set-password", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "account", + "unlimited": false + }, + { + "action": "set-password", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "account", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "account", + "unlimited": false + }, + { + "action": "update", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "account", + "unlimited": false + }, + { + "action": "update", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "account", + "unlimited": false + } + ] + }, + "auth-method": { + "authenticate": [ + { + "action": "authenticate", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "authenticate", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "authenticate", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "auth-method", + "unlimited": false + } + ], + "change-state": [ + { + "action": "change-state", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "change-state", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "change-state", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "auth-method", + "unlimited": false + } + ], + "create": [ + { + "action": "create", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "create", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "create", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "auth-method", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "delete", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "delete", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "auth-method", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "auth-method", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "auth-method", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "auth-method", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "update", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "auth-method", + "unlimited": false + }, + { + "action": "update", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "auth-method", + "unlimited": false + } + ] + }, + "auth-token": { + "delete": [ + { + "action": "delete", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "delete", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "delete", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "auth-token", + "unlimited": false + } + ], + "delete:self": [ + { + "action": "delete:self", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "delete:self", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "delete:self", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "auth-token", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "auth-token", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "auth-token", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "auth-token", + "unlimited": false + } + ], + "read:self": [ + { + "action": "read:self", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "read:self", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "auth-token", + "unlimited": false + }, + { + "action": "read:self", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "auth-token", + "unlimited": false + } + ] + }, + "credential": { + "create": [ + { + "action": "create", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "credential", + "unlimited": false + }, + { + "action": "create", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "credential", + "unlimited": false + }, + { + "action": "create", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "credential", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "credential", + "unlimited": false + }, + { + "action": "delete", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "credential", + "unlimited": false + }, + { + "action": "delete", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "credential", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "credential", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "credential", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "credential", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "credential", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "credential", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "credential", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "credential", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "credential", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "credential", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "credential", + "unlimited": false + }, + { + "action": "update", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "credential", + "unlimited": false + }, + { + "action": "update", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "credential", + "unlimited": false + } + ] + }, + "credential-library": { + "create": [ + { + "action": "create", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "create", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "create", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "credential-library", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "delete", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "delete", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "credential-library", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "credential-library", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "credential-library", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "credential-library", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "update", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "credential-library", + "unlimited": false + }, + { + "action": "update", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "credential-library", + "unlimited": false + } + ] + }, + "credential-store": { + "create": [ + { + "action": "create", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "create", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "create", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "credential-store", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "delete", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "delete", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "credential-store", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "credential-store", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "credential-store", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "credential-store", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "update", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "credential-store", + "unlimited": false + }, + { + "action": "update", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "credential-store", + "unlimited": false + } + ] + }, + "group": { + "add-members": [ + { + "action": "add-members", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "group", + "unlimited": false + }, + { + "action": "add-members", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "group", + "unlimited": false + }, + { + "action": "add-members", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "group", + "unlimited": false + } + ], + "create": [ + { + "action": "create", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "group", + "unlimited": false + }, + { + "action": "create", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "group", + "unlimited": false + }, + { + "action": "create", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "group", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "group", + "unlimited": false + }, + { + "action": "delete", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "group", + "unlimited": false + }, + { + "action": "delete", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "group", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "group", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "group", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "group", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "group", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "group", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "group", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "group", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "group", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "group", + "unlimited": false + } + ], + "remove-members": [ + { + "action": "remove-members", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "group", + "unlimited": false + }, + { + "action": "remove-members", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "group", + "unlimited": false + }, + { + "action": "remove-members", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "group", + "unlimited": false + } + ], + "set-members": [ + { + "action": "set-members", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "group", + "unlimited": false + }, + { + "action": "set-members", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "group", + "unlimited": false + }, + { + "action": "set-members", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "group", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "group", + "unlimited": false + }, + { + "action": "update", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "group", + "unlimited": false + }, + { + "action": "update", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "group", + "unlimited": false + } + ] + }, + "host": { + "create": [ + { + "action": "create", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "host", + "unlimited": false + }, + { + "action": "create", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "host", + "unlimited": false + }, + { + "action": "create", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "host", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "host", + "unlimited": false + }, + { + "action": "delete", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "host", + "unlimited": false + }, + { + "action": "delete", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "host", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "host", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "host", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "host", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "host", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "host", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "host", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "host", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "host", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "host", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "host", + "unlimited": false + }, + { + "action": "update", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "host", + "unlimited": false + }, + { + "action": "update", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "host", + "unlimited": false + } + ] + }, + "host-catalog": { + "create": [ + { + "action": "create", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "create", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "create", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "host-catalog", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "delete", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "delete", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "host-catalog", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "host-catalog", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "host-catalog", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "host-catalog", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "update", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "host-catalog", + "unlimited": false + }, + { + "action": "update", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "host-catalog", + "unlimited": false + } + ] + }, + "host-set": { + "add-hosts": [ + { + "action": "add-hosts", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "add-hosts", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "add-hosts", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "host-set", + "unlimited": false + } + ], + "create": [ + { + "action": "create", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "create", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "create", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "host-set", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "delete", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "delete", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "host-set", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "host-set", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "host-set", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "host-set", + "unlimited": false + } + ], + "remove-hosts": [ + { + "action": "remove-hosts", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "remove-hosts", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "remove-hosts", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "host-set", + "unlimited": false + } + ], + "set-hosts": [ + { + "action": "set-hosts", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "set-hosts", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "set-hosts", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "host-set", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "update", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "host-set", + "unlimited": false + }, + { + "action": "update", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "host-set", + "unlimited": false + } + ] + }, + "managed-group": { + "create": [ + { + "action": "create", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "create", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "create", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "managed-group", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "delete", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "delete", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "managed-group", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "managed-group", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "managed-group", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "managed-group", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "update", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "managed-group", + "unlimited": false + }, + { + "action": "update", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "managed-group", + "unlimited": false + } + ] + }, + "role": { + "add-grants": [ + { + "action": "add-grants", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "role", + "unlimited": false + }, + { + "action": "add-grants", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "role", + "unlimited": false + }, + { + "action": "add-grants", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "role", + "unlimited": false + } + ], + "add-principals": [ + { + "action": "add-principals", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "role", + "unlimited": false + }, + { + "action": "add-principals", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "role", + "unlimited": false + }, + { + "action": "add-principals", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "role", + "unlimited": false + } + ], + "create": [ + { + "action": "create", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "role", + "unlimited": false + }, + { + "action": "create", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "role", + "unlimited": false + }, + { + "action": "create", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "role", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "role", + "unlimited": false + }, + { + "action": "delete", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "role", + "unlimited": false + }, + { + "action": "delete", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "role", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "role", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "role", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "role", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "role", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "role", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "role", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "role", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "role", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "role", + "unlimited": false + } + ], + "remove-grants": [ + { + "action": "remove-grants", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "role", + "unlimited": false + }, + { + "action": "remove-grants", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "role", + "unlimited": false + }, + { + "action": "remove-grants", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "role", + "unlimited": false + } + ], + "remove-principals": [ + { + "action": "remove-principals", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "role", + "unlimited": false + }, + { + "action": "remove-principals", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "role", + "unlimited": false + }, + { + "action": "remove-principals", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "role", + "unlimited": false + } + ], + "set-grants": [ + { + "action": "set-grants", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "role", + "unlimited": false + }, + { + "action": "set-grants", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "role", + "unlimited": false + }, + { + "action": "set-grants", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "role", + "unlimited": false + } + ], + "set-principals": [ + { + "action": "set-principals", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "role", + "unlimited": false + }, + { + "action": "set-principals", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "role", + "unlimited": false + }, + { + "action": "set-principals", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "role", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "role", + "unlimited": false + }, + { + "action": "update", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "role", + "unlimited": false + }, + { + "action": "update", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "role", + "unlimited": false + } + ] + }, + "scope": { + "create": [ + { + "action": "create", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "scope", + "unlimited": false + }, + { + "action": "create", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "scope", + "unlimited": false + }, + { + "action": "create", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "scope", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "scope", + "unlimited": false + }, + { + "action": "delete", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "scope", + "unlimited": false + }, + { + "action": "delete", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "scope", + "unlimited": false + } + ], + "destroy-key-version": [ + { + "action": "destroy-key-version", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "scope", + "unlimited": false + }, + { + "action": "destroy-key-version", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "scope", + "unlimited": false + }, + { + "action": "destroy-key-version", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "scope", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "scope", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "scope", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "scope", + "unlimited": false + } + ], + "list-key-version-destruction-jobs": [ + { + "action": "list-key-version-destruction-jobs", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "scope", + "unlimited": false + }, + { + "action": "list-key-version-destruction-jobs", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "scope", + "unlimited": false + }, + { + "action": "list-key-version-destruction-jobs", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "scope", + "unlimited": false + } + ], + "list-keys": [ + { + "action": "list-keys", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "scope", + "unlimited": false + }, + { + "action": "list-keys", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "scope", + "unlimited": false + }, + { + "action": "list-keys", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "scope", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "scope", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "scope", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "scope", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "scope", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "scope", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "scope", + "unlimited": false + } + ], + "rotate-keys": [ + { + "action": "rotate-keys", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "scope", + "unlimited": false + }, + { + "action": "rotate-keys", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "scope", + "unlimited": false + }, + { + "action": "rotate-keys", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "scope", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "scope", + "unlimited": false + }, + { + "action": "update", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "scope", + "unlimited": false + }, + { + "action": "update", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "scope", + "unlimited": false + } + ] + }, + "session": { + "cancel": [ + { + "action": "cancel", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "session", + "unlimited": false + }, + { + "action": "cancel", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "session", + "unlimited": false + }, + { + "action": "cancel", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "session", + "unlimited": false + } + ], + "cancel:self": [ + { + "action": "cancel:self", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "session", + "unlimited": false + }, + { + "action": "cancel:self", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "session", + "unlimited": false + }, + { + "action": "cancel:self", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "session", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "session", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "session", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "session", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "session", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "session", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "session", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "session", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "session", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "session", + "unlimited": false + } + ], + "read:self": [ + { + "action": "read:self", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "session", + "unlimited": false + }, + { + "action": "read:self", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "session", + "unlimited": false + }, + { + "action": "read:self", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "session", + "unlimited": false + } + ] + }, + "session-recording": { + "download": [ + { + "action": "download", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "session-recording", + "unlimited": false + }, + { + "action": "download", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "session-recording", + "unlimited": false + }, + { + "action": "download", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "session-recording", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "session-recording", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "session-recording", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "session-recording", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "session-recording", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "session-recording", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "session-recording", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "session-recording", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "session-recording", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "session-recording", + "unlimited": false + } + ] + }, + "storage-bucket": { + "create": [ + { + "action": "create", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "create", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "create", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "storage-bucket", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "delete", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "delete", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "storage-bucket", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "storage-bucket", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "storage-bucket", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "storage-bucket", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "update", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "storage-bucket", + "unlimited": false + }, + { + "action": "update", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "storage-bucket", + "unlimited": false + } + ] + }, + "target": { + "add-credential-sources": [ + { + "action": "add-credential-sources", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "target", + "unlimited": false + }, + { + "action": "add-credential-sources", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "target", + "unlimited": false + }, + { + "action": "add-credential-sources", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "target", + "unlimited": false + } + ], + "add-host-sources": [ + { + "action": "add-host-sources", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "target", + "unlimited": false + }, + { + "action": "add-host-sources", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "target", + "unlimited": false + }, + { + "action": "add-host-sources", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "target", + "unlimited": false + } + ], + "authorize-session": [ + { + "action": "authorize-session", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "target", + "unlimited": false + }, + { + "action": "authorize-session", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "target", + "unlimited": false + }, + { + "action": "authorize-session", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "target", + "unlimited": false + } + ], + "create": [ + { + "action": "create", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "target", + "unlimited": false + }, + { + "action": "create", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "target", + "unlimited": false + }, + { + "action": "create", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "target", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "target", + "unlimited": false + }, + { + "action": "delete", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "target", + "unlimited": false + }, + { + "action": "delete", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "target", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "target", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "target", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "target", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "target", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "target", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "target", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "target", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "target", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "target", + "unlimited": false + } + ], + "remove-credential-sources": [ + { + "action": "remove-credential-sources", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "target", + "unlimited": false + }, + { + "action": "remove-credential-sources", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "target", + "unlimited": false + }, + { + "action": "remove-credential-sources", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "target", + "unlimited": false + } + ], + "remove-host-sources": [ + { + "action": "remove-host-sources", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "target", + "unlimited": false + }, + { + "action": "remove-host-sources", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "target", + "unlimited": false + }, + { + "action": "remove-host-sources", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "target", + "unlimited": false + } + ], + "set-credential-sources": [ + { + "action": "set-credential-sources", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "target", + "unlimited": false + }, + { + "action": "set-credential-sources", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "target", + "unlimited": false + }, + { + "action": "set-credential-sources", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "target", + "unlimited": false + } + ], + "set-host-sources": [ + { + "action": "set-host-sources", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "target", + "unlimited": false + }, + { + "action": "set-host-sources", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "target", + "unlimited": false + }, + { + "action": "set-host-sources", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "target", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "target", + "unlimited": false + }, + { + "action": "update", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "target", + "unlimited": false + }, + { + "action": "update", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "target", + "unlimited": false + } + ] + }, + "user": { + "add-accounts": [ + { + "action": "add-accounts", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "user", + "unlimited": false + }, + { + "action": "add-accounts", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "user", + "unlimited": false + }, + { + "action": "add-accounts", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "user", + "unlimited": false + } + ], + "create": [ + { + "action": "create", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "user", + "unlimited": false + }, + { + "action": "create", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "user", + "unlimited": false + }, + { + "action": "create", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "user", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "user", + "unlimited": false + }, + { + "action": "delete", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "user", + "unlimited": false + }, + { + "action": "delete", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "user", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "user", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "user", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "user", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "user", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "user", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "user", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "user", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "user", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "user", + "unlimited": false + } + ], + "remove-accounts": [ + { + "action": "remove-accounts", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "user", + "unlimited": false + }, + { + "action": "remove-accounts", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "user", + "unlimited": false + }, + { + "action": "remove-accounts", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "user", + "unlimited": false + } + ], + "set-accounts": [ + { + "action": "set-accounts", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "user", + "unlimited": false + }, + { + "action": "set-accounts", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "user", + "unlimited": false + }, + { + "action": "set-accounts", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "user", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "user", + "unlimited": false + }, + { + "action": "update", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "user", + "unlimited": false + }, + { + "action": "update", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "user", + "unlimited": false + } + ] + }, + "worker": { + "add-worker-tags": [ + { + "action": "add-worker-tags", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "worker", + "unlimited": false + }, + { + "action": "add-worker-tags", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "worker", + "unlimited": false + }, + { + "action": "add-worker-tags", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "worker", + "unlimited": false + } + ], + "create:controller-led": [ + { + "action": "create:controller-led", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "worker", + "unlimited": false + }, + { + "action": "create:controller-led", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "worker", + "unlimited": false + }, + { + "action": "create:controller-led", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "worker", + "unlimited": false + } + ], + "create:worker-led": [ + { + "action": "create:worker-led", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "worker", + "unlimited": false + }, + { + "action": "create:worker-led", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "worker", + "unlimited": false + }, + { + "action": "create:worker-led", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "worker", + "unlimited": false + } + ], + "delete": [ + { + "action": "delete", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "worker", + "unlimited": false + }, + { + "action": "delete", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "worker", + "unlimited": false + }, + { + "action": "delete", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "worker", + "unlimited": false + } + ], + "list": [ + { + "action": "list", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "worker", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "worker", + "unlimited": false + }, + { + "action": "list", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "worker", + "unlimited": false + } + ], + "no-op": [ + { + "action": "no-op", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "worker", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "worker", + "unlimited": false + }, + { + "action": "no-op", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "worker", + "unlimited": false + } + ], + "read": [ + { + "action": "read", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "worker", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "worker", + "unlimited": false + }, + { + "action": "read", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "worker", + "unlimited": false + } + ], + "read-certificate-authority": [ + { + "action": "read-certificate-authority", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "worker", + "unlimited": false + }, + { + "action": "read-certificate-authority", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "worker", + "unlimited": false + }, + { + "action": "read-certificate-authority", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "worker", + "unlimited": false + } + ], + "reinitialize-certificate-authority": [ + { + "action": "reinitialize-certificate-authority", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "worker", + "unlimited": false + }, + { + "action": "reinitialize-certificate-authority", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "worker", + "unlimited": false + }, + { + "action": "reinitialize-certificate-authority", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "worker", + "unlimited": false + } + ], + "remove-worker-tags": [ + { + "action": "remove-worker-tags", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "worker", + "unlimited": false + }, + { + "action": "remove-worker-tags", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "worker", + "unlimited": false + }, + { + "action": "remove-worker-tags", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "worker", + "unlimited": false + } + ], + "set-worker-tags": [ + { + "action": "set-worker-tags", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "worker", + "unlimited": false + }, + { + "action": "set-worker-tags", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "worker", + "unlimited": false + }, + { + "action": "set-worker-tags", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "worker", + "unlimited": false + } + ], + "update": [ + { + "action": "update", + "limit": 100, + "per": "total", + "period": "1m0s", + "resource": "worker", + "unlimited": false + }, + { + "action": "update", + "limit": 100, + "per": "auth-token", + "period": "1m0s", + "resource": "worker", + "unlimited": false + }, + { + "action": "update", + "limit": 100, + "per": "ip-address", + "period": "1m0s", + "resource": "worker", + "unlimited": false + } + ] + } + }, + "max_size": 296148, + "msg": "controller api rate limiter" + }, + "op": "controller.(rateLimiterConfig).writeSysEvent", + "version": "v0.1" + }, + "datacontentype": "application/cloudevents" +} From 35b91c66dedb9ccda1ee1a845d6cc0399bdbf504 Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Mon, 4 Dec 2023 17:25:16 +0000 Subject: [PATCH 21/26] refact(ratelimit): Rename MaxEntries to MaxQuotas --- internal/cmd/config/config.go | 10 ++--- internal/cmd/config/config_load_test.go | 10 ++--- internal/cmd/config/config_test.go | 16 +++---- internal/daemon/controller/rate_limiter.go | 4 +- .../daemon/controller/rate_limiter_test.go | 42 +++++++++---------- internal/daemon/controller/testing.go | 2 +- internal/ratelimit/config.go | 16 +++---- internal/ratelimit/config_test.go | 4 +- internal/ratelimit/limiter.go | 4 +- 9 files changed, 54 insertions(+), 54 deletions(-) diff --git a/internal/cmd/config/config.go b/internal/cmd/config/config.go index 144bb5769..ac5e2b9d8 100644 --- a/internal/cmd/config/config.go +++ b/internal/cmd/config/config.go @@ -215,9 +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"` - ApiRateLimitDisable bool `hcl:"api_rate_limit_disable"` + ApiRateLimits ratelimit.Configs `hcl:"-"` + ApiRateLimiterMaxQuotas int `hcl:"api_rate_limit_max_quotas"` + ApiRateLimitDisable bool `hcl:"api_rate_limit_disable"` // License is the license used by HCP builds License string `hcl:"license"` @@ -693,8 +693,8 @@ func Parse(d string) (*Config, error) { return nil, err } - if result.Controller.ApiRateLimiterMaxEntries <= 0 { - result.Controller.ApiRateLimiterMaxEntries = ratelimit.DefaultLimiterMaxEntries() + if result.Controller.ApiRateLimiterMaxQuotas <= 0 { + result.Controller.ApiRateLimiterMaxQuotas = ratelimit.DefaultLimiterMaxQuotas() } } diff --git a/internal/cmd/config/config_load_test.go b/internal/cmd/config/config_load_test.go index ac76012dd..949fed77e 100644 --- a/internal/cmd/config/config_load_test.go +++ b/internal/cmd/config/config_load_test.go @@ -419,7 +419,7 @@ func TestLoad(t *testing.T) { WorkerStatusGracePeriodDuration: 0, LivenessTimeToStaleDuration: 0, ApiRateLimits: make(ratelimit.Configs, 0), - ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), }, DevController: false, DevUiPassthroughDir: "", @@ -844,7 +844,7 @@ func TestLoad(t *testing.T) { WorkerStatusGracePeriodDuration: 0, LivenessTimeToStaleDuration: 0, ApiRateLimits: make(ratelimit.Configs, 0), - ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), }, DevController: false, DevUiPassthroughDir: "", @@ -1283,7 +1283,7 @@ func TestLoad(t *testing.T) { Unlimited: false, }, }, - ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), }, DevController: false, DevUiPassthroughDir: "", @@ -1703,7 +1703,7 @@ func TestLoad(t *testing.T) { WorkerStatusGracePeriodDuration: 0, LivenessTimeToStaleDuration: 0, ApiRateLimits: make(ratelimit.Configs, 0), - ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), }, DevController: false, DevUiPassthroughDir: "", @@ -1794,7 +1794,7 @@ func TestLoad(t *testing.T) { WorkerStatusGracePeriodDuration: 0, LivenessTimeToStaleDuration: 0, ApiRateLimits: make(ratelimit.Configs, 0), - ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), }, DevController: false, DevUiPassthroughDir: "", diff --git a/internal/cmd/config/config_test.go b/internal/cmd/config/config_test.go index 845edeb0f..0c4bce20e 100644 --- a/internal/cmd/config/config_test.go +++ b/internal/cmd/config/config_test.go @@ -110,10 +110,10 @@ func TestDevController(t *testing.T) { }, }, Controller: &Controller{ - Name: "dev-controller", - Description: "A default controller created in dev mode", - ApiRateLimits: make(ratelimit.Configs, 0), - ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), + Name: "dev-controller", + Description: "A default controller created in dev mode", + ApiRateLimits: make(ratelimit.Configs, 0), + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), }, DevController: true, } @@ -485,10 +485,10 @@ func TestDevCombined(t *testing.T) { }, }, Controller: &Controller{ - Name: "dev-controller", - Description: "A default controller created in dev mode", - ApiRateLimits: make(ratelimit.Configs, 0), - ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), + Name: "dev-controller", + Description: "A default controller created in dev mode", + ApiRateLimits: make(ratelimit.Configs, 0), + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), }, DevController: true, Worker: &Worker{ diff --git a/internal/daemon/controller/rate_limiter.go b/internal/daemon/controller/rate_limiter.go index f32e4b773..c6301943f 100644 --- a/internal/daemon/controller/rate_limiter.go +++ b/internal/daemon/controller/rate_limiter.go @@ -141,7 +141,7 @@ func (c *Controller) initializeRateLimiter(conf *config.Config) error { rlConfig, err := newRateLimiterConfig( c.baseContext, conf.Controller.ApiRateLimits, - conf.Controller.ApiRateLimiterMaxEntries, + conf.Controller.ApiRateLimiterMaxQuotas, conf.Controller.ApiRateLimitDisable, ) if err != nil { @@ -189,7 +189,7 @@ func (c *Controller) ReloadRateLimiter(newConfig *config.Config) error { rlConfig, err := newRateLimiterConfig( c.baseContext, newConfig.Controller.ApiRateLimits, - newConfig.Controller.ApiRateLimiterMaxEntries, + newConfig.Controller.ApiRateLimiterMaxQuotas, newConfig.Controller.ApiRateLimitDisable, ) if err != nil { diff --git a/internal/daemon/controller/rate_limiter_test.go b/internal/daemon/controller/rate_limiter_test.go index 704ac3976..b0112858b 100644 --- a/internal/daemon/controller/rate_limiter_test.go +++ b/internal/daemon/controller/rate_limiter_test.go @@ -60,7 +60,7 @@ func Test_newRateLimiterConfig(t *testing.T) { { "defaults", nil, - ratelimit.DefaultLimiterMaxEntries(), + ratelimit.DefaultLimiterMaxQuotas(), false, &rateLimiterConfig{ maxSize: 296148, @@ -82,7 +82,7 @@ func Test_newRateLimiterConfig(t *testing.T) { Unlimited: false, }, }, - ratelimit.DefaultLimiterMaxEntries(), + ratelimit.DefaultLimiterMaxQuotas(), true, nil, fmt.Errorf("controller.newRateLimiterConfig: disabled rate limiter with rate limit configs: configuration issue: error #5000"), @@ -131,7 +131,7 @@ func TestController_initializeRateLimiter(t *testing.T) { "defaults", &config.Config{ Controller: &config.Controller{ - ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), }, }, false, @@ -151,8 +151,8 @@ func TestController_initializeRateLimiter(t *testing.T) { Unlimited: false, }, }, - ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), - ApiRateLimitDisable: true, + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), + ApiRateLimitDisable: true, }, }, false, @@ -216,7 +216,7 @@ func TestControllerReloadRateLimiter(t *testing.T) { func() *Controller { conf := &config.Config{ Controller: &config.Controller{ - ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), }, } c := &Controller{ @@ -241,7 +241,7 @@ func TestControllerReloadRateLimiter(t *testing.T) { Unlimited: false, }, }, - ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), }, }, true, @@ -252,7 +252,7 @@ func TestControllerReloadRateLimiter(t *testing.T) { func() *Controller { conf := &config.Config{ Controller: &config.Controller{ - ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), }, } c := &Controller{ @@ -267,7 +267,7 @@ func TestControllerReloadRateLimiter(t *testing.T) { }(), &config.Config{ Controller: &config.Controller{ - ApiRateLimiterMaxEntries: 3000, + ApiRateLimiterMaxQuotas: 3000, }, }, true, @@ -278,7 +278,7 @@ func TestControllerReloadRateLimiter(t *testing.T) { func() *Controller { conf := &config.Config{ Controller: &config.Controller{ - ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), }, } c := &Controller{ @@ -319,7 +319,7 @@ func TestControllerReloadRateLimiter(t *testing.T) { }(), &config.Config{ Controller: &config.Controller{ - ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), }, }, true, @@ -330,7 +330,7 @@ func TestControllerReloadRateLimiter(t *testing.T) { func() *Controller { conf := &config.Config{ Controller: &config.Controller{ - ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), }, } c := &Controller{ @@ -345,7 +345,7 @@ func TestControllerReloadRateLimiter(t *testing.T) { }(), &config.Config{ Controller: &config.Controller{ - ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), ApiRateLimits: ratelimit.Configs{ { Resources: []string{"*"}, @@ -367,7 +367,7 @@ func TestControllerReloadRateLimiter(t *testing.T) { func() *Controller { conf := &config.Config{ Controller: &config.Controller{ - ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), }, } c := &Controller{ @@ -382,7 +382,7 @@ func TestControllerReloadRateLimiter(t *testing.T) { }(), &config.Config{ Controller: &config.Controller{ - ApiRateLimiterMaxEntries: 0, + ApiRateLimiterMaxQuotas: 0, ApiRateLimits: ratelimit.Configs{ { Resources: []string{"*"}, @@ -403,7 +403,7 @@ func TestControllerReloadRateLimiter(t *testing.T) { func() *Controller { conf := &config.Config{ Controller: &config.Controller{ - ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), }, } c := &Controller{ @@ -418,7 +418,7 @@ func TestControllerReloadRateLimiter(t *testing.T) { }(), &config.Config{ Controller: &config.Controller{ - ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), }, }, false, @@ -429,7 +429,7 @@ func TestControllerReloadRateLimiter(t *testing.T) { func() *Controller { conf := &config.Config{ Controller: &config.Controller{ - ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), }, } c := &Controller{ @@ -453,7 +453,7 @@ func TestControllerReloadRateLimiter(t *testing.T) { func() *Controller { conf := &config.Config{ Controller: &config.Controller{ - ApiRateLimiterMaxEntries: ratelimit.DefaultLimiterMaxEntries(), + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), }, } c := &Controller{ @@ -522,7 +522,7 @@ func Test_rateLimiterConfig_writeSysEvent(t *testing.T) { func() { event.TestResetSystEventer(t) }, c.AllEvents.Name(), nil, - ratelimit.DefaultLimiterMaxEntries(), + ratelimit.DefaultLimiterMaxQuotas(), false, }, { @@ -558,7 +558,7 @@ func Test_rateLimiterConfig_writeSysEvent(t *testing.T) { Unlimited: false, }, }, - ratelimit.DefaultLimiterMaxEntries(), + ratelimit.DefaultLimiterMaxQuotas(), false, }, { diff --git a/internal/daemon/controller/testing.go b/internal/daemon/controller/testing.go index ac3bb7d22..8fb99e592 100644 --- a/internal/daemon/controller/testing.go +++ b/internal/daemon/controller/testing.go @@ -657,7 +657,7 @@ func TestControllerConfig(t testing.TB, ctx context.Context, tc *TestController, } } opts.Config.Controller.Scheduler.JobRunIntervalDuration = opts.SchedulerRunJobInterval - opts.Config.Controller.ApiRateLimiterMaxEntries = ratelimit.DefaultLimiterMaxEntries() + opts.Config.Controller.ApiRateLimiterMaxQuotas = ratelimit.DefaultLimiterMaxQuotas() if opts.EnableEventing { opts.Config.Eventing = &event.EventerConfig{ diff --git a/internal/ratelimit/config.go b/internal/ratelimit/config.go index 9162d5c2f..ac2453394 100644 --- a/internal/ratelimit/config.go +++ b/internal/ratelimit/config.go @@ -50,17 +50,17 @@ const ( DefaultListPeriod = time.Second * 30 ) -// defaultLimiterMaxEntries is the default maximum number of quotas that +// 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 defaultLimiterMaxEntries int +var defaultLimiterMaxQuotas int -var initDefaultLimiterMaxEntries sync.Once +var initDefaultLimiterMaxQuotas sync.Once -// DefaultLimiterMaxEntries returns the default maximum number of quotas that +// DefaultLimiterMaxQuotas returns the default maximum number of quotas that // can be tracked by the rate limiter. -func DefaultLimiterMaxEntries() int { - initDefaultLimiterMaxEntries.Do(func() { +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 @@ -86,11 +86,11 @@ func DefaultLimiterMaxEntries() int { endpointCount += len(actions) } - defaultLimiterMaxEntries = (endpointCount * quotasPerInTotal) + + defaultLimiterMaxQuotas = (endpointCount * quotasPerInTotal) + (endpointCount * quotasPerAuthToken) + (endpointCount * quotasPerIpAddress) }) - return defaultLimiterMaxEntries + return defaultLimiterMaxQuotas } // Config is used to configure rate limits. Each config is used to specify diff --git a/internal/ratelimit/config_test.go b/internal/ratelimit/config_test.go index 54c4f20b3..3493a40f0 100644 --- a/internal/ratelimit/config_test.go +++ b/internal/ratelimit/config_test.go @@ -779,7 +779,7 @@ func TestConfigsLimits(t *testing.T) { } } -func TestDefaulLimiterMaxEntries(t *testing.T) { +func TestDefaulLimiterMaxQuotas(t *testing.T) { var want int var endpointCount int @@ -795,6 +795,6 @@ func TestDefaulLimiterMaxEntries(t *testing.T) { } want = endpointCount*2000 + endpointCount - got := DefaultLimiterMaxEntries() + got := DefaultLimiterMaxQuotas() assert.Equal(t, want, got) } diff --git a/internal/ratelimit/limiter.go b/internal/ratelimit/limiter.go index 78f25ce37..5be0f2e75 100644 --- a/internal/ratelimit/limiter.go +++ b/internal/ratelimit/limiter.go @@ -19,10 +19,10 @@ type Limiter interface { } // NewLimiter creates a rate.Limiter. -func NewLimiter(limits []rate.Limit, maxEntries int) (*rate.Limiter, error) { +func NewLimiter(limits []rate.Limit, maxQuotas int) (*rate.Limiter, error) { return rate.NewLimiter( limits, - maxEntries, + maxQuotas, rate.WithQuotaStorageUsageMetric(rateLimitQuotaUsage), rate.WithQuotaStorageCapacityMetric(rateLimitQuotaStorageCapacity), ) From da5dd383ad08d3977bdf94a40158526238b0f514 Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Mon, 4 Dec 2023 18:04:38 +0000 Subject: [PATCH 22/26] feat(api): Use Retry-After header when retrying a request If the api.Client receives a 429 or 503 along with a Retry-After header, the client will automatically wait the amount of time specified, and then re-try the request. --- api/client.go | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/api/client.go b/api/client.go index 3cd67871b..cfb20229e 100644 --- a/api/client.go +++ b/api/client.go @@ -136,6 +136,26 @@ type TLSConfig struct { Insecure bool } +// RateLimitLinearJitterBackoff wraps the retryablehttp.LinearJitterBackoff. +// It first checks if the response status code is http.StatusTooManyRequests +// (HTTP Code 429) or http.StatusServiceUnavailable (HTTP Code 503). If it is +// and the response contains a Retry-After response header, it will wait the +// amount of time specified by the header. Otherwise, this calls +// LinearJitterBackoff. +// See: https://pkg.go.dev/github.com/hashicorp/go-retryablehttp#LinearJitterBackoff +func RateLimitLinearJitterBackoff(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration { + if resp != nil { + if resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode == http.StatusServiceUnavailable { + if s, ok := resp.Header["Retry-After"]; ok { + if sleep, err := strconv.ParseInt(s[0], 10, 64); err == nil { + return time.Second * time.Duration(sleep) + } + } + } + } + return retryablehttp.LinearJitterBackoff(min, max, attemptNum, resp) +} + // DefaultConfig returns a default configuration for the client. It is // safe to modify the return value of this function. // @@ -163,7 +183,7 @@ func DefaultConfig() (*Config, error) { MinVersion: tls.VersionTLS12, } - config.Backoff = retryablehttp.LinearJitterBackoff + config.Backoff = RateLimitLinearJitterBackoff config.MaxRetries = 2 config.Headers = make(http.Header) From 6e233c879f145088da9d225bbc7b2c91f6b63b2e Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Mon, 4 Dec 2023 21:35:20 +0000 Subject: [PATCH 23/26] fix(ratelimit): Round up when setting Retry-After header Previously this would truncate to the nearest integer, which could result in clients retrying before the quota has reset, and getting rate limited again. This also includes an update from go-rate that will round up when reporting the reset time for the RateLimit header. Note that in the case of a 503, the Retry-After header was already being set correctly. See: https://github.com/hashicorp/go-rate/pull/8 --- go.mod | 2 +- go.sum | 4 +-- .../controller_ratelimit_reload_test.go | 28 +++++++++---------- internal/ratelimit/handler.go | 2 +- internal/ratelimit/handler_test.go | 16 +++++------ 5 files changed, 26 insertions(+), 26 deletions(-) diff --git a/go.mod b/go.mod index 2306fad94..0dc95d8f4 100644 --- a/go.mod +++ b/go.mod @@ -94,7 +94,7 @@ require ( github.com/hashicorp/cap/ldap v0.0.0-20231012003312-273118a6e3b8 github.com/hashicorp/dbassert v0.0.0-20231012105025-1bc1bd88e22b github.com/hashicorp/go-kms-wrapping/extras/kms/v2 v2.0.0-20231124110655-4315424d22d0 - github.com/hashicorp/go-rate v0.0.0-20231130213406-dd41f530f095 + github.com/hashicorp/go-rate v0.0.0-20231204194614-cc8d401f70ab github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/nodeenrollment v0.2.9 github.com/jackc/pgx/v5 v5.4.3 diff --git a/go.sum b/go.sum index a7fcad51d..44c2385bb 100644 --- a/go.sum +++ b/go.sum @@ -260,8 +260,8 @@ github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9 github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= github.com/hashicorp/go-plugin v1.5.2 h1:aWv8eimFqWlsEiMrYZdPYl+FdHaBJSN4AWwGWfT1G2Y= github.com/hashicorp/go-plugin v1.5.2/go.mod h1:w1sAEES3g3PuV/RzUrgow20W2uErMly84hhD3um1WL4= -github.com/hashicorp/go-rate v0.0.0-20231130213406-dd41f530f095 h1:l/scdJ8Uy823fhxFEdwlDR5NkDRdoOOaH6g36Uqz1AM= -github.com/hashicorp/go-rate v0.0.0-20231130213406-dd41f530f095/go.mod h1:uTQC66EFZuSKunHYx0hHIfdH3nhRrv7UOOE8Ug0yI0A= +github.com/hashicorp/go-rate v0.0.0-20231204194614-cc8d401f70ab h1:XIeGyyCCS80b5MKfU4E6ioLocIYtT4FmfdNH0uNYQi8= +github.com/hashicorp/go-rate v0.0.0-20231204194614-cc8d401f70ab/go.mod h1:uTQC66EFZuSKunHYx0hHIfdH3nhRrv7UOOE8Ug0yI0A= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZnpUv3/+BxzFA= github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= diff --git a/internal/cmd/commands/server/controller_ratelimit_reload_test.go b/internal/cmd/commands/server/controller_ratelimit_reload_test.go index 24fd44bdc..154b17f0d 100644 --- a/internal/cmd/commands/server/controller_ratelimit_reload_test.go +++ b/internal/cmd/commands/server/controller_ratelimit_reload_test.go @@ -233,7 +233,7 @@ func TestRealodControllerRateLimits(t *testing.T) { require.NoError(t, err) // unauthed request, so we expect a 400 assert.Equal(t, http.StatusBadRequest, r.StatusCode) - assert.Equal(t, `limit=2, remaining=1, reset=59`, r.Header.Get("Ratelimit")) + assert.Equal(t, `limit=2, remaining=1, reset=60`, r.Header.Get("Ratelimit")) assert.Equal(t, `2;w=60;comment="total", 60;w=30;comment="ip-address", 60;w=30;comment="auth-token"`, r.Header.Get("Ratelimit-Policy")) r, err = c.Do(func() *http.Request { @@ -244,7 +244,7 @@ func TestRealodControllerRateLimits(t *testing.T) { require.NoError(t, err) // unauthed request, so we expect a 400 assert.Equal(t, http.StatusBadRequest, r.StatusCode) - assert.Equal(t, `limit=2, remaining=0, reset=59`, r.Header.Get("Ratelimit")) + assert.Equal(t, `limit=2, remaining=0, reset=60`, r.Header.Get("Ratelimit")) assert.Equal(t, `2;w=60;comment="total", 60;w=30;comment="ip-address", 60;w=30;comment="auth-token"`, r.Header.Get("Ratelimit-Policy")) r, err = c.Do(func() *http.Request { @@ -255,7 +255,7 @@ func TestRealodControllerRateLimits(t *testing.T) { require.NoError(t, err) // out of quota, so we expect a 429 assert.Equal(t, http.StatusTooManyRequests, r.StatusCode) - assert.Equal(t, `limit=2, remaining=0, reset=59`, r.Header.Get("Ratelimit")) + assert.Equal(t, `limit=2, remaining=0, reset=60`, r.Header.Get("Ratelimit")) assert.Equal(t, `2;w=60;comment="total", 60;w=30;comment="ip-address", 60;w=30;comment="auth-token"`, r.Header.Get("Ratelimit-Policy")) cmd.SighupCh <- struct{}{} @@ -275,7 +275,7 @@ func TestRealodControllerRateLimits(t *testing.T) { require.NoError(t, err) // unauthed request, so we expect a 400 assert.Equal(t, http.StatusBadRequest, r.StatusCode) - assert.Equal(t, `limit=5, remaining=4, reset=59`, r.Header.Get("Ratelimit")) + assert.Equal(t, `limit=5, remaining=4, reset=60`, r.Header.Get("Ratelimit")) assert.Equal(t, `5;w=60;comment="total", 60;w=30;comment="ip-address", 60;w=30;comment="auth-token"`, r.Header.Get("Ratelimit-Policy")) cmd.ShutdownCh <- struct{}{} @@ -328,7 +328,7 @@ func TestRealodControllerRateLimitsSameConfig(t *testing.T) { require.NoError(t, err) // unauthed request, so we expect a 400 assert.Equal(t, http.StatusBadRequest, r.StatusCode) - assert.Equal(t, `limit=2, remaining=1, reset=59`, r.Header.Get("Ratelimit")) + assert.Equal(t, `limit=2, remaining=1, reset=60`, r.Header.Get("Ratelimit")) assert.Equal(t, `2;w=60;comment="total", 60;w=30;comment="ip-address", 60;w=30;comment="auth-token"`, r.Header.Get("Ratelimit-Policy")) r, err = c.Do(func() *http.Request { @@ -339,7 +339,7 @@ func TestRealodControllerRateLimitsSameConfig(t *testing.T) { require.NoError(t, err) // unauthed request, so we expect a 400 assert.Equal(t, http.StatusBadRequest, r.StatusCode) - assert.Equal(t, `limit=2, remaining=0, reset=59`, r.Header.Get("Ratelimit")) + assert.Equal(t, `limit=2, remaining=0, reset=60`, r.Header.Get("Ratelimit")) assert.Equal(t, `2;w=60;comment="total", 60;w=30;comment="ip-address", 60;w=30;comment="auth-token"`, r.Header.Get("Ratelimit-Policy")) r, err = c.Do(func() *http.Request { @@ -350,7 +350,7 @@ func TestRealodControllerRateLimitsSameConfig(t *testing.T) { require.NoError(t, err) // out of quota, so we expect a 429 assert.Equal(t, http.StatusTooManyRequests, r.StatusCode) - assert.Equal(t, `limit=2, remaining=0, reset=59`, r.Header.Get("Ratelimit")) + assert.Equal(t, `limit=2, remaining=0, reset=60`, r.Header.Get("Ratelimit")) assert.Equal(t, `2;w=60;comment="total", 60;w=30;comment="ip-address", 60;w=30;comment="auth-token"`, r.Header.Get("Ratelimit-Policy")) cmd.SighupCh <- struct{}{} @@ -370,7 +370,7 @@ func TestRealodControllerRateLimitsSameConfig(t *testing.T) { require.NoError(t, err) // should still be rate limited, so 429 assert.Equal(t, http.StatusTooManyRequests, r.StatusCode) - assert.Equal(t, `limit=2, remaining=0, reset=59`, r.Header.Get("Ratelimit")) + assert.Equal(t, `limit=2, remaining=0, reset=60`, r.Header.Get("Ratelimit")) assert.Equal(t, `2;w=60;comment="total", 60;w=30;comment="ip-address", 60;w=30;comment="auth-token"`, r.Header.Get("Ratelimit-Policy")) cmd.ShutdownCh <- struct{}{} @@ -426,7 +426,7 @@ func TestRealodControllerRateLimitsDisable(t *testing.T) { require.NoError(t, err) // unauthed request, so we expect a 400 assert.Equal(t, http.StatusBadRequest, r.StatusCode) - assert.Equal(t, `limit=2, remaining=1, reset=59`, r.Header.Get("Ratelimit")) + assert.Equal(t, `limit=2, remaining=1, reset=60`, r.Header.Get("Ratelimit")) assert.Equal(t, `2;w=60;comment="total", 60;w=30;comment="ip-address", 60;w=30;comment="auth-token"`, r.Header.Get("Ratelimit-Policy")) r, err = c.Do(func() *http.Request { @@ -437,7 +437,7 @@ func TestRealodControllerRateLimitsDisable(t *testing.T) { require.NoError(t, err) // unauthed request, so we expect a 400 assert.Equal(t, http.StatusBadRequest, r.StatusCode) - assert.Equal(t, `limit=2, remaining=0, reset=59`, r.Header.Get("Ratelimit")) + assert.Equal(t, `limit=2, remaining=0, reset=60`, r.Header.Get("Ratelimit")) assert.Equal(t, `2;w=60;comment="total", 60;w=30;comment="ip-address", 60;w=30;comment="auth-token"`, r.Header.Get("Ratelimit-Policy")) r, err = c.Do(func() *http.Request { @@ -448,7 +448,7 @@ func TestRealodControllerRateLimitsDisable(t *testing.T) { require.NoError(t, err) // out of quota, so we expect a 429 assert.Equal(t, http.StatusTooManyRequests, r.StatusCode) - assert.Equal(t, `limit=2, remaining=0, reset=59`, r.Header.Get("Ratelimit")) + assert.Equal(t, `limit=2, remaining=0, reset=60`, r.Header.Get("Ratelimit")) assert.Equal(t, `2;w=60;comment="total", 60;w=30;comment="ip-address", 60;w=30;comment="auth-token"`, r.Header.Get("Ratelimit-Policy")) cmd.SighupCh <- struct{}{} @@ -545,7 +545,7 @@ func TestRealodControllerRateLimitsEnable(t *testing.T) { require.NoError(t, err) // unauthed request, so we expect a 400 assert.Equal(t, http.StatusBadRequest, r.StatusCode) - assert.Equal(t, `limit=2, remaining=1, reset=59`, r.Header.Get("Ratelimit")) + assert.Equal(t, `limit=2, remaining=1, reset=60`, r.Header.Get("Ratelimit")) assert.Equal(t, `2;w=60;comment="total", 60;w=30;comment="ip-address", 60;w=30;comment="auth-token"`, r.Header.Get("Ratelimit-Policy")) r, err = c.Do(func() *http.Request { @@ -556,7 +556,7 @@ func TestRealodControllerRateLimitsEnable(t *testing.T) { require.NoError(t, err) // unauthed request, so we expect a 400 assert.Equal(t, http.StatusBadRequest, r.StatusCode) - assert.Equal(t, `limit=2, remaining=0, reset=59`, r.Header.Get("Ratelimit")) + assert.Equal(t, `limit=2, remaining=0, reset=60`, r.Header.Get("Ratelimit")) assert.Equal(t, `2;w=60;comment="total", 60;w=30;comment="ip-address", 60;w=30;comment="auth-token"`, r.Header.Get("Ratelimit-Policy")) r, err = c.Do(func() *http.Request { @@ -567,7 +567,7 @@ func TestRealodControllerRateLimitsEnable(t *testing.T) { require.NoError(t, err) // out of quota, so we expect a 429 assert.Equal(t, http.StatusTooManyRequests, r.StatusCode) - assert.Equal(t, `limit=2, remaining=0, reset=59`, r.Header.Get("Ratelimit")) + assert.Equal(t, `limit=2, remaining=0, reset=60`, r.Header.Get("Ratelimit")) assert.Equal(t, `2;w=60;comment="total", 60;w=30;comment="ip-address", 60;w=30;comment="auth-token"`, r.Header.Get("Ratelimit-Policy")) cmd.ShutdownCh <- struct{}{} diff --git a/internal/ratelimit/handler.go b/internal/ratelimit/handler.go index 626daa221..2244be7b2 100644 --- a/internal/ratelimit/handler.go +++ b/internal/ratelimit/handler.go @@ -172,7 +172,7 @@ func Handler(ctx context.Context, f LimiterFunc, next http.Handler) http.Handler } if !allowed { - rw.Header().Add("Retry-After", fmt.Sprintf("%.0f", quota.ResetsIn().Seconds())) + rw.Header().Add("Retry-After", fmt.Sprintf("%.0f", math.Ceil(quota.ResetsIn().Seconds()))) rw.WriteHeader(http.StatusTooManyRequests) return } diff --git a/internal/ratelimit/handler_test.go b/internal/ratelimit/handler_test.go index a4019b7ff..34d9c1b71 100644 --- a/internal/ratelimit/handler_test.go +++ b/internal/ratelimit/handler_test.go @@ -72,7 +72,7 @@ func TestHandler(t *testing.T) { http.StatusOK, http.Header{ "RateLimit-Policy": []string{`10;w=60;comment="total", 10;w=60;comment="ip-address", 10;w=60;comment="auth-token"`}, - "RateLimit": []string{`limit=10, remaining=9, reset=59`}, + "RateLimit": []string{`limit=10, remaining=9, reset=60`}, }, }, { @@ -114,7 +114,7 @@ func TestHandler(t *testing.T) { http.StatusOK, http.Header{ "RateLimit-Policy": []string{`10;w=60;comment="total", 10;w=60;comment="ip-address", 10;w=60;comment="auth-token"`}, - "RateLimit": []string{`limit=10, remaining=9, reset=59`}, + "RateLimit": []string{`limit=10, remaining=9, reset=60`}, }, }, { @@ -156,7 +156,7 @@ func TestHandler(t *testing.T) { http.StatusOK, http.Header{ "RateLimit-Policy": []string{`10;w=60;comment="total", 10;w=60;comment="ip-address", 10;w=60;comment="auth-token"`}, - "RateLimit": []string{`limit=10, remaining=9, reset=59`}, + "RateLimit": []string{`limit=10, remaining=9, reset=60`}, }, }, { @@ -198,7 +198,7 @@ func TestHandler(t *testing.T) { http.StatusOK, http.Header{ "RateLimit-Policy": []string{`10;w=60;comment="total", 10;w=60;comment="ip-address", 10;w=60;comment="auth-token"`}, - "RateLimit": []string{`limit=10, remaining=9, reset=59`}, + "RateLimit": []string{`limit=10, remaining=9, reset=60`}, }, }, { @@ -240,7 +240,7 @@ func TestHandler(t *testing.T) { http.StatusOK, http.Header{ "RateLimit-Policy": []string{`10;w=60;comment="total", 10;w=60;comment="ip-address", 10;w=60;comment="auth-token"`}, - "RateLimit": []string{`limit=10, remaining=9, reset=59`}, + "RateLimit": []string{`limit=10, remaining=9, reset=60`}, }, }, { @@ -282,7 +282,7 @@ func TestHandler(t *testing.T) { http.StatusOK, http.Header{ "RateLimit-Policy": []string{`10;w=60;comment="total", 10;w=60;comment="ip-address", 10;w=60;comment="auth-token"`}, - "RateLimit": []string{`limit=10, remaining=9, reset=59`}, + "RateLimit": []string{`limit=10, remaining=9, reset=60`}, }, }, { @@ -324,7 +324,7 @@ func TestHandler(t *testing.T) { http.StatusOK, http.Header{ "RateLimit-Policy": []string{`10;w=60;comment="total", 10;w=60;comment="ip-address", 10;w=60;comment="auth-token"`}, - "RateLimit": []string{`limit=10, remaining=9, reset=59`}, + "RateLimit": []string{`limit=10, remaining=9, reset=60`}, }, }, { @@ -375,7 +375,7 @@ func TestHandler(t *testing.T) { http.Header{ "Retry-After": []string{"60"}, "RateLimit-Policy": []string{`2;w=60;comment="total", 2;w=60;comment="ip-address", 2;w=60;comment="auth-token"`}, - "RateLimit": []string{`limit=2, remaining=0, reset=59`}, + "RateLimit": []string{`limit=2, remaining=0, reset=60`}, }, }, { From a7730b02ac41391de9b6349873b13709f5c5cf54 Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Mon, 4 Dec 2023 21:38:31 +0000 Subject: [PATCH 24/26] test(host_catalog): Fix flaky assertion --- .../handlers/host_catalogs/host_catalog_service_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/daemon/controller/handlers/host_catalogs/host_catalog_service_test.go b/internal/daemon/controller/handlers/host_catalogs/host_catalog_service_test.go index a33ef8f5a..9500a4df9 100644 --- a/internal/daemon/controller/handlers/host_catalogs/host_catalog_service_test.go +++ b/internal/daemon/controller/handlers/host_catalogs/host_catalog_service_test.go @@ -1551,6 +1551,9 @@ func TestUpdate_Plugin(t *testing.T) { cmpopts.SortSlices(func(a, b protocmp.Message) bool { return a.String() < b.String() }), + cmpopts.SortSlices(func(a, b *structpb.Value) bool { + return a.String() < b.String() + }), )) }, }, From cd21f10a3bc3659217662459a8c0657dd165e0bd Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Mon, 4 Dec 2023 22:23:50 +0000 Subject: [PATCH 25/26] chore(lint): Update version of golangci-lint Updating this helps resolve some false positives that were happening in CI when detecting linter issues on PRs. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 98cd82253..7cb5ed6b9 100644 --- a/Makefile +++ b/Makefile @@ -37,7 +37,7 @@ golangci-lint: if [ "$(GOLINT_INSTALLED)" = "" ]; then \ curl -sSfL \ - https://raw.githubusercontent.com/golangci/golangci-lint/9a8a056e9fe49c0e9ed2287aedce1022c79a115b/install.sh | sh -s -- -b $(GO_PATH)/bin v1.52.2; \ + https://raw.githubusercontent.com/golangci/golangci-lint/9a8a056e9fe49c0e9ed2287aedce1022c79a115b/install.sh | sh -s -- -b $(GO_PATH)/bin v1.55.2; \ fi; .PHONY: cleangen From 758e7b8bf1bb0534a6ec2175dfdac563cbceb4e6 Mon Sep 17 00:00:00 2001 From: Timothy Messier Date: Mon, 4 Dec 2023 22:40:39 +0000 Subject: [PATCH 26/26] test(credentials): Disable rate limiting for test This test quickly polls a list endpoint, which exceeds the default rate limits before the test can complete in CI. --- internal/daemon/controller/testing.go | 6 ++++++ internal/tests/api/credentials/credentials_test.go | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/internal/daemon/controller/testing.go b/internal/daemon/controller/testing.go index 8fb99e592..04431068e 100644 --- a/internal/daemon/controller/testing.go +++ b/internal/daemon/controller/testing.go @@ -495,6 +495,8 @@ type TestControllerOpts struct { // Toggle worker auth debugging WorkerAuthDebuggingEnabled *atomic.Bool + + DisableRateLimiting bool } func NewTestController(t testing.TB, opts *TestControllerOpts) *TestController { @@ -806,6 +808,10 @@ func TestControllerConfig(t testing.TB, ctx context.Context, tc *TestController, } } + if opts.DisableRateLimiting { + opts.Config.Controller.ApiRateLimitDisable = true + } + return &Config{ RawConfig: opts.Config, Server: tc.b, diff --git a/internal/tests/api/credentials/credentials_test.go b/internal/tests/api/credentials/credentials_test.go index 397eeed2e..e98f58725 100644 --- a/internal/tests/api/credentials/credentials_test.go +++ b/internal/tests/api/credentials/credentials_test.go @@ -415,7 +415,13 @@ func TestUpdateAfterKeyRotation(t *testing.T) { // This prevents us from running tests in parallel. tg.SetupSuiteTargetFilters(t) - tc := controller.NewTestController(t, &controller.TestControllerOpts{SchedulerRunJobInterval: 100 * time.Millisecond}) + tc := controller.NewTestController( + t, + &controller.TestControllerOpts{ + SchedulerRunJobInterval: 100 * time.Millisecond, + DisableRateLimiting: true, + }, + ) ctx := tc.Context() client := tc.Client() token := tc.Token()