Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
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
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
28 changes: 28 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.

1 change: 1 addition & 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
91 changes: 90 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) error
IsCCLAEnabledForCompany(ctx context.Context, companyID string) (bool, error)
}

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

// CreateCompany creates a new company record
// 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"
}

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),
}

if _, err := repo.dynamoDBClient.UpdateItem(input); err != 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".
// A ConditionalCheckFailedException (manual/absent origin) is silently ignored.
func (repo repository) ClearCompanySanctionStatusIfSSS(ctx context.Context, companyID string) 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("sss")},
},
}

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 nil
}
log.WithFields(f).Warnf("error clearing company sanction status: %v", err)
return err
}
return nil
}

func (repo repository) CreateCompany(ctx context.Context, in *models.Company) (*models.Company, error) {
f := logrus.Fields{
"functionName": "company.repository.CreateCompany",
Expand Down
5 changes: 5 additions & 0 deletions cla-backend-go/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ type SSS struct {
// 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
Expand Down
28 changes: 28 additions & 0 deletions cla-backend-go/config/ssm.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ func loadSSMConfig(awsSession *session.Session, stage string) Config { //nolint
func loadOptionalSSSConfig(ssmClient *ssm.SSM, stage string, config *Config, f logrus.Fields) {
config.SSS.BaseURL = getOptionalSSMString(ssmClient, fmt.Sprintf("cla-sss-base-url-%s", stage), f)
config.SSS.Audience = getOptionalSSMString(ssmClient, fmt.Sprintf("cla-sss-auth0-audience-%s", stage), f)
config.SSS.Required = getOptionalSSMBool(ssmClient, fmt.Sprintf("cla-sss-required-%s", stage), f)
}

// getOptionalSSMString fetches a parameter that may legitimately be absent while
Expand All @@ -309,3 +310,30 @@ func getOptionalSSMString(ssmClient *ssm.SSM, key string, f logrus.Fields) strin

return strings.TrimSpace(*out.Parameter.Value)
}

// getOptionalSSMBool fetches an optional boolean parameter. It logs exactly once:
// a missing parameter is reported at debug (an expected, benign state), while any
// other failure - IAM, throttling, parse errors, etc. - is reported as a warning.
// Returns false (the default) when the value is unreadable or the parameter is absent.
func getOptionalSSMBool(ssmClient *ssm.SSM, key string, f logrus.Fields) bool {
out, err := ssmClient.GetParameter(&ssm.GetParameterInput{
Name: aws.String(key),
WithDecryption: aws.Bool(false),
})
if err != nil {
if aerr, ok := err.(awserr.Error); ok && aerr.Code() == ssm.ErrCodeParameterNotFound {
log.WithFields(f).Debugf("optional SSM key %s not provisioned - using default value false", key)
} else {
log.WithFields(f).WithError(err).Warnf("unable to read optional SSM key %s - using default value false", key)
}
return false
}

boolVal, err := strconv.ParseBool(strings.TrimSpace(*out.Parameter.Value))
if err != nil {
log.WithFields(f).WithError(err).Warnf("unable to parse optional SSM key %s as boolean - using default value false", key)
return false
}

return boolVal
}
5 changes: 4 additions & 1 deletion cla-backend-go/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ module github.com/linuxfoundation/easycla/cla-backend-go

go 1.25.0

toolchain go1.25.10
toolchain go1.25.11

replace github.com/awslabs/aws-lambda-go-api-proxy => github.com/LF-Engineering/aws-lambda-go-api-proxy v0.3.2

replace github.com/linuxfoundation/easycla/cla-sss-base => ../cla-sss-base

require (
github.com/LF-Engineering/aws-lambda-go-api-proxy v0.3.2
github.com/LF-Engineering/lfx-kit v0.1.39
Expand Down Expand Up @@ -40,6 +42,7 @@ require (
github.com/juju/zip v0.0.0-20160205105221-f6b1e93fa2e2
github.com/kr/pretty v0.3.1 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/linuxfoundation/easycla/cla-sss-base v0.0.0
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
Expand Down
18 changes: 4 additions & 14 deletions cla-backend-go/sss/auth.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,10 @@
// Copyright The Linux Foundation and each contributor to CommunityBridge.
// SPDX-License-Identifier: MIT

// Package sss re-exports Auth functions from the shared cla-sss-base module.
package sss

// authRequest is the payload used for the Auth0 client credentials request.
type authRequest struct {
GrantType string `json:"grant_type"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
Audience string `json:"audience"`
}
import sssbase "github.com/linuxfoundation/easycla/cla-sss-base"

// authResponse is the Auth0 token response payload.
type authResponse struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
Scope string `json:"scope"`
TokenType string `json:"token_type"`
}
// Re-export factory function for backward compatibility
var NewClientFromPlatformCredentials = sssbase.NewClientFromPlatformCredentials
Loading
Loading