github.com/letsencrypt/boulder@v0.20251208.0/ratelimits/transaction_test.go (about)

     1  package ratelimits
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"net/netip"
     8  	"sort"
     9  	"testing"
    10  	"time"
    11  
    12  	io_prometheus_client "github.com/prometheus/client_model/go"
    13  	"google.golang.org/grpc"
    14  	"google.golang.org/protobuf/types/known/durationpb"
    15  	"google.golang.org/protobuf/types/known/emptypb"
    16  
    17  	"github.com/letsencrypt/boulder/config"
    18  	"github.com/letsencrypt/boulder/core"
    19  	"github.com/letsencrypt/boulder/identifier"
    20  	blog "github.com/letsencrypt/boulder/log"
    21  	"github.com/letsencrypt/boulder/metrics"
    22  	"github.com/letsencrypt/boulder/mocks"
    23  	sapb "github.com/letsencrypt/boulder/sa/proto"
    24  	"github.com/letsencrypt/boulder/test"
    25  )
    26  
    27  func TestNewTransactionBuilderFromFiles_WithBadLimitsPath(t *testing.T) {
    28  	t.Parallel()
    29  	_, err := NewTransactionBuilderFromFiles("testdata/does-not-exist.yml", "", metrics.NoopRegisterer, blog.NewMock())
    30  	test.AssertError(t, err, "should error")
    31  
    32  	_, err = NewTransactionBuilderFromFiles("testdata/defaults.yml", "testdata/does-not-exist.yml", metrics.NoopRegisterer, blog.NewMock())
    33  	test.AssertError(t, err, "should error")
    34  }
    35  
    36  func sortTransactions(txns []Transaction) []Transaction {
    37  	sort.Slice(txns, func(i, j int) bool {
    38  		return txns[i].bucketKey < txns[j].bucketKey
    39  	})
    40  	return txns
    41  }
    42  
    43  func TestNewRegistrationsPerIPAddressTransactions(t *testing.T) {
    44  	t.Parallel()
    45  
    46  	tb, err := NewTransactionBuilderFromFiles("../test/config-next/ratelimit-defaults.yml", "", metrics.NoopRegisterer, blog.NewMock())
    47  	test.AssertNotError(t, err, "creating TransactionBuilder")
    48  
    49  	// A check-and-spend transaction for the global limit.
    50  	txn, err := tb.registrationsPerIPAddressTransaction(netip.MustParseAddr("1.2.3.4"))
    51  	test.AssertNotError(t, err, "creating transaction")
    52  	test.AssertEquals(t, txn.bucketKey, "1:1.2.3.4")
    53  	test.Assert(t, txn.check && txn.spend, "should be check-and-spend")
    54  }
    55  
    56  func TestNewRegistrationsPerIPv6AddressTransactions(t *testing.T) {
    57  	t.Parallel()
    58  
    59  	tb, err := NewTransactionBuilderFromFiles("../test/config-next/ratelimit-defaults.yml", "", metrics.NoopRegisterer, blog.NewMock())
    60  	test.AssertNotError(t, err, "creating TransactionBuilder")
    61  
    62  	// A check-and-spend transaction for the global limit.
    63  	txn, err := tb.registrationsPerIPv6RangeTransaction(netip.MustParseAddr("2001:db8::1"))
    64  	test.AssertNotError(t, err, "creating transaction")
    65  	test.AssertEquals(t, txn.bucketKey, "2:2001:db8::/48")
    66  	test.Assert(t, txn.check && txn.spend, "should be check-and-spend")
    67  }
    68  
    69  func TestNewOrdersPerAccountTransactions(t *testing.T) {
    70  	t.Parallel()
    71  
    72  	tb, err := NewTransactionBuilderFromFiles("../test/config-next/ratelimit-defaults.yml", "", metrics.NoopRegisterer, blog.NewMock())
    73  	test.AssertNotError(t, err, "creating TransactionBuilder")
    74  
    75  	// A check-and-spend transaction for the global limit.
    76  	txn, err := tb.ordersPerAccountTransaction(123456789)
    77  	test.AssertNotError(t, err, "creating transaction")
    78  	test.AssertEquals(t, txn.bucketKey, "3:123456789")
    79  	test.Assert(t, txn.check && txn.spend, "should be check-and-spend")
    80  }
    81  
    82  func TestFailedAuthorizationsPerDomainPerAccountTransactions(t *testing.T) {
    83  	t.Parallel()
    84  
    85  	tb, err := NewTransactionBuilderFromFiles("../test/config-next/ratelimit-defaults.yml", "testdata/working_override_13371338.yml", metrics.NoopRegisterer, blog.NewMock())
    86  	test.AssertNotError(t, err, "creating TransactionBuilder")
    87  	err = tb.loadOverrides(context.Background())
    88  	test.AssertNotError(t, err, "loading overrides")
    89  
    90  	// A check-only transaction for the default per-account limit.
    91  	txns, err := tb.FailedAuthorizationsPerDomainPerAccountCheckOnlyTransactions(123456789, identifier.NewDNSSlice([]string{"so.many.labels.here.example.com"}))
    92  	test.AssertNotError(t, err, "creating transactions")
    93  	test.AssertEquals(t, len(txns), 1)
    94  	test.AssertEquals(t, txns[0].bucketKey, "4:123456789:so.many.labels.here.example.com")
    95  	test.Assert(t, txns[0].checkOnly(), "should be check-only")
    96  	test.Assert(t, !txns[0].limit.isOverride, "should not be an override")
    97  
    98  	// A spend-only transaction for the default per-account limit.
    99  	txn, err := tb.FailedAuthorizationsPerDomainPerAccountSpendOnlyTransaction(123456789, identifier.NewDNS("so.many.labels.here.example.com"))
   100  	test.AssertNotError(t, err, "creating transaction")
   101  	test.AssertEquals(t, txn.bucketKey, "4:123456789:so.many.labels.here.example.com")
   102  	test.Assert(t, txn.spendOnly(), "should be spend-only")
   103  	test.Assert(t, !txn.limit.isOverride, "should not be an override")
   104  
   105  	// A check-only transaction for the per-account limit override.
   106  	txns, err = tb.FailedAuthorizationsPerDomainPerAccountCheckOnlyTransactions(13371338, identifier.NewDNSSlice([]string{"so.many.labels.here.example.com"}))
   107  	test.AssertNotError(t, err, "creating transactions")
   108  	test.AssertEquals(t, len(txns), 1)
   109  	test.AssertEquals(t, txns[0].bucketKey, "4:13371338:so.many.labels.here.example.com")
   110  	test.Assert(t, txns[0].checkOnly(), "should be check-only")
   111  	test.Assert(t, txns[0].limit.isOverride, "should be an override")
   112  
   113  	// A spend-only transaction for the per-account limit override.
   114  	txn, err = tb.FailedAuthorizationsPerDomainPerAccountSpendOnlyTransaction(13371338, identifier.NewDNS("so.many.labels.here.example.com"))
   115  	test.AssertNotError(t, err, "creating transaction")
   116  	test.AssertEquals(t, txn.bucketKey, "4:13371338:so.many.labels.here.example.com")
   117  	test.Assert(t, txn.spendOnly(), "should be spend-only")
   118  	test.Assert(t, txn.limit.isOverride, "should be an override")
   119  }
   120  
   121  func TestFailedAuthorizationsForPausingPerDomainPerAccountTransactions(t *testing.T) {
   122  	t.Parallel()
   123  
   124  	tb, err := NewTransactionBuilderFromFiles("../test/config-next/ratelimit-defaults.yml", "testdata/working_override_13371338.yml", metrics.NoopRegisterer, blog.NewMock())
   125  	test.AssertNotError(t, err, "creating TransactionBuilder")
   126  	err = tb.loadOverrides(context.Background())
   127  	test.AssertNotError(t, err, "loading overrides")
   128  
   129  	// A transaction for the per-account limit override.
   130  	txn, err := tb.FailedAuthorizationsForPausingPerDomainPerAccountTransaction(13371338, identifier.NewDNS("so.many.labels.here.example.com"))
   131  	test.AssertNotError(t, err, "creating transaction")
   132  	test.AssertEquals(t, txn.bucketKey, "8:13371338:so.many.labels.here.example.com")
   133  	test.Assert(t, txn.check && txn.spend, "should be check and spend")
   134  	test.Assert(t, txn.limit.isOverride, "should be an override")
   135  }
   136  
   137  func TestCertificatesPerDomainTransactions(t *testing.T) {
   138  	t.Parallel()
   139  
   140  	tb, err := NewTransactionBuilderFromFiles("../test/config-next/ratelimit-defaults.yml", "", metrics.NoopRegisterer, blog.NewMock())
   141  	test.AssertNotError(t, err, "creating TransactionBuilder")
   142  
   143  	// One check-only transaction for the global limit.
   144  	txns, err := tb.certificatesPerDomainCheckOnlyTransactions(123456789, identifier.NewDNSSlice([]string{"so.many.labels.here.example.com"}))
   145  	test.AssertNotError(t, err, "creating transactions")
   146  	test.AssertEquals(t, len(txns), 1)
   147  	test.AssertEquals(t, txns[0].bucketKey, "5:example.com")
   148  	test.Assert(t, txns[0].checkOnly(), "should be check-only")
   149  
   150  	// One spend-only transaction for the global limit.
   151  	txns, err = tb.CertificatesPerDomainSpendOnlyTransactions(123456789, identifier.NewDNSSlice([]string{"so.many.labels.here.example.com"}))
   152  	test.AssertNotError(t, err, "creating transactions")
   153  	test.AssertEquals(t, len(txns), 1)
   154  	test.AssertEquals(t, txns[0].bucketKey, "5:example.com")
   155  	test.Assert(t, txns[0].spendOnly(), "should be spend-only")
   156  }
   157  
   158  func TestCertificatesPerDomainPerAccountTransactions(t *testing.T) {
   159  	t.Parallel()
   160  
   161  	tb, err := NewTransactionBuilderFromFiles("../test/config-next/ratelimit-defaults.yml", "testdata/working_override_13371338.yml", metrics.NoopRegisterer, blog.NewMock())
   162  	test.AssertNotError(t, err, "creating TransactionBuilder")
   163  	err = tb.loadOverrides(context.Background())
   164  	test.AssertNotError(t, err, "loading overrides")
   165  
   166  	// We only expect a single check-only transaction for the per-account limit
   167  	// override. We can safely ignore the global limit when an override is
   168  	// present.
   169  	txns, err := tb.certificatesPerDomainCheckOnlyTransactions(13371338, identifier.NewDNSSlice([]string{"so.many.labels.here.example.com"}))
   170  	test.AssertNotError(t, err, "creating transactions")
   171  	test.AssertEquals(t, len(txns), 1)
   172  	test.AssertEquals(t, txns[0].bucketKey, "6:13371338:example.com")
   173  	test.Assert(t, txns[0].checkOnly(), "should be check-only")
   174  	test.Assert(t, txns[0].limit.isOverride, "should be an override")
   175  
   176  	// Same as above, but with multiple example.com domains.
   177  	txns, err = tb.certificatesPerDomainCheckOnlyTransactions(13371338, identifier.NewDNSSlice([]string{"so.many.labels.here.example.com", "z.example.com"}))
   178  	test.AssertNotError(t, err, "creating transactions")
   179  	test.AssertEquals(t, len(txns), 1)
   180  	test.AssertEquals(t, txns[0].bucketKey, "6:13371338:example.com")
   181  	test.Assert(t, txns[0].checkOnly(), "should be check-only")
   182  	test.Assert(t, txns[0].limit.isOverride, "should be an override")
   183  
   184  	// Same as above, but with different domains.
   185  	txns, err = tb.certificatesPerDomainCheckOnlyTransactions(13371338, identifier.NewDNSSlice([]string{"so.many.labels.here.example.com", "z.example.net"}))
   186  	test.AssertNotError(t, err, "creating transactions")
   187  	txns = sortTransactions(txns)
   188  	test.AssertEquals(t, len(txns), 2)
   189  	test.AssertEquals(t, txns[0].bucketKey, "6:13371338:example.com")
   190  	test.Assert(t, txns[0].checkOnly(), "should be check-only")
   191  	test.Assert(t, txns[0].limit.isOverride, "should be an override")
   192  	test.AssertEquals(t, txns[1].bucketKey, "6:13371338:example.net")
   193  	test.Assert(t, txns[1].checkOnly(), "should be check-only")
   194  	test.Assert(t, txns[1].limit.isOverride, "should be an override")
   195  
   196  	// Two spend-only transactions, one for the global limit and one for the
   197  	// per-account limit override.
   198  	txns, err = tb.CertificatesPerDomainSpendOnlyTransactions(13371338, identifier.NewDNSSlice([]string{"so.many.labels.here.example.com"}))
   199  	test.AssertNotError(t, err, "creating TransactionBuilder")
   200  	test.AssertEquals(t, len(txns), 2)
   201  	txns = sortTransactions(txns)
   202  	test.AssertEquals(t, txns[0].bucketKey, "5:example.com")
   203  	test.Assert(t, txns[0].spendOnly(), "should be spend-only")
   204  	test.Assert(t, !txns[0].limit.isOverride, "should not be an override")
   205  
   206  	test.AssertEquals(t, txns[1].bucketKey, "6:13371338:example.com")
   207  	test.Assert(t, txns[1].spendOnly(), "should be spend-only")
   208  	test.Assert(t, txns[1].limit.isOverride, "should be an override")
   209  }
   210  
   211  func TestCertificatesPerFQDNSetTransactions(t *testing.T) {
   212  	t.Parallel()
   213  
   214  	tb, err := NewTransactionBuilderFromFiles("../test/config-next/ratelimit-defaults.yml", "", metrics.NoopRegisterer, blog.NewMock())
   215  	test.AssertNotError(t, err, "creating TransactionBuilder")
   216  
   217  	// A single check-only transaction for the global limit.
   218  	txn, err := tb.certificatesPerFQDNSetCheckOnlyTransaction(identifier.NewDNSSlice([]string{"example.com", "example.net", "example.org"}))
   219  	test.AssertNotError(t, err, "creating transaction")
   220  	namesHash := fmt.Sprintf("%x", core.HashIdentifiers(identifier.NewDNSSlice([]string{"example.com", "example.net", "example.org"})))
   221  	test.AssertEquals(t, txn.bucketKey, "7:"+namesHash)
   222  	test.Assert(t, txn.checkOnly(), "should be check-only")
   223  	test.Assert(t, !txn.limit.isOverride, "should not be an override")
   224  }
   225  
   226  // NewTransactionBuilder's metrics are tested in TestLoadOverrides.
   227  func TestNewTransactionBuilder(t *testing.T) {
   228  	t.Parallel()
   229  
   230  	expectedBurst := int64(10000)
   231  	expectedCount := int64(10000)
   232  	expectedPeriod := config.Duration{Duration: time.Hour * 168}
   233  
   234  	tb, err := NewTransactionBuilder(LimitConfigs{
   235  		NewRegistrationsPerIPAddress.String(): &LimitConfig{
   236  			Burst:  expectedBurst,
   237  			Count:  expectedCount,
   238  			Period: expectedPeriod},
   239  	}, nil, metrics.NoopRegisterer, blog.NewMock())
   240  	test.AssertNotError(t, err, "creating TransactionBuilder")
   241  
   242  	newRegDefault, ok := tb.limitRegistry.defaults[NewRegistrationsPerIPAddress.EnumString()]
   243  	test.Assert(t, ok, "NewRegistrationsPerIPAddress was not populated in registry")
   244  	test.AssertEquals(t, newRegDefault.Burst, expectedBurst)
   245  	test.AssertEquals(t, newRegDefault.Count, expectedCount)
   246  	test.AssertEquals(t, newRegDefault.Period, expectedPeriod)
   247  }
   248  
   249  func TestNewTransactionBuilderFromDatabase(t *testing.T) {
   250  	t.Parallel()
   251  
   252  	tests := []struct {
   253  		name                 string
   254  		overrides            GetOverridesFunc
   255  		expectOverrides      map[string]Limit
   256  		expectError          string
   257  		expectLog            string
   258  		expectOverrideErrors float64
   259  	}{
   260  		{
   261  			name: "error fetching enabled overrides",
   262  			overrides: func(context.Context, *emptypb.Empty, ...grpc.CallOption) (grpc.ServerStreamingClient[sapb.RateLimitOverrideResponse], error) {
   263  				return nil, errors.New("lol no")
   264  			},
   265  			expectError: "fetching enabled overrides: lol no",
   266  		},
   267  		{
   268  			name: "empty results",
   269  			overrides: func(context.Context, *emptypb.Empty, ...grpc.CallOption) (grpc.ServerStreamingClient[sapb.RateLimitOverrideResponse], error) {
   270  				return &mocks.ServerStreamClient[sapb.RateLimitOverrideResponse]{Results: []*sapb.RateLimitOverrideResponse{}}, nil
   271  			},
   272  		},
   273  		{
   274  			name: "gRPC error",
   275  			overrides: func(context.Context, *emptypb.Empty, ...grpc.CallOption) (grpc.ServerStreamingClient[sapb.RateLimitOverrideResponse], error) {
   276  				return &mocks.ServerStreamClient[sapb.RateLimitOverrideResponse]{Err: errors.New("i ate ur toast m8")}, nil
   277  			},
   278  			expectError: "reading overrides stream: i ate ur toast m8",
   279  		},
   280  		{
   281  			name: "2 valid overrides",
   282  			overrides: func(context.Context, *emptypb.Empty, ...grpc.CallOption) (grpc.ServerStreamingClient[sapb.RateLimitOverrideResponse], error) {
   283  				return &mocks.ServerStreamClient[sapb.RateLimitOverrideResponse]{Results: []*sapb.RateLimitOverrideResponse{
   284  					{Override: &sapb.RateLimitOverride{LimitEnum: int64(StringToName["CertificatesPerDomain"]), BucketKey: "example.com", Period: &durationpb.Duration{Seconds: 1}, Count: 1, Burst: 1}},
   285  					{Override: &sapb.RateLimitOverride{LimitEnum: int64(StringToName["CertificatesPerDomain"]), BucketKey: "example.net", Period: &durationpb.Duration{Seconds: 1}, Count: 1, Burst: 1}},
   286  				}}, nil
   287  			},
   288  			expectOverrides: map[string]Limit{
   289  				"example.com": {Burst: 1, Count: 1, Period: config.Duration{Duration: time.Second}, Name: CertificatesPerDomain, Comment: "Last Updated: 1970-01-01 - ", emissionInterval: 1000000000, burstOffset: 1000000000, isOverride: true},
   290  				"example.net": {Burst: 1, Count: 1, Period: config.Duration{Duration: time.Second}, Name: CertificatesPerDomain, Comment: "Last Updated: 1970-01-01 - ", emissionInterval: 1000000000, burstOffset: 1000000000, isOverride: true},
   291  			},
   292  		},
   293  		{
   294  			name: "2 valid & 4 incomplete overrides",
   295  			overrides: func(context.Context, *emptypb.Empty, ...grpc.CallOption) (grpc.ServerStreamingClient[sapb.RateLimitOverrideResponse], error) {
   296  				return &mocks.ServerStreamClient[sapb.RateLimitOverrideResponse]{Results: []*sapb.RateLimitOverrideResponse{
   297  					{Override: &sapb.RateLimitOverride{LimitEnum: int64(StringToName["CertificatesPerDomain"]), BucketKey: "example.com", Period: &durationpb.Duration{Seconds: 1}, Count: 1, Burst: 1}},
   298  					{Override: &sapb.RateLimitOverride{LimitEnum: int64(StringToName["CertificatesPerDomain"]), BucketKey: "example.net", Period: &durationpb.Duration{Seconds: 1}, Count: 1, Burst: 1}},
   299  					{Override: &sapb.RateLimitOverride{LimitEnum: int64(StringToName["CertificatesPerDomain"]), BucketKey: "bad-example.com"}},
   300  					{Override: &sapb.RateLimitOverride{LimitEnum: int64(StringToName["CertificatesPerDomain"]), BucketKey: "bad-example.net"}},
   301  					{Override: &sapb.RateLimitOverride{LimitEnum: int64(StringToName["CertificatesPerDomain"]), BucketKey: "worse-example.com"}},
   302  					{Override: &sapb.RateLimitOverride{LimitEnum: int64(StringToName["CertificatesPerDomain"]), BucketKey: "even-worse-example.xyz"}},
   303  				}}, nil
   304  			},
   305  			expectOverrides: map[string]Limit{
   306  				"example.com": {Burst: 1, Count: 1, Period: config.Duration{Duration: time.Second}, Name: CertificatesPerDomain, Comment: "Last Updated: 1970-01-01 - ", emissionInterval: 1000000000, burstOffset: 1000000000, isOverride: true},
   307  				"example.net": {Burst: 1, Count: 1, Period: config.Duration{Duration: time.Second}, Name: CertificatesPerDomain, Comment: "Last Updated: 1970-01-01 - ", emissionInterval: 1000000000, burstOffset: 1000000000, isOverride: true},
   308  			},
   309  			expectLog:            "ERR: [AUDIT] hydrating CertificatesPerDomain override with key \"bad-example.com\": invalid burst '0', must be > 0",
   310  			expectOverrideErrors: 4,
   311  		},
   312  	}
   313  
   314  	for _, tc := range tests {
   315  		t.Run(tc.name, func(t *testing.T) {
   316  			mockLog := blog.NewMock()
   317  			tb, err := NewTransactionBuilderFromDatabase("../test/config-next/ratelimit-defaults.yml", tc.overrides, metrics.NoopRegisterer, mockLog)
   318  			test.AssertNotError(t, err, "creating TransactionBuilder")
   319  			err = tb.limitRegistry.loadOverrides(context.Background())
   320  			if tc.expectError != "" {
   321  				if err == nil {
   322  					t.Errorf("expected error for test %q but got none", tc.name)
   323  				}
   324  				test.AssertContains(t, err.Error(), tc.expectError)
   325  			} else {
   326  				test.AssertNotError(t, err, tc.name)
   327  
   328  				if tc.expectLog != "" {
   329  					test.AssertSliceContains(t, mockLog.GetAll(), tc.expectLog)
   330  				}
   331  
   332  				for bucketKey, limit := range tc.expectOverrides {
   333  					test.AssertDeepEquals(t, tb.overrides[bucketKey], &limit)
   334  				}
   335  				test.AssertEquals(t, len(tb.overrides), len(tc.expectOverrides))
   336  
   337  				var iom io_prometheus_client.Metric
   338  				err = tb.limitRegistry.overridesErrors.Write(&iom)
   339  				test.AssertNotError(t, err, "encoding overridesErrors metric")
   340  				test.AssertEquals(t, iom.Gauge.GetValue(), tc.expectOverrideErrors)
   341  			}
   342  		})
   343  	}
   344  }