Skip to content

feat(sign): integrate sanctions screening service enforcement#5078

Open
psrsingh wants to merge 7 commits into
linuxfoundation:devfrom
psrsingh:feat/sss-enforcement
Open

feat(sign): integrate sanctions screening service enforcement#5078
psrsingh wants to merge 7 commits into
linuxfoundation:devfrom
psrsingh:feat/sss-enforcement

Conversation

@psrsingh
Copy link
Copy Markdown
Contributor

@psrsingh psrsingh commented May 30, 2026

Summary

Integrates Sanctions Screening Service (SSS) enforcement into EasyCLA signing flows.

Changes

  • Added SSS compliance checks for CCLA signing.
  • Added SSS compliance checks for ECLA acknowledgement.
  • Added company sanction status persistence via UpdateCompanySanctionStatus.
  • Added organization lookup and SSS request construction.
  • Uses organization domain from Organization Service.
  • Sends SFDC ID only when company_external_id starts with 001.
  • Skips screening when no organization domain is available.
  • Added nil-safety checks for organization and company lookups.

Notes

Closes #4988

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 30, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds SSS configuration and startup wiring, extends the v2 sign service to accept an *sss.Client and sssRequired flag, implements a TTL-cached checkCompanyCompliance (calls SSS, persists flagged companies), and blocks corporate/employee signature flows when a company is sanctioned. Tests and repo persistence added.

Changes

Sanctions Screening Service Integration

Layer / File(s) Summary
SSS configuration and load defaults
cla-backend-go/config/config.go, cla-backend-go/config/local.go, cla-backend-go/config/ssm.go
Adds exported SSS struct and Config.SSS field; sets SSS.Required default to true in local and SSM loaders and reads optional cla-sss-required-<stage> SSM flag.
Server startup SSS client wiring
cla-backend-go/cmd/server.go, cla-backend-go/cmd/s3_upload/main.go
Imports sss; conditionally constructs an *sss.Client at startup with configured timeout (fallback to 30s) and passes the client plus SSS.Required into sign.NewService(...); failures log and continue with nil.
Service struct and NewService signature
cla-backend-go/v2/sign/service.go
Imports sync and sss; adds sssClient *sss.Client, sssRequired bool, a TTL constant and a mutex-protected compliance cache to the service struct; updates NewService to accept and wire the SSS client and flag.
Company compliance check and signature gates
cla-backend-go/v2/sign/service.go, cla-backend-go/v2/sign/helpers.go
Adds checkCompanyCompliance (TTL cache, domain resolution, SSS GetOrganizationStatus, SSS error handling conditioned on sssRequired, persist SSS-flagged status) and replaces direct IsSanctioned checks / adds nil-company failure paths in corporate and employee signature flows to call this helper.
Persist company sanction status
cla-backend-go/company/repository.go
Adds UpdateCompanySanctionStatus to company.IRepository and implements it to fetch current company, skip update when unchanged, or update is_sanctioned and date_modified when changed.
SSS integration unit tests
cla-backend-go/v2/sign/service_sss_test.go
Adds tests for domain resolution preference, SSS error handling when sssRequired is required vs optional, and compliance-cache keying and expiry behavior.

Sequence Diagram

