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  }