diff --git a/cmd/sfe/main.go b/cmd/sfe/main.go index 3c7b91109df..5eaa9e9b00e 100644 --- a/cmd/sfe/main.go +++ b/cmd/sfe/main.go @@ -94,7 +94,14 @@ type Config struct { // 20 minutes. Interval config.Duration `validate:"omitempty,required_with=Mode,min=1200s"` } `validate:"omitempty,dive"` - Features features.Config + + // AutoApproveOverrides enables automatic approval of override requests + // for the following limits and tiers: + // - NewOrdersPerAccount: 1000 + // - CertificatesPerDomain: 300 + // - CertificatesPerDomainPerAccount: 300 + AutoApproveOverrides bool `validate:"-"` + Features features.Config } Syslog cmd.SyslogConfig @@ -232,6 +239,7 @@ func main() { zendeskClient, limiter, txnBuilder, + c.SFE.AutoApproveOverrides, ) cmd.FailOnError(err, "Unable to create SFE") diff --git a/sfe/overrides.go b/sfe/overrides.go index e48c087a9ff..e313f27b756 100644 --- a/sfe/overrides.go +++ b/sfe/overrides.go @@ -1,6 +1,7 @@ package sfe import ( + "context" "encoding/json" "errors" "fmt" @@ -269,56 +270,39 @@ func makeInitialComment(organization, useCase, tier string) string { ) } -// createNewOrdersPerAccountOverrideTicket creates a new Zendesk ticket for a -// NewOrdersPerAccount override request. All fields are required. -func createNewOrdersPerAccountOverrideTicket(client *zendesk.Client, requesterEmail, useCase, organization, tier, accountID string) (int64, error) { - return client.CreateTicket( - requesterEmail, - makeSubject(rl.NewOrdersPerAccount, organization), - makeInitialComment(organization, useCase, tier), - map[string]string{ - RateLimitFieldName: rl.NewOrdersPerAccount.String(), - ReviewStatusFieldName: reviewStatusDefault, - OrganizationFieldName: organization, - TierFieldName: tier, - AccountURIFieldName: accountID, - }, - ) -} +// createOverrideRequestZendeskTicket creates a new Zendesk ticket for manual +// review of a rate limit override request. It returns the ID of the created +// ticket or an error. +func createOverrideRequestZendeskTicket(client *zendesk.Client, rateLimit, requesterEmail, useCase, organization, tier, accountURI, registeredDomain, ipAddress string) (int64, error) { + // Some rateLimitField values include suffixes to indicate whether an + // accountURI, registeredDomain, or ipAddress is expected. + limitStr := strings.TrimSuffix(strings.TrimSuffix(rateLimit, perDNSNameSuffix), perIPSuffix) + limit, ok := rl.StringToName[limitStr] + if !ok { + // This should never happen, it indicates a bug in our validation. + return 0, errors.New("invalid rate limit prevented ticket creation") + } + + if registeredDomain == "" && ipAddress == "" && accountURI == "" { + // This should never happen, it indicates a bug in our validation. + return 0, errors.New("one of accountURI, registeredDomain, or ipAddress must be provided") + } -// createCertificatesPerDomainOverrideTicket creates a new Zendesk ticket for a -// CertificatesPerDomain override request. Only registeredDomain or ipAddress -// should be provided, not both. All other fields are required. -func createCertificatesPerDomainOverrideTicket(client *zendesk.Client, requesterEmail, useCase, organization, tier, registeredDomain, ipAddress string) (int64, error) { return client.CreateTicket( requesterEmail, - makeSubject(rl.CertificatesPerDomain, organization), + // The stripped form of the rateLimitField value must be used here. + makeSubject(limit, organization), makeInitialComment(organization, useCase, tier), map[string]string{ - RateLimitFieldName: rl.CertificatesPerDomain.String(), + // The original rateLimitField value must be used here, the + // overridesimporter depends on the suffixes for validation. + RateLimitFieldName: rateLimit, + TierFieldName: tier, ReviewStatusFieldName: reviewStatusDefault, OrganizationFieldName: organization, - TierFieldName: tier, RegisteredDomainFieldName: registeredDomain, IPAddressFieldName: ipAddress, - }, - ) -} - -// createCertificatesPerDomainPerAccountOverrideTicket creates a new Zendesk -// ticket for a CertificatesPerDomainPerAccount override request. All fields are -// required. -func createCertificatesPerDomainPerAccountOverrideTicket(client *zendesk.Client, requesterEmail, useCase, organization, tier, accountID string) (int64, error) { - return client.CreateTicket( - requesterEmail, - makeSubject(rl.CertificatesPerDomainPerAccount, organization), - makeInitialComment(organization, useCase, tier), - map[string]string{ - RateLimitFieldName: rl.CertificatesPerDomainPerAccount.String(), - ReviewStatusFieldName: reviewStatusDefault, - OrganizationFieldName: organization, - TierFieldName: tier, - AccountURIFieldName: accountID, + AccountURIFieldName: accountURI, }, ) } @@ -489,12 +473,13 @@ func (sfe *SelfServiceFrontEndImpl) makeOverrideRequestFormHandler(formHTML temp func (sfe *SelfServiceFrontEndImpl) overrideRequestHandler(w http.ResponseWriter, formHTML template.HTML, rateLimit, displayRateLimit string) { setOverrideRequestFormHeaders(w) sfe.renderTemplate(w, "overrideForm.html", map[string]any{ - "FormHTML": formHTML, - "RateLimit": rateLimit, - "DisplayRateLimit": displayRateLimit, - "ValidateFieldPath": overridesValidateField, - "SubmitRequestPath": overridesSubmitRequest, - "SubmitSuccessPath": overridesSubmitSuccess, + "FormHTML": formHTML, + "RateLimit": rateLimit, + "DisplayRateLimit": displayRateLimit, + "ValidateFieldPath": overridesValidateField, + "SubmitRequestPath": overridesSubmitRequest, + "AutoApprovedSuccessPath": overridesAutoApprovedSuccess, + "RequestSubmittedSuccessPath": overridesRequestSubmittedSuccess, }) } @@ -542,10 +527,17 @@ func (sfe *SelfServiceFrontEndImpl) validateOverrideFieldHandler(w http.Response } } -// overrideSuccessHandler renders the success page after a successful override -// request submission. -func (sfe *SelfServiceFrontEndImpl) overrideSuccessHandler(w http.ResponseWriter, r *http.Request) { - sfe.renderTemplate(w, "overrideSuccess.html", nil) +// overrideAutoApprovedSuccessHandler renders the success page after a +// successful override request submission which was automatically approved. +func (sfe *SelfServiceFrontEndImpl) overrideAutoApprovedSuccessHandler(w http.ResponseWriter, r *http.Request) { + sfe.renderTemplate(w, "overrideAutoApprovedSuccess.html", nil) +} + +// overrideRequestSubmittedSuccessHandler renders the success page after a +// successful override request submission created a Zendesk ticket for manual +// review. +func (sfe *SelfServiceFrontEndImpl) overrideRequestSubmittedSuccessHandler(w http.ResponseWriter, r *http.Request) { + sfe.renderTemplate(w, "overrideRequestSubmittedSuccess.html", nil) } type overrideRequest struct { @@ -555,9 +547,11 @@ type overrideRequest struct { // submitOverrideRequestHandler handles the submission of override requests. It // expects a POST request with a JSON payload (overrideRequest). It validates -// each of the form fields and creates a Zendesk ticket based on the specified -// rate limit. It returns a 200 OK response on success, or an error response if -// the request is invalid or if ticket creation fails. +// each of the form fields and either: +// +// a. auto-approves the override request and returns 201 Created, or +// b. creates a Zendesk ticket for manual review, and returns 202 Accepted, or +// c. encounters an error and returns an appropriate 4xx or 5xx status code. // // The JavaScript frontend is configured to validate the form fields twice: once // when the requester inputs data, and once more just before submitting the @@ -566,7 +560,6 @@ type overrideRequest struct { // submitting (malformed) requests directly to this endpoint. func (sfe *SelfServiceFrontEndImpl) submitOverrideRequestHandler(w http.ResponseWriter, r *http.Request) { var refundLimits func() - var submissionSuccess bool if sfe.limiter != nil && sfe.txnBuilder != nil { requesterIP, err := web.ExtractRequesterIP(r) if err != nil { @@ -608,8 +601,9 @@ func (sfe *SelfServiceFrontEndImpl) submitOverrideRequestHandler(w http.Response } } } + var overrideRequestHandled bool defer func() { - if !submissionSuccess && refundLimits != nil { + if !overrideRequestHandled && refundLimits != nil { refundLimits() } }() @@ -635,7 +629,7 @@ func (sfe *SelfServiceFrontEndImpl) submitOverrideRequestHandler(w http.Response return val, nil } - var baseFields = make(map[string]string) + var validFields = make(map[string]string) for _, name := range []string{ // Note: not all of these fields will be included in the Zendesk ticket, // but they are all required for the submission to be considered valid. @@ -653,7 +647,24 @@ func (sfe *SelfServiceFrontEndImpl) submitOverrideRequestHandler(w http.Response http.Error(w, err.Error(), http.StatusBadRequest) return } - baseFields[name] = val + validFields[name] = val + } + + autoApproveOverride := func(ctx context.Context, rateLimitFieldValue string, fields map[string]string) bool { + if !sfe.autoApproveOverrides { + return false + } + req, _, err := makeAddOverrideRequest(rateLimitFieldValue, fields) + if err != nil { + sfe.log.Errf("failed to create automatically approved override request: %s", err) + return false + } + resp, err := sfe.ra.AddRateLimitOverride(ctx, req) + if err != nil { + sfe.log.Errf("failed to create automatically approved override request: %s", err) + return false + } + return resp.Enabled } switch req.RateLimit { @@ -663,22 +674,10 @@ func (sfe *SelfServiceFrontEndImpl) submitOverrideRequestHandler(w http.Response http.Error(w, err.Error(), http.StatusBadRequest) return } + validFields[AccountURIFieldName] = accountURI - // TODO(#8360): Skip ticket creation and insert an override for - // overrides matching the first N tiers of this limit. - - _, err = createNewOrdersPerAccountOverrideTicket( - sfe.zendeskClient, - baseFields[emailAddressFieldName], - baseFields[useCaseFieldName], - baseFields[OrganizationFieldName], - baseFields[TierFieldName], - accountURI, - ) - if err != nil { - sfe.log.Errf("failed to create override request ticket: %s", err) - http.Error(w, "failed to create override request ticket", http.StatusInternalServerError) - return + if validFields[TierFieldName] == newOrdersPerAccountTierOptions[0] { + overrideRequestHandled = autoApproveOverride(r.Context(), req.RateLimit, validFields) } case rl.CertificatesPerDomainPerAccount.String(): @@ -687,22 +686,10 @@ func (sfe *SelfServiceFrontEndImpl) submitOverrideRequestHandler(w http.Response http.Error(w, err.Error(), http.StatusBadRequest) return } + validFields[AccountURIFieldName] = accountURI - // TODO(#8360): Skip ticket creation and insert an override for - // overrides matching the first N tiers of this limit. - - _, err = createCertificatesPerDomainPerAccountOverrideTicket( - sfe.zendeskClient, - baseFields[emailAddressFieldName], - baseFields[useCaseFieldName], - baseFields[OrganizationFieldName], - baseFields[TierFieldName], - accountURI, - ) - if err != nil { - sfe.log.Errf("failed to create override request ticket: %s", err) - http.Error(w, "failed to create override request ticket", http.StatusInternalServerError) - return + if validFields[TierFieldName] == certificatesPerDomainPerAccountTierOptions[0] { + overrideRequestHandled = autoApproveOverride(r.Context(), req.RateLimit, validFields) } case rl.CertificatesPerDomain.String() + perDNSNameSuffix: @@ -711,23 +698,10 @@ func (sfe *SelfServiceFrontEndImpl) submitOverrideRequestHandler(w http.Response http.Error(w, err.Error(), http.StatusBadRequest) return } + validFields[RegisteredDomainFieldName] = registeredDomain - // TODO(#8360): Skip ticket creation and insert an override for - // overrides matching the first N tiers of this limit. - - _, err = createCertificatesPerDomainOverrideTicket( - sfe.zendeskClient, - baseFields[emailAddressFieldName], - baseFields[useCaseFieldName], - baseFields[OrganizationFieldName], - baseFields[TierFieldName], - registeredDomain, - "", - ) - if err != nil { - sfe.log.Errf("failed to create override request ticket: %s", err) - http.Error(w, "failed to create override request ticket", http.StatusInternalServerError) - return + if validFields[TierFieldName] == certificatesPerDomainTierOptions[0] { + overrideRequestHandled = autoApproveOverride(r.Context(), req.RateLimit, validFields) } case rl.CertificatesPerDomain.String() + perIPSuffix: @@ -736,23 +710,10 @@ func (sfe *SelfServiceFrontEndImpl) submitOverrideRequestHandler(w http.Response http.Error(w, err.Error(), http.StatusBadRequest) return } + validFields[IPAddressFieldName] = ipAddress - // TODO(#8360): Skip ticket creation and insert an override for - // overrides matching the first N tiers of this limit. - - _, err = createCertificatesPerDomainOverrideTicket( - sfe.zendeskClient, - baseFields[emailAddressFieldName], - baseFields[useCaseFieldName], - baseFields[OrganizationFieldName], - baseFields[TierFieldName], - "", - ipAddress, - ) - if err != nil { - sfe.log.Errf("failed to create override request ticket: %s", err) - http.Error(w, "failed to create override request ticket", http.StatusInternalServerError) - return + if validFields[TierFieldName] == certificatesPerDomainTierOptions[0] { + overrideRequestHandled = autoApproveOverride(r.Context(), req.RateLimit, validFields) } default: @@ -760,8 +721,8 @@ func (sfe *SelfServiceFrontEndImpl) submitOverrideRequestHandler(w http.Response return } - if sfe.ee != nil && baseFields[fundraisingFieldName] == fundraisingYesOption { - _, err := sfe.ee.SendContacts(r.Context(), &emailpb.SendContactsRequest{Emails: []string{baseFields[emailAddressFieldName]}}) + if sfe.ee != nil && validFields[fundraisingFieldName] == fundraisingYesOption { + _, err := sfe.ee.SendContacts(r.Context(), &emailpb.SendContactsRequest{Emails: []string{validFields[emailAddressFieldName]}}) if err != nil { sfe.log.Errf("failed to send contact to email service: %s", err) } @@ -770,6 +731,35 @@ func (sfe *SelfServiceFrontEndImpl) submitOverrideRequestHandler(w http.Response // TODO(#8362): If FundraisingFieldName value is true, use the Salesforce // API to create a new Lead record with the provided information. - submissionSuccess = true - w.WriteHeader(http.StatusOK) + if overrideRequestHandled { + sfe.log.Infof("automatically approved override request for %s", validFields[OrganizationFieldName]) + w.WriteHeader(http.StatusCreated) + return + } + + ticketID, err := createOverrideRequestZendeskTicket( + sfe.zendeskClient, + req.RateLimit, + validFields[emailAddressFieldName], + validFields[useCaseFieldName], + validFields[OrganizationFieldName], + validFields[TierFieldName], + + // Only one of these will be non-empty, depending on the + // rateLimitField value. + validFields[AccountURIFieldName], + validFields[RegisteredDomainFieldName], + validFields[IPAddressFieldName], + ) + if err != nil { + sfe.log.Errf("failed to create override request Zendesk ticket: %s", err) + http.Error(w, "failed to create support ticket", http.StatusInternalServerError) + return + } + + // If we got here the request has either been auto-approved or a Zendesk + // ticket has been created for manual review, so a refund is not needed. + overrideRequestHandled = true + sfe.log.Infof("created override request Zendesk ticket %d", ticketID) + w.WriteHeader(http.StatusAccepted) } diff --git a/sfe/overrides_test.go b/sfe/overrides_test.go index b1d7dec60d8..c5154829a9c 100644 --- a/sfe/overrides_test.go +++ b/sfe/overrides_test.go @@ -2,6 +2,7 @@ package sfe import ( "bytes" + "context" "encoding/json" "html/template" "maps" @@ -11,9 +12,11 @@ import ( "testing" "github.com/letsencrypt/boulder/mocks" + rapb "github.com/letsencrypt/boulder/ra/proto" rl "github.com/letsencrypt/boulder/ratelimits" "github.com/letsencrypt/boulder/sfe/zendesk" "github.com/letsencrypt/boulder/test/zendeskfake" + "google.golang.org/grpc" ) const ( @@ -244,7 +247,7 @@ func TestSubmitOverrideRequestHandlerSuccess(t *testing.T) { RegisteredDomainFieldName: "bar.co", }, zendeskMatch: map[string]string{ - RateLimitFieldName: rl.CertificatesPerDomain.String(), + RateLimitFieldName: rl.CertificatesPerDomain.String() + perDNSNameSuffix, RegisteredDomainFieldName: "bar.co", }, }, @@ -256,7 +259,7 @@ func TestSubmitOverrideRequestHandlerSuccess(t *testing.T) { IPAddressFieldName: "2606:4700:4700::1111", }, zendeskMatch: map[string]string{ - RateLimitFieldName: rl.CertificatesPerDomain.String(), + RateLimitFieldName: rl.CertificatesPerDomain.String() + perIPSuffix, IPAddressFieldName: "2606:4700:4700::1111", }, }, @@ -268,7 +271,7 @@ func TestSubmitOverrideRequestHandlerSuccess(t *testing.T) { IPAddressFieldName: "64.112.11.11", }, zendeskMatch: map[string]string{ - RateLimitFieldName: rl.CertificatesPerDomain.String(), + RateLimitFieldName: rl.CertificatesPerDomain.String() + perIPSuffix, IPAddressFieldName: "64.112.11.11", }, }, @@ -296,8 +299,8 @@ func TestSubmitOverrideRequestHandlerSuccess(t *testing.T) { sfe.submitOverrideRequestHandler(rec, req) - if rec.Code != http.StatusOK { - t.Errorf("Unexpected status=%d, expected status=200", rec.Code) + if rec.Code != http.StatusAccepted { + t.Errorf("Unexpected status=%d, expected status=202", rec.Code) } got, err := client.FindTickets(tt.zendeskMatch, "") @@ -457,8 +460,8 @@ func TestSubmitOverrideRequestHandlerRateLimited(t *testing.T) { sfe.submitOverrideRequestHandler(rec, req) if attempt < 100 { - if rec.Code != http.StatusOK { - t.Errorf("Unexpected status=%d, expected status=200", rec.Code) + if rec.Code != http.StatusAccepted { + t.Errorf("Unexpected status=%d, expected status=202", rec.Code) } } else { if rec.Code != http.StatusTooManyRequests { @@ -470,3 +473,81 @@ func TestSubmitOverrideRequestHandlerRateLimited(t *testing.T) { } } } + +type addedOverrideEnabledRA struct { + rapb.RegistrationAuthorityClient +} + +func (f *addedOverrideEnabledRA) AddRateLimitOverride(ctx context.Context, req *rapb.AddRateLimitOverrideRequest, opts ...grpc.CallOption) (*rapb.AddRateLimitOverrideResponse, error) { + return &rapb.AddRateLimitOverrideResponse{Enabled: true}, nil +} + +type addedOverrideDisabledRA struct { + rapb.RegistrationAuthorityClient +} + +func (f *addedOverrideDisabledRA) AddRateLimitOverride(ctx context.Context, req *rapb.AddRateLimitOverrideRequest, opts ...grpc.CallOption) (*rapb.AddRateLimitOverrideResponse, error) { + return &rapb.AddRateLimitOverrideResponse{Enabled: false}, nil +} + +func TestSubmitOverrideRequestHandlerAutoApproved(t *testing.T) { + t.Parallel() + + sfe, _ := setupSFE(t) + sfe.templatePages = minimalTemplates(t) + _, client := createFakeZendeskClientServer(t) + sfe.zendeskClient = client + sfe.autoApproveOverrides = true + + reqObj := overrideRequest{ + RateLimit: rl.CertificatesPerDomainPerAccount.String(), + Fields: map[string]string{ + subscriberAgreementFieldName: "true", + privacyPolicyFieldName: "true", + mailingListFieldName: "false", + fundraisingFieldName: FundraisingOptions[0], + emailAddressFieldName: "foo@bar.co", + OrganizationFieldName: "Big Host Inc.", + useCaseFieldName: strings.Repeat("x", 60), + TierFieldName: certificatesPerDomainPerAccountTierOptions[0], + AccountURIFieldName: "https://acme-v02.api.letsencrypt.org/acme/acct/67890", + }, + } + reqObjBytes, err := json.Marshal(reqObj) + if err != nil { + t.Fatalf("marshal: %s", err) + } + + type testCase struct { + name string + ra rapb.RegistrationAuthorityClient + expectedCode int + } + cases := []testCase{ + { + name: "New override enabled", + ra: &addedOverrideEnabledRA{}, + expectedCode: http.StatusCreated, + }, + { + name: "Existing override disabled", + ra: &addedOverrideDisabledRA{}, + expectedCode: http.StatusAccepted, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + + req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(reqObjBytes)) + rec := httptest.NewRecorder() + + sfe.ra = tc.ra + sfe.submitOverrideRequestHandler(rec, req) + + if rec.Code != tc.expectedCode { + t.Errorf("Unexpected status=%d, expected status=%d", rec.Code, tc.expectedCode) + } + }) + } +} diff --git a/sfe/overridesimporter.go b/sfe/overridesimporter.go index e302c3dbb46..0a4a40f1b29 100644 --- a/sfe/overridesimporter.go +++ b/sfe/overridesimporter.go @@ -133,7 +133,7 @@ func (im *OverridesImporter) transitionToPendingWithComment(ticketID int64, caus } } -func (im *OverridesImporter) getValidatedFieldValue(fields map[string]string, fieldName, rateLimit string) (string, error) { +func getValidatedFieldValue(fields map[string]string, fieldName, rateLimit string) (string, error) { val := fields[fieldName] err := validateOverrideRequestField(fieldName, val, rateLimit) if err != nil { @@ -142,7 +142,7 @@ func (im *OverridesImporter) getValidatedFieldValue(fields map[string]string, fi return val, nil } -func (im *OverridesImporter) makeAddOverrideRequest(fields map[string]string) (*rapb.AddRateLimitOverrideRequest, string, error) { +func makeAddOverrideRequest(rateLimitFieldValue string, fields map[string]string) (*rapb.AddRateLimitOverrideRequest, string, error) { makeReq := func(limit rl.Name, bucket, organization string, tier int64) *rapb.AddRateLimitOverrideRequest { return &rapb.AddRateLimitOverrideRequest{ LimitEnum: int64(limit), @@ -154,11 +154,10 @@ func (im *OverridesImporter) makeAddOverrideRequest(fields map[string]string) (* } } - rateLimit, ok := fields[RateLimitFieldName] - if !ok { + if rateLimitFieldValue == "" { return nil, "", fmt.Errorf("missing rate limit field") } - tierStr, err := im.getValidatedFieldValue(fields, TierFieldName, rateLimit) + tierStr, err := getValidatedFieldValue(fields, TierFieldName, rateLimitFieldValue) if err != nil { return nil, "", fmt.Errorf("getting/validating tier field: %w", err) } @@ -166,7 +165,7 @@ func (im *OverridesImporter) makeAddOverrideRequest(fields map[string]string) (* if err != nil { return nil, "", fmt.Errorf("parsing tier: %w", err) } - organization, err := im.getValidatedFieldValue(fields, OrganizationFieldName, "") + organization, err := getValidatedFieldValue(fields, OrganizationFieldName, "") if err != nil { return nil, "", fmt.Errorf("getting/validating organization: %w", err) } @@ -174,9 +173,9 @@ func (im *OverridesImporter) makeAddOverrideRequest(fields map[string]string) (* var req *rapb.AddRateLimitOverrideRequest var accountDomainOrIP string - switch rateLimit { + switch rateLimitFieldValue { case rl.NewOrdersPerAccount.String(): - accountURI, err := im.getValidatedFieldValue(fields, AccountURIFieldName, "") + accountURI, err := getValidatedFieldValue(fields, AccountURIFieldName, "") if err != nil { return nil, "", fmt.Errorf("getting/validating accountURI: %w", err) } @@ -192,7 +191,7 @@ func (im *OverridesImporter) makeAddOverrideRequest(fields map[string]string) (* accountDomainOrIP = accountURI case rl.CertificatesPerDomainPerAccount.String(): - accountURI, err := im.getValidatedFieldValue(fields, AccountURIFieldName, "") + accountURI, err := getValidatedFieldValue(fields, AccountURIFieldName, "") if err != nil { return nil, "", fmt.Errorf("getting/validating accountURI: %w", err) } @@ -208,7 +207,7 @@ func (im *OverridesImporter) makeAddOverrideRequest(fields map[string]string) (* accountDomainOrIP = accountURI case rl.CertificatesPerDomain.String() + perDNSNameSuffix: - dnsName, err := im.getValidatedFieldValue(fields, RegisteredDomainFieldName, rateLimit) + dnsName, err := getValidatedFieldValue(fields, RegisteredDomainFieldName, rateLimitFieldValue) if err != nil { return nil, "", fmt.Errorf("getting/validating registeredDomain: %w", err) } @@ -220,7 +219,7 @@ func (im *OverridesImporter) makeAddOverrideRequest(fields map[string]string) (* req = makeReq(rl.CertificatesPerDomain, bucketKey, organization, tier) case rl.CertificatesPerDomain.String() + perIPSuffix: - ipAddrStr, err := im.getValidatedFieldValue(fields, IPAddressFieldName, rateLimit) + ipAddrStr, err := getValidatedFieldValue(fields, IPAddressFieldName, rateLimitFieldValue) if err != nil { return nil, "", fmt.Errorf("getting/validating ipAddress: %w", err) } @@ -233,7 +232,7 @@ func (im *OverridesImporter) makeAddOverrideRequest(fields map[string]string) (* return nil, "", fmt.Errorf("building bucket key: %w", err) } req = makeReq(rl.CertificatesPerDomain, bucketKey, organization, tier) - accountDomainOrIP = ipAddrStr + accountDomainOrIP = ipAddr.String() default: return nil, "", fmt.Errorf("unknown rate limit") @@ -242,7 +241,7 @@ func (im *OverridesImporter) makeAddOverrideRequest(fields map[string]string) (* } func (im *OverridesImporter) processTicket(ctx context.Context, ticketID int64, fields map[string]string) error { - req, accountDomainOrIP, err := im.makeAddOverrideRequest(fields) + req, accountDomainOrIP, err := makeAddOverrideRequest(fields[RateLimitFieldName], fields) if err != nil { // Move to "pending" so the next tick won't comment again. im.transitionToPendingWithComment(ticketID, err.Error()) diff --git a/sfe/sfe.go b/sfe/sfe.go index e8f3b6bfc0c..99dc11d0a94 100644 --- a/sfe/sfe.go +++ b/sfe/sfe.go @@ -38,7 +38,8 @@ const ( overridesCertificatesPerDomainPerAccount = overridesAPIPrefix + "/overrides/certificates-per-domain-per-account" overridesValidateField = overridesAPIPrefix + "/overrides/validate-field" overridesSubmitRequest = overridesAPIPrefix + "/overrides/submit-override-request" - overridesSubmitSuccess = overridesAPIPrefix + "/overrides/success" + overridesAutoApprovedSuccess = overridesAPIPrefix + "/overrides/auto-approved-success" + overridesRequestSubmittedSuccess = overridesAPIPrefix + "/overrides/request-submitted-success" ) var ( @@ -72,6 +73,10 @@ type SelfServiceFrontEndImpl struct { limiter *rl.Limiter txnBuilder *rl.TransactionBuilder + + // autoApproveOverrides only affects specific tiers and limits, see + // cmd/sfe/main.go for details. + autoApproveOverrides bool } // NewSelfServiceFrontEndImpl constructs a web service for Boulder @@ -87,6 +92,7 @@ func NewSelfServiceFrontEndImpl( zendeskClient *zendesk.Client, limiter *rl.Limiter, txnBuilder *rl.TransactionBuilder, + autoApproveOverrides bool, ) (SelfServiceFrontEndImpl, error) { // Parse the files once at startup to avoid each request causing the server @@ -98,18 +104,19 @@ func NewSelfServiceFrontEndImpl( } sfe := SelfServiceFrontEndImpl{ - log: logger, - clk: clk, - requestTimeout: requestTimeout, - ra: rac, - sa: sac, - ee: eec, - unpauseHMACKey: unpauseHMACKey, - zendeskClient: zendeskClient, - templatePages: tmplPages, - cop: http.NewCrossOriginProtection(), - limiter: limiter, - txnBuilder: txnBuilder, + log: logger, + clk: clk, + requestTimeout: requestTimeout, + ra: rac, + sa: sac, + ee: eec, + unpauseHMACKey: unpauseHMACKey, + zendeskClient: zendeskClient, + templatePages: tmplPages, + cop: http.NewCrossOriginProtection(), + limiter: limiter, + txnBuilder: txnBuilder, + autoApproveOverrides: autoApproveOverrides, } return sfe, nil @@ -170,7 +177,8 @@ func (sfe *SelfServiceFrontEndImpl) Handler(stats prometheus.Registerer, oTelHTT sfe.handleGet(mux, overridesCertificatesPerDomainPerAccount, sfe.makeOverrideRequestFormHandler( certificatesPerDomainPerAccountForm, rl.CertificatesPerDomainPerAccount.String(), rl.CertificatesPerDomainPerAccount.String()), ) - sfe.handleGet(mux, overridesSubmitSuccess, http.HandlerFunc(sfe.overrideSuccessHandler)) + sfe.handleGet(mux, overridesAutoApprovedSuccess, http.HandlerFunc(sfe.overrideAutoApprovedSuccessHandler)) + sfe.handleGet(mux, overridesRequestSubmittedSuccess, http.HandlerFunc(sfe.overrideRequestSubmittedSuccessHandler)) sfe.handlePost(mux, overridesValidateField, http.HandlerFunc(sfe.validateOverrideFieldHandler)) sfe.handlePost(mux, overridesSubmitRequest, http.HandlerFunc(sfe.submitOverrideRequestHandler)) } diff --git a/sfe/sfe_test.go b/sfe/sfe_test.go index 54eeeb243a7..27f602f3188 100644 --- a/sfe/sfe_test.go +++ b/sfe/sfe_test.go @@ -69,6 +69,7 @@ func setupSFE(t *testing.T) (SelfServiceFrontEndImpl, clock.FakeClock) { nil, limiter, txnBuilder, + false, ) test.AssertNotError(t, err, "Unable to create SFE") diff --git a/sfe/static/overriderequest.js b/sfe/static/overriderequest.js index ac39dfbc4c5..dbce1cae703 100644 --- a/sfe/static/overriderequest.js +++ b/sfe/static/overriderequest.js @@ -2,7 +2,8 @@ const form = document.getElementById('override-form'); const RATE_LIMIT = form.dataset.rateLimit; const VALIDATE_FIELD_PATH = form.dataset.validateFieldPath; const SUBMIT_REQUEST_PATH = form.dataset.submitRequestPath; -const SUBMIT_SUCCESS_PATH = form.dataset.submitSuccessPath; +const AUTO_APPROVED_SUCCESS_PATH = form.dataset.autoApprovedSuccessPath; +const REQUEST_SUBMITTED_SUCCESS_PATH = form.dataset.RequestSubmittedSuccessPath; const ERR_REQUIRED = "This field is required."; const ERR_VALIDATE = "Unable to validate this field due to timeout, please try again."; @@ -109,7 +110,12 @@ const submitForm = async (e) => { showBanner(d.error || ERR_SUBMIT); return; } - window.location.replace(SUBMIT_SUCCESS_PATH); + + if (r.status === 201) { + window.location.replace(AUTO_APPROVED_SUCCESS_PATH); + } else if (r.status === 202) { + window.location.replace(REQUEST_SUBMITTED_SUCCESS_PATH); + } } catch { showBanner(ERR_TIMEOUT); } diff --git a/sfe/templates/overrideAutoApprovedSuccess.html b/sfe/templates/overrideAutoApprovedSuccess.html new file mode 100644 index 00000000000..9e268c8e10f --- /dev/null +++ b/sfe/templates/overrideAutoApprovedSuccess.html @@ -0,0 +1,13 @@ +{{template "header"}} +
+

Override Request Successfully Approved and Applied

+ We have approved your request and applied the override to your account. + Please allow up to 30 minutes for this change to take effect. +
+ You will not receive an email notification regarding this approval. +
+

Need to make another request?

+ Please visit our rate + limits documentation to submit another request. +
+{{template "footer"}} diff --git a/sfe/templates/overrideSuccess.html b/sfe/templates/overrideRequestSubmittedSuccess.html similarity index 100% rename from sfe/templates/overrideSuccess.html rename to sfe/templates/overrideRequestSubmittedSuccess.html diff --git a/sfe/zendesk/zendesk.go b/sfe/zendesk/zendesk.go index ad1ce3667b5..e67a2309157 100644 --- a/sfe/zendesk/zendesk.go +++ b/sfe/zendesk/zendesk.go @@ -211,6 +211,11 @@ func (c *Client) CreateTicket(requesterEmail, subject, commentBody string, field }, } for name, value := range fields { + if value == "" { + // Zendesk will ignore empty custom fields, but we can avoid sending + // them across the wire at all. + continue + } id, ok := c.nameToFieldID[name] if !ok { return 0, fmt.Errorf("unknown custom field %q", name) diff --git a/test/config-next/sfe.json b/test/config-next/sfe.json index 3d036f1b717..fcfc49b2a5a 100644 --- a/test/config-next/sfe.json +++ b/test/config-next/sfe.json @@ -51,6 +51,7 @@ "mode": "all", "interval": "20m" }, + "autoApproveOverrides": true, "features": {} }, "syslog": {