sequenceDiagram
  participant RequestCorporateSig as RequestCorporateSignature
  participant CheckCompliance as checkCompanyCompliance
  participant OrgService as organizationService
  participant SSS as sss.Client
  participant CompRepo as companyRepository

  RequestCorporateSig->>CheckCompliance: checkCompanyCompliance(ctx, company)
  CheckCompliance->>OrgService: Get organization (Link, Domains)
  OrgService-->>CheckCompliance: organization data
  CheckCompliance->>SSS: GetOrganizationStatus(domain, SFDCID?)
  SSS-->>CheckCompliance: OrganizationStatus (Flagged/Clean)
  alt Status == Flagged
    CheckCompliance->>CompRepo: UpdateCompanySanctionStatus(companyID, true)
    CompRepo-->>CheckCompliance: persisted
  end
  CheckCompliance-->>RequestCorporateSig: (sanctioned bool, error)
  alt sanctioned == true
    RequestCorporateSig-->>RequestCorporateSig: return "company is sanctioned" error
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive While most changes align with issue #4988 requirements, the in-memory compliance cache implementation (mentioned in reviewer discussion) is not explicitly documented in the PR description or issue acceptance criteria. Clarify whether the TTL-based in-memory compliance cache is part of the scope or a separate optimization; confirm that cache expiration and cache-key logic aligns with the intended short-TTL caching strategy.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(sign): integrate sanctions screening service enforcement' clearly and concisely summarizes the main change—SSS integration into signing flows.
Description check ✅ Passed The description is well-related to the changeset, outlining SSS compliance checks, company sanction status persistence, and organization lookups that match the code changes.
Linked Issues check ✅ Passed The PR implements all primary objectives from issue #4988: SSS checks at CCLA signing and ECLA acknowledgement gates, company sanction persistence, organization lookup with domain resolution, SFDC ID filtering, and graceful handling of missing domains.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cla-backend-go/cmd/server.go`:
- Line 451: The v2SignService is being constructed with a nil SSS client which
leaves service.sssClient unset and causes checkCompanyCompliance to
short-circuit; replace the trailing nil in the sign.NewService(...) call (where
v2SignService is created) with the real SSS client instance (e.g., sssClient or
sanctionsService) that you initialize elsewhere in the server startup, ensuring
sign.NewService receives the proper SSS client so service.sssClient is set and
checkCompanyCompliance can perform sanctions checks.

In `@cla-backend-go/v2/sign/service.go`:
- Around line 2980-2989: The current logic only strips scheme and trailing
slashes from org.Link, which can leave paths, ports, or query strings in the
domain sent to sss.OrganizationStatusRequest; update the normalization to parse
org.Link with net/url (url.Parse) and use the parsed URL's hostname
(u.Hostname()) as the Domain for the sss.OrganizationStatusRequest (falling back
to the original org.Link trimmed if parsing fails), so
OrganizationStatusRequest.Domain is a bare hostname; update the code around
domain := org.Link and where req := sss.OrganizationStatusRequest{ Domain: ... }
to use the parsed hostname.
- Around line 249-264: The code currently skips compliance checks when comp ==
nil but later dereferences comp.CompanyID and calls requestCorporateSignature
with comp, which can panic if the company lookup returned (nil, nil); update the
guard in the sign flow to detect a missing company (comp == nil) and return a
clear not-found error (include input.CompanySfid if present for context) instead
of falling through; ensure this happens before any use of comp and before
calling checkCompanyCompliance/requestCorporateSignature so functions like
checkCompanyCompliance and requestCorporateSignature never receive a nil comp.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a559fa3a-3e68-4373-a0ff-cd157c5a1d8a

📥 Commits

Reviewing files that changed from the base of the PR and between 3b58bf9 and 337cc98.

📒 Files selected for processing (4)
  • cla-backend-go/cmd/s3_upload/main.go
  • cla-backend-go/cmd/server.go
  • cla-backend-go/v2/sign/helpers.go
  • cla-backend-go/v2/sign/service.go

Comment thread cla-backend-go/cmd/server.go Outdated
Comment thread cla-backend-go/v2/sign/service.go Outdated
Comment thread cla-backend-go/v2/sign/service.go Outdated
@psrsingh psrsingh force-pushed the feat/sss-enforcement branch from 337cc98 to fcd1463 Compare May 30, 2026 09:40
@psrsingh
Copy link
Copy Markdown
Contributor Author

@lukaszgryglicki
PTAL , meanwhile i will do codeRabbit fix .

@lukaszgryglicki
Copy link
Copy Markdown
Member

There are some questions/decision points: #4985 (comment).

I'm currently on the review...

@lukaszgryglicki
Copy link
Copy Markdown
Member

Regarding code rabbit and nil SSS client, I'm adding this in: #5082.

Copy link
Copy Markdown
Member

