From 6afba946e74b16e961e842df54d7178789535b54 Mon Sep 17 00:00:00 2001 From: Lukasz Gryglicki Date: Tue, 2 Jun 2026 09:13:48 +0200 Subject: [PATCH 1/2] Add SSS clent config Signed-off-by: Lukasz Gryglicki Assisted by [OpenAI](https://platform.openai.com/) Assisted by [GitHub Copilot](https://github.com/features/copilot) Assisted by [Claude](https://claude.ai) --- cla-backend-go/config/config.go | 24 ++++++++++ cla-backend-go/config/ssm.go | 27 ++++++++++++ cla-backend-go/sss/from_config.go | 38 ++++++++++++++++ cla-backend-go/sss/from_config_test.go | 61 ++++++++++++++++++++++++++ 4 files changed, 150 insertions(+) create mode 100644 cla-backend-go/sss/from_config.go create mode 100644 cla-backend-go/sss/from_config_test.go diff --git a/cla-backend-go/config/config.go b/cla-backend-go/config/config.go index 801e04240..e81b70170 100644 --- a/cla-backend-go/config/config.go +++ b/cla-backend-go/config/config.go @@ -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 @@ -116,6 +119,27 @@ 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"` +} + // Docraptor model type Docraptor struct { APIKey string `json:"apiKey"` diff --git a/cla-backend-go/config/ssm.go b/cla-backend-go/config/ssm.go index 21f97cf51..e38c1deb5 100644 --- a/cla-backend-go/config/ssm.go +++ b/cla-backend-go/config/ssm.go @@ -268,5 +268,32 @@ func loadSSMConfig(awsSession *session.Session, stage string) Config { //nolint } } + // Sanctions Screening Service (SSS) configuration is loaded leniently - the + // keys may not be provisioned in every environment yet, so look them up + // best-effort (non-fatal) rather than aborting startup like the keys above. + // This is only a rollout convenience: SSS is required by policy in deployed + // environments, and the caller must enforce that (see config.SSS). + loadOptionalSSSConfig(ssmClient, stage, &config, f) + return config } + +// loadOptionalSSSConfig fetches the SSS keys without aborting startup when they +// are missing, so the SSM parameters can be provisioned before the feature is +// switched on. A stage that requires SSS must reject an empty configuration at +// the call site rather than silently skipping the screening check. +func loadOptionalSSSConfig(ssmClient *ssm.SSM, stage string, config *Config, f logrus.Fields) { + baseURLKey := fmt.Sprintf("cla-sss-base-url-%s", stage) + if value, err := getSSMString(ssmClient, baseURLKey); err == nil { + config.SSS.BaseURL = value + } else { + log.WithFields(f).Warnf("optional SSM key %s not set - sanctions screening disabled", baseURLKey) + } + + audienceKey := fmt.Sprintf("cla-sss-auth0-audience-%s", stage) + if value, err := getSSMString(ssmClient, audienceKey); err == nil { + config.SSS.Audience = value + } else { + log.WithFields(f).Warnf("optional SSM key %s not set - sanctions screening disabled", audienceKey) + } +} diff --git a/cla-backend-go/sss/from_config.go b/cla-backend-go/sss/from_config.go new file mode 100644 index 000000000..4f54bac78 --- /dev/null +++ b/cla-backend-go/sss/from_config.go @@ -0,0 +1,38 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package sss + +import ( + "net/url" + "strings" +) + +// NewClientFromPlatformCredentials builds an SSS client that reuses the shared +// LFX platform M2M (Auth0) credentials already configured for EasyCLA. +// +// oauthTokenURL is the full Auth0 token endpoint used for platform auth +// (e.g. https:///oauth/token); its scheme+host is reused as the SSS +// client's Auth0 domain, since SSS authenticates with the same client against +// the same Auth0 tenant - only the requested audience differs. +// +// It returns (nil, nil) when baseURL or audience is empty so callers can treat +// an unconfigured SSS as a disabled, no-op feature rather than an error. +func NewClientFromPlatformCredentials(baseURL, audience, oauthTokenURL, clientID, clientSecret string) (*Client, error) { + if strings.TrimSpace(baseURL) == "" || strings.TrimSpace(audience) == "" { + return nil, nil + } + + auth0Domain := strings.TrimSpace(oauthTokenURL) + if u, err := url.Parse(auth0Domain); err == nil && u.Host != "" { + auth0Domain = u.Scheme + "://" + u.Host + } + + return NewClient(SSSConfig{ + BaseURL: baseURL, + Auth0Domain: auth0Domain, + Auth0ClientID: clientID, + Auth0ClientSecret: clientSecret, + Auth0Audience: audience, + }) +} diff --git a/cla-backend-go/sss/from_config_test.go b/cla-backend-go/sss/from_config_test.go new file mode 100644 index 000000000..b6c19b5eb --- /dev/null +++ b/cla-backend-go/sss/from_config_test.go @@ -0,0 +1,61 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package sss + +import "testing" + +func TestNewClientFromPlatformCredentials_DisabledWhenBaseURLMissing(t *testing.T) { + client, err := NewClientFromPlatformCredentials("", "https://sss.example/", "https://tenant.auth0.com/oauth/token", "id", "secret") + if err != nil { + t.Fatalf("expected no error when disabled, got: %v", err) + } + if client != nil { + t.Fatalf("expected nil client when base URL is empty") + } +} + +func TestNewClientFromPlatformCredentials_DisabledWhenAudienceMissing(t *testing.T) { + client, err := NewClientFromPlatformCredentials("https://sss.example", "", "https://tenant.auth0.com/oauth/token", "id", "secret") + if err != nil { + t.Fatalf("expected no error when disabled, got: %v", err) + } + if client != nil { + t.Fatalf("expected nil client when audience is empty") + } +} + +func TestNewClientFromPlatformCredentials_DerivesAuth0DomainFromTokenURL(t *testing.T) { + client, err := NewClientFromPlatformCredentials( + "https://sanctions-screening.dev.v2.cluster.linuxfound.info", + "https://sanctions-screening.dev.v2.cluster.linuxfound.info/", + "https://linuxfoundation.auth0.com/oauth/token", + "client-id", + "client-secret", + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if client == nil { + t.Fatal("expected a configured client") + } + + if got, want := client.cfg.Auth0Domain, "https://linuxfoundation.auth0.com"; got != want { + t.Errorf("Auth0Domain: got %q, want %q", got, want) + } + // The derived domain must yield the original token endpoint, not a doubled path. + if got, want := client.authTokenURL(), "https://linuxfoundation.auth0.com/oauth/token"; got != want { + t.Errorf("authTokenURL: got %q, want %q", got, want) + } +} + +func TestNewClientFromPlatformCredentials_MissingCredentialsErrors(t *testing.T) { + // base URL + audience present but no Auth0 client credentials -> NewClient validates and errors. + client, err := NewClientFromPlatformCredentials("https://sss.example", "https://sss.example/", "https://tenant.auth0.com/oauth/token", "", "") + if err == nil { + t.Fatal("expected an error when client credentials are missing") + } + if client != nil { + t.Fatal("expected nil client on error") + } +} From ae016814d879c1cfc174e6c7e59a1d7a1b443c3d Mon Sep 17 00:00:00 2001 From: Lukasz Gryglicki Date: Tue, 2 Jun 2026 10:12:04 +0200 Subject: [PATCH 2/2] Address AI feedback Signed-off-by: Lukasz Gryglicki Assisted by [OpenAI](https://platform.openai.com/) Assisted by [GitHub Copilot](https://github.com/features/copilot) Assisted by [Claude](https://claude.ai) --- cla-backend-go/config/ssm.go | 34 +++++++---- cla-backend-go/sss/from_config.go | 37 +++++++++--- cla-backend-go/sss/from_config_test.go | 79 ++++++++++++++++++++++++-- 3 files changed, 124 insertions(+), 26 deletions(-) diff --git a/cla-backend-go/config/ssm.go b/cla-backend-go/config/ssm.go index e38c1deb5..a29107fb0 100644 --- a/cla-backend-go/config/ssm.go +++ b/cla-backend-go/config/ssm.go @@ -13,6 +13,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/ssm" ) @@ -283,17 +284,28 @@ func loadSSMConfig(awsSession *session.Session, stage string) Config { //nolint // switched on. A stage that requires SSS must reject an empty configuration at // the call site rather than silently skipping the screening check. func loadOptionalSSSConfig(ssmClient *ssm.SSM, stage string, config *Config, f logrus.Fields) { - baseURLKey := fmt.Sprintf("cla-sss-base-url-%s", stage) - if value, err := getSSMString(ssmClient, baseURLKey); err == nil { - config.SSS.BaseURL = value - } else { - log.WithFields(f).Warnf("optional SSM key %s not set - sanctions screening disabled", baseURLKey) - } + 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) +} - audienceKey := fmt.Sprintf("cla-sss-auth0-audience-%s", stage) - if value, err := getSSMString(ssmClient, audienceKey); err == nil { - config.SSS.Audience = value - } else { - log.WithFields(f).Warnf("optional SSM key %s not set - sanctions screening disabled", audienceKey) +// getOptionalSSMString fetches a parameter that may legitimately be absent while +// the feature is being rolled out. It logs exactly once: a missing parameter is +// reported at debug (an expected, benign state), while any other failure - IAM, +// throttling, etc. - is reported as a warning with the underlying error so it is +// not silently misattributed to "not set". Returns "" when the value is unreadable. +func getOptionalSSMString(ssmClient *ssm.SSM, key string, f logrus.Fields) string { + 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 - sanctions screening disabled until it is set", key) + } else { + log.WithFields(f).WithError(err).Warnf("unable to read optional SSM key %s - sanctions screening disabled", key) + } + return "" } + + return strings.TrimSpace(*out.Parameter.Value) } diff --git a/cla-backend-go/sss/from_config.go b/cla-backend-go/sss/from_config.go index 4f54bac78..ec43f10ed 100644 --- a/cla-backend-go/sss/from_config.go +++ b/cla-backend-go/sss/from_config.go @@ -19,20 +19,39 @@ import ( // It returns (nil, nil) when baseURL or audience is empty so callers can treat // an unconfigured SSS as a disabled, no-op feature rather than an error. func NewClientFromPlatformCredentials(baseURL, audience, oauthTokenURL, clientID, clientSecret string) (*Client, error) { - if strings.TrimSpace(baseURL) == "" || strings.TrimSpace(audience) == "" { + baseURL = strings.TrimSpace(baseURL) + audience = strings.TrimSpace(audience) + if baseURL == "" || audience == "" { return nil, nil } - auth0Domain := strings.TrimSpace(oauthTokenURL) - if u, err := url.Parse(auth0Domain); err == nil && u.Host != "" { - auth0Domain = u.Scheme + "://" + u.Host - } - return NewClient(SSSConfig{ BaseURL: baseURL, - Auth0Domain: auth0Domain, - Auth0ClientID: clientID, - Auth0ClientSecret: clientSecret, + Auth0Domain: auth0DomainFromTokenURL(oauthTokenURL), + Auth0ClientID: strings.TrimSpace(clientID), + Auth0ClientSecret: strings.TrimSpace(clientSecret), Auth0Audience: audience, }) } + +// auth0DomainFromTokenURL reduces a full Auth0 token endpoint to its scheme+host +// (e.g. https://tenant.auth0.com), which is what the SSS client expects as its +// Auth0 domain. It tolerates a missing scheme: url.Parse on a scheme-less value +// puts the whole string in Path and leaves Host empty, so a value like +// "tenant.auth0.com/oauth/token" would otherwise be passed through verbatim and +// produce a doubled "/oauth/token" when the client builds the token URL. +func auth0DomainFromTokenURL(oauthTokenURL string) string { + oauthTokenURL = strings.TrimSpace(oauthTokenURL) + if oauthTokenURL == "" { + return "" + } + + parseTarget := oauthTokenURL + if !strings.Contains(parseTarget, "://") { + parseTarget = "https://" + parseTarget + } + if u, err := url.Parse(parseTarget); err == nil && u.Host != "" { + return u.Scheme + "://" + u.Host + } + return oauthTokenURL +} diff --git a/cla-backend-go/sss/from_config_test.go b/cla-backend-go/sss/from_config_test.go index b6c19b5eb..0b437e979 100644 --- a/cla-backend-go/sss/from_config_test.go +++ b/cla-backend-go/sss/from_config_test.go @@ -5,8 +5,13 @@ package sss import "testing" +const ( + testAuth0Domain = "https://linuxfoundation.auth0.com" + testAuth0TokenURL = testAuth0Domain + "/oauth/token" +) + func TestNewClientFromPlatformCredentials_DisabledWhenBaseURLMissing(t *testing.T) { - client, err := NewClientFromPlatformCredentials("", "https://sss.example/", "https://tenant.auth0.com/oauth/token", "id", "secret") + client, err := NewClientFromPlatformCredentials("", "https://sss.example/", testAuth0TokenURL, "id", "secret") if err != nil { t.Fatalf("expected no error when disabled, got: %v", err) } @@ -16,7 +21,7 @@ func TestNewClientFromPlatformCredentials_DisabledWhenBaseURLMissing(t *testing. } func TestNewClientFromPlatformCredentials_DisabledWhenAudienceMissing(t *testing.T) { - client, err := NewClientFromPlatformCredentials("https://sss.example", "", "https://tenant.auth0.com/oauth/token", "id", "secret") + client, err := NewClientFromPlatformCredentials("https://sss.example", "", testAuth0TokenURL, "id", "secret") if err != nil { t.Fatalf("expected no error when disabled, got: %v", err) } @@ -29,7 +34,7 @@ func TestNewClientFromPlatformCredentials_DerivesAuth0DomainFromTokenURL(t *test client, err := NewClientFromPlatformCredentials( "https://sanctions-screening.dev.v2.cluster.linuxfound.info", "https://sanctions-screening.dev.v2.cluster.linuxfound.info/", - "https://linuxfoundation.auth0.com/oauth/token", + testAuth0TokenURL, "client-id", "client-secret", ) @@ -40,18 +45,52 @@ func TestNewClientFromPlatformCredentials_DerivesAuth0DomainFromTokenURL(t *test t.Fatal("expected a configured client") } - if got, want := client.cfg.Auth0Domain, "https://linuxfoundation.auth0.com"; got != want { + if got, want := client.cfg.Auth0Domain, testAuth0Domain; got != want { t.Errorf("Auth0Domain: got %q, want %q", got, want) } // The derived domain must yield the original token endpoint, not a doubled path. - if got, want := client.authTokenURL(), "https://linuxfoundation.auth0.com/oauth/token"; got != want { + if got, want := client.authTokenURL(), testAuth0TokenURL; got != want { t.Errorf("authTokenURL: got %q, want %q", got, want) } } +func TestNewClientFromPlatformCredentials_DerivesAuth0DomainFromSchemelessTokenURL(t *testing.T) { + cases := []struct { + name string + tokenURL string + }{ + {"scheme-less with path", "linuxfoundation.auth0.com/oauth/token"}, + {"scheme-less host only", "linuxfoundation.auth0.com"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + client, err := NewClientFromPlatformCredentials( + "https://sanctions-screening.dev.v2.cluster.linuxfound.info", + "https://sanctions-screening.dev.v2.cluster.linuxfound.info/", + tc.tokenURL, + "client-id", + "client-secret", + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if client == nil { + t.Fatal("expected a configured client") + } + if got, want := client.cfg.Auth0Domain, testAuth0Domain; got != want { + t.Errorf("Auth0Domain: got %q, want %q", got, want) + } + // A scheme-less token URL must not double the "/oauth/token" path. + if got, want := client.authTokenURL(), testAuth0TokenURL; got != want { + t.Errorf("authTokenURL: got %q, want %q", got, want) + } + }) + } +} + func TestNewClientFromPlatformCredentials_MissingCredentialsErrors(t *testing.T) { // base URL + audience present but no Auth0 client credentials -> NewClient validates and errors. - client, err := NewClientFromPlatformCredentials("https://sss.example", "https://sss.example/", "https://tenant.auth0.com/oauth/token", "", "") + client, err := NewClientFromPlatformCredentials("https://sss.example", "https://sss.example/", testAuth0TokenURL, "", "") if err == nil { t.Fatal("expected an error when client credentials are missing") } @@ -59,3 +98,31 @@ func TestNewClientFromPlatformCredentials_MissingCredentialsErrors(t *testing.T) t.Fatal("expected nil client on error") } } + +func TestNewClientFromPlatformCredentials_TrimsValues(t *testing.T) { + client, err := NewClientFromPlatformCredentials( + " https://sanctions-screening.dev.v2.cluster.linuxfound.info ", + " https://sanctions-screening.dev.v2.cluster.linuxfound.info/ ", + " https://linuxfoundation.auth0.com/oauth/token ", + " client-id ", + " client-secret ", + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if client == nil { + t.Fatal("expected a configured client") + } + if got, want := client.cfg.BaseURL, "https://sanctions-screening.dev.v2.cluster.linuxfound.info"; got != want { + t.Errorf("BaseURL not trimmed: got %q, want %q", got, want) + } + if got, want := client.cfg.Auth0Audience, "https://sanctions-screening.dev.v2.cluster.linuxfound.info/"; got != want { + t.Errorf("Auth0Audience not trimmed: got %q, want %q", got, want) + } + if got, want := client.cfg.Auth0ClientID, "client-id"; got != want { + t.Errorf("Auth0ClientID not trimmed: got %q, want %q", got, want) + } + if got, want := client.cfg.Auth0ClientSecret, "client-secret"; got != want { + t.Errorf("Auth0ClientSecret not trimmed: got %q, want %q", got, want) + } +}