Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
6afba94
Add SSS clent config
lukaszgryglicki Jun 2, 2026
ae01681
Address AI feedback
lukaszgryglicki Jun 2, 2026
3f3ecbd
Merge pull request #5082 from linuxfoundation/unicron-sss-ecla-check
lukaszgryglicki Jun 2, 2026
b0bb85d
Fix the CI
lukaszgryglicki Jun 2, 2026
73ab951
Merge pull request #5084 from linuxfoundation/unicron-fix-ci-double-t…
lukaszgryglicki Jun 2, 2026
18261b8
feat(sign): integrate sanctions screening service
psrsingh May 30, 2026
112609e
fix(sign): improve company compliance check and error handling
psrsingh Jun 1, 2026
8f3a851
fix(sign): address SSS review feedback
psrsingh Jun 2, 2026
ab0669f
fix(sign): address SSS review feedback
psrsingh Jun 2, 2026
ac79c85
fix(sign): address SSS review feedback
psrsingh Jun 2, 2026
c173a49
fix(sign): address SSS review feedback
psrsingh Jun 2, 2026
3918235
feat(sign): add shared SSS client and ECLA screening
psrsingh Jun 2, 2026
760d660
feat(sign): integrate sanctions screening service enforcement
psrsingh Jun 2, 2026
af53c56
fix(sss): address review feedback and align with backend behavior
psrsingh Jun 3, 2026
901c026
chore(go): update toolchain to go1.25.11
psrsingh Jun 3, 2026
6303d33
fix(sss): address remaining review feedback
psrsingh Jun 3, 2026
19f7ba8
Merge pull request #5078 from psrsingh/feat/sss-enforcement
lukaszgryglicki Jun 3, 2026
65ee644
Fix CI lint test failures
lukaszgryglicki Jun 3, 2026
2b40b7c
Merge pull request #5086 from linuxfoundation/unicron-fix-ci-lint-tes…
lukaszgryglicki Jun 3, 2026
02b15e7
Followup SSS integration
lukaszgryglicki Jun 9, 2026
5d15e53
address AI feedback
lukaszgryglicki Jun 9, 2026
d51af38
Address AI feedback
lukaszgryglicki Jun 9, 2026
938ba2a
Address AI feedback
lukaszgryglicki Jun 9, 2026
8018705
Address AI feedback
lukaszgryglicki Jun 9, 2026
7feba2a
Address AI feedback
lukaszgryglicki Jun 9, 2026
23307ed
Address AI feedback
lukaszgryglicki Jun 9, 2026
12a9383
Address AI feedback
lukaszgryglicki Jun 9, 2026
a305ab2
Update comments
lukaszgryglicki Jun 9, 2026
30f6e9f
Merge pull request #5087 from linuxfoundation/unicron-sss-integration…
lukaszgryglicki Jun 10, 2026
2043b16
Merge branch 'main' into dev
lukaszgryglicki Jun 16, 2026
b87aa51
Two more SSS checks updates
lukaszgryglicki Jun 16, 2026
98ae496
Address AI feedback
lukaszgryglicki Jun 16, 2026
43a5371
Address AI feedback
lukaszgryglicki Jun 16, 2026
b8639ab
Address AI feedback
lukaszgryglicki Jun 16, 2026
74864cc
Merge pull request #5091 from linuxfoundation/unicron-sss-easycla-fix
lukaszgryglicki Jun 17, 2026
296c6bb
Fix SSS updates in optional mode
lukaszgryglicki Jun 17, 2026
32e3322
Add test coverage
lukaszgryglicki Jun 17, 2026
281329e
Update the test coverage
lukaszgryglicki Jun 17, 2026
9917f65
Merge pull request #5092 from linuxfoundation/unicron-sss-easycla-fix-2
lukaszgryglicki Jun 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/build-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,22 @@ jobs:
- name: Go Lint CLA Legacy Backend
working-directory: cla-backend-legacy
run: make lint

- name: Go Setup CLA SSS Base
working-directory: cla-sss-base
run: |
go mod tidy

- name: Go Build CLA SSS Base
working-directory: cla-sss-base
run: go build ./...

- name: Go Test CLA SSS Base
working-directory: cla-sss-base
run: go test ./...

- name: Go Lint CLA SSS Base
working-directory: cla-sss-base
run: |
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.8
golangci-lint run ./...
2 changes: 1 addition & 1 deletion .github/workflows/deploy-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ env:

