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 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) 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/go.mod b/go.mod index 07ed60968..0dc95d8f4 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-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 86b7dcd1b..44c2385bb 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-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/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/cmd/commands/server/controller_ratelimit_reload_test.go b/internal/cmd/commands/server/controller_ratelimit_reload_test.go new file mode 100644 index 000000000..154b17f0d --- /dev/null +++ b/internal/cmd/commands/server/controller_ratelimit_reload_test.go @@ -0,0 +1,575 @@ +// 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" +} +` + + 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" +} +` +) + +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 ready 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=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 { + 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=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 { + 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=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{}{} + 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=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{}{} + 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=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 { + 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=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 { + 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=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{}{} + 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=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{}{} + 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=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 { + 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=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 { + 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=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{}{} + 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=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 { + 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=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 { + 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=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{}{} + wg.Wait() +} diff --git a/internal/cmd/commands/server/server.go b/internal/cmd/commands/server/server.go index 19187e993..b1aa44472 100644 --- a/internal/cmd/commands/server/server.go +++ b/internal/cmd/commands/server/server.go @@ -841,6 +841,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 +956,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) +} + // 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/cmd/config/config.go b/internal/cmd/config/config.go index 945e48d86..ac5e2b9d8 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,10 @@ type Controller struct { // TODO: This field is currently internal. SchedulerRunJobInterval time.Duration `hcl:"-"` + 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"` } @@ -682,6 +687,15 @@ func Parse(d string) (*Config, error) { } } + + result.Controller.ApiRateLimits, err = parseApiRateLimits(obj.Node) + if err != nil { + return nil, err + } + + if result.Controller.ApiRateLimiterMaxQuotas <= 0 { + result.Controller.ApiRateLimiterMaxQuotas = ratelimit.DefaultLimiterMaxQuotas() + } } // Parse worker tags @@ -944,6 +958,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..949fed77e 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), + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), }, DevController: false, DevUiPassthroughDir: "", @@ -839,6 +843,8 @@ func TestLoad(t *testing.T) { GracefulShutdownWaitDuration: 0, WorkerStatusGracePeriodDuration: 0, LivenessTimeToStaleDuration: 0, + ApiRateLimits: make(ratelimit.Configs, 0), + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), }, 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, + }, + }, + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), }, DevController: false, DevUiPassthroughDir: "", @@ -1675,6 +1702,8 @@ func TestLoad(t *testing.T) { GracefulShutdownWaitDuration: 0, WorkerStatusGracePeriodDuration: 0, LivenessTimeToStaleDuration: 0, + ApiRateLimits: make(ratelimit.Configs, 0), + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), }, DevController: false, DevUiPassthroughDir: "", @@ -1764,6 +1793,8 @@ func TestLoad(t *testing.T) { GracefulShutdownWaitDuration: 0, WorkerStatusGracePeriodDuration: 0, LivenessTimeToStaleDuration: 0, + ApiRateLimits: make(ratelimit.Configs, 0), + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), }, DevController: false, DevUiPassthroughDir: "", diff --git a/internal/cmd/config/config_test.go b/internal/cmd/config/config_test.go index 581b4c111..0c4bce20e 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), + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), }, 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), + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), }, 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" + } } 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/config.go b/internal/daemon/controller/config.go index 2e742f4a2..337af919d 100644 --- a/internal/daemon/controller/config.go +++ b/internal/daemon/controller/config.go @@ -21,4 +21,9 @@ type Config struct { DisableAuthorizationFailures bool // Override worker auth CA certificate lifetime for testing TestOverrideWorkerAuthCaCertificateLifetime time.Duration + + // 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 a56ed92bc..e38dc889b 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" @@ -119,6 +120,9 @@ type Controller struct { apiGrpcServerListener grpcServerListener apiGrpcGatewayTicket string + rateLimiter ratelimit.Limiter + rateLimiterMu sync.RWMutex + // Repo factory methods AuthTokenRepoFn common.AuthTokenRepoFactory VaultCredentialRepoFn common.VaultCredentialRepoFactory @@ -156,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"), @@ -245,6 +250,10 @@ func New(ctx context.Context, conf *Config) (*Controller, error) { } c.clusterListener = clusterListeners[0] + if err := c.initializeRateLimiter(conf.RawConfig); 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..7ef1d4a08 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.baseContext, c.getRateLimiter, grpcGwMux)) mux.Handle(uiPath, handleUi(c)) isUiRequest := func(req *http.Request) bool { @@ -410,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 diff --git a/internal/daemon/controller/handlers/accounts/account_service.go b/internal/daemon/controller/handlers/accounts/account_service.go index d537a4f7f..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" @@ -70,34 +71,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() { @@ -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/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/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 435524adc..853fc4d79 100644 --- a/internal/daemon/controller/handlers/authtokens/authtoken_service.go +++ b/internal/daemon/controller/handlers/authtokens/authtoken_service.go @@ -27,21 +27,26 @@ 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, - } + ) ) +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/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..f58d69ab2 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, @@ -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/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..beb9c5cba 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() { @@ -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/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..b73a42083 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, @@ -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/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..cc12412df 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() { @@ -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/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..22be14e28 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, - }, + ), }, } ) @@ -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_catalogs/host_catalog_service_test.go b/internal/daemon/controller/handlers/host_catalogs/host_catalog_service_test.go index f6c8c8a8a..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 @@ -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,20 @@ 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() + }), + cmpopts.SortSlices(func(a, b *structpb.Value) 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..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" @@ -40,7 +41,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 +49,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" @@ -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/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..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" ) @@ -37,24 +38,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" @@ -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/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..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" ) @@ -45,26 +46,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() { @@ -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/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..4a7551abe 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() { @@ -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/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..f385fac10 100644 --- a/internal/daemon/controller/handlers/scopes/scope_service.go +++ b/internal/daemon/controller/handlers/scopes/scope_service.go @@ -49,24 +49,25 @@ 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, - } + ) + // TODO: get this from action registry scopeCollectionTypeMapMap = map[string]map[resource.Type]action.ActionSet{ scope.Global.String(): { resource.AuthMethod: authmethods.CollectionActions, @@ -96,9 +97,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, }, } ) @@ -112,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. @@ -252,7 +261,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..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" ) @@ -19,19 +20,24 @@ 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, - } + ) ) +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 2032234ea..4c24a8878 100644 --- a/internal/daemon/controller/handlers/sessions/session_service.go +++ b/internal/daemon/controller/handlers/sessions/session_service.go @@ -29,21 +29,26 @@ 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, - } + ) ) +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/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..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" ) @@ -17,21 +18,26 @@ 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, - } + ) ) +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 a1e5a2ae1..2e95e52e1 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 @@ -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/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..f66b175c4 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() { @@ -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/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..16a1d154c 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 @@ -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 { 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/daemon/controller/rate_limiter.go b/internal/daemon/controller/rate_limiter.go new file mode 100644 index 000000000..c6301943f --- /dev/null +++ b/internal/daemon/controller/rate_limiter.go @@ -0,0 +1,225 @@ +// 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/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 + 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 +} + +// 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 { + 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.ApiRateLimiterMaxQuotas, + 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 + + rlConfig.writeSysEvent(c.baseContext) + 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.ApiRateLimiterMaxQuotas, + 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() + 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 new file mode 100644 index 000000000..b0112858b --- /dev/null +++ b/internal/daemon/controller/rate_limiter_test.go @@ -0,0 +1,671 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package controller + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "sync" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/boundary/internal/cmd/config" + "github.com/hashicorp/boundary/internal/event" + "github.com/hashicorp/boundary/internal/ratelimit" + "github.com/hashicorp/eventlogger/formatter_filters/cloudevents" + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/go-rate" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func testLogger(t *testing.T, testLock hclog.Locker) hclog.Logger { + t.Helper() + return hclog.New(&hclog.LoggerOptions{ + Mutex: testLock, + Name: "test", + JSONFormat: true, + }) +} + +func Test_newRateLimiterConfig(t *testing.T) { + ctx := context.Background() + + var configs ratelimit.Configs + defaultLimits, err := configs.Limits(ctx) + require.NoError(t, err) + + cases := []struct { + name string + configs ratelimit.Configs + maxSize int + disabled bool + want *rateLimiterConfig + wantErr error + }{ + { + "disabled", + nil, + 0, + true, + &rateLimiterConfig{disabled: true}, + nil, + }, + { + "defaults", + nil, + ratelimit.DefaultLimiterMaxQuotas(), + false, + &rateLimiterConfig{ + maxSize: 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.DefaultLimiterMaxQuotas(), + true, + nil, + fmt.Errorf("controller.newRateLimiterConfig: disabled rate limiter with rate limit configs: configuration issue: error #5000"), + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got, err := newRateLimiterConfig(ctx, tc.configs, tc.maxSize, tc.disabled) + if tc.wantErr != nil { + assert.EqualError(t, err, tc.wantErr.Error()) + return + } + require.NoError(t, err) + assert.Empty(t, cmp.Diff( + tc.want, + got, + cmp.AllowUnexported(rateLimiterConfig{}), + cmpopts.IgnoreFields(rateLimiterConfig{}, "limits"), + )) + assert.ElementsMatch(t, got.limits, tc.want.limits) + }) + } +} + +func TestController_initializeRateLimiter(t *testing.T) { + // Disabling eventing so reduce noise. + event.TestWithoutEventing(t) + + cases := []struct { + name string + conf *config.Config + wantNopLimiter bool + wantErr error + }{ + { + "disabled", + &config.Config{ + Controller: &config.Controller{ + ApiRateLimitDisable: true, + }, + }, + true, + nil, + }, + { + "defaults", + &config.Config{ + Controller: &config.Controller{ + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), + }, + }, + false, + nil, + }, + { + "invalid", + &config.Config{ + Controller: &config.Controller{ + ApiRateLimits: ratelimit.Configs{ + { + Resources: []string{"*"}, + Actions: []string{"*"}, + Per: "total", + Limit: 100, + Period: time.Minute, + Unlimited: false, + }, + }, + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), + ApiRateLimitDisable: true, + }, + }, + false, + fmt.Errorf("controller.newRateLimiterConfig: disabled rate limiter with rate limit configs: configuration issue: error #5000"), + }, + { + "nilConfig", + nil, + false, + fmt.Errorf("controller.(Controller).initializeRateLimiter: nil config: parameter violation: error #100"), + }, + { + "nilConfigController", + &config.Config{ + Controller: nil, + }, + false, + fmt.Errorf("controller.(Controller).initializeRateLimiter: nil config.Controller: parameter violation: error #100"), + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + c := &Controller{ + baseContext: context.Background(), + conf: &Config{ + RawConfig: tc.conf, + }, + } + err := c.initializeRateLimiter(tc.conf) + if tc.wantErr != nil { + assert.EqualError(t, err, tc.wantErr.Error()) + return + } + require.NoError(t, err) + + if tc.wantNopLimiter { + assert.Equal(t, rate.NopLimiter, c.rateLimiter) + return + } + + _, ok := c.rateLimiter.(*rate.Limiter) + assert.True(t, ok, "expected rate.Limiter") + assert.NotNil(t, c.rateLimiter) + }) + } +} + +func TestControllerReloadRateLimiter(t *testing.T) { + // Disabling eventing so reduce noise. + event.TestWithoutEventing(t) + + cases := []struct { + name string + c *Controller + conf *config.Config + wantNewLimiter bool + wantErr error + }{ + { + "newConfigs", + func() *Controller { + conf := &config.Config{ + Controller: &config.Controller{ + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), + }, + } + c := &Controller{ + baseContext: context.Background(), + conf: &Config{ + RawConfig: conf, + }, + } + err := c.initializeRateLimiter(conf) + require.NoError(t, err) + return c + }(), + &config.Config{ + Controller: &config.Controller{ + ApiRateLimits: ratelimit.Configs{ + { + Resources: []string{"*"}, + Actions: []string{"*"}, + Per: "total", + Limit: 100, + Period: time.Minute, + Unlimited: false, + }, + }, + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), + }, + }, + true, + nil, + }, + { + "newMaxSize", + func() *Controller { + conf := &config.Config{ + Controller: &config.Controller{ + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), + }, + } + c := &Controller{ + baseContext: context.Background(), + conf: &Config{ + RawConfig: conf, + }, + } + err := c.initializeRateLimiter(conf) + require.NoError(t, err) + return c + }(), + &config.Config{ + Controller: &config.Controller{ + ApiRateLimiterMaxQuotas: 3000, + }, + }, + true, + nil, + }, + { + "newDisabled", + func() *Controller { + conf := &config.Config{ + Controller: &config.Controller{ + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), + }, + } + c := &Controller{ + baseContext: context.Background(), + conf: &Config{ + RawConfig: conf, + }, + } + err := c.initializeRateLimiter(conf) + require.NoError(t, err) + return c + }(), + &config.Config{ + Controller: &config.Controller{ + ApiRateLimitDisable: true, + }, + }, + true, + nil, + }, + { + "newEnabled", + func() *Controller { + conf := &config.Config{ + Controller: &config.Controller{ + ApiRateLimitDisable: true, + }, + } + c := &Controller{ + baseContext: context.Background(), + conf: &Config{ + RawConfig: conf, + }, + } + err := c.initializeRateLimiter(conf) + require.NoError(t, err) + return c + }(), + &config.Config{ + Controller: &config.Controller{ + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), + }, + }, + true, + nil, + }, + { + "newInvalid", + func() *Controller { + conf := &config.Config{ + Controller: &config.Controller{ + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), + }, + } + c := &Controller{ + baseContext: context.Background(), + conf: &Config{ + RawConfig: conf, + }, + } + err := c.initializeRateLimiter(conf) + require.NoError(t, err) + return c + }(), + &config.Config{ + Controller: &config.Controller{ + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), + ApiRateLimits: ratelimit.Configs{ + { + Resources: []string{"*"}, + Actions: []string{"*"}, + Per: "total", + Limit: 100, + Period: time.Minute, + Unlimited: false, + }, + }, + ApiRateLimitDisable: true, + }, + }, + false, + fmt.Errorf("controller.newRateLimiterConfig: disabled rate limiter with rate limit configs: configuration issue: error #5000"), + }, + { + "newInvalidMaxSize", + func() *Controller { + conf := &config.Config{ + Controller: &config.Controller{ + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), + }, + } + c := &Controller{ + baseContext: context.Background(), + conf: &Config{ + RawConfig: conf, + }, + } + err := c.initializeRateLimiter(conf) + require.NoError(t, err) + return c + }(), + &config.Config{ + Controller: &config.Controller{ + ApiRateLimiterMaxQuotas: 0, + ApiRateLimits: ratelimit.Configs{ + { + Resources: []string{"*"}, + Actions: []string{"*"}, + Per: "total", + Limit: 100, + Period: time.Minute, + Unlimited: false, + }, + }, + }, + }, + false, + fmt.Errorf("controller.(Controller).ReloadRateLimiter: unknown, unknown: error #0: rate.NewLimiter: rate.newExpirableStore: max size must be greater than zero: invalid max size"), + }, + { + "newConfigsNoChange", + func() *Controller { + conf := &config.Config{ + Controller: &config.Controller{ + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), + }, + } + c := &Controller{ + baseContext: context.Background(), + conf: &Config{ + RawConfig: conf, + }, + } + err := c.initializeRateLimiter(conf) + require.NoError(t, err) + return c + }(), + &config.Config{ + Controller: &config.Controller{ + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), + }, + }, + false, + nil, + }, + { + "nilNewConfigController", + func() *Controller { + conf := &config.Config{ + Controller: &config.Controller{ + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), + }, + } + c := &Controller{ + baseContext: context.Background(), + conf: &Config{ + RawConfig: conf, + }, + } + err := c.initializeRateLimiter(conf) + require.NoError(t, err) + return c + }(), + &config.Config{ + Controller: nil, + }, + false, + fmt.Errorf("controller.(Controller).ReloadRateLimiter: nil config.Controller: parameter violation: error #100"), + }, + { + "nilNewConfig", + func() *Controller { + conf := &config.Config{ + Controller: &config.Controller{ + ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), + }, + } + c := &Controller{ + baseContext: context.Background(), + conf: &Config{ + RawConfig: conf, + }, + } + err := c.initializeRateLimiter(conf) + require.NoError(t, err) + return c + }(), + nil, + false, + fmt.Errorf("controller.(Controller).ReloadRateLimiter: nil config: parameter violation: error #100"), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + prevLimiter := tc.c.getRateLimiter() + err := tc.c.ReloadRateLimiter(tc.conf) + if tc.wantErr != nil { + assert.EqualError(t, err, tc.wantErr.Error()) + } else { + require.NoError(t, err) + } + if tc.wantNewLimiter { + assert.NotSame(t, prevLimiter, tc.c.getRateLimiter()) + return + } + assert.Same(t, prevLimiter, tc.c.getRateLimiter()) + }) + } +} + +func Test_rateLimiterConfig_writeSysEvent(t *testing.T) { + c := event.TestEventerConfig(t, t.Name()) + + testLock := &sync.Mutex{} + testLogger := testLogger(t, testLock) + e, err := event.NewEventer(testLogger, testLock, t.Name(), c.EventerConfig) + require.NoError(t, err) + + info := &event.RequestInfo{Id: "867-5309", EventId: "411"} + + testCtx, err := event.NewEventerContext(context.Background(), e) + require.NoError(t, err) + testCtx, err = event.NewRequestInfoContext(testCtx, info) + require.NoError(t, err) + + cases := []struct { + name string + setup func(n string) error + cleanup func() + sinkFileName string + configs ratelimit.Configs + maxSize int + disabled bool + }{ + { + "defaults", + func(n string) error { + return event.InitSysEventer(testLogger, testLock, n, event.WithEventerConfig(&c.EventerConfig)) + }, + func() { event.TestResetSystEventer(t) }, + c.AllEvents.Name(), + nil, + ratelimit.DefaultLimiterMaxQuotas(), + 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.DefaultLimiterMaxQuotas(), + 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" +} diff --git a/internal/daemon/controller/testing.go b/internal/daemon/controller/testing.go index 40c29fee8..04431068e 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" @@ -494,6 +495,8 @@ type TestControllerOpts struct { // Toggle worker auth debugging WorkerAuthDebuggingEnabled *atomic.Bool + + DisableRateLimiting bool } func NewTestController(t testing.TB, opts *TestControllerOpts) *TestController { @@ -656,6 +659,7 @@ func TestControllerConfig(t testing.TB, ctx context.Context, tc *TestController, } } opts.Config.Controller.Scheduler.JobRunIntervalDuration = opts.SchedulerRunJobInterval + opts.Config.Controller.ApiRateLimiterMaxQuotas = ratelimit.DefaultLimiterMaxQuotas() if opts.EnableEventing { opts.Config.Eventing = &event.EventerConfig{ @@ -804,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/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) { 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/ratelimit/config.go b/internal/ratelimit/config.go new file mode 100644 index 000000000..ac2453394 --- /dev/null +++ b/internal/ratelimit/config.go @@ -0,0 +1,282 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package ratelimit + +import ( + "context" + "fmt" + "reflect" + "sync" + "time" + + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/types/action" + "github.com/hashicorp/boundary/internal/types/resource" + "github.com/hashicorp/go-rate" + + // Imported to register all actions for all resources + _ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/accounts" + _ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/authmethods" + _ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/authtokens" + _ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/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 ( + DefaultInTotalRequestLimit = 3000 + DefaultIpAddressRequestLimit = 3000 + DefaultAuthTokenRequestLimit = 3000 + DefaultPeriod = time.Second * 30 + DefaultInTotalListRequestLimit = 60 + DefaultIpAddressListRequestLimit = 60 + DefaultAuthTokenListRequestLimit = 60 + DefaultListPeriod = time.Second * 30 +) + +// defaultLimiterMaxQuotas is the default maximum number of quotas that +// can be tracked by the rate limiter. +// This is determined at initialization time based on the number of endpoints. +var defaultLimiterMaxQuotas int + +var initDefaultLimiterMaxQuotas sync.Once + +// DefaultLimiterMaxQuotas returns the default maximum number of quotas that +// can be tracked by the rate limiter. +func DefaultLimiterMaxQuotas() int { + initDefaultLimiterMaxQuotas.Do(func() { + // Calculate the default max number of quotas that the rate limiter can + // store. This is calculated based on the number of endpoints and a + // static number of quotas per endpoint. This seems like a reasonable + // way to determine a sane default value. However, it should be noted + // that this total is shared across all endpoints, and some endpoints + // will be used more frequently than others. + + const quotasPerInTotal = 1 + const quotasPerIpAddress = 1000 + const quotasPerAuthToken = 1000 + + var endpointCount int + for _, res := range resource.Map { + switch res { + case resource.Unknown, resource.All, resource.Controller: + continue + } + + actions, err := action.ActionSetForResource(res) + if err != nil { + panic(fmt.Sprintf("No actions registered for resource %q", res.String())) + } + endpointCount += len(actions) + } + + defaultLimiterMaxQuotas = (endpointCount * quotasPerInTotal) + + (endpointCount * quotasPerAuthToken) + + (endpointCount * quotasPerIpAddress) + }) + return defaultLimiterMaxQuotas +} + +// Config is used to configure rate limits. Each config is used to specify +// the maximum number of requests that can be made in a time period for the +// corresponding resources and actions. +type Config struct { + Resources []string `hcl:"resources"` + Actions []string `hcl:"actions"` + Per string `hcl:"per"` + Limit int `hcl:"limit"` + PeriodHCL string `hcl:"period"` + Period time.Duration `hcl:"-"` + Unlimited bool `hcl:"unlimited"` +} + +// Configs is an ordered set of Config. +type Configs []*Config + +// Equal checks if a set of Configs is equal to another set of Configs. +func (c Configs) Equal(o Configs) bool { + return reflect.DeepEqual(c, o) +} + +// Limits creates a slice of rate.Limit from the Configs. This will enumerate +// every combination of resource+action, defining a Limit for each. +func (c Configs) Limits(ctx context.Context) ([]rate.Limit, error) { + const op = "ratelimit.(Configs).Limits" + + defaults := make(map[string]rate.Limit, len(resource.Map)*len(action.Map)) + + allResources := make([]resource.Type, 0, len(resource.Map)) + for _, res := range resource.Map { + switch res { + case resource.Unknown, resource.All, resource.Controller: + continue + } + allResources = append(allResources, res) + } + + for _, res := range allResources { + validActions, err := action.ActionSetForResource(res) + if err != nil { + // This shouldn't be possible, since we should have encountered + // this error during init and panicked already. If for some reason + // that was not the case, it seems like a good idea to panic here. + panic(fmt.Sprintf("No actions registered for resource %q", res.String())) + } + + for a := range validActions { + inTotalKey := fmt.Sprintf("%s:%s:%s", res.String(), a.String(), rate.LimitPerTotal) + authTokenKey := fmt.Sprintf("%s:%s:%s", res.String(), a.String(), rate.LimitPerAuthToken) + ipAddressKey := fmt.Sprintf("%s:%s:%s", res.String(), a.String(), rate.LimitPerIPAddress) + switch a { + case action.List: + defaults[inTotalKey] = &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + MaxRequests: DefaultInTotalListRequestLimit, + Period: DefaultListPeriod, + } + defaults[authTokenKey] = &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerAuthToken, + MaxRequests: DefaultAuthTokenListRequestLimit, + Period: DefaultListPeriod, + } + defaults[ipAddressKey] = &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerIPAddress, + MaxRequests: DefaultIpAddressListRequestLimit, + Period: DefaultListPeriod, + } + default: + defaults[inTotalKey] = &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + MaxRequests: DefaultInTotalRequestLimit, + Period: DefaultPeriod, + } + defaults[authTokenKey] = &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerAuthToken, + MaxRequests: DefaultAuthTokenRequestLimit, + Period: DefaultPeriod, + } + defaults[ipAddressKey] = &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerIPAddress, + MaxRequests: DefaultIpAddressRequestLimit, + Period: DefaultPeriod, + } + } + } + } + + for _, cc := range c { + var resourceSet []resource.Type + switch { + case len(cc.Resources) == 1 && cc.Resources[0] == resource.All.String(): + resourceSet = allResources + default: + for _, r := range cc.Resources { + rr, ok := resource.Map[r] + if !ok { + return nil, errors.New(ctx, errors.InvalidConfiguration, op, "", errors.WithMsg("unknown resource %s", r)) + } + resourceSet = append(resourceSet, rr) + } + } + + switch { + case len(cc.Actions) == 1 && cc.Actions[0] == action.All.String(): + for _, res := range resourceSet { + validActions, err := action.ActionSetForResource(res) + if err != nil { + return nil, err + } + for a := range validActions { + key := fmt.Sprintf("%s:%s:%s", res.String(), a.String(), rate.LimitPer(cc.Per)) + + switch { + case cc.Unlimited: + defaults[key] = &rate.Unlimited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPer(cc.Per), + } + default: + defaults[key] = &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPer(cc.Per), + MaxRequests: uint64(cc.Limit), + Period: cc.Period, + } + } + } + } + default: + for _, res := range resourceSet { + validActions, err := action.ActionSetForResource(res) + if err != nil { + return nil, err + } + validActionMap := make(map[string]action.Type, len(validActions)) + for a := range validActions { + validActionMap[a.String()] = a + } + + for _, aStr := range cc.Actions { + a, ok := validActionMap[aStr] + if !ok { + return nil, errors.New(ctx, errors.InvalidConfiguration, op, "", errors.WithMsg("action %s not valid for resource %s", aStr, res.String())) + } + key := fmt.Sprintf("%s:%s:%s", res.String(), a.String(), rate.LimitPer(cc.Per)) + + switch { + case cc.Unlimited: + defaults[key] = &rate.Unlimited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPer(cc.Per), + } + default: + defaults[key] = &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPer(cc.Per), + MaxRequests: uint64(cc.Limit), + Period: cc.Period, + } + } + } + } + } + } + + limits := make([]rate.Limit, 0, len(defaults)) + for _, v := range defaults { + limits = append(limits, v) + } + return limits, nil +} diff --git a/internal/ratelimit/config_test.go b/internal/ratelimit/config_test.go new file mode 100644 index 000000000..3493a40f0 --- /dev/null +++ b/internal/ratelimit/config_test.go @@ -0,0 +1,800 @@ +// 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.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + MaxRequests: DefaultInTotalListRequestLimit, + Period: DefaultListPeriod, + }, + &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerIPAddress, + MaxRequests: DefaultIpAddressListRequestLimit, + Period: DefaultListPeriod, + }, + &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerAuthToken, + MaxRequests: DefaultAuthTokenListRequestLimit, + Period: DefaultListPeriod, + }, + ) + default: + limits = append( + limits, + &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + MaxRequests: DefaultInTotalRequestLimit, + Period: DefaultPeriod, + }, + &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerIPAddress, + MaxRequests: DefaultIpAddressRequestLimit, + Period: DefaultPeriod, + }, + &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerAuthToken, + MaxRequests: DefaultAuthTokenRequestLimit, + Period: DefaultPeriod, + }, + ) + } + } + } + return limits + }(), + nil, + }, + { + "single-config-all-resources-all-actions", + Configs{ + { + Resources: []string{"*"}, + Actions: []string{"*"}, + Per: "total", + 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)) + 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.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + MaxRequests: 20, + Period: time.Minute * 5, + }, + &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerIPAddress, + MaxRequests: 10, + Period: time.Minute * 5, + }, + &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerAuthToken, + MaxRequests: 5, + 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{"*"}, + 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"}, + Per: "total", + 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)) + 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.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + MaxRequests: 10, + Period: time.Minute, + }, + &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerIPAddress, + MaxRequests: 5, + Period: time.Minute, + }, + &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerAuthToken, + MaxRequests: 1, + Period: time.Minute, + }, + ) + continue + } + fallthrough + default: + limits = append( + limits, + &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + MaxRequests: 20, + Period: time.Minute * 5, + }, + &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerIPAddress, + MaxRequests: 20, + Period: time.Minute * 5, + }, + &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerAuthToken, + 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, + }, + { + 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)) + 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.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + MaxRequests: 10, + Period: time.Minute, + }, + &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerIPAddress, + MaxRequests: 5, + Period: time.Minute, + }, + &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerAuthToken, + MaxRequests: 1, + 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.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + MaxRequests: DefaultInTotalListRequestLimit, + Period: DefaultListPeriod, + }, + &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerIPAddress, + MaxRequests: DefaultIpAddressListRequestLimit, + Period: DefaultListPeriod, + }, + &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerAuthToken, + MaxRequests: DefaultAuthTokenListRequestLimit, + Period: DefaultListPeriod, + }, + ) + default: + limits = append( + limits, + &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + MaxRequests: DefaultInTotalRequestLimit, + Period: DefaultPeriod, + }, + &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerIPAddress, + MaxRequests: DefaultIpAddressRequestLimit, + Period: DefaultPeriod, + }, + &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerAuthToken, + MaxRequests: DefaultAuthTokenRequestLimit, + Period: DefaultPeriod, + }, + ) + } + } + } + } + return limits + }(), + nil, + }, + { + "multiple-actions", + Configs{ + { + Resources: []string{"target", "session"}, + Actions: []string{"list", "read"}, + Per: "total", + 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)) + 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.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + MaxRequests: 10, + Period: time.Minute, + }, + &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerIPAddress, + MaxRequests: 5, + Period: time.Minute, + }, + &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerAuthToken, + MaxRequests: 1, + Period: time.Minute, + }, + ) + default: + limits = append( + limits, + &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + MaxRequests: DefaultInTotalRequestLimit, + Period: DefaultPeriod, + }, + &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerIPAddress, + MaxRequests: DefaultIpAddressRequestLimit, + Period: DefaultPeriod, + }, + &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerAuthToken, + MaxRequests: DefaultAuthTokenRequestLimit, + 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.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + MaxRequests: DefaultInTotalListRequestLimit, + Period: DefaultListPeriod, + }, + &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerIPAddress, + MaxRequests: DefaultIpAddressListRequestLimit, + Period: DefaultListPeriod, + }, + &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerAuthToken, + MaxRequests: DefaultAuthTokenListRequestLimit, + Period: DefaultListPeriod, + }, + ) + default: + limits = append( + limits, + &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + MaxRequests: DefaultInTotalRequestLimit, + Period: DefaultPeriod, + }, + &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerIPAddress, + MaxRequests: DefaultIpAddressRequestLimit, + Period: DefaultPeriod, + }, + &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerAuthToken, + MaxRequests: DefaultAuthTokenRequestLimit, + 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 + case action.List: + limits = append( + limits, + &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + MaxRequests: 20, + Period: time.Minute * 5, + }, + &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerIPAddress, + MaxRequests: DefaultIpAddressListRequestLimit, + Period: DefaultListPeriod, + }, + &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerAuthToken, + MaxRequests: DefaultAuthTokenListRequestLimit, + Period: DefaultListPeriod, + }, + ) + default: + limits = append( + limits, + &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + MaxRequests: 20, + Period: time.Minute * 5, + }, + &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerIPAddress, + MaxRequests: DefaultIpAddressRequestLimit, + Period: DefaultPeriod, + }, + &rate.Limited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerAuthToken, + MaxRequests: DefaultAuthTokenRequestLimit, + Period: DefaultPeriod, + }, + ) + } + } + } + return limits + }(), + nil, + }, + { + "no-limits", + Configs{ + { + Resources: []string{"*"}, + Actions: []string{"*"}, + 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)) + 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.Unlimited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerTotal, + }, + &rate.Unlimited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerIPAddress, + }, + &rate.Unlimited{ + Resource: res.String(), + Action: a.String(), + Per: rate.LimitPerAuthToken, + }, + ) + } + } + } + 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 TestDefaulLimiterMaxQuotas(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 := DefaultLimiterMaxQuotas() + assert.Equal(t, want, got) +} 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 diff --git a/internal/ratelimit/handler.go b/internal/ratelimit/handler.go new file mode 100644 index 000000000..2244be7b2 --- /dev/null +++ b/internal/ratelimit/handler.go @@ -0,0 +1,182 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package ratelimit + +import ( + "context" + "fmt" + "math" + "net/http" + "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" + "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[^:]+))?(:(?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 +} + +// LimiterFunc returns a 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 +// 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, 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) + 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) + return + } + rw.WriteHeader(http.StatusInternalServerError) + return + } + + 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. + rw.WriteHeader(http.StatusInternalServerError) + 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", math.Ceil(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..34d9c1b71 --- /dev/null +++ b/internal/ratelimit/handler_test.go @@ -0,0 +1,815 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +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" + "github.com/hashicorp/go-rate" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +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 + }{ + { + "AllowedList", + func() *rate.Limiter { + r, err := rate.NewLimiter([]rate.Limit{ + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerTotal, + MaxRequests: 10, + Period: time.Minute, + }, + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerIPAddress, + MaxRequests: 10, + Period: time.Minute, + }, + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerAuthToken, + 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 + }, + "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=60`}, + }, + }, + { + "AllowedCreate", + func() *rate.Limiter { + r, err := rate.NewLimiter([]rate.Limit{ + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.Create.String(), + Per: rate.LimitPerTotal, + MaxRequests: 10, + Period: time.Minute, + }, + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.Create.String(), + Per: rate.LimitPerIPAddress, + MaxRequests: 10, + Period: time.Minute, + }, + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.Create.String(), + Per: rate.LimitPerAuthToken, + 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 + }, + "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=60`}, + }, + }, + { + "AllowedRead", + func() *rate.Limiter { + r, err := rate.NewLimiter([]rate.Limit{ + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.Read.String(), + Per: rate.LimitPerTotal, + MaxRequests: 10, + Period: time.Minute, + }, + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.Read.String(), + Per: rate.LimitPerIPAddress, + MaxRequests: 10, + Period: time.Minute, + }, + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.Read.String(), + Per: rate.LimitPerAuthToken, + 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 + }, + "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=60`}, + }, + }, + { + "AllowedUpdate", + func() *rate.Limiter { + r, err := rate.NewLimiter([]rate.Limit{ + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.Update.String(), + Per: rate.LimitPerTotal, + MaxRequests: 10, + Period: time.Minute, + }, + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.Update.String(), + Per: rate.LimitPerIPAddress, + MaxRequests: 10, + Period: time.Minute, + }, + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.Update.String(), + Per: rate.LimitPerAuthToken, + 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 + }, + "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=60`}, + }, + }, + { + "AllowedDelete", + func() *rate.Limiter { + r, err := rate.NewLimiter([]rate.Limit{ + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.Delete.String(), + Per: rate.LimitPerTotal, + MaxRequests: 10, + Period: time.Minute, + }, + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.Delete.String(), + Per: rate.LimitPerIPAddress, + MaxRequests: 10, + Period: time.Minute, + }, + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.Delete.String(), + Per: rate.LimitPerAuthToken, + 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 + }, + "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=60`}, + }, + }, + { + "AllowedAuthorizeSession", + func() *rate.Limiter { + r, err := rate.NewLimiter([]rate.Limit{ + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.AuthorizeSession.String(), + Per: rate.LimitPerTotal, + MaxRequests: 10, + Period: time.Minute, + }, + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.AuthorizeSession.String(), + Per: rate.LimitPerIPAddress, + MaxRequests: 10, + Period: time.Minute, + }, + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.AuthorizeSession.String(), + Per: rate.LimitPerAuthToken, + 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/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=60`}, + }, + }, + { + "AllowedAuthorizeSessionTargetNameSlash", + func() *rate.Limiter { + r, err := rate.NewLimiter([]rate.Limit{ + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.AuthorizeSession.String(), + Per: rate.LimitPerTotal, + MaxRequests: 10, + Period: time.Minute, + }, + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.AuthorizeSession.String(), + Per: rate.LimitPerIPAddress, + MaxRequests: 10, + Period: time.Minute, + }, + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.AuthorizeSession.String(), + Per: rate.LimitPerAuthToken, + 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 + }, + "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=60`}, + }, + }, + { + "Limited", + func() *rate.Limiter { + r, err := rate.NewLimiter([]rate.Limit{ + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerTotal, + MaxRequests: 2, + Period: time.Minute, + }, + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerIPAddress, + MaxRequests: 2, + Period: time.Minute, + }, + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerAuthToken, + 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 + }, + "127.0.0.1", + "authtoken", + http.StatusTooManyRequests, + 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=60`}, + }, + }, + { + "LimiterFull", + func() *rate.Limiter { + r, err := rate.NewLimiter([]rate.Limit{ + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerTotal, + MaxRequests: 2, + Period: time.Minute, + }, + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.Read.String(), + Per: rate.LimitPerTotal, + MaxRequests: 2, + Period: time.Minute, + }, + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerIPAddress, + MaxRequests: 2, + Period: time.Minute, + }, + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.Read.String(), + Per: rate.LimitPerIPAddress, + MaxRequests: 2, + Period: time.Minute, + }, + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerAuthToken, + MaxRequests: 2, + Period: time.Minute, + }, + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.Read.String(), + Per: rate.LimitPerAuthToken, + MaxRequests: 2, + Period: time.Minute, + }, + }, 3) + 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 + }, + "127.0.0.1", + "authtoken", + http.StatusServiceUnavailable, + http.Header{ + "Retry-After": []string{"1"}, + }, + }, + { + "MissingLimit", + func() *rate.Limiter { + r, err := rate.NewLimiter([]rate.Limit{ + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerTotal, + MaxRequests: 2, + Period: time.Minute, + }, + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerIPAddress, + MaxRequests: 2, + Period: time.Minute, + }, + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerAuthToken, + 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 + }, + "127.0.0.1", + "authtoken", + http.StatusInternalServerError, + http.Header{}, + }, + { + "UnknownResource", + func() *rate.Limiter { + r, err := rate.NewLimiter([]rate.Limit{ + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerTotal, + MaxRequests: 2, + Period: time.Minute, + }, + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerIPAddress, + MaxRequests: 2, + Period: time.Minute, + }, + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerAuthToken, + 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 + }, + "127.0.0.1", + "authtoken", + http.StatusNotFound, + http.Header{}, + }, + { + "UnknownAction", + func() *rate.Limiter { + r, err := rate.NewLimiter([]rate.Limit{ + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerTotal, + MaxRequests: 2, + Period: time.Minute, + }, + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerIPAddress, + MaxRequests: 2, + Period: time.Minute, + }, + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerAuthToken, + 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 + }, + "127.0.0.1", + "authtoken", + http.StatusBadRequest, + http.Header{}, + }, + { + "UnknownActionInvalidIdHttpMethod", + func() *rate.Limiter { + r, err := rate.NewLimiter([]rate.Limit{ + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerTotal, + MaxRequests: 2, + Period: time.Minute, + }, + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerIPAddress, + MaxRequests: 2, + Period: time.Minute, + }, + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerAuthToken, + 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 + }, + "127.0.0.1", + "authtoken", + http.StatusMethodNotAllowed, + http.Header{}, + }, + { + "UnknownActionInvalidHttpMethod", + func() *rate.Limiter { + r, err := rate.NewLimiter([]rate.Limit{ + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerTotal, + MaxRequests: 2, + Period: time.Minute, + }, + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerIPAddress, + MaxRequests: 2, + Period: time.Minute, + }, + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerAuthToken, + 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 + }, + "127.0.0.1", + "authtoken", + http.StatusMethodNotAllowed, + http.Header{}, + }, + { + "UnsupportedAction", + func() *rate.Limiter { + r, err := rate.NewLimiter([]rate.Limit{ + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerTotal, + MaxRequests: 2, + Period: time.Minute, + }, + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerIPAddress, + MaxRequests: 2, + Period: time.Minute, + }, + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerAuthToken, + 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 + }, + "127.0.0.1", + "authtoken", + http.StatusMethodNotAllowed, + http.Header{}, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + 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, func() ratelimit.Limiter { return 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)) + } + }) + } +} + +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{ + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerTotal, + MaxRequests: 10, + Period: time.Minute, + }, + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerIPAddress, + MaxRequests: 10, + Period: time.Minute, + }, + &rate.Limited{ + Resource: resource.Target.String(), + Action: action.List.String(), + Per: rate.LimitPerAuthToken, + 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, func() ratelimit.Limiter { return 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) + }) + } +} diff --git a/internal/ratelimit/limiter.go b/internal/ratelimit/limiter.go new file mode 100644 index 000000000..5be0f2e75 --- /dev/null +++ b/internal/ratelimit/limiter.go @@ -0,0 +1,29 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package ratelimit + +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, maxQuotas int) (*rate.Limiter, error) { + return rate.NewLimiter( + limits, + maxQuotas, + 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, + ) +} 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/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() 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) + }) + } +} 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) + }) + } + }) +} 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,