Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
24 changes: 24 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,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"`
Expand Down
39 changes: 39 additions & 0 deletions cla-backend-go/config/ssm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -268,5 +269,43 @@ 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) {
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)
}

// 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)
}
Comment thread
lukaszgryglicki marked this conversation as resolved.
57 changes: 57 additions & 0 deletions cla-backend-go/sss/from_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// 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://<tenant>/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) {
baseURL = strings.TrimSpace(baseURL)
audience = strings.TrimSpace(audience)
if baseURL == "" || audience == "" {
return nil, nil
}

return NewClient(SSSConfig{
BaseURL: baseURL,
Auth0Domain: auth0DomainFromTokenURL(oauthTokenURL),
Auth0ClientID: strings.TrimSpace(clientID),
Auth0ClientSecret: strings.TrimSpace(clientSecret),
Auth0Audience: audience,
})
Comment thread
lukaszgryglicki marked this conversation as resolved.
}

// 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
}
128 changes: 128 additions & 0 deletions cla-backend-go/sss/from_config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Copyright The Linux Foundation and each contributor to CommunityBridge.
// SPDX-License-Identifier: MIT

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/", testAuth0TokenURL, "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", "", testAuth0TokenURL, "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/",
testAuth0TokenURL,
"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)
}
// The derived domain must yield the original token endpoint, not a doubled path.
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/", testAuth0TokenURL, "", "")
if err == nil {
t.Fatal("expected an error when client credentials are missing")
}
if client != nil {
t.Fatal("expected nil client on error")
}
}
Comment thread
lukaszgryglicki marked this conversation as resolved.

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