@lukaszgryglicki lukaszgryglicki left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action items

  • [Blocking] Won't compile: checkCompanyCompliance calls s.companyRepo.UpdateCompanySanctionStatus(...), which exists on neither company.IRepository (company/repository.go:35) nor any implementation. Add it to the interface + DynamoDB repo (mirror UpdateCompanyAccessList, repository.go:1239, updating the is_sanctioned attribute).

  • [Blocking] Feature is inert: the SSS client is nil at cmd/server.go:451 and cmd/s3_upload/main.go:60, so nothing is screened in any environment. Construct it from config and pass it. Ready on branch unicron-sss-ecla-check: config keys cla-sss-base-url-<stage> + cla-sss-auth0-audience-<stage> (reusing the existing Auth0Platform M2M creds) and sss.NewClientFromPlatformCredentials(...). Wire it once this PR adds the sssClient param to sign.NewService. I'm implementing this in #5082

  • [High] Don't drop the legacy flag, and don't let SSS clear a manual block (D3): keep "block if comp.IsSanctioned OR SSS flagged"; when persisting, only ever set the flag to true from SSS — never overwrite a true with a false from a (possibly stale) SSS "clean".

  • [High] Make SSS unavailability explicit: branch on *sss.RetryableError / *sss.TimeoutError / *sss.AuthError vs a definitive flagged; implement the chosen fail-open/closed policy; and stop mapping SSS outages to HTTP 400 (handlers.go only special-cases "is sanctioned") — return a 5xx with a clear "screening temporarily unavailable" message.

  • [High] SSS required, not optional via flag: when SSS is required for the stage, a nil/misconfigured client must be an error, not a silent skip in checkCompanyCompliance. Pair with a cla-sss-required-<stage> gate so rollout can provision SSM params first.

  • [High] PR-gating hot path + negative cache: hasUserSigned runs per commit author (concurrently) and now adds an org-service GetOrganization + SSS round-trip each; on error, GetCommitAuthorSignedStatus (github/github_repository.go) marks the author unsigned and writes a negative-TTL cache entry, so a transient SSS failure pins a contributor as unsigned past recovery, and already-"signed" authors (no-TTL positive cache) are never re-screened. Prefer gating on the stored flag here and doing the live SSS call only at sign time; never negative-cache an SSS-error outcome. Eventually some caching mechanism at least in current lambda scope so no org check happens twice - check once and then use memory cache.

  • [Medium] Domain source: read the org-service Domains field first (organization-service/models/organization.go:66); only parse Link as a fallback, and when you do, prepend https:// before url.Parse so Hostname() works on scheme-less values, and strip a leading www..

  • [Medium] Persistence churn: UpdateCompanySanctionStatus is called on every check, including unchanged/clean → a DynamoDB write per signing/recheck. Write only on change.

  • **[Medium] Caching: Org-service calls are a concern, cache the resolved domain on the company record; the SSS status itself should stay authoritative via the flag (updated on writes), with staleness handled on the SSS side.

-- **[Medium]: actual ECLA check happen in cla-backend-legacy request_employee_signature / check_and_prepare_employee_signature - this is where it should block ECLA flow IMHO. This is even before we start preparing ECLA.

@psrsingh
Copy link
Copy Markdown
Contributor Author

psrsingh commented Jun 2, 2026

Action items

  • [Blocking] Won't compile: checkCompanyCompliance calls s.companyRepo.UpdateCompanySanctionStatus(...), which exists on neither company.IRepository (company/repository.go:35) nor any implementation. Add it to the interface + DynamoDB repo (mirror UpdateCompanyAccessList, repository.go:1239, updating the is_sanctioned attribute).
  • [Blocking] Feature is inert: the SSS client is nil at cmd/server.go:451 and cmd/s3_upload/main.go:60, so nothing is screened in any environment. Construct it from config and pass it. Ready on branch unicron-sss-ecla-check: config keys cla-sss-base-url-<stage> + cla-sss-auth0-audience-<stage> (reusing the existing Auth0Platform M2M creds) and sss.NewClientFromPlatformCredentials(...). Wire it once this PR adds the sssClient param to sign.NewService. I'm implementing this in Add SSS client config #5082
  • [High] Don't drop the legacy flag, and don't let SSS clear a manual block (D3): keep "block if comp.IsSanctioned OR SSS flagged"; when persisting, only ever set the flag to true from SSS — never overwrite a true with a false from a (possibly stale) SSS "clean".
  • [High] Make SSS unavailability explicit: branch on *sss.RetryableError / *sss.TimeoutError / *sss.AuthError vs a definitive flagged; implement the chosen fail-open/closed policy; and stop mapping SSS outages to HTTP 400 (handlers.go only special-cases "is sanctioned") — return a 5xx with a clear "screening temporarily unavailable" message.
  • [High] SSS required, not optional via flag: when SSS is required for the stage, a nil/misconfigured client must be an error, not a silent skip in checkCompanyCompliance. Pair with a cla-sss-required-<stage> gate so rollout can provision SSM params first.
  • [High] PR-gating hot path + negative cache: hasUserSigned runs per commit author (concurrently) and now adds an org-service GetOrganization + SSS round-trip each; on error, GetCommitAuthorSignedStatus (github/github_repository.go) marks the author unsigned and writes a negative-TTL cache entry, so a transient SSS failure pins a contributor as unsigned past recovery, and already-"signed" authors (no-TTL positive cache) are never re-screened. Prefer gating on the stored flag here and doing the live SSS call only at sign time; never negative-cache an SSS-error outcome. Eventually some caching mechanism at least in current lambda scope so no org check happens twice - check once and then use memory cache.
  • [Medium] Domain source: read the org-service Domains field first (organization-service/models/organization.go:66); only parse Link as a fallback, and when you do, prepend https:// before url.Parse so Hostname() works on scheme-less values, and strip a leading www..
  • [Medium] Persistence churn: UpdateCompanySanctionStatus is called on every check, including unchanged/clean → a DynamoDB write per signing/recheck. Write only on change.
  • **[Medium] Caching: Org-service calls are a concern, cache the resolved domain on the company record; the SSS status itself should stay authoritative via the flag (updated on writes), with staleness handled on the SSS side.

