Skip to content

Commit 2573039

Browse files
Merge pull request #5105 from linuxfoundation/unicron-sss-enabled-flag-prod
Add SSS enabled/disabled switch to EasyCLA (prod)
2 parents 04abb7b + 92190da commit 2573039

7 files changed

Lines changed: 85 additions & 27 deletions

File tree

cla-backend-go/cmd/s3_upload/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func init() {
5757
if err != nil {
5858
log.Fatal(err)
5959
}
60-
signService = sign.NewService("", "", companyRepo, nil, nil, nil, nil, configFile.DocuSignPrivateKey, nil, nil, nil, nil, githubOrgService, nil, "", "", nil, nil, nil, nil, nil, nil, false)
60+
signService = sign.NewService("", "", companyRepo, nil, nil, nil, nil, configFile.DocuSignPrivateKey, nil, nil, nil, nil, githubOrgService, nil, "", "", nil, nil, nil, nil, nil, nil, false, false)
6161
// projectRepo = repository.NewRepository(awsSession, stage, nil, nil, nil)
6262
utils.SetS3Storage(awsSession, configFile.SignatureFilesBucket)
6363
}

cla-backend-go/cmd/server.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -453,20 +453,21 @@ func server(localMode bool) http.Handler {
453453
// Initialize SSS (Sanctions Screening Service) client if configured.
454454
// The sssRequired flag is controlled by the cla-sss-required-{stage} SSM parameter.
455455
sssRequired := configFile.SSS.Required
456+
sssEnabled := configFile.SSS.Enabled
456457
var sssClient *sss.Client
457458
sssClient, err = sss.NewClientFromPlatformCredentials(configFile.SSS.BaseURL, configFile.SSS.Audience, configFile.Auth0Platform.URL, configFile.Auth0Platform.ClientID, configFile.Auth0Platform.ClientSecret)
458459
if err != nil {
459-
if sssRequired {
460+
if sssEnabled && sssRequired {
460461
log.WithFields(f).WithError(err).Fatal("failed to initialize required SSS client")
461462
}
462463
log.WithFields(f).WithError(err).Warn("failed to initialize optional SSS client, screening will be unavailable")
463464
sssClient = nil
464465
}
465-
if sssRequired && sssClient == nil {
466+
if sssEnabled && sssRequired && sssClient == nil {
466467
log.WithFields(f).Fatal("SSS is required but not configured")
467468
}
468469

469-
v2SignService := sign.NewService(configFile.ClaAPIV4Base, configFile.ClaV1ApiURL, v1CompanyRepo, v1CLAGroupRepo, v1ProjectClaGroupRepo, v1CompanyService, v2ClaGroupService, configFile.DocuSignPrivateKey, usersService, v1SignaturesService, storeRepository, v1RepositoriesService, githubOrganizationsService, gitlabOrganizationsService, configFile.CLALandingPage, configFile.CLALogoURL, emailService, eventsService, gitlabActivityService, gitlabApp, gerritService, sssClient, sssRequired)
470+
v2SignService := sign.NewService(configFile.ClaAPIV4Base, configFile.ClaV1ApiURL, v1CompanyRepo, v1CLAGroupRepo, v1ProjectClaGroupRepo, v1CompanyService, v2ClaGroupService, configFile.DocuSignPrivateKey, usersService, v1SignaturesService, storeRepository, v1RepositoriesService, githubOrganizationsService, gitlabOrganizationsService, configFile.CLALandingPage, configFile.CLALogoURL, emailService, eventsService, gitlabActivityService, gitlabApp, gerritService, sssClient, sssRequired, sssEnabled)
470471

471472
sessionStore, err := dynastore.New(dynastore.Path("/"), dynastore.HTTPOnly(), dynastore.TableName(configFile.SessionStoreTableName), dynastore.DynamoDB(dynamodb.New(awsSession)))
472473
if err != nil {

cla-backend-go/config/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@ type SSS struct {
143143
// will block the operation. When false, SSS errors are logged but do not block.
144144
// This flag is loaded from the SSM parameter cla-sss-required-{stage}.
145145
Required bool `json:"required"`
146+
// Enabled is the SSS kill switch (cla-sss-enabled-{stage}, default true): when false,
147+
// checkCompanyCompliance skips the live SSS check; persisted is_sanctioned still blocks elsewhere.
148+
Enabled bool `json:"enabled"`
146149
}
147150

148151
// Docraptor model

cla-backend-go/config/ssm.go

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ func loadOptionalSSSConfig(ssmClient *ssm.SSM, stage string, config *Config, f l
287287
config.SSS.BaseURL = getOptionalSSMString(ssmClient, fmt.Sprintf("cla-sss-base-url-%s", stage), f)
288288
config.SSS.Audience = getOptionalSSMString(ssmClient, fmt.Sprintf("cla-sss-auth0-audience-%s", stage), f)
289289
config.SSS.Required = getOptionalSSMBool(ssmClient, fmt.Sprintf("cla-sss-required-%s", stage), f)
290+
config.SSS.Enabled = getOptionalSSMBoolDefault(ssmClient, fmt.Sprintf("cla-sss-enabled-%s", stage), true, f)
290291
}
291292

292293
// getOptionalSSMString fetches a parameter that may legitimately be absent while
@@ -311,28 +312,33 @@ func getOptionalSSMString(ssmClient *ssm.SSM, key string, f logrus.Fields) strin
311312
return strings.TrimSpace(*out.Parameter.Value)
312313
}
313314

314-
// getOptionalSSMBool fetches an optional boolean parameter. It logs exactly once:
315-
// a missing parameter is reported at debug (an expected, benign state), while any
316-
// other failure - IAM, throttling, parse errors, etc. - is reported as a warning.
317-
// Returns false (the default) when the value is unreadable or the parameter is absent.
315+
// getOptionalSSMBool fetches an optional boolean parameter, returning false (the default)
316+
// when the value is unreadable, absent, or malformed.
318317
func getOptionalSSMBool(ssmClient *ssm.SSM, key string, f logrus.Fields) bool {
318+
return getOptionalSSMBoolDefault(ssmClient, key, false, f)
319+
}
320+
321+
// getOptionalSSMBoolDefault is getOptionalSSMBool with a caller-supplied default for a
322+
// missing/unreadable parameter - used for cla-sss-enabled, which must default to true so a
323+
// not-yet-provisioned key never silently disables screening.
324+
func getOptionalSSMBoolDefault(ssmClient *ssm.SSM, key string, def bool, f logrus.Fields) bool {
319325
out, err := ssmClient.GetParameter(&ssm.GetParameterInput{
320326
Name: aws.String(key),
321327
WithDecryption: aws.Bool(false),
322328
})
323329
if err != nil {
324330
if aerr, ok := err.(awserr.Error); ok && aerr.Code() == ssm.ErrCodeParameterNotFound {
325-
log.WithFields(f).Debugf("optional SSM key %s not provisioned - using default value false", key)
331+
log.WithFields(f).Debugf("optional SSM key %s not provisioned - using default value %t", key, def)
326332
} else {
327-
log.WithFields(f).WithError(err).Warnf("unable to read optional SSM key %s - using default value false", key)
333+
log.WithFields(f).WithError(err).Warnf("unable to read optional SSM key %s - using default value %t", key, def)
328334
}
329-
return false
335+
return def
330336
}
331337

332338
boolVal, err := strconv.ParseBool(strings.TrimSpace(*out.Parameter.Value))
333339
if err != nil {
334-
log.WithFields(f).WithError(err).Warnf("unable to parse optional SSM key %s as boolean - using default value false", key)
335-
return false
340+
log.WithFields(f).WithError(err).Warnf("unable to parse optional SSM key %s as boolean - using default value %t", key, def)
341+
return def
336342
}
337343

338344
return boolVal

cla-backend-go/v2/sign/service.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ type service struct {
125125
gerritService gerrits.Service
126126
sssClient *sss.Client
127127
sssRequired bool
128+
sssEnabled bool
128129
complianceCache map[string]complianceCacheEntry
129130
complianceCacheMu *sync.Mutex
130131
}
@@ -137,7 +138,7 @@ type complianceCacheEntry struct {
137138
// NewService returns an instance of v2 project service
138139
func NewService(apiURL, v1API string, compRepo company.IRepository, projectRepo ProjectRepo, pcgRepo projects_cla_groups.Repository, compService company.IService, claGroupService cla_groups.Service, docsignPrivateKey string, userService users.Service, signatureService signatures.SignatureService, storeRepository store.Repository,
139140
repositoryService repositories.Service, githubOrgService github_organizations.Service, gitlabOrgService gitlab_organizations.ServiceInterface, claLandingPage string, claLogoURL string, emailTemplateService emails.EmailTemplateService, eventsService events.Service, gitlabActivityService gitlab_activity.Service, gitlabApp *gitlab_api.App,
140-
gerritService gerrits.Service, sssClient *sss.Client, sssRequired bool) Service {
141+
gerritService gerrits.Service, sssClient *sss.Client, sssRequired bool, sssEnabled bool) Service {
141142
return &service{
142143
ClaV4ApiURL: apiURL,
143144
ClaV1ApiURL: v1API,
@@ -162,6 +163,7 @@ func NewService(apiURL, v1API string, compRepo company.IRepository, projectRepo
162163
eventsService: eventsService,
163164
sssClient: sssClient,
164165
sssRequired: sssRequired,
166+
sssEnabled: sssEnabled,
165167
complianceCache: make(map[string]complianceCacheEntry),
166168
complianceCacheMu: &sync.Mutex{},
167169
}
@@ -3003,6 +3005,11 @@ func (s *service) checkCompanyCompliance(ctx context.Context, company *v1Models.
30033005
return true, nil
30043006
}
30053007

3008+
if !s.sssEnabled {
3009+
log.WithFields(f).Warn("sanctions screening disabled (cla-sss-enabled=false); skipping SSS check")
3010+
return false, nil
3011+
}
3012+
30063013
cacheKey := s.complianceCacheKey(company)
30073014
if cached, ok := s.getComplianceCache(cacheKey); ok {
30083015
log.WithFields(f).Debugf("using cached compliance result for organization/company: %s", cacheKey)

cla-backend-go/v2/sign/service_sss_test.go

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func TestResolveDomainFallsBackToParsedLink(t *testing.T) {
4242
}
4343

4444
func TestCheckCompanyComplianceRequiredBlocksMissingClient(t *testing.T) {
45-
svc := &service{sssRequired: true}
45+
svc := &service{sssRequired: true, sssEnabled: true}
4646

4747
_, err := svc.checkCompanyCompliance(context.Background(), &models.Company{
4848
CompanyID: "company-id",
@@ -54,7 +54,7 @@ func TestCheckCompanyComplianceRequiredBlocksMissingClient(t *testing.T) {
5454
}
5555

5656
func TestCheckCompanyComplianceOptionalAllowsMissingClient(t *testing.T) {
57-
svc := &service{sssRequired: false}
57+
svc := &service{sssRequired: false, sssEnabled: true}
5858

5959
blocked, err := svc.checkCompanyCompliance(context.Background(), &models.Company{
6060
CompanyID: "company-id",
@@ -141,7 +141,7 @@ func TestSSSStatusActionable(t *testing.T) {
141141
}
142142

143143
func TestCheckCompanyComplianceRequiredBlocksMissingExternalID(t *testing.T) {
144-
svc := &service{sssRequired: true, sssClient: newTestSSSClient(t)}
144+
svc := &service{sssRequired: true, sssEnabled: true, sssClient: newTestSSSClient(t)}
145145

146146
_, err := svc.checkCompanyCompliance(context.Background(), &models.Company{
147147
CompanyID: "company-id",
@@ -154,7 +154,7 @@ func TestCheckCompanyComplianceRequiredBlocksMissingExternalID(t *testing.T) {
154154
}
155155

156156
func TestCheckCompanyComplianceOptionalAllowsMissingExternalID(t *testing.T) {
157-
svc := &service{sssRequired: false, sssClient: newTestSSSClient(t)}
157+
svc := &service{sssRequired: false, sssEnabled: true, sssClient: newTestSSSClient(t)}
158158

159159
blocked, err := svc.checkCompanyCompliance(context.Background(), &models.Company{
160160
CompanyID: "company-id",
@@ -170,7 +170,7 @@ func TestCheckCompanyComplianceOptionalAllowsMissingExternalID(t *testing.T) {
170170
}
171171

172172
func TestCheckCompanyComplianceOptionalBlocksPersistedSanctionMissingExternalID(t *testing.T) {
173-
svc := &service{sssRequired: false, sssClient: newTestSSSClient(t)}
173+
svc := &service{sssRequired: false, sssEnabled: true, sssClient: newTestSSSClient(t)}
174174

175175
// origin=sss bypasses the manual-block short-circuit so the missing-external-ID early
176176
// exit is the path under test: in optional mode it must honor the persisted sanction.
@@ -266,6 +266,7 @@ func TestCheckCompanyComplianceCacheHitMutatesModel(t *testing.T) {
266266
// A cached "clean" result must clear the stale loaded model so downstream gates
267267
// (e.g. ProcessEmployeeSignature) in the same request stay consistent.
268268
svc := &service{
269+
sssEnabled: true,
269270
complianceCache: map[string]complianceCacheEntry{
270271
"external-id": {sanctioned: false, expiresAt: time.Now().Add(time.Minute)},
271272
},
@@ -294,7 +295,7 @@ func TestCheckCompanyComplianceCacheHitMutatesModel(t *testing.T) {
294295
func TestCheckCompanyComplianceOptionalHonorsPersistedSSSFlag(t *testing.T) {
295296
// Optional mode with no SSS client: an already-persisted SSS-origin block must keep
296297
// blocking until a live clean result can clear it (do not fail open on the flag).
297-
svc := &service{sssRequired: false}
298+
svc := &service{sssRequired: false, sssEnabled: true}
298299

299300
blocked, err := svc.checkCompanyCompliance(context.Background(), &models.Company{
300301
CompanyID: "company-id",
@@ -313,7 +314,7 @@ func TestCheckCompanyComplianceOptionalHonorsPersistedSSSFlag(t *testing.T) {
313314
func TestCheckCompanyComplianceAdminBlockAlwaysBlocks(t *testing.T) {
314315
// A manual/admin block (is_sanctioned=true, no/!=sss origin) must short-circuit and
315316
// block regardless of mode or SSS availability.
316-
svc := &service{sssRequired: false}
317+
svc := &service{sssRequired: false, sssEnabled: true}
317318

318319
blocked, err := svc.checkCompanyCompliance(context.Background(), &models.Company{
319320
CompanyID: "company-id",
@@ -326,3 +327,22 @@ func TestCheckCompanyComplianceAdminBlockAlwaysBlocks(t *testing.T) {
326327
t.Fatal("expected a manual/admin sanction (no origin) to always block")
327328
}
328329
}
330+
331+
func TestCheckCompanyComplianceDisabledSkipsSSS(t *testing.T) {
332+
// Kill switch off (sssEnabled=false): the live SSS check is skipped and the company is
333+
// not blocked, even in required mode with a persisted sss-origin flag.
334+
svc := &service{sssRequired: true, sssEnabled: false, sssClient: newTestSSSClient(t)}
335+
336+
blocked, err := svc.checkCompanyCompliance(context.Background(), &models.Company{
337+
CompanyID: "company-id",
338+
CompanyName: "Company",
339+
IsSanctioned: true,
340+
SanctionOrigin: "sss",
341+
})
342+
if err != nil {
343+
t.Fatalf("expected disabled SSS to skip without error, got %v", err)
344+
}
345+
if blocked {
346+
t.Fatal("expected disabled SSS not to block")
347+
}
348+
}

cla-backend-legacy/internal/api/handlers.go

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ type Handlers struct {
8080
userService *userservicelegacy.Client
8181
sssClient *sss.Client
8282
sssRequired bool
83+
sssEnabled bool
8384
}
8485

8586
func NewHandlers() *Handlers {
@@ -215,16 +216,18 @@ func NewHandlers() *Handlers {
215216
sssBaseURL := getOptionalSSMString(ctx, ssmClient, fmt.Sprintf("cla-sss-base-url-%s", stage), f)
216217
sssAudience := getOptionalSSMString(ctx, ssmClient, fmt.Sprintf("cla-sss-auth0-audience-%s", stage), f)
217218
sssRequired := getOptionalSSMBool(ctx, ssmClient, fmt.Sprintf("cla-sss-required-%s", stage), f)
219+
sssEnabled := getOptionalSSMBoolDefault(ctx, ssmClient, fmt.Sprintf("cla-sss-enabled-%s", stage), true, f)
218220

219221
auth0ClientID := strings.TrimSpace(os.Getenv("PLATFORM_AUTH0_CLIENT_ID"))
220222
auth0ClientSecret := strings.TrimSpace(os.Getenv("PLATFORM_AUTH0_CLIENT_SECRET"))
221223
auth0URL := strings.TrimSpace(os.Getenv("PLATFORM_AUTH0_URL"))
222224

223225
h.sssRequired = sssRequired
226+
h.sssEnabled = sssEnabled
224227
if sssBaseURL != "" && sssAudience != "" {
225228
sssClient, err := sss.NewClientFromPlatformCredentials(sssBaseURL, sssAudience, auth0URL, auth0ClientID, auth0ClientSecret)
226229
if err != nil {
227-
if sssRequired {
230+
if sssEnabled && sssRequired {
228231
logging.Fatalf("failed to initialize required SSS client: %v", err)
229232
}
230233
logging.Warnf("failed to initialize optional SSS client, screening will be unavailable: %v", err)
@@ -234,7 +237,7 @@ func NewHandlers() *Handlers {
234237
}
235238
}
236239

237-
if sssRequired && h.sssClient == nil {
240+
if sssEnabled && sssRequired && h.sssClient == nil {
238241
logging.Fatalf("SSS is required but not configured (base URL or audience missing in SSM)")
239242
}
240243

@@ -266,13 +269,26 @@ func getOptionalSSMString(ctx context.Context, ssmClient *ssm.Client, key string
266269
}
267270

268271
// getOptionalSSMBool retrieves a boolean parameter from SSM.
269-
// It returns false (the safe default) when the value is unreadable or the parameter is absent.
272+
// It returns false (the safe default) when the value is unreadable, absent, or malformed.
270273
func getOptionalSSMBool(ctx context.Context, ssmClient *ssm.Client, key string, f logrus.Fields) bool {
271-
val := getOptionalSSMString(ctx, ssmClient, key, f)
274+
return getOptionalSSMBoolDefault(ctx, ssmClient, key, false, f)
275+
}
276+
277+
// getOptionalSSMBoolDefault is getOptionalSSMBool with a caller-supplied default for a
278+
// missing parameter - used for cla-sss-enabled, which must default to true so a
279+
// not-yet-provisioned key never silently disables screening. A malformed value also
280+
// falls back to the default rather than being treated as false.
281+
func getOptionalSSMBoolDefault(ctx context.Context, ssmClient *ssm.Client, key string, def bool, f logrus.Fields) bool {
282+
val := strings.TrimSpace(getOptionalSSMString(ctx, ssmClient, key, f))
272283
if val == "" {
273-
return false
284+
return def
285+
}
286+
boolVal, err := strconv.ParseBool(val)
287+
if err != nil {
288+
logging.Warnf("unable to parse optional SSM key %s=%q as boolean - using default value %t", key, val, def)
289+
return def
274290
}
275-
return strings.ToLower(val) == "true"
291+
return boolVal
276292
}
277293

278294
// formatPynamoDateTimeUTC formats timestamps the way Python's pynamodb
@@ -8918,6 +8934,11 @@ func (h *Handlers) checkCompanyCompliance(ctx context.Context, company map[strin
89188934
return true, nil
89198935
}
89208936

8937+
if !h.sssEnabled {
8938+
logging.Warnf("sanctions screening disabled (cla-sss-enabled=false); skipping SSS check for company %s", companyID)
8939+
return false, nil
8940+
}
8941+
89218942
// In the fallbacks below (no live SSS check possible) we honor the persisted state:
89228943
// a manual/admin block was already handled by the short-circuit above, and a stale
89238944
// sss-origin block keeps blocking until a live "clean" can clear it. The mode only

0 commit comments

Comments
 (0)