github.com/letsencrypt/boulder@v0.20251208.0/sfe/overridesimporter.go (about) 1 package sfe 2 3 import ( 4 "context" 5 "fmt" 6 "net/netip" 7 "net/url" 8 "strconv" 9 "strings" 10 "time" 11 12 "github.com/jmhodges/clock" 13 "google.golang.org/protobuf/types/known/durationpb" 14 15 "github.com/letsencrypt/boulder/identifier" 16 blog "github.com/letsencrypt/boulder/log" 17 rapb "github.com/letsencrypt/boulder/ra/proto" 18 rl "github.com/letsencrypt/boulder/ratelimits" 19 "github.com/letsencrypt/boulder/sfe/zendesk" 20 ) 21 22 // ProcessMode determines which ticket IDs the importer will process. 23 type ProcessMode string 24 25 const ( 26 // ProcessAll indicates that all tickets should be processed, as opposed to 27 // just even or odd numbered tickets. 28 ProcessAll ProcessMode = "all" 29 processEven ProcessMode = "even" 30 processOdd ProcessMode = "odd" 31 ) 32 33 type OverridesImporter struct { 34 mode ProcessMode 35 interval time.Duration 36 37 zendesk *zendesk.Client 38 ra rapb.RegistrationAuthorityClient 39 40 clk clock.Clock 41 log blog.Logger 42 } 43 44 // NewOverridesImporter creates a new OverridesImporter that will process 45 // tickets in the given mode at the given interval. An error is returned if the 46 // interval is left unspecified. 47 func NewOverridesImporter(mode ProcessMode, interval time.Duration, client *zendesk.Client, sa rapb.RegistrationAuthorityClient, clk clock.Clock, log blog.Logger) (*OverridesImporter, error) { 48 if interval <= 0 { 49 return nil, fmt.Errorf("interval cannot be 0") 50 } 51 return &OverridesImporter{ 52 mode: mode, 53 interval: interval, 54 zendesk: client, 55 ra: sa, 56 clk: clk, 57 log: log, 58 }, nil 59 } 60 61 // Start begins the periodic import of approved override requests from Zendesk. 62 // This method blocks until the provided context is cancelled. 63 func (im *OverridesImporter) Start(ctx context.Context) { 64 ticker := time.NewTicker(im.interval) 65 defer ticker.Stop() 66 67 for { 68 select { 69 case <-ctx.Done(): 70 return 71 case <-ticker.C: 72 im.tick(ctx) 73 } 74 } 75 } 76 77 // tick performs a single import pass, serially processing all tickets in the 78 // configured mode that have been marked "open" and "approved". 79 func (im *OverridesImporter) tick(ctx context.Context) { 80 tickets, err := im.zendesk.FindTickets(map[string]string{ReviewStatusFieldName: reviewStatusApproved}, "open") 81 if err != nil { 82 im.log.Errf("while searching zendesk for solved and approved tickets: %s", err) 83 return 84 } 85 86 processed := 0 87 failures := 0 88 for id, fields := range tickets { 89 switch im.mode { 90 case processEven: 91 if id%2 != 0 { 92 continue 93 } 94 case processOdd: 95 if id%2 == 0 { 96 continue 97 } 98 } 99 100 err = im.processTicket(ctx, id, fields) 101 if err != nil { 102 im.log.Errf("while processing ticket %d: %s", id, err) 103 failures++ 104 continue 105 } 106 processed++ 107 } 108 im.log.Infof("overrides importer processed %d tickets with %d failures", processed, failures) 109 } 110 111 func accountURIToID(s string) (int64, error) { 112 u, err := url.Parse(s) 113 if err != nil { 114 return 0, err 115 } 116 rawID := strings.TrimPrefix(u.Path, "/acme/acct/") 117 return strconv.ParseInt(rawID, 10, 64) 118 } 119 120 // transitionToPendingWithComment sets the status of the given ticket to 121 // "pending" and adds a private comment with the given cause. If updating the 122 // ticket fails, the error is logged. 123 func (im *OverridesImporter) transitionToPendingWithComment(ticketID int64, cause string) { 124 privateBody := fmt.Sprintf( 125 "A failure occurred while importing this override:\n\n%s\n\n"+ 126 "This ticket's status has been set to pending.\n\n"+ 127 "Once the error has been corrected, change the status back to \"open\" to retry.\n", 128 cause, 129 ) 130 err := im.zendesk.UpdateTicketStatus(ticketID, "pending", privateBody, false) 131 if err != nil { 132 im.log.Errf("failed to update ticket %d: %s", ticketID, err) 133 } 134 } 135 136 func getValidatedFieldValue(fields map[string]string, fieldName, rateLimit string) (string, error) { 137 val := fields[fieldName] 138 err := validateOverrideRequestField(fieldName, val, rateLimit) 139 if err != nil { 140 return "", err 141 } 142 return val, nil 143 } 144 145 func makeAddOverrideRequest(rateLimitFieldValue string, fields map[string]string) (*rapb.AddRateLimitOverrideRequest, string, error) { 146 makeReq := func(limit rl.Name, bucket, organization string, tier int64) *rapb.AddRateLimitOverrideRequest { 147 return &rapb.AddRateLimitOverrideRequest{ 148 LimitEnum: int64(limit), 149 BucketKey: bucket, 150 Count: tier, 151 Burst: tier, 152 Period: durationpb.New(7 * 24 * time.Hour), 153 Comment: organization, 154 } 155 } 156 157 if rateLimitFieldValue == "" { 158 return nil, "", fmt.Errorf("missing rate limit field") 159 } 160 tierStr, err := getValidatedFieldValue(fields, TierFieldName, rateLimitFieldValue) 161 if err != nil { 162 return nil, "", fmt.Errorf("getting/validating tier field: %w", err) 163 } 164 tier, err := strconv.ParseInt(tierStr, 10, 64) 165 if err != nil { 166 return nil, "", fmt.Errorf("parsing tier: %w", err) 167 } 168 organization, err := getValidatedFieldValue(fields, OrganizationFieldName, "") 169 if err != nil { 170 return nil, "", fmt.Errorf("getting/validating organization: %w", err) 171 } 172 173 var req *rapb.AddRateLimitOverrideRequest 174 var accountDomainOrIP string 175 176 switch rateLimitFieldValue { 177 case rl.NewOrdersPerAccount.String(): 178 accountURI, err := getValidatedFieldValue(fields, AccountURIFieldName, "") 179 if err != nil { 180 return nil, "", fmt.Errorf("getting/validating accountURI: %w", err) 181 } 182 accountID, err := accountURIToID(accountURI) 183 if err != nil { 184 return nil, "", fmt.Errorf("parsing accountURI to accountID: %w", err) 185 } 186 bucketKey, err := rl.BuildBucketKey(rl.NewOrdersPerAccount, accountID, identifier.ACMEIdentifier{}, identifier.ACMEIdentifiers{}, netip.Addr{}) 187 if err != nil { 188 return nil, "", fmt.Errorf("building bucket key: %w", err) 189 } 190 req = makeReq(rl.NewOrdersPerAccount, bucketKey, organization, tier) 191 accountDomainOrIP = accountURI 192 193 case rl.CertificatesPerDomainPerAccount.String(): 194 accountURI, err := getValidatedFieldValue(fields, AccountURIFieldName, "") 195 if err != nil { 196 return nil, "", fmt.Errorf("getting/validating accountURI: %w", err) 197 } 198 accountID, err := accountURIToID(accountURI) 199 if err != nil { 200 return nil, "", fmt.Errorf("parsing accountURI to accountID: %w", err) 201 } 202 bucketKey, err := rl.BuildBucketKey(rl.CertificatesPerDomainPerAccount, accountID, identifier.ACMEIdentifier{}, identifier.ACMEIdentifiers{}, netip.Addr{}) 203 if err != nil { 204 return nil, "", fmt.Errorf("building bucket key: %w", err) 205 } 206 req = makeReq(rl.CertificatesPerDomainPerAccount, bucketKey, organization, tier) 207 accountDomainOrIP = accountURI 208 209 case rl.CertificatesPerDomain.String() + perDNSNameSuffix: 210 dnsName, err := getValidatedFieldValue(fields, RegisteredDomainFieldName, rateLimitFieldValue) 211 if err != nil { 212 return nil, "", fmt.Errorf("getting/validating registeredDomain: %w", err) 213 } 214 bucketKey, err := rl.BuildBucketKey(rl.CertificatesPerDomain, 0, identifier.NewDNS(dnsName), identifier.ACMEIdentifiers{}, netip.Addr{}) 215 if err != nil { 216 return nil, "", fmt.Errorf("building bucket key: %w", err) 217 } 218 accountDomainOrIP = dnsName 219 req = makeReq(rl.CertificatesPerDomain, bucketKey, organization, tier) 220 221 case rl.CertificatesPerDomain.String() + perIPSuffix: 222 ipAddrStr, err := getValidatedFieldValue(fields, IPAddressFieldName, rateLimitFieldValue) 223 if err != nil { 224 return nil, "", fmt.Errorf("getting/validating ipAddress: %w", err) 225 } 226 ipAddr, err := netip.ParseAddr(ipAddrStr) 227 if err != nil { 228 return nil, "", fmt.Errorf("parsing ipAddress: %w", err) 229 } 230 bucketKey, err := rl.BuildBucketKey(rl.CertificatesPerDomain, 0, identifier.NewIP(ipAddr), identifier.ACMEIdentifiers{}, netip.Addr{}) 231 if err != nil { 232 return nil, "", fmt.Errorf("building bucket key: %w", err) 233 } 234 req = makeReq(rl.CertificatesPerDomain, bucketKey, organization, tier) 235 accountDomainOrIP = ipAddr.String() 236 237 default: 238 return nil, "", fmt.Errorf("unknown rate limit") 239 } 240 return req, accountDomainOrIP, nil 241 } 242 243 func (im *OverridesImporter) processTicket(ctx context.Context, ticketID int64, fields map[string]string) error { 244 req, accountDomainOrIP, err := makeAddOverrideRequest(fields[RateLimitFieldName], fields) 245 if err != nil { 246 // Move to "pending" so the next tick won't comment again. 247 im.transitionToPendingWithComment(ticketID, err.Error()) 248 return fmt.Errorf("preparing override request: %w", err) 249 } 250 251 resp, err := im.ra.AddRateLimitOverride(ctx, req) 252 if err != nil { 253 // This is likely a transient error, we'll re-attempt on the next pass. 254 return fmt.Errorf("calling ra.AddRateLimitOverride: %w", err) 255 } 256 257 rateLimit := rl.Name(req.LimitEnum).String() 258 if !resp.Enabled { 259 // Move to "pending" so the next tick won't comment again. 260 im.transitionToPendingWithComment(ticketID, "An existing override for this limit and requester is currently administratively disabled.") 261 return fmt.Errorf("override for rate limit %s and account/domain/IP: %s is administratively disabled", rateLimit, accountDomainOrIP) 262 } 263 264 // The "30 minutes" promised in the comment below must be kept in sync with 265 // the override reloading interval values (overrideRefresherShutdown) in: 266 // - cmd/boulder-ra/main.go and 267 // - cmd/boulder-wfe/main.go 268 successCommentBody := fmt.Sprintf( 269 "Your override request for rate limit %s and account/domain/IP: %s "+ 270 "has been approved. Your new limit is %d per period (see: https://letsencrypt.org/docs/rate-limits for the period). "+ 271 "Please allow up to 30 minutes for this change to take effect.", 272 rateLimit, accountDomainOrIP, req.Count, 273 ) 274 275 err = im.zendesk.UpdateTicketStatus(ticketID, "solved", successCommentBody, true) 276 if err != nil { 277 return fmt.Errorf("transitioning ticket %d to solved with a comment: %w", ticketID, err) 278 } 279 return nil 280 }