concurrency:
group: deploy-dev
cancel-in-progress: true
cancel-in-progress: false

jobs:
build-deploy-dev:
Expand Down
2 changes: 1 addition & 1 deletion cla-backend-go/cmd/s3_upload/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func init() {
if err != nil {
log.Fatal(err)
}
signService = sign.NewService("", "", companyRepo, nil, nil, nil, nil, configFile.DocuSignPrivateKey, nil, nil, nil, nil, githubOrgService, nil, "", "", nil, nil, nil, nil, nil)
signService = sign.NewService("", "", companyRepo, nil, nil, nil, nil, configFile.DocuSignPrivateKey, nil, nil, nil, nil, githubOrgService, nil, "", "", nil, nil, nil, nil, nil, nil, false)
// projectRepo = repository.NewRepository(awsSession, stage, nil, nil, nil)
utils.SetS3Storage(awsSession, configFile.SignatureFilesBucket)
}
Expand Down
20 changes: 19 additions & 1 deletion cla-backend-go/cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import (

"github.com/linuxfoundation/easycla/cla-backend-go/api_logs"
"github.com/linuxfoundation/easycla/cla-backend-go/signatures"
"github.com/linuxfoundation/easycla/cla-backend-go/sss"
"github.com/linuxfoundation/easycla/cla-backend-go/telemetry"
v2Signatures "github.com/linuxfoundation/easycla/cla-backend-go/v2/signatures"

Expand Down Expand Up @@ -448,7 +449,24 @@ func server(localMode bool) http.Handler {
v2GithubActivityService := v2GithubActivity.NewService(gitV1Repository, githubOrganizationsRepo, eventsService, autoEnableService, emailService)

v2ClaGroupService := cla_groups.NewService(v1ProjectService, templateService, v1ProjectClaGroupRepo, v1ClaManagerService, v1SignaturesService, metricsRepo, gerritService, v1RepositoriesService, eventsService)
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)

// Initialize SSS (Sanctions Screening Service) client if configured.
// The sssRequired flag is controlled by the cla-sss-required-{stage} SSM parameter.
sssRequired := configFile.SSS.Required
var sssClient *sss.Client
sssClient, err = sss.NewClientFromPlatformCredentials(configFile.SSS.BaseURL, configFile.SSS.Audience, configFile.Auth0Platform.URL, configFile.Auth0Platform.ClientID, configFile.Auth0Platform.ClientSecret)
if err != nil {
if sssRequired {
log.WithFields(f).WithError(err).Fatal("failed to initialize required SSS client")
}
log.WithFields(f).WithError(err).Warn("failed to initialize optional SSS client, screening will be unavailable")
sssClient = nil
}
if sssRequired && sssClient == nil {
log.WithFields(f).Fatal("SSS is required but not configured")
}

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)