-- **[Medium]: actual ECLA check happen in cla-backend-legacy request_employee_signature / check_and_prepare_employee_signature - this is where it should block ECLA flow IMHO. This is even before we start preparing ECLA.

Thanks for the detailed review.

I agree with keeping is_sanctioned as an independent/manual block and not allowing a clean SSS result to clear it. I also agree that the ECLA enforcement point should be in request_employee_signature / check_and_prepare_employee_signature, before any ECLA preparation begins.

I'll address the missing repository method, wire up the SSS client from config, switch domain resolution to use Domains first, and avoid unnecessary writes when the sanction status hasn't changed.

For PR-gating, I agree that introducing a live SSS dependency on the hot path is risky. Using the persisted flag there and limiting live SSS checks to signing flows seems like the better approach.

@lukaszgryglicki
Copy link
Copy Markdown
Member

For PR-gating, I agree that introducing a live SSS dependency on the hot path is risky. Using the persisted flag there and limiting live SSS checks to signing flows seems like the better approach. - this still can be OK-ish but in such case just use in-memory cache so no call will happen more than once per org. This cache is per-lambda invocation or give it a short TTL like 5 mins.

@lukaszgryglicki
Copy link
Copy Markdown
Member

Also note that cla-backend-legacy has no SSS client currently, only cla-backend-go have it.

@psrsingh
Copy link
Copy Markdown
Contributor Author

psrsingh commented Jun 2, 2026

For PR-gating, I agree that introducing a live SSS dependency on the hot path is risky. Using the persisted flag there and limiting live SSS checks to signing flows seems like the better approach. - this still can be OK-ish but in such case just use in-memory cache so no call will happen more than once per org. This cache is per-lambda invocation or give it a short TTL like 5 mins.

I'll add a small in-memory cache keyed by company/org so each organization is checked at most once per execution window (or with a short TTL of around 5 minutes), which should avoid repeated org-service and SSS calls on the hot path.

@lukaszgryglicki
Copy link
Copy Markdown
Member

You don't need to care about staleness inside SSS - this will be handled as a separate feature - like we will probably have some vendor maintained queue that contains updates since last check which SSS will poll for changes periodically. This will be a new feature.
easyCLA should assume data returned from SSS is always up-to-date, but also should do >1 SSS calls for the same org in a singe PR check - that means we can do short-lived cache for this.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
cla-backend-go/v2/sign/service_sss_test.go (1)

63-72: ⚡ Quick win

Align the test name with the error type under test.

