Merge pull request #4092 from hashicorp/llb-rate-limiting

Add support for rate limiting

This introduces rate limiting for the Controller API endpoints. Limits
can be set separately for each combination of resource and action. And
each combination has a limit per auth-token, IP address, and in total.

By default each list action for all resources has a limit of 120
requests per minute. All other actions for all resources have a limit of
3,000 requests per minute. This can be overridden by including
`api_rate_limit` stanzas in the controller configuration. For example to
set a limit on target list requests per auth token:

    controller {
      api_rate_limit {
            resources = ["target"]
            actions   = ["list"]
            per 	= "auth-token"
            limit 	= 10
            period	= "1s"
      }
      // other controller configuration
    }

Or to completely override all of the defaults:

    controller {
      // total limit for all resources and actions
      api_rate_limit {
            resources = ["*"]
            actions   = ["*"]
            per  	= "total"
            limit 	= 500
            period	= "1s"
      }

      // limit for ip addresses to all resources and actions
      api_rate_limit {
            resources = ["*"]
            actions   = ["*"]
            per 	= "ip-address"
            limit 	= 100
            period	= "1s"
      }

      // Limit for auth tokens to all resources and actions
      api_rate_limit {
            resources = ["*"]
            actions   = ["*"]
            per 	= "auth-token"
            limit 	= 50
            period	= "1s"
      }
      // other controller configuration
    }

All rate limiting can be disabled by setting the
`api_rate_limit_disable` configuration variable:

    controller {
      api_rate_limit_disable = true
      // other controller configuration
    }

When rate limiting is enabled, boundary will create quotas to track the
number of requests used within a time period. Boundary will reserve a
set amount of memory for tracking quotas. This ensures that boundary
does not consume too much memory if there is a sudden burst of requests
from many different clients. However this does mean if boundary is
unable to store a quota, it will limit the request with a 503 HTTP
status code. The amount of memory can be controlled with the
`api_rate_limit_max_quotas` configuration variable. This can also be
monitored via two new metrics:

- `boundary_controller_api_ratelimiter_quota_storage_capacity`
- `boundary_controller_api_ratelimiter_quota_storage_usage`

Clients making requests to the controller API can inspect HTTP response
headers to understand the configured limits and current usage. Each
response will contain the following headers:

- RateLimit: Provides the current limit, number of remaining requests,
  and the time the quota will reset for the limit that is closest to
  being exhausted for the requested resource and action.
- RateLimit-Policy: Describes the limits for the requested resource and
  action.

If a request is limited, the client will receive a 429 HTTP status code,
and the Retry-After header will be set to the number of seconds the
client should wait before attempting the request again.
This commit is contained in:
Timothy Messier authored and GitHub committed 2023-12-05 08:06:23 -05:00
commit 52cab530f2
91 files changed
+17411 -294

No files matched your search

+1 -1
View File
@@ -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
+21 -1
View File
@@ -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)
+5
View File
@@ -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
+1
View File
@@ -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
+2
View File
@@ -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=
@@ -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 {
@@ -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()
}
+11
View File
@@ -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.
+46
View File
@@ -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")
+31
View File
@@ -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: "",
+211 -4
View File
@@ -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
@@ -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" {
@@ -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"
}
}
+5 -5
View File
@@ -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 {
@@ -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 {
+5
View File
@@ -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
}
+9
View File
@@ -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 {
+3 -1
View File
@@ -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
@@ -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.
@@ -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) {
@@ -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,
@@ -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"),
)
}
@@ -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 (
@@ -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()
@@ -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
@@ -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,
)
})
@@ -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.
@@ -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)
@@ -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)
}
@@ -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
@@ -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)
@@ -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.
@@ -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
}),
))
})
}
@@ -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.
@@ -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
}),
))
})
}
@@ -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.
@@ -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 = "<hmac>"
}
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()
}),
))
})
}
@@ -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.
@@ -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)
})
}
}
@@ -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 {
@@ -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()
}),
))
},
},
{
@@ -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 {
@@ -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)
})
}
}
@@ -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 {
@@ -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)
})
}
}
@@ -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.
@@ -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)
})
}
}
@@ -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.
@@ -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)
})
}
}
@@ -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()))
@@ -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)
})
}
}
@@ -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,
@@ -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
@@ -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)
@@ -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,
@@ -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")
}
@@ -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
}),
))
})
}
}
@@ -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.
@@ -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)
})
}
}
@@ -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 {
@@ -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)
})
}
}
+225
View File
@@ -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()
}
@@ -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)
}
}
})
}
}
File diff suppressed because it is too large. Load diff
@@ -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"
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+8
View File
@@ -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,
+3
View File
@@ -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.
)
+5
View File
@@ -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) {
+4
View File
@@ -343,4 +343,8 @@ var errorCodeInfo = map[Code]Info{
Message: "checksum mismatch",
Kind: Integrity,
},
InvalidConfiguration: {
Message: "invalid configuration",
Kind: Configuration,
},
}
+13 -11
View File
@@ -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]
}
+5
View File
@@ -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) {
+3 -3
View File
@@ -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()
+19 -19
View File
@@ -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",
+282
View File
@@ -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
}
+800
View File
@@ -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)
}
+6
View File
@@ -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
+182
View File
@@ -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<resource>[\w-]+)((/(?P<id>[^:]+))?(:(?P<action>[\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)
})
}
+815
View File
@@ -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)
})
}
}
+29
View File
@@ -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),
)
}
+43
View File
@@ -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,
)
}
@@ -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) {
@@ -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()
+13 -1
View File
@@ -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)
@@ -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
}),
),
)
+44 -12
View File
@@ -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
}
+232 -13
View File
@@ -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)
})
}
}
+73
View File
@@ -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
}
@@ -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)
})
}
})
}
+14 -1
View File
@@ -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,