sessionStore, err := dynastore.New(dynastore.Path("/"), dynastore.HTTPOnly(), dynastore.TableName(configFile.SessionStoreTableName), dynastore.DynamoDB(dynamodb.New(awsSession)))
if err != nil {
Expand Down
29 changes: 29 additions & 0 deletions cla-backend-go/company/mocks/mock_repo.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions cla-backend-go/company/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type DBModel struct {
Updated string `dynamodbav:"date_modified" json:"date_modified"`
Note string `dynamodbav:"note" json:"note"`
IsSanctioned bool `dynamodbav:"is_sanctioned" json:"is_sanctioned"`
SanctionOrigin string `dynamodbav:"sanction_origin" json:"sanction_origin,omitempty"`
Version string `dynamodbav:"version" json:"version"`
}

Expand Down Expand Up @@ -87,6 +88,7 @@ func (dbCompanyModel *DBModel) toModel() (*models.Company, error) {
Updated: strfmt.DateTime(updateDateTime),
Note: dbCompanyModel.Note,
IsSanctioned: dbCompanyModel.IsSanctioned,
SanctionOrigin: dbCompanyModel.SanctionOrigin,
Version: dbCompanyModel.Version,
}, nil
}
Expand Down Expand Up @@ -148,6 +150,7 @@ func toSwaggerModel(dbCompanyModel *DBModel) (*models.Company, error) {
CompanyName: dbCompanyModel.CompanyName,
SigningEntityName: dbCompanyModel.SigningEntityName,
IsSanctioned: dbCompanyModel.IsSanctioned,
SanctionOrigin: dbCompanyModel.SanctionOrigin,
CompanyExternalID: dbCompanyModel.CompanyExternalID,
CompanyManagerID: dbCompanyModel.CompanyManagerID,
Created: strfmt.DateTime(createdDateTime),
Expand Down
1 change: 1 addition & 0 deletions cla-backend-go/company/projections.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func buildCompanyProjection() expression.ProjectionBuilder {
expression.Name("date_modified"),
expression.Name("note"),
expression.Name("is_sanctioned"),
expression.Name("sanction_origin"),
expression.Name("version"),
)
}
Expand Down
117 changes: 116 additions & 1 deletion cla-backend-go/company/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
log "github.com/linuxfoundation/easycla/cla-backend-go/logging"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
Expand Down Expand Up @@ -53,6 +54,8 @@ type IRepository interface { //nolint
ApproveCompanyAccessRequest(ctx context.Context, companyInviteID string) error
RejectCompanyAccessRequest(ctx context.Context, companyInviteID string) error
UpdateCompanyAccessList(ctx context.Context, companyID string, companyACL []string) error
UpdateCompanySanctionStatus(ctx context.Context, companyID string, sanctioned bool, origin string) error
ClearCompanySanctionStatusIfSSS(ctx context.Context, companyID string) (bool, error)
IsCCLAEnabledForCompany(ctx context.Context, companyID string) (bool, error)
}

Expand Down Expand Up @@ -1276,7 +1279,119 @@ func (repo repository) UpdateCompanyAccessList(ctx context.Context, companyID st
return nil
}

// CreateCompany creates a new company record
// sanctionOriginSSS is the sanction_origin value written by the Sanctions Screening Service.
const sanctionOriginSSS = "sss"

// UpdateCompanySanctionStatus sets is_sanctioned and, when origin is non-empty, sanction_origin.
// Pass origin="sss" when flagging via SSS; pass origin="" for manual admin updates.
func (repo repository) UpdateCompanySanctionStatus(ctx context.Context, companyID string, sanctioned bool, origin string) error {
f := logrus.Fields{
"functionName": "company.repository.UpdateCompanySanctionStatus",
utils.XREQUESTID: ctx.Value(utils.XREQUESTID),
"companyID": companyID,
"sanctioned": sanctioned,
"origin": origin,
}

_, now := utils.CurrentTime()

names := map[string]*string{
"#S": aws.String("is_sanctioned"),
"#M": aws.String("date_modified"),
}
values := map[string]*dynamodb.AttributeValue{
":s": {BOOL: aws.Bool(sanctioned)},
":m": {S: aws.String(now)},
}
updateExpr := "SET #S = :s, #M = :m"

if origin != "" {
names["#O"] = aws.String("sanction_origin")
values[":o"] = &dynamodb.AttributeValue{S: aws.String(origin)}
updateExpr += ", #O = :o"
} else {
// Manual/admin update: remove any stale SSS-set origin so the record becomes a
// sticky admin block (origin absent) that SSS will never auto-clear.
names["#O"] = aws.String("sanction_origin")
updateExpr += " REMOVE #O"
}

input := &dynamodb.UpdateItemInput{
ExpressionAttributeNames: names,
ExpressionAttributeValues: values,
TableName: aws.String(repo.companyTableName),
Key: map[string]*dynamodb.AttributeValue{
"company_id": {S: aws.String(companyID)},
},
UpdateExpression: aws.String(updateExpr),
}

// When SSS sets a block, never overwrite a manual/admin block (is_sanctioned=true
// with absent or non-"sss" origin). Only set the SSS flag when the company is
// currently unblocked or already SSS-blocked. A ConditionalCheckFailedException
// therefore means a manual/admin block is already in place and must be preserved.
sssSettingBlock := sanctioned && origin == sanctionOriginSSS
if sssSettingBlock {
values[":false"] = &dynamodb.AttributeValue{BOOL: aws.Bool(false)}
input.ConditionExpression = aws.String("attribute_not_exists(#S) OR #S = :false OR #O = :o")
}

if _, err := repo.dynamoDBClient.UpdateItem(input); err != nil {
if sssSettingBlock {
if aerr, ok := err.(awserr.Error); ok && aerr.Code() == dynamodb.ErrCodeConditionalCheckFailedException {
log.WithFields(f).Debugf("company %s already has a manual/admin sanction block; preserving it and not overwriting origin with sss", companyID)
return nil
}
}
log.WithFields(f).Warnf("error updating company sanction status, error: %v", err)
return err
}
return nil
}

