github.com/letsencrypt/boulder@v0.20251208.0/sfe/overrides_test.go (about) 1 package sfe 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "html/template" 8 "maps" 9 "net/http" 10 "net/http/httptest" 11 "strings" 12 "testing" 13 14 "github.com/letsencrypt/boulder/mocks" 15 rapb "github.com/letsencrypt/boulder/ra/proto" 16 rl "github.com/letsencrypt/boulder/ratelimits" 17 "github.com/letsencrypt/boulder/sfe/zendesk" 18 "github.com/letsencrypt/boulder/test/zendeskfake" 19 "google.golang.org/grpc" 20 ) 21 22 const ( 23 apiTokenEmail = "tester@example.com" 24 apiToken = "someToken" 25 ) 26 27 func createFakeZendeskClientServer(t *testing.T) (*zendeskfake.Server, *zendesk.Client) { 28 t.Helper() 29 30 server := zendeskfake.NewServer(apiTokenEmail, apiToken, nil) 31 ts := httptest.NewServer(server.Handler()) 32 t.Cleanup(ts.Close) 33 34 client, err := zendesk.NewClient(ts.URL, apiTokenEmail, apiToken, map[string]int64{ 35 "organization": 1234567, 36 "tier": 2345678, 37 "rateLimit": 3456789, 38 "reviewStatus": 34567890, 39 "accountURI": 45678901, 40 "registeredDomain": 56789012, 41 "ipAddress": 67890123, 42 }) 43 if err != nil { 44 t.Errorf("NewClient(%q) returned error: %s", ts.URL, err) 45 } 46 return server, client 47 } 48 49 func minimalTemplates(t *testing.T) *template.Template { 50 t.Helper() 51 tpl, err := template.New("pages").Parse(` 52 {{define "overrideForm.html"}}RL={{.RateLimit}};{{.FormHTML}}{{end}} 53 {{define "overrideSuccess.html"}}ok{{end}} 54 `) 55 if err != nil { 56 t.Errorf("parse templates: %s", err) 57 } 58 return tpl 59 } 60 61 func TestSetOverrideRequestFormHeaders(t *testing.T) { 62 t.Parallel() 63 64 rec := httptest.NewRecorder() 65 setOverrideRequestFormHeaders(rec) 66 67 h := rec.Header() 68 got := h.Get("X-Frame-Options") 69 if got != "DENY" { 70 t.Errorf("Unexpected X-Frame-Options=%q, expected Unexpected X-Frame-Options=\"DENY\"", got) 71 } 72 csp := h.Get("Content-Security-Policy") 73 if !strings.Contains(csp, "default-src 'self' https:") || 74 !strings.Contains(csp, "script-src 'self'") || 75 !strings.Contains(csp, "style-src 'self'") || 76 !strings.Contains(csp, "object-src 'none'") || 77 !strings.Contains(csp, "frame-ancestors 'none'") { 78 t.Errorf("Unexpected Content-Security-Policy=%q", csp) 79 } 80 got = h.Get("Cross-Origin-Opener-Policy") 81 if got != "same-origin" { 82 t.Errorf("Unexpected COOP=%q, expected COOP=\"same-origin\"", got) 83 } 84 got = h.Get("Cross-Origin-Resource-Policy") 85 if got != "same-site" { 86 t.Errorf("Unexpected CORP=%q, expected CORP=\"same-site\"", got) 87 } 88 got = h.Get("Cache-Control") 89 if !strings.Contains(got, "no-store") { 90 t.Errorf("Unexpected Cache-Control=%q, expected Cache-Control=\"no-store\"", got) 91 } 92 } 93 94 func TestValidateOverrideFieldHandlerBadJSON(t *testing.T) { 95 t.Parallel() 96 97 sfe, _ := setupSFE(t) 98 rec := httptest.NewRecorder() 99 req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("{")) 100 101 sfe.validateOverrideFieldHandler(rec, req) 102 103 if rec.Code != http.StatusBadRequest { 104 t.Errorf("Unexpected status=%d; expected status=400", rec.Code) 105 } 106 } 107 108 func TestValidateOverrideFieldHandlerInvalidValue(t *testing.T) { 109 t.Parallel() 110 111 sfe, _ := setupSFE(t) 112 113 payload := validationRequest{ 114 RateLimit: rl.NewOrdersPerAccount.String(), 115 Field: emailAddressFieldName, 116 Value: "definitely-not-an-email", 117 } 118 body, err := json.Marshal(payload) 119 if err != nil { 120 t.Errorf("marshal: %s", err) 121 } 122 123 rec := httptest.NewRecorder() 124 req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body)) 125 126 sfe.validateOverrideFieldHandler(rec, req) 127 128 if rec.Code != http.StatusOK { 129 t.Errorf("Unexpected status=%d, expected status=200", rec.Code) 130 } 131 var resp validationResponse 132 err = json.Unmarshal(rec.Body.Bytes(), &resp) 133 if err != nil { 134 t.Errorf("Unexpected failure to unmarshal JSON validationResponse: %s", err) 135 } 136 if resp.Valid { 137 t.Errorf("Valid=true; expect false") 138 } 139 if resp.Error == "" { 140 t.Errorf("Error empty; expect message") 141 } 142 } 143 144 func TestSubmitOverrideRequestHandlerErrors(t *testing.T) { 145 t.Parallel() 146 147 sfe, _ := setupSFE(t) 148 sfe.templatePages = minimalTemplates(t) 149 _, client := createFakeZendeskClientServer(t) 150 sfe.zendeskClient = client 151 mockImpl := mocks.NewMockSalesforceClientImpl() 152 sfe.ee = mocks.NewMockExporterImpl(mockImpl) 153 154 // Submit valid JSON with no rateLimit field. 155 rec := httptest.NewRecorder() 156 req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{"fields":{}}`)) 157 sfe.submitOverrideRequestHandler(rec, req) 158 if rec.Code != http.StatusBadRequest { 159 t.Errorf("Missing ratelimit: status=%d; expect 400", rec.Code) 160 } 161 162 // Submit valid JSON with a valid registered domain AND a valid IPv4 address 163 // when only one of these is allowed. 164 body, err := json.Marshal(overrideRequest{ 165 RateLimit: rl.CertificatesPerDomain.String(), Fields: map[string]string{ 166 subscriberAgreementFieldName: "true", 167 privacyPolicyFieldName: "true", 168 mailingListFieldName: "false", 169 fundraisingFieldName: FundraisingOptions[0], 170 emailAddressFieldName: "foo@bar.co", 171 OrganizationFieldName: "Big Host Inc.", 172 useCaseFieldName: strings.Repeat("x", 60), 173 TierFieldName: certificatesPerDomainTierOptions[0], 174 RegisteredDomainFieldName: "bar.co", 175 IPAddressFieldName: "2606:4700:4700::1111", 176 }, 177 }) 178 if err != nil { 179 t.Errorf("Unexpected failure to marshal JSON overrideRequest: %s", err) 180 } 181 rec = httptest.NewRecorder() 182 req = httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body)) 183 sfe.submitOverrideRequestHandler(rec, req) 184 if rec.Code != http.StatusBadRequest { 185 t.Errorf("Both domain and IP: status=%d; expect 400", rec.Code) 186 } 187 if len(mockImpl.GetCreatedContacts()) != 0 { 188 t.Errorf("PardotClient.SendContact called unexpectedly") 189 } 190 if len(mockImpl.GetCreatedCases()) != 0 { 191 t.Errorf("PardotClient.SendCase called unexpectedly") 192 } 193 } 194 195 func TestSubmitOverrideRequestHandlerSuccess(t *testing.T) { 196 t.Parallel() 197 198 sfe, _ := setupSFE(t) 199 sfe.templatePages = minimalTemplates(t) 200 _, client := createFakeZendeskClientServer(t) 201 sfe.zendeskClient = client 202 203 // All of these fields are perfectly valid. 204 testBase := map[string]string{ 205 subscriberAgreementFieldName: "true", 206 privacyPolicyFieldName: "true", 207 mailingListFieldName: "true", 208 fundraisingFieldName: FundraisingOptions[0], 209 emailAddressFieldName: "foo@bar.co", 210 OrganizationFieldName: "Big Host Inc.", 211 useCaseFieldName: strings.Repeat("x", 60), 212 TierFieldName: newOrdersPerAccountTierOptions[0], 213 } 214 215 type tc struct { 216 name string 217 rateLimit string 218 fields map[string]string 219 zendeskMatch map[string]string 220 } 221 tests := []tc{ 222 { 223 name: "NewOrdersPerAccount with valid Account URI", 224 rateLimit: rl.NewOrdersPerAccount.String(), 225 fields: map[string]string{ 226 AccountURIFieldName: "https://acme-v02.api.letsencrypt.org/acme/acct/12345", 227 }, 228 zendeskMatch: map[string]string{ 229 RateLimitFieldName: rl.NewOrdersPerAccount.String(), 230 AccountURIFieldName: "https://acme-v02.api.letsencrypt.org/acme/acct/12345", 231 }, 232 }, 233 { 234 name: "CertificatesPerDomainPerAccount with valid Account URI", 235 rateLimit: rl.CertificatesPerDomainPerAccount.String(), 236 fields: map[string]string{ 237 TierFieldName: certificatesPerDomainPerAccountTierOptions[0], 238 AccountURIFieldName: "https://acme-v02.api.letsencrypt.org/acme/acct/67890", 239 }, 240 zendeskMatch: map[string]string{ 241 RateLimitFieldName: rl.CertificatesPerDomainPerAccount.String(), 242 AccountURIFieldName: "https://acme-v02.api.letsencrypt.org/acme/acct/67890", 243 }, 244 }, 245 { 246 name: "CertificatesPerDomain with valid Registered Domain", 247 rateLimit: rl.CertificatesPerDomain.String() + perDNSNameSuffix, 248 fields: map[string]string{ 249 TierFieldName: certificatesPerDomainTierOptions[0], 250 RegisteredDomainFieldName: "bar.co", 251 }, 252 zendeskMatch: map[string]string{ 253 RateLimitFieldName: rl.CertificatesPerDomain.String() + perDNSNameSuffix, 254 RegisteredDomainFieldName: "bar.co", 255 }, 256 }, 257 { 258 name: "CertificatesPerDomain with valid IPv6 Address", 259 rateLimit: rl.CertificatesPerDomain.String() + perIPSuffix, 260 fields: map[string]string{ 261 TierFieldName: certificatesPerDomainTierOptions[0], 262 IPAddressFieldName: "2606:4700:4700::1111", 263 }, 264 zendeskMatch: map[string]string{ 265 RateLimitFieldName: rl.CertificatesPerDomain.String() + perIPSuffix, 266 IPAddressFieldName: "2606:4700:4700::1111", 267 }, 268 }, 269 { 270 name: "CertificatesPerDomain with valid IPv4 Address", 271 rateLimit: rl.CertificatesPerDomain.String() + perIPSuffix, 272 fields: map[string]string{ 273 TierFieldName: certificatesPerDomainTierOptions[0], 274 IPAddressFieldName: "64.112.11.11", 275 }, 276 zendeskMatch: map[string]string{ 277 RateLimitFieldName: rl.CertificatesPerDomain.String() + perIPSuffix, 278 IPAddressFieldName: "64.112.11.11", 279 }, 280 }, 281 } 282 283 for _, tt := range tests { 284 t.Run(tt.name, func(t *testing.T) { 285 mockImpl := mocks.NewMockSalesforceClientImpl() 286 sfe.ee = mocks.NewMockExporterImpl(mockImpl) 287 288 iterationBase := map[string]string{} 289 maps.Copy(iterationBase, testBase) 290 maps.Copy(iterationBase, tt.fields) 291 292 reqObj := overrideRequest{ 293 RateLimit: tt.rateLimit, 294 Fields: iterationBase, 295 } 296 body, err := json.Marshal(reqObj) 297 if err != nil { 298 t.Errorf("marshal: %s", err) 299 } 300 rec := httptest.NewRecorder() 301 req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body)) 302 303 sfe.submitOverrideRequestHandler(rec, req) 304 305 if rec.Code != http.StatusAccepted { 306 t.Errorf("Unexpected status=%d, expected status=202", rec.Code) 307 } 308 309 got, err := client.FindTickets(tt.zendeskMatch, "") 310 if err != nil { 311 t.Errorf("FindTickets(%+v) returned error: %s", tt.zendeskMatch, err) 312 } 313 if len(got) == 0 { 314 t.Errorf("FindTickets(%+v) returned no tickets, should have found one", tt.zendeskMatch) 315 } 316 for _, fields := range got { 317 for fieldName, fieldValue := range tt.zendeskMatch { 318 _, ok := fields[fieldName] 319 if !ok { 320 t.Errorf("The resulting ticket is missing expected field %q in %+v", fieldName, fields) 321 } 322 if fields[fieldName] != fieldValue { 323 t.Errorf("The resulting ticket is missing an expected field value %q=%q in %+v", fieldName, fieldValue, fields) 324 } 325 } 326 break 327 } 328 if len(mockImpl.GetCreatedContacts()) != 1 { 329 t.Errorf("PardotClient.SendContact not called exactly once") 330 } 331 if len(mockImpl.GetCreatedCases()) != 1 { 332 t.Errorf("PardotClient.SendCase not called exactly once") 333 } 334 }) 335 } 336 } 337 338 func TestValidateOverrideRequestField(t *testing.T) { 339 type testCase struct { 340 name string 341 fieldName string 342 fieldValue string 343 ratelimitName string 344 expectErr bool 345 expectErrContains string 346 } 347 348 var cases []testCase 349 // Empty Field 350 cases = append(cases, 351 testCase{"Empty field name", "", "x", "rl", true, "field name cannot be empty"}, 352 testCase{"Empty field value", "some", "", "rl", true, "cannot be empty"}, 353 testCase{"Tier without rate limit", TierFieldName, "10", "", true, "must be specified"}, 354 testCase{"Unknown field", "not-a-field", "x", "rl", true, "unknown field"}, 355 ) 356 // MailingListFieldName 357 cases = append(cases, 358 testCase{"MailingList true", mailingListFieldName, "true", "", false, ""}, 359 testCase{"MailingList false", mailingListFieldName, "false", "", false, ""}, 360 testCase{"MailingList yup", mailingListFieldName, "yup", "", true, "true or false"}, 361 ) 362 // SubscriberAgreement/PrivacyPolicy 363 for _, fieldName := range []string{subscriberAgreementFieldName, privacyPolicyFieldName} { 364 cases = append(cases, 365 testCase{fieldName + " true", fieldName, "true", "", false, ""}, 366 testCase{fieldName + " false", fieldName, "false", "", true, "required"}, 367 testCase{fieldName + " yep", fieldName, "yep", "", true, "true or false"}, 368 ) 369 } 370 // FundraisingFieldName 371 cases = append(cases, 372 testCase{"Fundraising valid", fundraisingFieldName, FundraisingOptions[0], "", false, ""}, 373 testCase{"Fundraising invalid", fundraisingFieldName, "explicitly not an option", "", true, "valid options are"}, 374 ) 375 // EmailAddressFieldName 376 cases = append(cases, 377 testCase{"EmailAddress valid email", emailAddressFieldName, "foo@bar.co", "", false, ""}, 378 testCase{"EmailAddress invalid", emailAddressFieldName, "foo@", "", true, "invalid"}, 379 ) 380 // OrganizationFieldName 381 cases = append(cases, 382 testCase{"Organization valid", OrganizationFieldName, "Big Host Inc", "", false, ""}, 383 testCase{"Organization too short", OrganizationFieldName, "Big", "", true, "at least five"}, 384 ) 385 // UseCaseFieldName 386 cases = append(cases, 387 testCase{"UseCase exactly long enough", useCaseFieldName, strings.Repeat("x", 60), "", false, ""}, 388 testCase{"UseCase too short", useCaseFieldName, strings.Repeat("x", 59), "", true, "at least 60"}, 389 ) 390 // IPAddressFieldName 391 cases = append(cases, 392 testCase{"IPAddress IPv4 valid", IPAddressFieldName, "64.112.11.11", "", false, ""}, 393 testCase{"IPAddress IPv4 invalid", IPAddressFieldName, "64.112.11.256", "", true, "invalid"}, 394 testCase{"IPAddress IPv6 valid", IPAddressFieldName, "2606:4700:4700::1111", "", false, ""}, 395 testCase{"IPAddress IPv6 invalid", IPAddressFieldName, "2606:4700:4700::1111:12345", "", true, "invalid"}, 396 ) 397 // RegisteredDomainFieldName 398 cases = append(cases, 399 testCase{"RegisteredDomain valid eTLD+1", RegisteredDomainFieldName, "example.com", "", false, ""}, 400 testCase{"RegisteredDomain bare TLD", RegisteredDomainFieldName, "com", "", true, "registered domain name is invalid"}, 401 testCase{"RegisteredDomain eTLD+2", RegisteredDomainFieldName, "foo.bar.example.com", "", true, "only the eTLD+1"}, 402 testCase{"RegisteredDomain invalid syntax", RegisteredDomainFieldName, "not even close to a domain", "", true, "invalid"}, 403 ) 404 // AccountURIFieldName 405 cases = append(cases, 406 testCase{"AccountURI valid", AccountURIFieldName, "https://acme-v02.api.letsencrypt.org/acme/acct/12345", "", false, ""}, 407 testCase{"AccountURI bad host", AccountURIFieldName, "https://acme-v02.ap1.letsencrypt.org/acme/acct/1", "", true, "account URI is invalid"}, 408 testCase{"AccountURI bad id", AccountURIFieldName, "https://acme-v02.api.letsencrypt.org/acme/acct/notnum", "", true, "positive integer"}, 409 testCase{"AccountURI bad path shape", AccountURIFieldName, "https://acme-v02.api.letsencrypt.org/acme/acct/1/extra", "", true, "path must be"}, 410 ) 411 // TierFieldName 412 cases = append(cases, 413 testCase{"Tier valid", TierFieldName, "1000", "NewOrdersPerAccount", false, ""}, 414 testCase{"Tier invalid option", TierFieldName, "999", "NewOrdersPerAccount", true, "valid options are"}, 415 testCase{"Tier unknown rl", TierFieldName, "10", "DoesNotExist", true, "unknown rate limit"}, 416 ) 417 418 for _, tc := range cases { 419 t.Run(tc.name, func(t *testing.T) { 420 err := validateOverrideRequestField(tc.fieldName, tc.fieldValue, tc.ratelimitName) 421 if tc.expectErr { 422 if err == nil { 423 t.Fatalf("expected error, got nil") 424 } 425 if tc.expectErrContains != "" && !strings.Contains(err.Error(), tc.expectErrContains) { 426 t.Fatalf("Error %q does not contain %q", err.Error(), tc.expectErrContains) 427 } 428 return 429 } 430 if err != nil { 431 t.Fatalf("unexpected error, got %s", err) 432 } 433 }) 434 } 435 } 436 437 func TestSubmitOverrideRequestHandlerRateLimited(t *testing.T) { 438 t.Parallel() 439 440 sfe, _ := setupSFE(t) 441 sfe.templatePages = minimalTemplates(t) 442 _, client := createFakeZendeskClientServer(t) 443 sfe.zendeskClient = client 444 445 for attempt := range 101 { 446 reqObj := overrideRequest{ 447 RateLimit: rl.CertificatesPerDomainPerAccount.String(), 448 Fields: map[string]string{ 449 subscriberAgreementFieldName: "true", 450 privacyPolicyFieldName: "true", 451 mailingListFieldName: "false", 452 fundraisingFieldName: FundraisingOptions[0], 453 emailAddressFieldName: "foo@bar.co", 454 OrganizationFieldName: "Big Host Inc.", 455 useCaseFieldName: strings.Repeat("x", 60), 456 TierFieldName: certificatesPerDomainPerAccountTierOptions[0], 457 AccountURIFieldName: "https://acme-v02.api.letsencrypt.org/acme/acct/67890", 458 }, 459 } 460 body, err := json.Marshal(reqObj) 461 if err != nil { 462 t.Errorf("Unexpected failure to marshal JSON overrideRequest: %s", err) 463 } 464 rec := httptest.NewRecorder() 465 req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body)) 466 467 sfe.submitOverrideRequestHandler(rec, req) 468 if attempt < 100 { 469 if rec.Code != http.StatusAccepted { 470 t.Errorf("Unexpected status=%d, expected status=202", rec.Code) 471 } 472 } else { 473 if rec.Code != http.StatusTooManyRequests { 474 t.Errorf("Unexpected status=%d, expected status=429", rec.Code) 475 } 476 if !strings.Contains(rec.Body.String(), "too many override request form submissions (100)") { 477 t.Errorf("Expected rate limit error message, got: %s", rec.Body.String()) 478 } 479 } 480 } 481 } 482 483 type addedOverrideEnabledRA struct { 484 rapb.RegistrationAuthorityClient 485 } 486 487 func (f *addedOverrideEnabledRA) AddRateLimitOverride(ctx context.Context, req *rapb.AddRateLimitOverrideRequest, opts ...grpc.CallOption) (*rapb.AddRateLimitOverrideResponse, error) { 488 return &rapb.AddRateLimitOverrideResponse{Enabled: true}, nil 489 } 490 491 type addedOverrideDisabledRA struct { 492 rapb.RegistrationAuthorityClient 493 } 494 495 func (f *addedOverrideDisabledRA) AddRateLimitOverride(ctx context.Context, req *rapb.AddRateLimitOverrideRequest, opts ...grpc.CallOption) (*rapb.AddRateLimitOverrideResponse, error) { 496 return &rapb.AddRateLimitOverrideResponse{Enabled: false}, nil 497 } 498 499 func TestSubmitOverrideRequestHandlerAutoApproved(t *testing.T) { 500 t.Parallel() 501 502 sfe, _ := setupSFE(t) 503 sfe.templatePages = minimalTemplates(t) 504 _, client := createFakeZendeskClientServer(t) 505 sfe.zendeskClient = client 506 sfe.autoApproveOverrides = true 507 508 reqObj := overrideRequest{ 509 RateLimit: rl.CertificatesPerDomainPerAccount.String(), 510 Fields: map[string]string{ 511 subscriberAgreementFieldName: "true", 512 privacyPolicyFieldName: "true", 513 mailingListFieldName: "false", 514 fundraisingFieldName: FundraisingOptions[0], 515 emailAddressFieldName: "foo@bar.co", 516 OrganizationFieldName: "Big Host Inc.", 517 useCaseFieldName: strings.Repeat("x", 60), 518 TierFieldName: certificatesPerDomainPerAccountTierOptions[0], 519 AccountURIFieldName: "https://acme-v02.api.letsencrypt.org/acme/acct/67890", 520 }, 521 } 522 reqObjBytes, err := json.Marshal(reqObj) 523 if err != nil { 524 t.Fatalf("marshal: %s", err) 525 } 526 527 type testCase struct { 528 name string 529 ra rapb.RegistrationAuthorityClient 530 expectedCode int 531 } 532 cases := []testCase{ 533 { 534 name: "New override enabled", 535 ra: &addedOverrideEnabledRA{}, 536 expectedCode: http.StatusCreated, 537 }, 538 { 539 name: "Existing override disabled", 540 ra: &addedOverrideDisabledRA{}, 541 expectedCode: http.StatusAccepted, 542 }, 543 } 544 545 for _, tc := range cases { 546 t.Run(tc.name, func(t *testing.T) { 547 548 req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(reqObjBytes)) 549 rec := httptest.NewRecorder() 550 551 sfe.ra = tc.ra 552 sfe.submitOverrideRequestHandler(rec, req) 553 554 if rec.Code != tc.expectedCode { 555 t.Errorf("Unexpected status=%d, expected status=%d", rec.Code, tc.expectedCode) 556 } 557 }) 558 } 559 }