Line 63 says this covers availability errors, but Line 66 uses sss.AuthError. As written, it won't catch regressions in optional-mode retryable/timeout handling. Either rename the test to auth-specific behavior or switch the fixture to sss.RetryableError.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cla-backend-go/v2/sign/service_sss_test.go` around lines 63 - 72, The test
TestHandleSSSErrorOptionalAllowsAvailabilityErrors mismatches its name and
fixture: it uses sss.AuthError but intends to cover availability/retryable
errors. Update the test either by renaming
TestHandleSSSErrorOptionalAllowsAvailabilityErrors to reflect auth behavior
(e.g., TestHandleSSSErrorOptionalAllowsAuthErrors) or change the injected error
in the call to svc.handleSSSError to an availability-type error such as
sss.RetryableError (or sss.TimeoutError) so the test actually exercises
optional-mode retryable/timeout handling; ensure the assertion logic remains the
same.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cla-backend-go/company/repository.go`:
- Around line 1290-1332: Current code blindly flips is_sanctioned based on the
caller value, which allows a false update to clear a manual block; update the
logic so clears cannot override manual/other-source blocks: either 1) only allow
setting true (if sanctioned==false return nil early), or 2) make the flag
source-aware by adding/using a source attribute and a conditional UpdateItem
that only allows clearing when the existing source is the SSS-managed source.
Concretely, modify the path around repo.GetCompany/currentCompany.IsSanctioned
to (a) return early when sanctioned==false if you choose the "only-set-true"
approach, or (b) when supporting sources add an attribute like
"is_sanctioned_source" (or read an existing source field on currentCompany) and
change the UpdateItem to include a ConditionExpression that prevents setting ":s
= false" unless the existing source equals the SSS source; update the
UpdateExpression/ExpressionAttributeNames/Values and the call to
repo.dynamoDBClient.UpdateItem accordingly.

---

Nitpick comments:
In `@cla-backend-go/v2/sign/service_sss_test.go`:
- Around line 63-72: The test TestHandleSSSErrorOptionalAllowsAvailabilityErrors
mismatches its name and fixture: it uses sss.AuthError but intends to cover
availability/retryable errors. Update the test either by renaming
TestHandleSSSErrorOptionalAllowsAvailabilityErrors to reflect auth behavior
(e.g., TestHandleSSSErrorOptionalAllowsAuthErrors) or change the injected error
in the call to svc.handleSSSError to an availability-type error such as
sss.RetryableError (or sss.TimeoutError) so the test actually exercises
optional-mode retryable/timeout handling; ensure the assertion logic remains the
same.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 78a5478f-96f0-4fe0-a53e-ae9abfd6cc90

📥 Commits

Reviewing files that changed from the base of the PR and between bb11b46 and 0e98f7c.

📒 Files selected for processing (8)
  • cla-backend-go/cmd/s3_upload/main.go
  • cla-backend-go/cmd/server.go
  • cla-backend-go/company/repository.go
  • cla-backend-go/config/config.go
  • cla-backend-go/config/local.go
  • cla-backend-go/config/ssm.go
  • cla-backend-go/v2/sign/service.go
  • cla-backend-go/v2/sign/service_sss_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • cla-backend-go/cmd/s3_upload/main.go

Comment thread cla-backend-go/company/repository.go Outdated
@psrsingh
Copy link
Copy Markdown
Contributor Author

psrsingh commented Jun 2, 2026

@lukaszgryglicki PTAL, I've pushed the requested changes and addressed the remaining comments.

@lukaszgryglicki
Copy link
Copy Markdown
Member

Need to finish a few items, will get to this soon.

@psrsingh
Copy link
Copy Markdown
Contributor Author

psrsingh commented Jun 2, 2026

Need to finish a few items, will get to this soon.

okay

@lukaszgryglicki
Copy link
Copy Markdown
Member

I'm on it now, is this rebased with current dev ?

psrsingh added 2 commits June 2, 2026 16:17
Signed-off-by: psrsingh <psr.singh336@gmail.com>
Signed-off-by: psrsingh <psr.singh336@gmail.com>
Copy link
Copy Markdown
Member

@lukaszgryglicki lukaszgryglicki left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • [Blocking] Rebase + reconcile with the already-merged #5082. This PR is based on dev from before #5082 landed: patch 3 adds a second type SSS struct and SSS SSS field to config/config.go, rewrites loadSSMConfig, and re-wires the client in cmd/server.go — but dev already has config.SSS{BaseURL, Audience}, config/ssm.go loadOptionalSSSConfig (loads cla-sss-base-url-<stage> + cla-sss-auth0-audience-<stage>), and sss/from_config.go (NewClientFromPlatformCredentials). On rebase this is a duplicate-type/field compile error and overlapping edits. Collapse to ONE config approach (keep the merged one; see next two items).

  • [Blocking] SSS is never actually screened in production. checkCompanyCompliance passes *organization-service/models.Organization to resolveDomain, which does org.(interface{ GetDomains() []string }) and org.(interface{ GetLink() string }). That model has fields Domains string and Link string and NO GetDomains()/GetLink() methods (verified: none in v2/organization-service/models/), so both assertions fail at runtime, resolveDomain returns "", and the SSS call is skipped for every org. service_sss_test.go passes only because testOrg implements those methods. Fix: read the concrete fields — org.Domains (a string; split on ,, take first non-empty), fall back to parsing org.Link — keeping the prepend-https:// + Hostname() + strip-www. logic. Drop the interface{} indirection.

  • [Blocking] Use the shared platform M2M credentials, not new dedicated SSS creds. Per auth0-terraform the SSS audience grant is on the existing "LF CLA Lambda Service" client (grants_sanctions_screening.tf:6); there is no dedicated EasyCLA-SSS Auth0 client, and no cla-sss-auth0-client-id/secret/domain exist in SSM or lfx-secrets-management. This PR's SSS.{Auth0Domain,Auth0ClientID,Auth0ClientSecret} would therefore be empty in every deployed env and the client would never build. Reuse Auth0Platform.{ClientID,ClientSecret,URL} (exactly what the merged sss.NewClientFromPlatformCredentials already does); only BaseURL + Auth0Audience are SSS-specific and both are already loaded from SSM on dev.

  • [Blocking] This PR's loadSSMConfig loads only cla-sss-required-<stage>BaseURL/Auth0* are never read from SSM, so a deployed Lambda builds no client even before the conflict above. Resolved automatically by adopting the merged loader per items 1 and 3; otherwise add the missing SSM reads.

  • [High] A nil client ignores sssRequired. checkCompanyCompliance does if s.sssClient == nil { return false, nil }, allowing the action even when sssRequired == true — which defeats "required". When required and the client is nil it must block (error). Also cmd/server.go silently leaves sssClient = nil when SSS config is incomplete with no warning; when Required is true it should fail fast (or at least log loudly) at startup, not silently disable screening.

  • [High] The contributor ECLA path is still not screened live. The real employee-acknowledgement endpoints are in cla-backend-legacy (request_employee_signature / check_and_prepare_employee_signature), which still gate only on the stored is_sanctioned flag (handlers.go:8865). This PR's hasUserSigned change is the GitHub PR-status path in cla-backend-go, not the contributor ECLA endpoint. For full coverage, add the live SSS check in the legacy endpoints (around the existing flag check). Note cla-backend-legacy is a separate module with no SSS client today — decide: copy a minimal client / shared module / call v3 — or scope it as an explicit follow-up. Ideally both cla-backend-legacy and cla-backend-go shoudl us ethe same shared client for them both.

  • [Medium] Caching hard errors for 5 min extends outages. complianceCache stores err and serves it for complianceCacheTTL (5m), so in a required stage a transient SSS timeout keeps blocking an org for up to 5 min after SSS recovers. Cache only definitive clean/flagged results; don't cache transient/availability errors (or give errors a much shorter TTL). No negative results cache please.

  • [Medium] handleSSSError blocks on BadRequest even when not required. A 400 (our own malformed request, e.g. a bad domain) returns an error unconditionally; when sssRequired == false it should route through the same allowWhenOptional path rather than blocking the user for our bug.

  • [Low] complianceCache is unbounded — one entry per distinct org for the warm Lambda's lifetime, evicted only on access-after-expiry. Fine short-term; add a size cap or periodic sweep if org volume is high.

  • [Low] Confirm orgClient.GetOrganization(ctx, company.CompanyExternalID) is keyed correctly (Salesforce org SFID) and that CompanyExternalID is populated for companies reaching this path; it's used both as the lookup id and as SFDCID.

  • [Sync check] EasyCLA must not add its own long result cache — keep the in-process cache short and never persist a stale "clean".

psrsingh added 2 commits June 2, 2026 16:23
Signed-off-by: psrsingh <psr.singh336@gmail.com>
Signed-off-by: psrsingh <psr.singh336@gmail.com>
Signed-off-by: psrsingh <psr.singh336@gmail.com>
@psrsingh psrsingh force-pushed the feat/sss-enforcement branch from fed873d to ac79c85 Compare June 2, 2026 11:29
@psrsingh
Copy link
Copy Markdown
Contributor Author

psrsingh commented Jun 2, 2026

I'm on it now, is this rebased with current dev ?

I've gone through the feedback and updated the branch.

The changes now use the SSS implementation already merged on dev, remove the duplicate config/types, switch to the shared platform Auth0 credentials, fix domain resolution, and update the cache/error-handling logic.

One thing I wasn't completely sure about is required-mode enforcement after removing the old SSS.Required config path. Right now SSS is treated as required outside local development. If there's a preferred way to determine that, I'm happy to update it.

@lukaszgryglicki
Copy link
Copy Markdown
Member

Regarding required - we want this as ssm parameter.
If that parameter is set - then SSS check is required.
If that parameter is not set - then SSS check is optional - means failures won't block the flow, but if SSS actually works and reports that org is flagged - then we block, but if there is a failure (like SSS down or error or an org has no domain, or anything else - them we treat failure as "allow", while with the required flag - we treat ALL failures and sanctioned response as "block" and only allow passing if SSS returns "non sanctioned").

Signed-off-by: psrsingh <psr.singh336@gmail.com>
@psrsingh
Copy link
Copy Markdown
Contributor Author

psrsingh commented Jun 2, 2026

Regarding required - we want this as ssm parameter. If that parameter is set - then SSS check is required. If that parameter is not set - then SSS check is optional - means failures won't block the flow, but if SSS actually works and reports that org is flagged - then we block, but if there is a failure (like SSS down or error or an org has no domain, or anything else - them we treat failure as "allow", while with the required flag - we treat ALL failures and sanctioned response as "block" and only allow passing if SSS returns "non sanctioned").

Thanks, I updated the implementation to use the SSM-backed required flag and pushed the latest changes. Would appreciate another look when you have time.

@lukaszgryglicki
Copy link
Copy Markdown
Member

Checking.

Copy link
Copy Markdown
Member

@lukaszgryglicki lukaszgryglicki left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • [Main remaining gap] Share the SSS client across both backends and add the ECLA prepare-signature check. The contributor ECLA endpoints live in cla-backend-legacy (request_employee_signature, plus its gerrit variant / the check_and_prepare precheck) and still gate only on the stored is_sanctioned flag. To get the check at BOTH levels as intended — prepare-signature (CCLA ✓ in RequestCorporateSignature, ECLA ✗) and PR-gate (✓ in hasUserSigned):

    • (a) Extract cla-backend-go/sss into a small stdlib-only shared module (it has zero internal deps, verified) that BOTH backends import — rather than duplicating it or having legacy pull all of cla-backend-go.
    • (b) Initialize the client in cla-backend-legacy startup the same way as cla-backend-go: sss.NewClientFromPlatformCredentials(...) from the shared cla-sss-base-url-<stage> / cla-sss-auth0-audience-<stage> + reused Auth0Platform creds, with the same Required flag.
    • (c) Call it in request_employee_signature (around the existing is_sanctioned flag check, before preparing the ECLA), applying the required-flag semantics below and persisting is_sanctioned=true on a flagged result so the consoles, CCLA, and ECLA paths all stay consistent.
  • [Required-flag semantics — lock this and apply identically in both backends]

    • required=true → block UNLESS SSS runs and returns a clear "not sanctioned". I.e. block on: nil/unconfigured client, org-lookup failure, missing domain, ANY SSS error (timeout / auth / retryable / bad-request / etc.), AND any non-clean outcome; allow ONLY on an explicit SSS clean.
    • required=false → block ONLY when SSS runs and returns flagged; otherwise allow (nil client, SSS down/broken, any error, no domain → allow).
    • Current PR: required=false matches exactly. required=true matches for nil-client / org errors / missing-domain / SSS errors / flagged / clean — EXCEPT two cases that currently allow regardless of the flag and must block when required: (1) *sss.NotFoundError (404) returns false, nil unconditionally — route it through allowWhenOptional; (2) an unexpected 200 status that is neither clean nor flagged is treated as clean — in required mode treat only an explicit clean as "not sanctioned" and block everything else.
    • Implication to keep in mind (intended, not a bug): with required=true this strictness also applies to the PR-gate path (hasUserSigned), so an SSS outage will block PR re-checks (authors marked unsigned + negative-cached). That's the strict-compliance behavior, so set the flag per stage/rollout (start false, flip to true once SSS is provisioned and stable).
  • [Note — rollout] Required defaults to false when cla-sss-required-<stage> is absent, and the client only builds when cla-sss-base-url-<stage> + cla-sss-auth0-audience-<stage> are set. So screening stays effectively off (fail-open + skip) until ops set all three. Rollout order: provision base-url + audience, verify a live call, then flip cla-sss-required-<stage>=true.

This is new, but I think will be very useful - just an idea during review:

  • [Requested behavior — track origin and let SSS clear only its own blocks] EasyCLA should actively clear is_sanctioned back to false, but ONLY when the current true was set by SSS — never an admin/manual block. Code should handle sanctioning like this:

    • Add a sanction_origin attribute set to "sss" ONLY when SSS sets is_sanctioned=true (in UpdateCompanySanctionStatus, cla-backend-go). Any company with no sanction_origin (admin-set or pre-existing) is treated as manual and is never auto-cleared. (The clear's ConditionExpression: sanction_origin = "sss" is already false when the attribute is absent, so those rows are protected without an extra existence check.)
    • On a confirmed SSS clean result, clear the flag with a CONDITIONAL update — set is_sanctioned=false only when sanction_origin="sss" (DynamoDB ConditionExpression); a failed condition (manual/absent) is a no-op (swallow ConditionalCheckFailedException). On flagged, set is_sanctioned=true + sanction_origin="sss". (Extend UpdateCompanySanctionStatus to carry origin, or add a companion conditional-clear method.)
    • Make the manual-block short-circuit origin-aware. Today checkCompanyCompliance does if company.IsSanctioned { return true }, which blocks AND skips SSS entirely. Keep that short-circuit ONLY for manual/non-sss origin. For an existing sss-origin block, do NOT short-circuit — call SSS so a now-clean result can clear it; otherwise an SSS-flagged org can never recover after being de-listed. Manual/admin blocks stay sticky (SSS never clears them; admins can still clear manually anytime).
    • Clear ONLY on a definitive clean from a successful call — never on an SSS error/timeout/unavailable/missing-domain (leave the flag untouched there; the block-vs-allow decision then follows the required-flag semantics above). This guarantees SSS only ever clears a block it can positively confirm is clean.
    • This replaces the current blanket "only ever set true, never clear" guard (patch 4) and subsumes CodeRabbit's source-aware suggestion.
  • [Nit] resolveDomain(f, org interface{}) + reflect.FieldByName works but is fragile (silently returns "" if a field is renamed — no compile error). Prefer passing the concrete *organization-service/models.Organization and reading org.Domains/org.Link directly; keep the interface{} only if the test fake truly needs it (it doesn't — it can use the real type).

  • [Nit] complianceCacheEntry.err is now always nil (errors aren't cached), and getComplianceCache returns cached.err which is therefore always nil — drop the err field/param for clarity.

  • [Nit] complianceCache is unbounded (one entry per org for the warm Lambda's lifetime, evicted only on access-after-expiry); add a size cap or periodic sweep if org volume is high. Or add TTL which is quite short, like 5 mins.

  • [Low] Confirm orgClient.GetOrganization(ctx, company.CompanyExternalID) keys on the correct id (Salesforce org SFID) and that CompanyExternalID is populated for companies reaching this path; it's used both as the lookup key and as SFDCID.

@lukaszgryglicki
Copy link
Copy Markdown
Member

Sorry for multiple changes requests but we're really getting closer with this - and this PR si actually big and implement a lot of very useful features.

@psrsingh
Copy link
Copy Markdown
Contributor Author

psrsingh commented Jun 2, 2026

Sorry for multiple changes requests but we're really getting closer with this - and this PR si actually big and implement a lot of very useful features.

its fine , btw i have exam tomorrow lol !!

Comment thread cla-backend-legacy/go.mod
Signed-off-by: psrsingh <psr.singh336@gmail.com>
@psrsingh psrsingh force-pushed the feat/sss-enforcement branch from 1c3583c to 3918235 Compare June 2, 2026 15:27
@psrsingh
Copy link
Copy Markdown
Contributor Author

psrsingh commented Jun 2, 2026

@lukaszgryglicki I've pushed an updated version of the branch that includes the shared SSS module, ECLA screening integration, required-mode handling updates, and aligned configuration across both backends.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Check sanctions status via SSS during CCLA signing and ECLA acknowledgement

2 participants