// ClearCompanySanctionStatusIfSSS clears is_sanctioned only when sanction_origin="sss".
// It returns (cleared, err): cleared is true only when the conditional matched and the
// record was actually cleared; a ConditionalCheckFailedException (manual/absent origin)
// returns (false, nil) so callers can leave any manual/admin block in place.
func (repo repository) ClearCompanySanctionStatusIfSSS(ctx context.Context, companyID string) (bool, error) {
f := logrus.Fields{
"functionName": "company.repository.ClearCompanySanctionStatusIfSSS",
utils.XREQUESTID: ctx.Value(utils.XREQUESTID),
"companyID": companyID,
}

_, now := utils.CurrentTime()

input := &dynamodb.UpdateItemInput{
TableName: aws.String(repo.companyTableName),
Key: map[string]*dynamodb.AttributeValue{
"company_id": {S: aws.String(companyID)},
},
UpdateExpression: aws.String("SET #S = :false, #M = :m REMOVE #O"),
ConditionExpression: aws.String("#O = :sss"),
ExpressionAttributeNames: map[string]*string{
"#S": aws.String("is_sanctioned"),
"#M": aws.String("date_modified"),
"#O": aws.String("sanction_origin"),
},
ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
":false": {BOOL: aws.Bool(false)},
":m": {S: aws.String(now)},
":sss": {S: aws.String(sanctionOriginSSS)},
},
}

if _, err := repo.dynamoDBClient.UpdateItem(input); err != nil {
if aerr, ok := err.(awserr.Error); ok && aerr.Code() == dynamodb.ErrCodeConditionalCheckFailedException {
log.WithFields(f).Debugf("sanction_origin != sss for company %s; not auto-clearing (manual/admin block)", companyID)
return false, nil
}
log.WithFields(f).Warnf("error clearing company sanction status: %v", err)
return false, err
}
return true, nil
}

func (repo repository) CreateCompany(ctx context.Context, in *models.Company) (*models.Company, error) {
f := logrus.Fields{
"functionName": "company.repository.CreateCompany",
Expand Down
29 changes: 29 additions & 0 deletions cla-backend-go/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ type Config struct {

// DocuSignPrivateKey is the private key for the DocuSign API
DocuSignPrivateKey string `json:"docuSignPrivateKey"`

// SSS holds the Sanctions Screening Service client configuration
SSS SSS `json:"sss"`
}

// Auth0 model
Expand All @@ -116,6 +119,32 @@ type Auth0Platform struct {
URL string `json:"url"`
}

// SSS holds the Sanctions Screening Service client configuration. The SSS
// integration reuses the shared LFX platform M2M (Auth0Platform) credentials,
// so only the SSS-specific base URL and audience are configured here.
//
// Sanctions screening is required by policy in deployed environments - these
// values are only loaded leniently (see config/ssm.go) so the SSM parameters
// can be provisioned before the feature is switched on. Whether a missing
// configuration is tolerated (no-op) or fatal must be enforced by the caller
// per stage; it must NOT be silently skipped in dev/staging/prod.
type SSS struct {
// BaseURL is the SSS host root WITHOUT the /api/v1 suffix - the client
// appends /api/v1/organizations/status itself (e.g.
// https://sanctions-screening.dev.v2.cluster.linuxfound.info)
BaseURL string `json:"base_url"`
// Audience is the Auth0 resource-server identifier for SSS, including any
// trailing slash - it must match the auth0-terraform resource server
// identifier exactly (e.g.
// https://sanctions-screening.dev.v2.cluster.linuxfound.info/)
Audience string `json:"audience"`
// Required is a flag controlling whether SSS screening is required or optional.
// When true, any SSS errors (unavailable, timeout, config errors, or missing domain)
// will block the operation. When false, SSS errors are logged but do not block.
// This flag is loaded from the SSM parameter cla-sss-required-{stage}.
Required bool `json:"required"`
}

// Docraptor model
type Docraptor struct {
APIKey string `json:"apiKey"`
Expand Down
Loading
Loading