github.com/letsencrypt/boulder@v0.20251208.0/sfe/overridesimporter_test.go (about) 1 package sfe 2 3 import ( 4 "context" 5 "net" 6 "strings" 7 "sync" 8 "testing" 9 "time" 10 11 blog "github.com/letsencrypt/boulder/log" 12 rapb "github.com/letsencrypt/boulder/ra/proto" 13 rl "github.com/letsencrypt/boulder/ratelimits" 14 "github.com/letsencrypt/boulder/sfe/zendesk" 15 16 "github.com/jmhodges/clock" 17 "google.golang.org/grpc" 18 "google.golang.org/grpc/codes" 19 "google.golang.org/grpc/credentials/insecure" 20 "google.golang.org/grpc/status" 21 ) 22 23 type raBehavior int 24 25 const ( 26 ok raBehavior = iota 27 alwaysError 28 alwaysAdministrativelyDisabled 29 ) 30 31 type raFakeServer struct { 32 rapb.UnimplementedRegistrationAuthorityServer 33 behavior raBehavior 34 35 mu sync.Mutex 36 lastRequest *rapb.AddRateLimitOverrideRequest 37 allRequests []*rapb.AddRateLimitOverrideRequest 38 } 39 40 func (s *raFakeServer) AddRateLimitOverride(ctx context.Context, r *rapb.AddRateLimitOverrideRequest) (*rapb.AddRateLimitOverrideResponse, error) { 41 s.mu.Lock() 42 defer s.mu.Unlock() 43 44 s.lastRequest = r 45 s.allRequests = append(s.allRequests, r) 46 47 switch s.behavior { 48 case ok: 49 return &rapb.AddRateLimitOverrideResponse{Enabled: true}, nil 50 case alwaysAdministrativelyDisabled: 51 return &rapb.AddRateLimitOverrideResponse{Enabled: false}, nil 52 case alwaysError: 53 return nil, status.Error(codes.Internal, "oh no, something has gone terribly awry!") 54 default: 55 return &rapb.AddRateLimitOverrideResponse{Enabled: true}, nil 56 } 57 } 58 59 func (s *raFakeServer) calls() []*rapb.AddRateLimitOverrideRequest { 60 s.mu.Lock() 61 defer s.mu.Unlock() 62 63 out := make([]*rapb.AddRateLimitOverrideRequest, len(s.allRequests)) 64 copy(out, s.allRequests) 65 return out 66 } 67 68 func startRAFakeSrv(t *testing.T, behavior raBehavior) (*raFakeServer, rapb.RegistrationAuthorityClient, func()) { 69 t.Helper() 70 71 lis, err := net.Listen("tcp", "127.0.0.1:0") 72 if err != nil { 73 t.Errorf("while creating listener: %s", err) 74 } 75 76 srv := grpc.NewServer() 77 fake := &raFakeServer{behavior: behavior} 78 rapb.RegisterRegistrationAuthorityServer(srv, fake) 79 80 done := make(chan struct{}) 81 go func() { 82 _ = srv.Serve(lis) 83 close(done) 84 }() 85 86 conn, err := grpc.NewClient(lis.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials())) 87 if err != nil { 88 t.Errorf("while creating grpc client: %s", err) 89 } 90 return fake, rapb.NewRegistrationAuthorityClient(conn), func() { 91 srv.GracefulStop() 92 <-done 93 _ = conn.Close() 94 _ = lis.Close() 95 } 96 } 97 98 func newImporter(t *testing.T, ra rapb.RegistrationAuthorityClient, zd *zendesk.Client, p ProcessMode) *OverridesImporter { 99 t.Helper() 100 101 var lg blog.Logger = blog.NewMock() 102 im, err := NewOverridesImporter(p, time.Minute, zd, ra, clock.New(), lg) 103 if err != nil { 104 t.Errorf("while creating OverridesImporter: %s", err) 105 } 106 return im 107 } 108 109 func createApprovedTicket(t *testing.T, c *zendesk.Client) int64 { 110 t.Helper() 111 112 fields := map[string]string{ 113 RateLimitFieldName: rl.NewOrdersPerAccount.String(), 114 TierFieldName: "1000", 115 OrganizationFieldName: "Acme Corp", 116 AccountURIFieldName: "https://acme-v02.api.letsencrypt.org/acme/acct/999", 117 ReviewStatusFieldName: reviewStatusApproved, 118 } 119 120 id, err := c.CreateTicket("foo@bar.biz", "Test Ticket", "Test Body", fields) 121 if err != nil { 122 t.Errorf("while creating test ticket: %s", err) 123 } 124 err = c.UpdateTicketStatus(id, "open", "", false) 125 if err != nil { 126 t.Errorf("while updating ticket %d to open: %s", id, err) 127 } 128 return id 129 } 130 131 func TestOverridesImporterProcessTicketHappyPath(t *testing.T) { 132 t.Parallel() 133 134 testCases := []struct { 135 name string 136 fields map[string]string 137 expectLimit rl.Name 138 expectBucketKey string 139 expectTier int64 140 expectBurst int64 141 expectCount int64 142 expectPeriod time.Duration 143 expectOrgComment string 144 expectLastCommentSubstring string 145 }{ 146 { 147 name: "NewOrdersPerAccount with valid Account URI", 148 fields: map[string]string{ 149 RateLimitFieldName: rl.NewOrdersPerAccount.String(), 150 TierFieldName: "1000", 151 OrganizationFieldName: "Acme Corp", 152 AccountURIFieldName: "https://acme-v02.api.letsencrypt.org/acme/acct/12345", 153 }, 154 expectLimit: rl.NewOrdersPerAccount, 155 expectBucketKey: "3:12345", 156 expectTier: 1000, 157 expectBurst: 1000, 158 expectCount: 1000, 159 expectPeriod: 7 * 24 * time.Hour, 160 expectOrgComment: "Acme Corp", 161 expectLastCommentSubstring: "has been approved. Your new limit is 1000 per period", 162 }, 163 { 164 name: "CertificatesPerDomain with valid Registered Domain", 165 fields: map[string]string{ 166 RateLimitFieldName: rl.CertificatesPerDomain.String() + perDNSNameSuffix, 167 TierFieldName: "300", 168 OrganizationFieldName: "Acme Corp", 169 RegisteredDomainFieldName: "example.com", 170 }, 171 expectLimit: rl.CertificatesPerDomain, 172 expectBucketKey: "5:example.com", 173 expectTier: 300, 174 expectBurst: 300, 175 expectCount: 300, 176 expectPeriod: 7 * 24 * time.Hour, 177 expectOrgComment: "Acme Corp", 178 expectLastCommentSubstring: "has been approved. Your new limit is 300 per period", 179 }, 180 { 181 name: "CertificatesPerDomain with valid IPv4 Address", 182 fields: map[string]string{ 183 RateLimitFieldName: rl.CertificatesPerDomain.String() + perIPSuffix, 184 TierFieldName: "300", 185 OrganizationFieldName: "Acme Corp", 186 IPAddressFieldName: "64.112.11.11", 187 }, 188 expectLimit: rl.CertificatesPerDomain, 189 expectBucketKey: "5:64.112.11.11/32", 190 expectTier: 300, 191 expectBurst: 300, 192 expectCount: 300, 193 expectPeriod: 7 * 24 * time.Hour, 194 expectOrgComment: "Acme Corp", 195 expectLastCommentSubstring: "has been approved. Your new limit is 300 per period", 196 }, 197 { 198 name: "CertificatesPerDomain with valid IPv6", 199 fields: map[string]string{ 200 RateLimitFieldName: rl.CertificatesPerDomain.String() + perIPSuffix, 201 TierFieldName: "300", 202 OrganizationFieldName: "Acme Corp", 203 IPAddressFieldName: "2606:4700:4700::1111", 204 }, 205 expectLimit: rl.CertificatesPerDomain, 206 expectBucketKey: "5:2606:4700:4700::/64", 207 expectTier: 300, expectBurst: 300, expectCount: 300, 208 expectPeriod: 7 * 24 * time.Hour, 209 expectOrgComment: "Acme Corp", 210 expectLastCommentSubstring: "has been approved. Your new limit is 300 per period", 211 }, 212 { 213 name: "CertificatesPerDomainPerAccount with valid Account URI", 214 fields: map[string]string{ 215 RateLimitFieldName: rl.CertificatesPerDomainPerAccount.String(), 216 TierFieldName: "300", 217 OrganizationFieldName: "Acme Corp", 218 AccountURIFieldName: "https://acme-v02.api.letsencrypt.org/acme/acct/12345", 219 }, 220 expectLimit: rl.CertificatesPerDomainPerAccount, 221 expectBucketKey: "6:12345", 222 expectTier: 300, 223 expectBurst: 300, 224 expectCount: 300, 225 expectPeriod: 7 * 24 * time.Hour, 226 expectOrgComment: "Acme Corp", 227 expectLastCommentSubstring: "has been approved. Your new limit is 300 per period", 228 }, 229 } 230 231 for _, tc := range testCases { 232 t.Run(tc.name, func(t *testing.T) { 233 t.Parallel() 234 235 zdServer, zdClient := createFakeZendeskClientServer(t) 236 raSrv, raClient, stopRA := startRAFakeSrv(t, ok) 237 defer stopRA() 238 239 ticketID := createApprovedTicket(t, zdClient) 240 241 im := newImporter(t, raClient, zdClient, ProcessAll) 242 err := im.processTicket(context.Background(), ticketID, tc.fields) 243 if err != nil { 244 t.Errorf("processTicket got an unexpected error: %s", err) 245 } 246 247 req := raSrv.lastRequest 248 if req == nil { 249 t.Errorf("RA AddRateLimitOverride was not called") 250 return 251 } 252 if req.LimitEnum != int64(tc.expectLimit) { 253 t.Errorf("got rapb.AddRateLimitOverrideRequest.LimitEnum=%d, expected %d", req.LimitEnum, tc.expectLimit) 254 } 255 if req.Comment != tc.expectOrgComment { 256 t.Errorf("got rapb.AddRateLimitOverrideRequest.Comment=%q, expected %q", req.Comment, tc.expectOrgComment) 257 } 258 if req.Count != tc.expectCount { 259 t.Errorf("got rapb.AddRateLimitOverrideRequest.Count=%d, expected %d", req.Count, tc.expectCount) 260 } 261 if req.Burst != tc.expectBurst { 262 t.Errorf("got rapb.AddRateLimitOverrideRequest.Burst=%d, expected %d", req.Burst, tc.expectBurst) 263 } 264 gotPeriod := req.Period.AsDuration() 265 if gotPeriod != tc.expectPeriod { 266 t.Errorf("got rapb.AddRateLimitOverrideRequest.Period=%s, expected %s", gotPeriod, tc.expectPeriod) 267 } 268 if req.BucketKey != tc.expectBucketKey { 269 t.Errorf("got rapb.AddRateLimitOverrideRequest.BucketKey=%q, expected %q", req.BucketKey, tc.expectBucketKey) 270 } 271 272 got, ok := zdServer.GetTicket(ticketID) 273 if !ok { 274 t.Errorf("ticket %d not found in zendesk store", ticketID) 275 } 276 if got.Status != "solved" { 277 // Ticket should remain "solved" after successful processing. 278 t.Errorf("unexpected ticket status=%q, expected solved", got.Status) 279 } 280 281 if tc.expectLastCommentSubstring == "" { 282 if len(got.Comments) != 1 { 283 t.Errorf("unexpected comments count: got %d, expected 1", len(got.Comments)) 284 } 285 } else { 286 if len(got.Comments) < 2 { 287 t.Errorf("expected an additional comment, got %d comments (%#v)", len(got.Comments), got.Comments) 288 } 289 last := got.Comments[len(got.Comments)-1] 290 if !last.Public { 291 t.Errorf("expected last comment to be public but it was private") 292 } 293 if !strings.Contains(last.Body, tc.expectLastCommentSubstring) { 294 t.Errorf("last comment body %q does not contain %q", last.Body, tc.expectLastCommentSubstring) 295 } 296 } 297 }) 298 } 299 } 300 301 func TestOverridesImporterProcessTicketSadPath(t *testing.T) { 302 t.Parallel() 303 304 testCases := []struct { 305 name string 306 tickerFields map[string]string 307 raFakeBehavior raBehavior 308 expectErrSubstring string 309 expectStatus string 310 expectLastCommentSubstring string 311 }{ 312 { 313 name: "missing rate limit field", 314 tickerFields: map[string]string{OrganizationFieldName: "Acme Corp"}, 315 raFakeBehavior: ok, 316 expectErrSubstring: "missing rate limit field", 317 expectStatus: "pending", 318 expectLastCommentSubstring: "missing rate limit field", 319 }, 320 { 321 name: "invalid tier option (validation error)", 322 tickerFields: map[string]string{ 323 RateLimitFieldName: rl.NewOrdersPerAccount.String(), 324 TierFieldName: "999", 325 OrganizationFieldName: "Acme Corp", 326 AccountURIFieldName: "https://acme-v02.api.letsencrypt.org/acme/acct/12345", 327 }, 328 raFakeBehavior: ok, 329 expectErrSubstring: "invalid request override quantity", 330 expectStatus: "pending", 331 expectLastCommentSubstring: "getting/validating tier field", 332 }, 333 { 334 name: "invalid account URI (validation error)", 335 tickerFields: map[string]string{ 336 RateLimitFieldName: rl.NewOrdersPerAccount.String(), 337 TierFieldName: "1000", 338 OrganizationFieldName: "Acme Corp", 339 AccountURIFieldName: "https://acme-v02.ap1.letsencrypt.org/acme/acct/1", 340 }, 341 raFakeBehavior: ok, 342 expectErrSubstring: "account URI is invalid", 343 expectStatus: "pending", 344 expectLastCommentSubstring: "getting/validating accountURI", 345 }, 346 { 347 name: "invalid IP (validation error)", 348 tickerFields: map[string]string{ 349 RateLimitFieldName: rl.CertificatesPerDomain.String() + perIPSuffix, 350 TierFieldName: "300", 351 OrganizationFieldName: "Acme Corp", 352 IPAddressFieldName: "2606:4700:4700::1111:12345", 353 }, 354 raFakeBehavior: ok, 355 356 expectErrSubstring: "IP address is invalid", 357 expectStatus: "pending", 358 expectLastCommentSubstring: "getting/validating ipAddress", 359 }, 360 { 361 name: "RA administratively disabled", 362 tickerFields: map[string]string{ 363 RateLimitFieldName: rl.NewOrdersPerAccount.String(), 364 TierFieldName: "1000", 365 OrganizationFieldName: "Acme Corp", 366 AccountURIFieldName: "https://acme-v02.api.letsencrypt.org/acme/acct/12345", 367 }, 368 raFakeBehavior: alwaysAdministrativelyDisabled, 369 expectErrSubstring: "administratively disabled", 370 expectStatus: "pending", 371 expectLastCommentSubstring: "administratively disabled", 372 }, 373 { 374 name: "RA internal error (no ticket update)", 375 tickerFields: map[string]string{ 376 RateLimitFieldName: rl.NewOrdersPerAccount.String(), 377 TierFieldName: "1000", 378 OrganizationFieldName: "Acme Corp", 379 AccountURIFieldName: "https://acme-v02.api.letsencrypt.org/acme/acct/12345", 380 }, 381 raFakeBehavior: alwaysError, 382 expectErrSubstring: "calling ra.AddRateLimitOverride", 383 expectStatus: "open", 384 }, 385 } 386 387 for _, tc := range testCases { 388 t.Run(tc.name, func(t *testing.T) { 389 zdServer, zdClient := createFakeZendeskClientServer(t) 390 391 _, raClient, stopRA := startRAFakeSrv(t, tc.raFakeBehavior) 392 defer stopRA() 393 394 ticketID := createApprovedTicket(t, zdClient) 395 396 im := newImporter(t, raClient, zdClient, ProcessAll) 397 398 err := im.processTicket(context.Background(), ticketID, tc.tickerFields) 399 if err == nil { 400 t.Errorf("processTicket error = nil, expected error containing %q", tc.expectErrSubstring) 401 } 402 if tc.expectErrSubstring != "" && !strings.Contains(err.Error(), tc.expectErrSubstring) { 403 t.Errorf("error=%q does not contain %q", err.Error(), tc.expectErrSubstring) 404 } 405 406 got, ok := zdServer.GetTicket(ticketID) 407 if !ok { 408 t.Errorf("ticket %d not found in zendesk store", ticketID) 409 } 410 if got.Status != tc.expectStatus { 411 t.Errorf("unexpected ticket status=%q, expected %q", got.Status, tc.expectStatus) 412 } 413 414 if tc.expectLastCommentSubstring == "" { 415 if len(got.Comments) != 1 { 416 t.Errorf("unexpected comments count: got %d; expected 1", len(got.Comments)) 417 } 418 } else { 419 if len(got.Comments) < 2 { 420 t.Errorf("expected an additional comment, got %d comments (%#v)", len(got.Comments), got.Comments) 421 } 422 last := got.Comments[len(got.Comments)-1] 423 if last.Public != false { 424 t.Errorf("last comment Public=%t, expected false; errors should not be shown to end users", last.Public) 425 } 426 if !strings.Contains(last.Body, tc.expectLastCommentSubstring) { 427 t.Errorf("last comment body %q does not contain %q", last.Body, tc.expectLastCommentSubstring) 428 } 429 430 } 431 }) 432 } 433 } 434 435 func TestTickProcessModes(t *testing.T) { 436 t.Parallel() 437 438 testCases := []struct { 439 name string 440 mode ProcessMode 441 expectTicketIDs []int64 442 expectRARequestCount int 443 }{ 444 { 445 name: "importer.tick() with Mode=ProcessAll", 446 mode: ProcessAll, 447 expectTicketIDs: []int64{1, 2, 3}, 448 expectRARequestCount: 3, 449 }, 450 { 451 name: "importer.tick() with Mode=ProcessEven", 452 mode: processEven, 453 expectTicketIDs: []int64{2}, 454 expectRARequestCount: 1, 455 }, 456 { 457 name: "importer.tick() with Mode=ProcessOdd", 458 mode: processOdd, 459 expectTicketIDs: []int64{1, 3}, 460 expectRARequestCount: 2, 461 }, 462 } 463 464 for _, tc := range testCases { 465 t.Run(tc.name, func(t *testing.T) { 466 t.Parallel() 467 468 zdServer, zdClient := createFakeZendeskClientServer(t) 469 raSrv, raClient, stopRA := startRAFakeSrv(t, ok) 470 defer stopRA() 471 472 var initialTicketIDs []int64 473 initialTicketIDToCommentsCount := make(map[int64]int) 474 for range 3 { 475 ticketID := createApprovedTicket(t, zdClient) 476 initialTicketIDs = append(initialTicketIDs, ticketID) 477 478 initialTicket, ok := zdServer.GetTicket(ticketID) 479 if !ok { 480 t.Errorf("ticket %d not found in zendesk store", ticketID) 481 } 482 initialTicketIDToCommentsCount[ticketID] = len(initialTicket.Comments) 483 } 484 485 im := newImporter(t, raClient, zdClient, tc.mode) 486 im.tick(context.Background()) 487 488 AddRateLimitOverrideCalls := raSrv.calls() 489 if len(AddRateLimitOverrideCalls) != tc.expectRARequestCount { 490 t.Errorf("got %d RA AddRateLimitOverride calls, expected %d", len(AddRateLimitOverrideCalls), tc.expectRARequestCount) 491 } 492 493 processedTickets := make(map[int64]bool) 494 for _, id := range initialTicketIDs { 495 resultingTicket, ok := zdServer.GetTicket(id) 496 if !ok { 497 t.Errorf("ticket %d not found after tick", id) 498 } 499 if len(resultingTicket.Comments) > initialTicketIDToCommentsCount[id] { 500 // We know that a ticket was processed if it has more comments than it started with. 501 processedTickets[id] = true 502 } 503 } 504 505 if len(processedTickets) != len(tc.expectTicketIDs) { 506 t.Errorf("got %d processed tickets, expected %d", len(processedTickets), len(tc.expectTicketIDs)) 507 } 508 for _, id := range tc.expectTicketIDs { 509 if !processedTickets[id] { 510 t.Errorf("expected ticket %d to be processed, but it was not", id) 511 } 512 } 513 }) 514 } 515 }