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 }