Skip to content

docs(spec): M2 Spec Kit feature slice (proactive picker → Console hand-off) - #5144

Open
mlehotskylf wants to merge 18 commits into
devfrom
docs/easycla-ss-m2-speckit
Open

docs(spec): M2 Spec Kit feature slice (proactive picker → Console hand-off)#5144
mlehotskylf wants to merge 18 commits into
devfrom
docs/easycla-ss-m2-speckit

Conversation

@mlehotskylf

@mlehotskylf mlehotskylf commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

What

Extracts Milestone 2 into its own self-contained Spec Kit feature folder — specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/ — based on dev, mirroring the existing m1-my-cla/ layout. This is the artifact Spec Kit (/speckit.plan/speckit.tasks/speckit.implement) will consume to plan and implement M2.

Intentionally separate from the program-level design PR (#5132), which keeps all six milestone briefs + overview + top-level spec.md. Splitting by artifact type (program design vs. implementable feature slice) keeps this PR small and free of cross-doc conflicts.

Scope reflected (revised through 2026-08-08, per Heather/PM; UI per mockup Final/v16)

M2 extends M1's My CLAs page (Me lens) with three additive capabilities, per the M2 mockup Final/v16 (source of truth):

  1. Sign a CLA — a "Sign CLA" modal search (project / CLA group / repo source / pasted repo link) that hands off to the existing Contributor Console decision screen for the actual signing (the ICLA/ECLA choice and the DocuSign ceremony stay in the Console). GitHub signers link their account once (picker when multiple are linked); Gerrit needs no account step (same LF SSO); GitLab is conditional on SS shipping GitLab linking.
  2. Invalidate a CLA — per-row ICLA/ECLA invalidation with typed-INVALIDATE confirmation modals, blocked server-side during Self Serve impersonation. PM-confirmed use cases: ICLA signed instead of ECLA; employer change.
  3. Status — a status column (Valid / Needs attention / Invalidated) with a "Request approval →" deep link into the Console. The evaluation already runs inside GET /v4/my-clas (M1) — M2 exposes it instead of collapsing it into a single boolean.

Guardrails: Self Serve runs no signing ceremony, makes no signing-initiation calls, and never talks to DocuSign; the Console is not cut over or retired; the PR-check remediation link is unchanged (no SSM flip).

Contents

  • spec.md — M2 feature spec: user story (P2), acceptance scenarios, FR-001…011 (incl. FR-008a notifications), success criteria, scope boundaries, a "Verified Console/backend facts" section, and the open-question ledger (1 and 3 resolved, 5 narrowed; 2 and 4 are the two remaining backend deliverables).
  • plan.md — implementation plan: summary, technical context, constitution check, project structure (extends M1's my-clas module + cla server seam), complexity tracking against the ~3-week budget.

Key verified findings baked into the spec

  • The existing invalidateICLA endpoint cannot be reused unchanged for self-service: no ownership check, sigs[0] on multiple matches (must be signature-ID-targeted), manager-framed email and project-deletion event data (must become actor-/reason-aware).
  • GET /v4/user-from-token resolves by LF username → email only; userID resolution must be platform-aware (GitHub ID first, fallback + enrichment via the existing v1 updateUser API).
  • Flagged for PM: the Final mockup shows "Download PDF" on ECLA rows, but no ECLA PDF exists — treated as a mockup oversight (ICLA-only) unless confirmed otherwise.

Deliberately not included

research.md / data-model.md / contracts/ are not hand-written — the Spec Kit planning flow generates them. The remaining genuinely open items (ECLA-invalidation endpoint execution, the no-PR ICLA request shape, the listing-endpoint choice) are marked for /speckit.clarify.

Note on links

The upward links (../spec.md, ../02-milestone-sign-icla-fable.md) point at program-level docs that live in #5132 (targeting dev) and resolve once that PR merges. The m1-my-cla/ sibling folder is already on dev.

🤖 Generated with Claude Code

Copilot AI balanced review requested due to automatic review settings August 4, 2026 13:31
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

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

The pull request adds the Milestone 2 specification and implementation plan for feature-flagged CLA selection, Contributor Console handoff, ICLA/ECLA invalidation, status display, ownership enforcement, and impersonation protection.

Changes

M2 Self Serve CLA handoff

Layer / File(s) Summary
M2 behavior contract
specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md
Defines CLA search and selection, account authorization, Contributor Console handoff, invalidation flows, status values, acceptance criteria, scope boundaries, and design references.
Verified backend constraints
specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md
Documents Console and backend behavior, server-side ownership enforcement, impersonation write protection, and open questions for account binding, ECLA invalidation, status evaluation, GitHub ICLA support, and CLA-Group listing.
M2 implementation plan
specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md
Defines the feature flag, stateless architecture, implementation scope, security constraints, testing, rollout, constitution checks, source structure, and delivery risks.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the M2 specification feature slice and its proactive picker-to-Console hand-off.
Description check ✅ Passed The description directly explains the new M2 Spec Kit folder, its scope, implementation artifacts, and key hand-off guardrails.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs/easycla-ss-m2-speckit

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Intended to add the M2 Spec Kit feature slice, but the diff also includes unrelated M1 API, SSS, email, test, and operational changes.

Changes:

  • Adds M2 signing hand-off specification and plan.
  • Adds the My CLAs API and documentation.
  • Changes SSS screening, email normalization, utilities, and tests.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

Show a summary per file
File Description
utils/search_aws_log_group.sh Improves AWS/jq error handling.
utils/my_clas.sh Adds My CLAs API utility.
utils/lookup_all_logs.sh Fails fast on lookup errors.
utils/local-fbsd-api.sh Adds local FreeBSD runner.
utils/get_user_svc.sh Adds user-service lookup utility.
utils/get_ddb_user_identities.sh Adds DynamoDB identity lookup.
utils/downcase_emails.sh Adds email normalization script.
tests/functional/cypress/e2e/v4/cla-manager.cy.ts Changes request failure handling.
specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md Defines the M2 feature.
specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md Plans M2 implementation.
specs/001-easycla-ss-integration-fable/m1-my-cla/spec.md Adds M1 specification.
specs/001-easycla-ss-integration-fable/m1-my-cla/plan.md Adds M1 plan.
specs/001-easycla-ss-integration-fable/m1-my-cla/data-model.md Documents M1 models.
specs/001-easycla-ss-integration-fable/m1-my-cla/contracts/upstream-easycla-api.md Documents upstream contracts.
docs/MY_CLAS_API.md Documents My CLAs endpoints.
CLAUDE.md Adds repository guidance.
cla-backend-legacy/internal/api/handlers.go Adds SSS switch and normalization.
cla-backend-go/v2/user-service/client.go Adds contextual user lookups.
cla-backend-go/v2/sign/service.go Integrates SSS switch and email changes.
cla-backend-go/v2/sign/service_sss_test.go Tests SSS behavior.
cla-backend-go/v2/sign/handlers.go Updates compliance errors.
cla-backend-go/v2/my_clas/service.go Implements My CLAs logic.
cla-backend-go/v2/my_clas/service_test.go Tests My CLAs service.
cla-backend-go/v2/my_clas/repository.go Adds My CLAs data access.
cla-backend-go/v2/my_clas/handlers.go Adds My CLAs handlers.
cla-backend-go/users/repository.go Normalizes stored emails.
cla-backend-go/swagger/common/my-identity-list.yaml Defines identity response.
cla-backend-go/swagger/common/my-cla.yaml Defines CLA response item.
cla-backend-go/swagger/common/my-cla-pdf.yaml Defines PDF response.
cla-backend-go/swagger/common/my-cla-list.yaml Defines CLA list response.
cla-backend-go/swagger/cla.v2.yaml Adds My CLAs API paths.
cla-backend-go/signatures/repository.go Normalizes removed emails.
cla-backend-go/emails/prefill_test.go Tests CLA Group prefill.
cla-backend-go/emails/docusign_templates.go Updates signed-document emails.
cla-backend-go/emails/docusign_templates_test.go Tests template rendering.
cla-backend-go/config/ssm.go Loads the SSS switch.
cla-backend-go/config/config.go Adds SSS enabled configuration.
cla-backend-go/cmd/server.go Wires My CLAs and SSS.
cla-backend-go/cmd/s3_upload/main.go Updates service construction.
cla-backend-go/approval_list/repository.go Normalizes requester email.
.gitignore Updates generated-file exclusions.

Copilot AI review requested due to automatic review settings August 4, 2026 13:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Suppressed comments (2)

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:63

  • This source map does not match the completed M1 Me-lens seam it says M2 will extend. In the current lfx-self-serve, M1 is implemented under app/modules/profile/clas, registered as /profile/clas in modules/profile/profile.routes.ts, and surfaced through ProfileLayoutComponent/buildProfileTabs; MainLayoutComponent and app.routes.ts do not own that tab. Following this plan would create a separate top-level surface instead of extending the existing Profile/Me hub. Update the structure to name the actual profile module, profile routes, tab builder/sidebar, and feature guard integration points.
├── app/modules/                                # NEW "Sign a CLA" Me-lens surface (picker)
│   └── sign-cla/                               # CLA-Group + org/repo + sign-type selection, hand-off
├── app/layouts/main-layout/main-layout.component.ts   # EDIT: add Me-lens entry behind flag
├── app/app.routes.ts                           # EDIT: register the picker route (lens: 'me', flag-guarded)

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md:27

  • FR-002 makes org/repo narrowing inside the Self Serve picker mandatory, but the corresponding open question on line 47 still allows deferring that selection to the Console. Those outcomes are mutually exclusive, so the acceptance criteria cannot be finalized or tested until one is selected. Either remove the defer-to-Console option or make this requirement conditional on the clarification result.
- **FR-002**: The picker MUST let the user select a CLA Group, and where a CLA Group spans multiple GitHub orgs/repos, MUST let the user narrow to a specific org/repo. *(Interaction depth is a [NEEDS CLARIFICATION] — see Open questions.)*

…d-off)

Extract Milestone 2 into its own implementable Spec Kit folder
(m2-sign-cla-handoff/), mirroring the existing m1-my-cla/ layout, based on
main so it is self-contained and independent of the program-level design PR
(#5132).

Reflects the 2026-08-04 revision (per Heather/PM): M2 is an additive,
PR-independent "Sign a CLA" picker in the Me lens that hands off to the
existing Contributor Console for signing — for both ICLA and CCLA paths. SS
runs no signing ceremony, makes no signing-initiation calls, and does not cut
over or retire the Console; the PR-check remediation link is unchanged.

Scaffolds spec.md + plan.md only. The hand-off contract, org/repo picker
scope, and CLA-Group discovery endpoint are left as explicit open questions
for the Spec Kit planning flow to resolve — research.md/data-model.md/
contracts/ are intentionally not hand-written.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Michal Lehotsky <mlehotsky@linuxfoundation.org>
@mlehotskylf
mlehotskylf force-pushed the docs/easycla-ss-m2-speckit branch from 3f3efd6 to b88e3ac Compare August 4, 2026 13:41
Copilot AI review requested due to automatic review settings August 4, 2026 13:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Suppressed comments (5)

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:56

  • The source plan omits required Contributor Console work. In the current Console, IndividualDashboardComponent.findActiveSignature() fails when no active signature exists and tells the user to restart from a pull request. Since FR-001 requires a no-PR flow and FR-005 forbids Self Serve from initiating signing, changing only lfx-self-serve cannot make the ICLA path completable; include easycla-contributor-console changes (and any supporting contract/backend work) to bootstrap this proactive flow.
Primary repo: `linuxfoundation/lfx-self-serve`

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:63

  • These frontend paths do not match the completed M1 seam this plan says it extends. M1 is implemented under app/modules/profile/clas, registered in app/modules/profile/profile.routes.ts, and exposed through the profile layout/sidebar—not through app/modules/my-clas, main-layout, or app.routes.ts. Update this tree against the current repository so generated tasks target real integration points.
├── app/modules/                                # NEW "Sign a CLA" Me-lens surface (picker)
│   └── sign-cla/                               # CLA-Group + org/repo + sign-type selection, hand-off
├── app/layouts/main-layout/main-layout.component.ts   # EDIT: add Me-lens entry behind flag
├── app/app.routes.ts                           # EDIT: register the picker route (lens: 'me', flag-guarded)

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:28

  • This constitution path is absent from the current branch, and PR #5132 also does not add the .specify tooling. As written, the plan claims to have evaluated a file that will not exist after these changes merge.
`.specify/memory/constitution.md` is the unratified template — no project-specific gates exist. Default gates applied:

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:17

  • The documented test stack is stale: the current lfx-self-serve app runs Vitest for unit tests and Playwright for E2E; it does not use Jest/Karma or Cypress. Correcting this prevents the generated tasks from targeting nonexistent harnesses.

This issue also appears in the following locations of the same file:

  • line 28
  • line 56
  • line 60
**Testing**: lfx-self-serve conventions — Jest/Karma unit tests for services/components, server route tests, Cypress/E2E per repo norms (verify exact harness during implementation).

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md:27

  • FR-002 already mandates that org/repo narrowing occurs in the Self Serve picker, but acceptance scenario 3 and the open question retain “defer to the Console” as a valid outcome; FR-008 likewise assumes org/repo is passed during hand-off. These normative requirements cannot all hold if clarification chooses deferral. Make them conditional on the design decision or remove the deferral alternative before generating tasks.
- **FR-002**: The picker MUST let the user select a CLA Group, and where a CLA Group spans multiple GitHub orgs/repos, MUST let the user narrow to a specific org/repo. *([NEEDS CLARIFICATION]: interaction depth.)*

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 16

🧹 Nitpick comments (6)
cla-backend-go/v2/my_clas/service_test.go (1)

23-70: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding error paths to fakeRepo.

Every fakeRepo method returns a nil error, so no test covers the failure branches. Two branches carry real behavior:

  • authorizeIdentity must fail closed when GetUsersByLFUsername returns an error.
  • GetMyClas must propagate a GetUserCLASignatures error instead of returning a partial list.

Add an error field per method and two short tests.

🤖 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/my_clas/service_test.go` around lines 23 - 70, Extend
fakeRepo with configurable errors for GetUsersByLFUsername and
GetUserCLASignatures, returning the corresponding error from those methods. Add
focused tests verifying authorizeIdentity fails closed when LF username lookup
errors and GetMyClas propagates signature lookup errors without returning
partial results.
cla-backend-go/v2/user-service/client.go (2)

295-296: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Escape the URL components.

lfUsername is placed in a query string and userSFID is placed in a path segment, both with plain fmt.Sprintf. Neither value is escaped. Today lfUsername comes from the authenticated principal and userSFID comes from the user-service response, so this is not exploitable from a request parameter. Escaping keeps the URLs correct if either source changes.

♻️ Proposed refactor
-	url := fmt.Sprintf("https://%s/user-service/v1/users?username=%s", usc.apiGwURL, lfUsername)
+	url := fmt.Sprintf("https://%s/user-service/v1/users?username=%s", usc.apiGwURL, neturl.QueryEscape(lfUsername))
-		url := fmt.Sprintf("https://%s/user-service/v1/users/%s/identities?pageSize=%d&offset=%d", usc.apiGwURL, userSFID, pageSize, offset)
+		url := fmt.Sprintf("https://%s/user-service/v1/users/%s/identities?pageSize=%d&offset=%d", usc.apiGwURL, neturl.PathEscape(userSFID), pageSize, offset)

Import net/url as neturl to avoid shadowing by the local url variables.

Also applies to: 362-362

🤖 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/user-service/client.go` around lines 295 - 296, Escape the
dynamic URL components in the user lookup request and the related request around
userSFID. Use net/url imported as neturl, encode lfUsername as a query parameter
and userSFID as a path segment before constructing each URL, while preserving
the existing request behavior.

306-307: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Reuse one http.Client instead of creating one per call.

Both new functions create &http.Client{Timeout: 30 * time.Second} on each invocation. Each client gets its own default transport, so connections are never reused and idle connections accumulate per call. Declare one package-level client and share it.

♻️ Proposed refactor
// package level, next to the other package vars
var userServiceHTTPClient = &http.Client{Timeout: 30 * time.Second}
-	httpClient := &http.Client{Timeout: 30 * time.Second}
-	response, err := httpClient.Do(request)
+	response, err := userServiceHTTPClient.Do(request)

Apply the same change in ListUserIdentities at line 358.

🤖 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/user-service/client.go` around lines 306 - 307, Create a
single package-level http.Client variable with the 30-second timeout at the top
of the client.go file alongside other package-level declarations. Replace the
inline httpClient instantiation at line 306-307 with a reference to this shared
package-level client. Apply the same change to the httpClient creation in the
ListUserIdentities function at line 358 to reuse the single shared client
instance across both functions.
cla-backend-go/v2/my_clas/repository.go (1)

137-188: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider bounding the users-table scan cost.

GetUsersBySecondaryEmails runs a full scan of cla-<stage>-users for every request that includes secondaryEmail. The scan reads all items and returns full items even though only a few attributes are used by toUserModels.

Two cheap improvements:

  • Add a ProjectionExpression for the attributes toUserModels reads, to cut read units and payload size.
  • Add a metric or log of the consumed capacity, so the cost of this endpoint stays visible in production.

The swagger description already warns callers, and the service only scans values the caller owns, so this is a cost concern rather than an abuse path.

🤖 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/my_clas/repository.go` around lines 137 - 188, Update
GetUsersBySecondaryEmails to request only the attributes consumed by
toUserModels via a DynamoDB ProjectionExpression, using the existing expression
attribute-name handling as needed. Enable consumed-capacity reporting on the
ScanInput and record the returned capacity through the repository’s established
metric or logging mechanism for each scan page.
cla-backend-go/v2/my_clas/handlers.go (1)

28-33: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Sensitive Data Exposure (CWE-532): Insertion of Sensitive Information into Log File

Reachability: External · Exploitability: Difficult

Reachability path
● Entry
  cla-backend-go/cmd/server.go:486
  Configure
│
▼
● Sink
  cla-backend-go/v2/my_clas/handlers.go

Remove authUserEmail from the My CLAs log fields if application logs are subject to PII controls.

The field is emitted on warning and error paths in all three handlers. The success paths do not write these fields. Apply the same change at lines 62–67 and 102–107.

🤖 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/my_clas/handlers.go` around lines 28 - 33, Remove the
authUserEmail field from the logrus.Fields definitions in all three My CLAs
handlers, including the blocks near GetMyClas and the corresponding sections
around lines 62–67 and 102–107. Keep the remaining functionName, XREQUESTID, and
authentication username fields unchanged.
cla-backend-go/v2/my_clas/service.go (1)

509-523: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Case-variant Ownership Widening (CWE-639): Authorization Bypass Through User-Controlled Key (IDOR)

Reachability: External · Exploitability: Theoretical

Reachability path
● Entry
  cla-backend-legacy/internal/api/handlers.go:54
  Handlers: Ported building blocks (incrementally used by endpoints as they are rewritten from Python).
│
▼
● Sink
  cla-backend-go/v2/my_clas/service.go

Defensive refactor: Remove the caller-supplied spelling from the approved-username list.

Append only the canonical variants proven to belong to the user; this keeps the ownership guarantee exact and avoids future case-sensitive identity collisions.

🤖 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/my_clas/service.go` around lines 509 - 523, Update
appendAllowedUsernames so approved usernames append only the canonical variants
returned by canon, removing the caller-supplied value from the destination list
while preserving skipped handling and deduplication.
🤖 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/approval_list/repository.go`:
- Line 90: Validate the normalized requester email in the request-writing flow
before constructing attributes or calling PutItem: trim whitespace and reject an
empty result with the repository’s established validation error pattern. Only
invoke addStringSliceAttribute for a non-empty value, preserving the lowercased
email behavior.

In `@cla-backend-go/config/config.go`:
- Around line 146-148: Update the comment for the Enabled field to accurately
describe behavior when the SSS kill switch is disabled: persisted SSS-origin
is_sanctioned blocks do not block compliance, while manual or admin-origin
blocks still do. Keep the documented behavior aligned with both compliance
implementations and the existing test expectations.

In `@cla-backend-go/signatures/repository.go`:
- Line 3345: Normalize every entry in params.RemoveEmailApprovalList with
TrimSpace and ToLower before passing it to buildApprovalAttributeList and
approvalList.ApprovalList, while preserving the existing normalized lookup
behavior. Update the removal-path updateApprovalTable call to pass
params.RemoveEmailApprovalList instead of params.AddEmailApprovalList.
- Line 3345: The email normalization in the loop over
params.RemoveEmailApprovalList converts whitespace-only entries to empty
strings, which causes GetProjectCompanyEmployeeSignatures to execute without an
email filter and return all company employee signatures instead of a filtered
result. This leads to unintended removal of signatures. Add a validation check
immediately after the email = strings.ToLower(strings.TrimSpace(email)) line to
skip the iteration if email is empty before the goroutine is spawned, preventing
the unfiltered query and subsequent invalidateSignatures call.

In `@cla-backend-go/users/repository.go`:
- Around line 390-393: After calling normalizeEmails() and assigning the result
to the normalized variable, check if the list is empty before assigning to
expressionAttributeValues. When normalized is empty, either add a REMOVE
operation for the `#UES` attribute to clear user_emails instead of assigning the
empty SS, or validate and reject the input before the UpdateItem call rather
than attempting to send an empty string set to DynamoDB.
- Around line 390-393: Update the Debug logging in the Save function, including
the log statement near normalizeEmails and the other sensitive-field logs
identified in the comment, so email addresses and related PII are not emitted.
Log only non-sensitive metadata such as collection counts, while preserving the
existing query-building behavior.

In `@cla-backend-go/v2/my_clas/service.go`:
- Around line 225-227: Update the sorting logic around result.Clas to parse each
SignedOn value and compare the parsed timestamps chronologically before any
FormatTimeString processing. Define and consistently apply behavior for invalid
or unsupported timestamps, then format the values afterward without using the
formatted strings for ordering.

In `@cla-backend-go/v2/user-service/client.go`:
- Around line 361-404: Bound the pagination loop around the user-identity
listing flow by tracking the number of pages and enforcing a finite maximum page
count. Also break immediately when identityList.Data is empty, while preserving
the existing short-page termination condition and response handling.

In `@docs/MY_CLAS_API.md`:
- Around line 599-608: Update the documented access policy for non-admin
GetMyClas and GetMyClaPdf to require immutable githubId/gitlabId corroboration
for historical records and signed PDFs, or explicitly record approved privacy
and security sign-off for the alias-based exception before release; do not
present the current-possession behavior as an unconditional accepted product
decision.

In
`@specs/001-easycla-ss-integration-fable/m1-my-cla/contracts/upstream-easycla-api.md`:
- Around line 3-5: Replace the superseded M1 contract in
specs/001-easycla-ss-integration-fable/m1-my-cla/contracts/upstream-easycla-api.md:3-5
with the authoritative /v4/my-clas, /v4/my-clas/{signatureID}/pdf, and
/v4/my-clas/identities definitions. Update
specs/001-easycla-ss-integration-fable/m1-my-cla/data-model.md:5-5 for current
signature fields, ECLA classification, response fields, and validity; remove
obsolete Self Serve aggregation and authorization requirements from
specs/001-easycla-ss-integration-fable/m1-my-cla/plan.md:8-10; and align
specs/001-easycla-ss-integration-fable/m1-my-cla/spec.md:21-27 with valid,
secondary email, GitLab/Gerrit support, and the current endpoint scope.

In `@tests/functional/cypress/e2e/v4/cla-manager.cy.ts`:
- Line 710: The failOnStatusCode property in the CLA Manager creation request is
unconditionally set to false, which masks unexpected HTTP errors. Make this
setting conditional based on the allowFail parameter so that failOnStatusCode is
false only when allowFail is true, allowing the test to properly fail on
unexpected status codes when allowFail is false.

In `@utils/downcase_emails.sh`:
- Around line 1-2: Add the required Linux Foundation copyright header
immediately after the shebang in utils/downcase_emails.sh lines 1-2, followed by
the MIT SPDX identifier. Add the Linux Foundation copyright header after the
shebang in utils/lookup_all_logs.sh lines 1-2, preserving its existing
applicable SPDX header.
- Around line 10-26: Update the update-item operation in the normalization loop
to condition the write on user_emails still matching the scanned item’s original
user_emails.SS value, while preserving the normalized :e update. Capture
conditional-check failures separately from successful updates and report the
affected uid for manual retry instead of treating them as successful writes.
- Line 10: Update the script around the DynamoDB scan to set a restrictive
umask, create the JSON output with mktemp, and store its path for reuse instead
of writing directly to the stage-named file. Register an EXIT trap to remove the
temporary file on termination, and ensure the scan command writes to that
temporary path while preserving its existing query options.

In `@utils/get_user_svc.sh`:
- Around line 51-59: Update the profile lookup curl invocation in
get_user_svc.sh to include the -f/--fail flag alongside the existing silent and
show-error options. Ensure HTTP 4xx/5xx responses cause curl to fail so the
surrounding profile and pagination flows do not treat error responses as empty
results.

In `@utils/local-fbsd-api.sh`:
- Around line 5-10: Update the setup and build flow in the local fBSD launch
script to enable strict shell failure handling and run the `cd cla-backend-go`
plus `go build` commands in a subshell. Ensure any failed directory change or
build terminates the script before sourcing environment files or executing the
binary.

---

Nitpick comments:
In `@cla-backend-go/v2/my_clas/handlers.go`:
- Around line 28-33: Remove the authUserEmail field from the logrus.Fields
definitions in all three My CLAs handlers, including the blocks near GetMyClas
and the corresponding sections around lines 62–67 and 102–107. Keep the
remaining functionName, XREQUESTID, and authentication username fields
unchanged.

In `@cla-backend-go/v2/my_clas/repository.go`:
- Around line 137-188: Update GetUsersBySecondaryEmails to request only the
attributes consumed by toUserModels via a DynamoDB ProjectionExpression, using
the existing expression attribute-name handling as needed. Enable
consumed-capacity reporting on the ScanInput and record the returned capacity
through the repository’s established metric or logging mechanism for each scan
page.

In `@cla-backend-go/v2/my_clas/service_test.go`:
- Around line 23-70: Extend fakeRepo with configurable errors for
GetUsersByLFUsername and GetUserCLASignatures, returning the corresponding error
from those methods. Add focused tests verifying authorizeIdentity fails closed
when LF username lookup errors and GetMyClas propagates signature lookup errors
without returning partial results.

In `@cla-backend-go/v2/my_clas/service.go`:
- Around line 509-523: Update appendAllowedUsernames so approved usernames
append only the canonical variants returned by canon, removing the
caller-supplied value from the destination list while preserving skipped
handling and deduplication.

In `@cla-backend-go/v2/user-service/client.go`:
- Around line 295-296: Escape the dynamic URL components in the user lookup
request and the related request around userSFID. Use net/url imported as neturl,
encode lfUsername as a query parameter and userSFID as a path segment before
constructing each URL, while preserving the existing request behavior.
- Around line 306-307: Create a single package-level http.Client variable with
the 30-second timeout at the top of the client.go file alongside other
package-level declarations. Replace the inline httpClient instantiation at line
306-307 with a reference to this shared package-level client. Apply the same
change to the httpClient creation in the ListUserIdentities function at line 358
to reuse the single shared client instance across both functions.
🪄 Autofix

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: 87870c4c-e5ee-4c24-ba79-049c40eeac83

📥 Commits

Reviewing files that changed from the base of the PR and between 7c70261 and 4990e8e.

📒 Files selected for processing (41)
  • .gitignore
  • CLAUDE.md
  • cla-backend-go/approval_list/repository.go
  • cla-backend-go/cmd/s3_upload/main.go
  • cla-backend-go/cmd/server.go
  • cla-backend-go/config/config.go
  • cla-backend-go/config/ssm.go
  • cla-backend-go/emails/docusign_templates.go
  • cla-backend-go/emails/docusign_templates_test.go
  • cla-backend-go/emails/prefill_test.go
  • cla-backend-go/signatures/repository.go
  • cla-backend-go/swagger/cla.v2.yaml
  • cla-backend-go/swagger/common/my-cla-list.yaml
  • cla-backend-go/swagger/common/my-cla-pdf.yaml
  • cla-backend-go/swagger/common/my-cla.yaml
  • cla-backend-go/swagger/common/my-identity-list.yaml
  • cla-backend-go/users/repository.go
  • cla-backend-go/v2/my_clas/handlers.go
  • cla-backend-go/v2/my_clas/repository.go
  • cla-backend-go/v2/my_clas/service.go
  • cla-backend-go/v2/my_clas/service_test.go
  • cla-backend-go/v2/sign/handlers.go
  • cla-backend-go/v2/sign/service.go
  • cla-backend-go/v2/sign/service_sss_test.go
  • cla-backend-go/v2/user-service/client.go
  • cla-backend-legacy/internal/api/handlers.go
  • docs/MY_CLAS_API.md
  • specs/001-easycla-ss-integration-fable/m1-my-cla/contracts/upstream-easycla-api.md
  • specs/001-easycla-ss-integration-fable/m1-my-cla/data-model.md
  • specs/001-easycla-ss-integration-fable/m1-my-cla/plan.md
  • specs/001-easycla-ss-integration-fable/m1-my-cla/spec.md
  • specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md
  • specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md
  • tests/functional/cypress/e2e/v4/cla-manager.cy.ts
  • utils/downcase_emails.sh
  • utils/get_ddb_user_identities.sh
  • utils/get_user_svc.sh
  • utils/local-fbsd-api.sh
  • utils/lookup_all_logs.sh
  • utils/my_clas.sh
  • utils/search_aws_log_group.sh

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

🛑 Comments failed to post (14)
cla-backend-go/approval_list/repository.go (1)

90-90: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- repository.go ---'
sed -n '1,180p' cla-backend-go/approval_list/repository.go
printf '%s\n' '--- addStringSliceAttribute definitions and uses ---'
rg -n -C 4 'addStringSliceAttribute|user_emails|requesterEmail' cla-backend-go

Repository: linuxfoundation/easycla

Length of output: 31945


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- helper ---'
cat -n cla-backend-go/approval_list/helpers.go
printf '%s\n' '--- AddCclaApprovalRequest callers ---'
rg -n -C 8 'AddCclaApprovalRequest|requester_email|requesterEmail' cla-backend-go/approval_list cla-backend-go | head -n 260
printf '%s\n' '--- module and local AWS SDK availability ---'
rg -n 'aws-sdk-go|go [0-9]+\.[0-9]+' cla-backend-go/go.mod go.mod 2>/dev/null || true
find . -path '*aws-sdk-go*attributevalue*' -o -path '*aws-sdk-go*dynamodbattribute*' | head -n 40

Repository: linuxfoundation/easycla

Length of output: 26332


🌐 Web query:

AWS DynamoDB String Set empty string member ValidationException

💡 Result:

In Amazon DynamoDB, you cannot store an empty Set (including String Sets, Number Sets, and Binary Sets). Attempting to perform a PutItem or UpdateItem operation with an empty set will result in a ValidationException [1][2][3]. It is important to distinguish between two related concepts: 1. Empty Sets are Not Allowed: DynamoDB explicitly prohibits sets that contain no elements [2][3]. If you attempt to persist a set with no members, the service will reject the request with a ValidationException, stating that a string set (or number/binary set) may not be empty [4][5]. 2. Empty Strings Within a Set: While the set itself cannot be empty, DynamoDB does permit the inclusion of empty string values as members within a non-empty String Set [4][6][3]. To resolve a ValidationException caused by an empty set, you should either: - Ensure the set contains at least one valid element before sending the request [7]. - If the attribute is optional and the set is empty, remove the attribute from the item entirely before sending the request to DynamoDB [7]. Note that while Lists and Maps allow empty structures, Sets strictly require at least one member to be considered valid by the DynamoDB API [8][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- AWS SDK module cache ---'
find "${GOMODCACHE:-/root/go/pkg/mod}" -path '*aws-sdk-go*' -type f 2>/dev/null | head -n 20 || true
printf '%s\n' '--- SDK v1.36.27 AttributeValue validation ---'
curl -fsSL https://raw.githubusercontent.com/aws/aws-sdk-go/v1.36.27/service/dynamodb/api.go |
  rg -n -C 12 'func \(s \*AttributeValue\) Validate|SS.*Validate|StringSet|empty'
printf '%s\n' '--- approval-list request argument definitions and validation ---'
sed -n '1,190p' cla-backend-go/approval_list/service.go
rg -n -C 8 'ContributorEmail|ContributorName|Request.*Access|approval.*request|allowlist|approval_list' cla-backend-go/approval_list cla-backend-go/swagger | head -n 320

Repository: linuxfoundation/easycla

Length of output: 50379


🌐 Web query:

site:docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html empty string values set

💡 Result:

In most programming contexts and data structures, an empty string is treated as a valid, distinct string value, and thus it can be contained within a set [1]. Because sets enforce the uniqueness of their elements, an empty string is considered a unique element that is distinct from other strings or null values [1]. For example, in systems like Amazon DynamoDB, empty string values are explicitly permitted within string sets [1]. While DynamoDB does not support empty sets themselves, it allows a string set to include an empty string as one of its members [1]. In common programming languages such as Python, Java, or JavaScript, the standard Set data structure similarly treats an empty string as a valid element [1]. You can include an empty string in a set without it being interpreted as an indicator of an empty set or a null value. Example of adding an empty string to a set: # Python my_set = {"apple", ""} print("" in my_set) # Returns True // JavaScript let mySet = new Set(["apple", ""]); console.log(mySet.has("")); // Returns true // Java Set mySet = new HashSet<>; mySet.add(""); System.out.println(mySet.contains("")); // Returns true

Citations:


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- approval-list service request path ---'
sed -n '1,175p' cla-backend-go/approval_list/service.go
printf '%s\n' '--- ContributorEmail declarations and validation ---'
rg -n -C 10 'ContributorEmail|contributor_email|contributorEmail' cla-backend-go/approval_list cla-backend-go/gen cla-backend-go/swagger | head -n 280
printf '%s\n' '--- request schema required fields ---'
rg -n -C 8 'ContributorEmail|contributor_email|contributor-email' cla-backend-go/swagger cla-backend-go/gen | head -n 220

Repository: linuxfoundation/easycla

Length of output: 24118


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
inputs = ["", "   ", "\t\n", " User@Example.COM "]
for raw in inputs:
    normalized = raw.strip().lower()
    attribute = {"SS": [normalized]}
    print({"input": repr(raw), "normalized": repr(normalized), "attribute": attribute,
           "set_member_count": len(attribute["SS"])})
PY

Repository: linuxfoundation/easycla

Length of output: 564


Reject an empty normalized requester email before writing the request.

strings.TrimSpace converts empty input to "", and the helper creates SS: [""]. DynamoDB accepts this non-empty string set, so PutItem can persist an invalid user_emails value. Return a validation error before PutItem.

🤖 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/approval_list/repository.go` at line 90, Validate the
normalized requester email in the request-writing flow before constructing
attributes or calling PutItem: trim whitespace and reject an empty result with
the repository’s established validation error pattern. Only invoke
addStringSliceAttribute for a non-empty value, preserving the lowercased email
behavior.
cla-backend-go/config/config.go (1)

146-148: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document the persisted SSS-origin behavior correctly.

The comment states that persisted is_sanctioned values still block. Both compliance implementations return false for an SSS-origin persisted block when sssEnabled is false. The new test also requires that behavior. Limit this statement to manual or admin blocks, or change the runtime behavior.

🤖 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/config/config.go` around lines 146 - 148, Update the comment
for the Enabled field to accurately describe behavior when the SSS kill switch
is disabled: persisted SSS-origin is_sanctioned blocks do not block compliance,
while manual or admin-origin blocks still do. Keep the documented behavior
aligned with both compliance implementations and the existing test expectations.
cla-backend-go/signatures/repository.go (1)

3345-3345: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 8 'buildApprovalAttributeList|RemoveEmailApprovalList|ApprovalList =|invalidateSignatures|updateApprovalTable' cla-backend-go

Repository: linuxfoundation/easycla

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- repository update path ---'
sed -n '3298,3365p' cla-backend-go/signatures/repository.go

printf '%s\n' '--- list removal helper ---'
rg -n -C 8 'func RemoveItemsFromList|RemoveItemsFromList' cla-backend-go

printf '%s\n' '--- approval-table implementation ---'
rg -n -C 12 'func .*updateApprovalTable|updateApprovalTable' cla-backend-go/signatures

printf '%s\n' '--- user search implementation ---'
rg -n -C 10 'func .*SearchUsers|SearchUsers\(' cla-backend-go | head -200

Repository: linuxfoundation/easycla

Length of output: 47562


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- exact removal semantics ---'
sed -n '110,155p' cla-backend-go/utils/utils.go

printf '%s\n' '--- approval-table removal path ---'
sed -n '3860,3925p' cla-backend-go/signatures/repository.go

printf '%s\n' '--- user search query construction ---'
sed -n '1255,1345p' cla-backend-go/users/repository.go

printf '%s\n' '--- converter behavior probe ---'
python3 - <<'PY'
existing = ["alice@example.com"]
remove = [" Alice@Example.com "]
updated = [value.strip() for value in existing]
result = [value for value in updated if value not in remove]
print({"existing": existing, "remove": remove, "updated": updated, "result": result})
PY

Repository: linuxfoundation/easycla

Length of output: 6907


Normalize and pass the removal email list to every consumer.

  • utils.RemoveItemsFromList uses exact equality. " Alice@Example.com " does not remove stored alice@example.com; line 3345 only normalizes the goroutine-local lookup. Normalize the removal slice before buildApprovalAttributeList and approvalList.ApprovalList.
  • The removal call to updateApprovalTable passes params.AddEmailApprovalList. Pass params.RemoveEmailApprovalList so removal-only requests deactivate the correct records.
🤖 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/signatures/repository.go` at line 3345, Normalize every entry
in params.RemoveEmailApprovalList with TrimSpace and ToLower before passing it
to buildApprovalAttributeList and approvalList.ApprovalList, while preserving
the existing normalized lookup behavior. Update the removal-path
updateApprovalTable call to pass params.RemoveEmailApprovalList instead of
params.AddEmailApprovalList.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 8 'SearchUsers|criteria\.UserEmail|GetProjectCompanyEmployeeSignatures|invalidateSignatures' cla-backend-go

Repository: linuxfoundation/easycla

Length of output: 50380


🏁 Script executed:

# Get context around line 3345 in signatures/repository.go
sed -n '3330,3360p' cla-backend-go/signatures/repository.go

# Get the GetProjectCompanyEmployeeSignatures function
sed -n '2200,2250p' cla-backend-go/signatures/repository.go

# Get the invalidateSignatures function signature and implementation
rg -n 'func.*invalidateSignatures' cla-backend-go/signatures/repository.go -A 25

Repository: linuxfoundation/easycla

Length of output: 5556


🏁 Script executed:

# Verify SearchUsers returns non-nil for empty input
sed -n '1255,1275p' cla-backend-go/users/repository.go

# Verify the condition check in the goroutine
sed -n '3344,3365p' cla-backend-go/signatures/repository.go

# Check buildApprovalAttributeList to see if it normalizes
rg -n 'buildApprovalAttributeList' cla-backend-go/signatures/repository.go -A 10

Repository: linuxfoundation/easycla

Length of output: 6297


Reject blank removal entries before starting the removal goroutines.

Line 3345 normalizes whitespace-only input to an empty string. When email becomes "", SearchUsers("user_emails", "", false) returns a non-nil empty result (not an error or nil), so the condition on line 3354 does not trigger early return. The code then calls GetProjectCompanyEmployeeSignatures with criteria.UserEmail = "". Since the filter is only applied when criteria.UserEmail != "" (line 2220), the query executes without the email filter and may return all employee signatures for the company and project. These unfiltered signatures are then passed to invalidateSignatures, causing unintended removals.

Add a check to reject empty normalized emails before the goroutine starts:

for _, email := range params.RemoveEmailApprovalList {
    email = strings.ToLower(strings.TrimSpace(email))
    if email == "" {
        log.WithFields(f).Debugf("skipping empty removal entry")
        continue
    }
    go func(email string) {
🤖 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/signatures/repository.go` at line 3345, The email
normalization in the loop over params.RemoveEmailApprovalList converts
whitespace-only entries to empty strings, which causes
GetProjectCompanyEmployeeSignatures to execute without an email filter and
return all company employee signatures instead of a filtered result. This leads
to unintended removal of signatures. Add a validation check immediately after
the email = strings.ToLower(strings.TrimSpace(email)) line to skip the iteration
if email is empty before the goroutine is spawned, preventing the unfiltered
query and subsequent invalidateSignatures call.
cla-backend-go/users/repository.go (1)

390-393: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 6 'normalizeEmails|AttributeValue.*SS|UpdateItem|user_emails' cla-backend-go/users/repository.go

Repository: linuxfoundation/easycla

Length of output: 6571


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf '%s\n' '--- Save implementation ---'
sed -n '300,465p' cla-backend-go/users/repository.go
printf '%s\n' '--- normalizeEmails implementation ---'
sed -n '888,925p' cla-backend-go/users/repository.go
printf '%s\n' '--- user email model and Save callers ---'
rg -n -C 4 'type User|Emails \[\]string|\.Save\(|Save\(.*User|user\.Emails' cla-backend-go/users cla-backend-go -g '*.go' | head -n 260
printf '%s\n' '--- tests mentioning email updates ---'
rg -n -C 5 'normalizeEmails|user_emails|Emails' cla-backend-go -g '*_test.go' | head -n 260

Repository: linuxfoundation/easycla

Length of output: 42023


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf '%s\n' '--- UserUpdate definition and API schema references ---'
rg -n -C 8 'type UserUpdate|UserUpdate struct|emails.*json|Emails.*json|emails:' cla-backend-go -g '*.go' -g '*.yaml' -g '*.yml' -g '*.json' | head -n 320
printf '%s\n' '--- update-user handler validation and generated parameters ---'
sed -n '1,165p' cla-backend-go/users/handlers.go
printf '%s\n' '--- module SDK version ---'
rg -n 'aws-sdk-go|go [0-9]' cla-backend-go/go.mod go.mod 2>/dev/null || true
printf '%s\n' '--- focused structural behavior probe ---'
python3 - <<'PY'
def normalize(emails):
    if emails is None:
        return None
    seen = set()
    out = []
    for email in emails:
        email = email.strip().lower()
        if not email or email in seen:
            continue
        seen.add(email)
        out.append(email)
    return out

for value in (None, [], [""], [" ", "\t"], ["A@example.org", " a@example.org "]):
    normalized = normalize(value)
    print(repr(value), "=>", repr(normalized),
          "non_nil_branch=", value is not None,
          "empty_SS=", value is not None and len(normalized) == 0)
PY

Repository: linuxfoundation/easycla

Length of output: 23625


🌐 Web query:

AWS DynamoDB UpdateItem AttributeValue SS empty set validation error

💡 Result:

In Amazon DynamoDB, set type attributes (String Set [SS], Number Set [NS], and Binary Set [BS]) cannot be empty [1][2][3]. If you attempt to update an item with an empty set, DynamoDB will reject the request with a ValidationException [1][2][4]. This restriction is a fundamental design constraint of the DynamoDB service [5][4]. The service requires that set attributes contain at least one element [1][2]. To resolve or avoid this validation error, consider the following approaches: 1. Remove the attribute: If the set is empty, it is often appropriate to remove the attribute from the item entirely rather than setting it to an empty collection [6][4]. 2. Validate before sending: Implement logic in your application or client-side code to check if the set is empty before performing the UpdateItem operation [6]. If the set is empty, skip the update or remove the attribute using a REMOVE expression [6]. 3. Ensure at least one element: If the business logic requires the attribute to exist, ensure that the set contains at least one valid element before sending the request [6]. Note that this behavior is specific to set types. While String and Binary scalar attributes also have length requirements (must be greater than zero), set types are uniquely restricted from being empty [1][2][5]. Attempting to send an AttributeValue structure that is empty or does not contain a supported, non-empty data type will trigger a ValidationException [4].

Citations:


Handle empty normalized email lists before assigning AttributeValue.SS.

When user.Emails is non-nil but normalizeEmails returns no values, Save sends an empty SS to DynamoDB. DynamoDB rejects empty string sets, so the update fails and cannot clear user_emails. Use REMOVE #UES`` when an empty list means clear; otherwise reject the input before UpdateItem.

🤖 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/users/repository.go` around lines 390 - 393, After calling
normalizeEmails() and assigning the result to the normalized variable, check if
the list is empty before assigning to expressionAttributeValues. When normalized
is empty, either add a REMOVE operation for the `#UES` attribute to clear
user_emails instead of assigning the empty SS, or validate and reject the input
before the UpdateItem call rather than attempting to send an empty string set to
DynamoDB.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# First, read the exact code at the specified lines
cat -n cla-backend-go/users/repository.go | sed -n '380,400p'

Repository: linuxfoundation/easycla

Length of output: 1405


🏁 Script executed:

# Check the context of this function and normalizeEmails implementation
ast-grep outline cla-backend-go/users/repository.go

Repository: linuxfoundation/easycla

Length of output: 2707


🏁 Script executed:

# Search for log level configuration in the codebase
rg -i "log.*level|debug" cla-backend-go --type go -B 2 -A 2 | head -100

Repository: linuxfoundation/easycla

Length of output: 7545


🏁 Script executed:

# Look for server/initialization configuration
fd config -e yaml -e yml -e go | head -20

Repository: linuxfoundation/easycla

Length of output: 288


🏁 Script executed:

# Check if there are any log redaction or sanitization mechanisms
rg -i "redact|sanitiz|mask" cla-backend-go --type go

Repository: linuxfoundation/easycla

Length of output: 2230


🏁 Script executed:

# Examine the config.go file for log level settings
cat -n cla-backend-go/config/config.go | head -100

Repository: linuxfoundation/easycla

Length of output: 4094


🏁 Script executed:

# Search for logrus or log level initialization
rg -i "logrus|setlevel|debug" cla-backend-go --type go | grep -i "level\|init\|new" | head -30

Repository: linuxfoundation/easycla

Length of output: 3346


🏁 Script executed:

# Check server.go for log initialization and configuration
cat -n cla-backend-go/cmd/server.go | sed -n '1,100p'

Repository: linuxfoundation/easycla

Length of output: 5137


🏁 Script executed:

# Look for environment variables controlling log levels
rg -i "log_level|loglevel" cla-backend-go --type go -B 2 -A 2

Repository: linuxfoundation/easycla

Length of output: 526


🏁 Script executed:

# Check if Debug logs are sent to DataDog or other persistent services
rg -i "datadog|cloudwatch|stderr|stdout" cla-backend-go --type go | head -40

Repository: linuxfoundation/easycla

Length of output: 3882


🏁 Script executed:

# Check the complete logging/logger.go to understand log level defaults and output
cat -n cla-backend-go/logging/logger.go

Repository: linuxfoundation/easycla

Length of output: 6979


🏁 Script executed:

# Look for log output configuration (file, stdout, etc.)
rg "AddHook|SetOutput" cla-backend-go --type go -B 2 -A 2

Repository: linuxfoundation/easycla

Length of output: 161


🏁 Script executed:

# Check if logs are sent to Datadog or other centralized services
rg "Hook|Formatter" cla-backend-go/logging --type go -B 2 -A 2

Repository: linuxfoundation/easycla

Length of output: 1681


🏁 Script executed:

# Check for deployment/infrastructure configuration that sets LOG_LEVEL
find . -type f \( -name "*.yml" -o -name "*.yaml" -o -name "*.tf" -o -name "*.json" -o -name "*.env*" \) | xargs grep -l "LOG_LEVEL" 2>/dev/null | head -20

Repository: linuxfoundation/easycla

Length of output: 161


🏁 Script executed:

# Check for any log redaction or sanitization at the application level
rg "email|Email" cla-backend-go/logging --type go -B 2 -A 2

Repository: linuxfoundation/easycla

Length of output: 161


🏁 Script executed:

# Verify if there's any configuration in the actual Save function that calls this code
cat -n cla-backend-go/users/repository.go | sed -n '331,470p'

Repository: linuxfoundation/easycla

Length of output: 7207


Sensitive Data Exposure (CWE-532): Insertion of Sensitive Information into Log File

Reachability: External

Redact the email list before logging, or log only the count.

Line 391 logs the full normalized email addresses at Debug level using %v. The default log level in the codebase is Debug (logging/logger.go:77), and no deployment-level LOG_LEVEL override is configured. In production Lambda or container environments, Debug logs are typically captured and retained in CloudWatch Logs or similar log aggregation services, creating a persistent PII copy. Replace the log statement with a redacted version—for example, log only the count of emails (len(normalized)) or redact the addresses to [email address] format. This pattern applies to other Debug logs in the Save function that also log sensitive fields (lines 369, 376, 383, 398, 405, 412, 419, 426).

🤖 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/users/repository.go` around lines 390 - 393, Update the Debug
logging in the Save function, including the log statement near normalizeEmails
and the other sensitive-field logs identified in the comment, so email addresses
and related PII are not emitted. Log only non-sensitive metadata such as
collection counts, while preserving the existing query-building behavior.
cla-backend-go/v2/my_clas/service.go (1)

225-227: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect FormatTimeString to confirm the output format is lexicographically sortable.
set -euo pipefail

rg -n --type=go -C15 'func FormatTimeString\s*\(' cla-backend-go/utils

# Show the layout constants it uses.
rg -n --type=go 'time\.RFC3339|2006-01-02|Jan  2|Layout' cla-backend-go/utils | head -40

Repository: linuxfoundation/easycla

Length of output: 3377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- utils parsing ---'
cat -n cla-backend-go/utils/utils.go | sed -n '40,115p'

printf '%s\n' '--- service context ---'
cat -n cla-backend-go/v2/my_clas/service.go | sed -n '180,245p'

printf '%s\n' '--- SignedOn declarations and assignments ---'
rg -n --type=go -C4 'SignedOn|signedOn|FormatTimeString|my-cla-list|sorted by signedOn' cla-backend-go/v2 cla-backend-go | head -240

printf '%s\n' '--- contract references ---'
rg -n -i --glob '*.{yaml,yml,go,md}' 'cla-list|signedOn|signed.on|chronolog|sort' . | head -240

Repository: linuxfoundation/easycla

Length of output: 48068


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- timestamp writers ---'
rg -n --type=go -C10 'func .*AddSignedOn|AddSignedOn\(|SignatureCreated|DateCreated:|date_created' cla-backend-go/v2/signatures cla-backend-go/signatures cla-backend-go/v2/sign cla-backend-go/v2/my_clas | head -280

printf '%s\n' '--- my-cla sorting tests and fixtures ---'
cat -n cla-backend-go/v2/my_clas/service_test.go | sed -n '1,220p'
rg -n --type=go -C8 'GetMyCla|ResultCount|SignedOn|signedOn' cla-backend-go/v2/my_clas/service_test.go

printf '%s\n' '--- API contract and response model ---'
cat -n cla-backend-go/swagger/common/my-cla-list.yaml | sed -n '1,65p'
cat -n docs/MY_CLAS_API.md | sed -n '235,255p;305,380p'

Repository: linuxfoundation/easycla

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- AddSignedOn implementation ---'
rg -n --type=go -C18 'func \(.*\) AddSignedOn' cla-backend-go/signatures

printf '%s\n' '--- all direct signed_on assignments near write paths ---'
rg -n --type=go -C3 'SignedOn:\s|signed_on.*currentTime|AddSignedOn' cla-backend-go/signatures cla-backend-go/v2/sign cla-backend-go/v2/dynamo_events | head -220

printf '%s\n' '--- contract wording ---'
cat -n cla-backend-go/swagger/common/my-cla-list.yaml | sed -n '25,38p'
cat -n docs/MY_CLAS_API.md | sed -n '238,248p'

Repository: linuxfoundation/easycla

Length of output: 19120


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from datetime import datetime, timezone

# Model the relevant Go behavior:
# ParseDateTime accepts RFC3339-like values, then FormatTimeString emits
# time.RFC3339, which has second precision and UTC normalization.
values = [
    "2025-01-01T00:00:00.900000Z",
    "2025-01-01T00:00:00.100000Z",
]
formatted = [
    datetime.fromisoformat(v.replace("Z", "+00:00"))
    .astimezone(timezone.utc)
    .strftime("%Y-%m-%dT%H:%M:%SZ")
    for v in values
]

print("raw chronological order:", values[0] > values[1])
print("formatted values:", formatted)
print("formatted comparator distinguishes them:", formatted[0] != formatted[1])
assert values[0] != values[1]
assert formatted[0] == formatted[1]
PY

Repository: linuxfoundation/easycla

Length of output: 305


Sort by parsed time before formatting.

FormatTimeString removes subsecond precision and returns the original string for unsupported values. String sorting can therefore violate chronological order. Sort by parsed timestamps and define the invalid-value behavior before formatting.

🤖 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/my_clas/service.go` around lines 225 - 227, Update the
sorting logic around result.Clas to parse each SignedOn value and compare the
parsed timestamps chronologically before any FormatTimeString processing. Define
and consistently apply behavior for invalid or unsupported timestamps, then
format the values afterward without using the formatted strings for ordering.
cla-backend-go/v2/user-service/client.go (1)

361-404: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Bound the pagination loop.

The loop only stops when a page returns fewer than pageSize records. If the upstream ignores offset and keeps returning exactly 100 records, the loop repeats until the request context is cancelled. The 30-second timeout applies per request, not to the loop.

Add a maximum page count, and stop when a page returns no records.

🛡️ Proposed guard
 	const pageSize = int64(100)
+	const maxPages = 100
 	var identities []*models.UserIdentity
-	for offset := int64(0); ; offset += pageSize {
+	for page := 0; page < maxPages; page++ {
+		offset := int64(page) * pageSize
 		url := fmt.Sprintf("https://%s/user-service/v1/users/%s/identities?pageSize=%d&offset=%d", usc.apiGwURL, userSFID, pageSize, offset)
@@
 		identities = append(identities, identityList.Data...)
-		if int64(len(identityList.Data)) < pageSize {
+		if int64(len(identityList.Data)) < pageSize {
 			break
 		}
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

	const pageSize = int64(100)
	const maxPages = 100
	var identities []*models.UserIdentity
	for page := 0; page < maxPages; page++ {
		offset := int64(page) * pageSize
		url := fmt.Sprintf("https://%s/user-service/v1/users/%s/identities?pageSize=%d&offset=%d", usc.apiGwURL, userSFID, pageSize, offset)
		request, reqErr := http.NewRequestWithContext(ctx, "GET", url, nil)
		if reqErr != nil {
			log.WithFields(f).WithError(reqErr).Warn("problem building new request")
			return nil, reqErr
		}

		request.Header.Set("X-API-KEY", usc.apiKey)
		request.Header.Set("Authorization", "Bearer "+tok)
		request.Header.Set("Content-Type", "application/json")

		response, doErr := httpClient.Do(request)
		if doErr != nil {
			log.WithFields(f).WithError(doErr).Warn("problem listing user identities")
			return nil, doErr
		}

		data, readErr := io.ReadAll(response.Body)
		closeErr := response.Body.Close()
		if closeErr != nil {
			log.WithFields(f).WithError(closeErr).Warn("error closing body")
		}
		if readErr != nil {
			log.WithFields(f).WithError(readErr).Warn("problem reading the user identities response")
			return nil, readErr
		}

		if response.StatusCode != http.StatusOK {
			log.WithFields(f).Warnf("problem listing user identities - status: %d", response.StatusCode)
			return nil, fmt.Errorf("listing user identities for user %s failed with status: %d", userSFID, response.StatusCode)
		}

		var identityList models.UserIdentityList
		if unmarshalErr := json.Unmarshal(data, &identityList); unmarshalErr != nil {
			log.WithFields(f).WithError(unmarshalErr).Warn("problem decoding the user identities response")
			return nil, unmarshalErr
		}

		identities = append(identities, identityList.Data...)
		if int64(len(identityList.Data)) < pageSize {
			break
		}
	}
🤖 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/user-service/client.go` around lines 361 - 404, Bound the
pagination loop around the user-identity listing flow by tracking the number of
pages and enforcing a finite maximum page count. Also break immediately when
identityList.Data is empty, while preserving the existing short-page termination
condition and response handling.
docs/MY_CLAS_API.md (1)

599-608: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

sed -n '383,520p' cla-backend-go/v2/my_clas/service.go
rg -n "authorizeIdentity|GithubID|GithubUsername|historical|reassign|recycl|GetMyClas" cla-backend-go/v2/my_clas docs/MY_CLAS_API.md

Repository: linuxfoundation/easycla

Length of output: 19136


🏁 Script executed:

sed -n '280,380p' cla-backend-go/v2/my_clas/service.go
sed -n '440,575p' cla-backend-go/v2/my_clas/service_test.go
sed -n '120,220p' cla-backend-go/v2/my_clas/service.go

Repository: linuxfoundation/easycla

Length of output: 12677


🏁 Script executed:

sed -n '200,290p' cla-backend-go/v2/my_clas/service.go
sed -n '55,115p' cla-backend-go/v2/my_clas/handlers.go
rg -n "Pdf|pdf|Download|download" cla-backend-go/v2/my_clas

Repository: linuxfoundation/easycla

Length of output: 10902


Historical CLA Disclosure Through Reassigned Identity (CWE-639): Authorization Bypass Through User-Controlled Key (IDOR)

Reachability: External · Exploitability: Moderate

Require privacy/security approval or immutable identity corroboration before release.

The non-admin GetMyClas and GetMyClaPdf paths authorize email and SCM username values from the authenticated LF account’s identities. A reassigned alias can expose the previous holder’s historical records and signed ICLA PDF. Require immutable githubId/gitlabId corroboration, or document explicit privacy and security approval for this exception.

🤖 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 `@docs/MY_CLAS_API.md` around lines 599 - 608, Update the documented access
policy for non-admin GetMyClas and GetMyClaPdf to require immutable
githubId/gitlabId corroboration for historical records and signed PDFs, or
explicitly record approved privacy and security sign-off for the alias-based
exception before release; do not present the current-possession behavior as an
unconditional accepted product decision.
specs/001-easycla-ss-integration-fable/m1-my-cla/contracts/upstream-easycla-api.md (1)

3-5: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use one authoritative M1 contract after PR #5125.

These artifacts still contain pre-PR #5125 endpoints, client-side authorization, old signature classification, and old status models while their banners say those designs are superseded.

  • specs/001-easycla-ss-integration-fable/m1-my-cla/contracts/upstream-easycla-api.md#L3-L5: replace the old endpoint contract with /v4/my-clas, /v4/my-clas/{signatureID}/pdf, and /v4/my-clas/identities.
  • specs/001-easycla-ss-integration-fable/m1-my-cla/data-model.md#L5-L5: update the signature fields, ECLA classification, response fields, and validity model.
  • specs/001-easycla-ss-integration-fable/m1-my-cla/plan.md#L8-L10: remove obsolete Self Serve aggregation and authorization requirements.
  • specs/001-easycla-ss-integration-fable/m1-my-cla/spec.md#L21-L27: align requirements with valid, secondary email, GitLab/Gerrit support, and current endpoint scope.
📍 Affects 4 files
  • specs/001-easycla-ss-integration-fable/m1-my-cla/contracts/upstream-easycla-api.md#L3-L5 (this comment)
  • specs/001-easycla-ss-integration-fable/m1-my-cla/data-model.md#L5-L5
  • specs/001-easycla-ss-integration-fable/m1-my-cla/plan.md#L8-L10
  • specs/001-easycla-ss-integration-fable/m1-my-cla/spec.md#L21-L27
🤖 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
`@specs/001-easycla-ss-integration-fable/m1-my-cla/contracts/upstream-easycla-api.md`
around lines 3 - 5, Replace the superseded M1 contract in
specs/001-easycla-ss-integration-fable/m1-my-cla/contracts/upstream-easycla-api.md:3-5
with the authoritative /v4/my-clas, /v4/my-clas/{signatureID}/pdf, and
/v4/my-clas/identities definitions. Update
specs/001-easycla-ss-integration-fable/m1-my-cla/data-model.md:5-5 for current
signature fields, ECLA classification, response fields, and validity; remove
obsolete Self Serve aggregation and authorization requirements from
specs/001-easycla-ss-integration-fable/m1-my-cla/plan.md:8-10; and align
specs/001-easycla-ss-integration-fable/m1-my-cla/spec.md:21-27 with valid,
secondary email, GitLab/Gerrit support, and the current endpoint scope.
tests/functional/cypress/e2e/v4/cla-manager.cy.ts (1)

710-710: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Restore conditional use of allowFail for failOnStatusCode.

This line sets failOnStatusCode: false unconditionally. This removes the ability to fail the test on unexpected HTTP error status codes. If the test relies on allowFail to distinguish expected failures from unexpected ones, this change masks real errors in the CLA Manager creation request.

Restore the previous conditional behavior, unless the intent is to always ignore status codes for this specific request.

🔧 Proposed fix
-      failOnStatusCode: false,
+      failOnStatusCode: allowFail ? false : true,
🤖 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 `@tests/functional/cypress/e2e/v4/cla-manager.cy.ts` at line 710, The
failOnStatusCode property in the CLA Manager creation request is unconditionally
set to false, which masks unexpected HTTP errors. Make this setting conditional
based on the allowFail parameter so that failOnStatusCode is false only when
allowFail is true, allowing the test to properly fail on unexpected status codes
when allowFail is false.
utils/downcase_emails.sh (3)

1-2: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add the required source headers to both shell scripts.

The CI header checker applies to shell files.

  • utils/downcase_emails.sh#L1-L2: add the Linux Foundation copyright header and # SPDX-License-Identifier: MIT after the shebang.
  • utils/lookup_all_logs.sh#L1-L2: add the Linux Foundation copyright header after the shebang and preserve the applicable SPDX header.

As per coding guidelines, source files under cla-backend-go and applicable utility scripts must include the required Linux Foundation copyright header, and new source files must include the MIT SPDX identifier.

📍 Affects 2 files
  • utils/downcase_emails.sh#L1-L2 (this comment)
  • utils/lookup_all_logs.sh#L1-L2
🤖 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 `@utils/downcase_emails.sh` around lines 1 - 2, Add the required Linux
Foundation copyright header immediately after the shebang in
utils/downcase_emails.sh lines 1-2, followed by the MIT SPDX identifier. Add the
Linux Foundation copyright header after the shebang in utils/lookup_all_logs.sh
lines 1-2, preserving its existing applicable SPDX header.

Source: Coding guidelines


10-10: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cat -n utils/downcase_emails.sh

Repository: linuxfoundation/easycla

Length of output: 1932


Sensitive Data Exposure (CWE-312): Cleartext Storage of Sensitive Information

Reachability: Internal

Write user email data to a secure temporary file with automatic cleanup.

Line 10 writes all user email addresses to ${STAGE}_user_emails.json without mode restrictions and without cleanup. Email addresses are personally identifiable information. A shared workstation or CI artifact process can expose this data through the persistent file.

Apply umask 077 before the aws dynamodb scan command, create a temporary file using mktemp, and register an EXIT trap to remove the file when the script terminates:

Proposed fix
+umask 077
+email_dump="$(mktemp "${TMPDIR:-/tmp}/${STAGE}_user_emails.XXXXXX.json")"
+trap 'rm -f "$email_dump"' EXIT
-aws dynamodb scan ... > "${STAGE}_user_emails.json"
-cat "${STAGE}_user_emails.json" | jq -c ...
+aws dynamodb scan ... > "$email_dump"
+jq -c '...' < "$email_dump"
🤖 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 `@utils/downcase_emails.sh` at line 10, Update the script around the DynamoDB
scan to set a restrictive umask, create the JSON output with mktemp, and store
its path for reuse instead of writing directly to the stage-named file. Register
an EXIT trap to remove the temporary file on termination, and ensure the scan
command writes to that temporary path while preserving its existing query
options.

10-26: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Prevent concurrent email updates from being overwritten.

The scan and update form an unguarded read-modify-write. If the application changes user_emails after Line 10, Line 23 replaces that newer value with the stale normalized set.

Add a condition that requires user_emails to equal the scanned value. Report conditional failures for manual retry.

Proposed fix
     uid=$(jq -r '.user_id.S' <<<"$item")
+    oldss=$(jq -c '.user_emails.SS' <<<"$item")
     newss=$(jq -c '[.user_emails.SS[] | ascii_downcase | gsub("^\\s+|\\s+$";"") | select(length > 0)] | unique' <<<"$item")
+    values=$(jq -cn --argjson old "$oldss" --argjson new "$newss" \
+      '{":old":{"SS":$old},":e":{"SS":$new}}')
 ...
         --update-expression 'SET user_emails = :e' \
-        --expression-attribute-values "{\":e\":{\"SS\":$newss}}" && echo "ok"
+        --condition-expression 'user_emails = :old' \
+        --expression-attribute-values "$values" && echo "ok"

As per path instructions, operational scripts can mutate DynamoDB in the AWS environment selected by the active credentials, including production.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

aws dynamodb scan --profile "$PROFILE" --region "$REGION" --table-name "$TABLE" --projection-expression 'user_id, user_emails' --filter-expression 'attribute_exists(user_emails)' --output json > "${STAGE}_user_emails.json"
cat "${STAGE}_user_emails.json" | jq -c '.Items[] | select(.user_emails.SS != null) | ([.user_emails.SS[] | ascii_downcase | gsub("^\\s+|\\s+$";"") | select(length > 0)] | unique) as $n | select(($n | length > 0) and ($n != (.user_emails.SS | sort)))' \
| while IFS= read -r item; do
    uid=$(jq -r '.user_id.S' <<<"$item")
    oldss=$(jq -c '.user_emails.SS' <<<"$item")
    newss=$(jq -c '[.user_emails.SS[] | ascii_downcase | gsub("^\\s+|\\s+$";"") | select(length > 0)] | unique' <<<"$item")   # lower + trim + drop-empty + dedupe
    values=$(jq -cn --argjson old "$oldss" --argjson new "$newss" \
      '{":old":{"SS":$old},":e":{"SS":$new}}')
    if [ "$newss" = "[]" ]
    then
      echo "skip $uid (no valid emails after normalize)" >&2
      continue
    fi
    echo "user $uid -> $newss"
    if [ "$APPLY" = "1" ]
    then
      aws dynamodb update-item --profile "$PROFILE" --region "$REGION" --table-name "$TABLE" \
        --key "{\"user_id\":{\"S\":\"$uid\"}}" \
        --update-expression 'SET user_emails = :e' \
        --condition-expression 'user_emails = :old' \
        --expression-attribute-values "$values" && echo "ok"
🤖 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 `@utils/downcase_emails.sh` around lines 10 - 26, Update the update-item
operation in the normalization loop to condition the write on user_emails still
matching the scanned item’s original user_emails.SS value, while preserving the
normalized :e update. Capture conditional-check failures separately from
successful updates and report the affected uid for manual retry instead of
treating them as successful writes.

Source: Path instructions

utils/get_user_svc.sh (1)

51-59: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cd /repo && pwd

Repository: linuxfoundation/easycla

Length of output: 1932


🏁 Script executed:

# First, check if the file exists and examine lines 51-59 and 70-73
if [ -f "utils/get_user_svc.sh" ]; then
  echo "=== Lines 51-59 ==="
  sed -n '51,59p' utils/get_user_svc.sh
  echo ""
  echo "=== Lines 70-73 ==="
  sed -n '70,73p' utils/get_user_svc.sh
else
  echo "File not found"
fi

Repository: linuxfoundation/easycla

Length of output: 1932


🏁 Script executed:

# Create a simple script to verify curl behavior with different flags
python3 - <<'PY'
import subprocess
import sys

# According to curl documentation:
# -s: silent mode (no progress meter)
# -S: show errors even in silent mode
# -f: fail on server errors (HTTP 4xx, 5xx)

# Test 1: Check curl man page or help for -f flag behavior
result = subprocess.run(['curl', '--help'], capture_output=True, text=True)
help_text = result.stdout + result.stderr

# Find the -f flag documentation
for line in help_text.split('\n'):
    if ' -f,' in line or line.strip().startswith('-f'):
        print(f"curl -f documentation: {line}")
        
# Also check explicit behavior description
print("\n--- Key curl behavior ---")
print("curl -sS: silent mode, but still return 0 for HTTP error codes")
print("curl -fsS: fail (non-zero exit) on HTTP 4xx/5xx, silent mode")
print("\nThe -f flag is critical for catching HTTP errors in scripts.")
PY

Repository: linuxfoundation/easycla

Length of output: 1932


🏁 Script executed:

# Get the full context of the file around those lines
head -80 utils/get_user_svc.sh | tail -35

Repository: linuxfoundation/easycla

Length of output: 1932


🏁 Script executed:

# Also check git log for Signed-off-by requirement
cd /repo && git log --oneline -10 -- utils/get_user_svc.sh 2>/dev/null || echo "No git history available"

Repository: linuxfoundation/easycla

Length of output: 1932


🌐 Web query:

curl -f flag fail on HTTP errors exit code documentation

💡 Result:

When using curl, the command line flag to make it return a non-zero exit code on HTTP errors (specifically for HTTP response codes 400 or greater) is -f or --fail [1][2]. When this flag is used, if the server returns an HTTP status code indicating an error (400 or above), curl will exit with error code 22 [3][1]. By default, curl does not consider HTTP response codes to be failures and will continue processing or return a success exit code (0) even if the server returns an error page [1][2]. Key details regarding this behavior: 1. No Output: When using --fail, curl does not output the response body when an HTTP error occurs [1][2]. 2. Alternative for Content: If you want to receive an error code 22 on HTTP failures while still saving or outputting the response body (the error page returned by the server), use --fail-with-body instead of --fail [4][1][2]. 3. Scope: Note that --fail is not a global option and is contained by --next [4][2]. If you are performing multiple transfers, you may need to ensure the flag is applied correctly to the relevant URLs. 4. Limitations: The --fail option is not entirely fail-safe; in some cases, such as certain authentication scenarios (e.g., response codes 401 and 407), the request may still appear to succeed in curl's logic [1][2]. If you are writing scripts, checking for exit code 22 is the standard way to detect these specific HTTP-based failures when the --fail flag is active [3].

Citations:


🌐 Web query:

curl -sS vs -fsS difference HTTP error handling

💡 Result:

In the context of curl, HTTP error handling is governed by a distinction between transport-level failures and application-level HTTP status codes [1][2]. By default, curl only considers transport-level issues (such as inability to resolve a host, timeout, or lost connection) as failures [1][2]. If a server successfully processes a request and returns an HTTP status code in the 4xx or 5xx range, curl treats this as a successful transfer, meaning it does not return a non-zero exit code [3][1][4]. To change this behavior and force curl to exit with a non-zero status when receiving 4xx or 5xx HTTP responses, you must explicitly enable error handling via command-line flags [1][5]: 1. --fail (-f): Instructs curl to exit with error code 22 if the server returns an HTTP status code of 400 or higher [1][6][5]. When used, curl suppresses the response body, which is often useful if you only need to verify success [6][2]. 2. --fail-with-body: Similar to --fail, this also causes curl to exit with error code 22 for HTTP errors >= 400, but it preserves and outputs the response body [6][5][2]. This is preferred for debugging or when the server's error response contains necessary information [2]. At the library level (libcurl), the behavior is similar. The library does not trigger an error for HTTP 4xx/5xx responses unless the CURLOPT_FAILONERROR option is set to 1L [3]. When enabled, libcurl will return the error code CURLE_HTTP_RETURNED_ERROR [3][7]. In summary, the key difference is that curl separates the success of the underlying HTTP protocol exchange (which is always successful if a response is received) from the meaning of the HTTP status code itself (which may represent an application-level error) [1][2]. Users must explicitly bridge this gap if they want HTTP errors to trigger shell exit codes [8][2].

Citations:


Add curl -f flag to fail on HTTP errors.

By default, curl -sS returns exit code 0 for HTTP 4xx and 5xx responses. The profile lookup then prints no user-service profile found and exits 0 instead of failing. The pagination loop also treats error responses as the final empty page.

Add the -f flag to make curl exit with code 22 on HTTP errors >= 400.

Proposed fix
-profile="$(curl -sS -G -XGET "${hdr[@]}" --data-urlencode "username=${lfid}" "${GW}/user-service/v1/users")"
+if ! profile="$(curl -fsS -G -XGET "${hdr[@]}" \
+  --data-urlencode "username=${lfid}" \
+  "${GW}/user-service/v1/users")"
+then
+  echo "user-service profile request failed" >&2
+  exit 3
+fi
 ...
-  page="$(curl -sS -XGET "${hdr[@]}" "${GW}/user-service/v1/users/${sfid}/identities?pageSize=100&offset=${offset}")"
+  if ! page="$(curl -fsS -XGET "${hdr[@]}" \
+    "${GW}/user-service/v1/users/${sfid}/identities?pageSize=100&offset=${offset}")"
+  then
+    echo "user-service identities request failed at offset ${offset}" >&2
+    exit 3
+  fi
🤖 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 `@utils/get_user_svc.sh` around lines 51 - 59, Update the profile lookup curl
invocation in get_user_svc.sh to include the -f/--fail flag alongside the
existing silent and show-error options. Ensure HTTP 4xx/5xx responses cause curl
to fail so the surrounding profile and pagination flows do not treat error
responses as empty results.
utils/local-fbsd-api.sh (1)

5-10: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Stop when directory setup or the build fails.

If cd or go build fails, the script continues. It can source the wrong environment file and execute an existing stale binary.

Use strict mode and perform the build in a subshell.

Proposed fix
 #!/bin/bash
+set -euo pipefail
 ...
-cd cla-backend-go
-go build -o bin/cla-fbsd main.go
-cd ..
+(cd cla-backend-go && go build -o bin/cla-fbsd main.go)
 source setenv.sh
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

set -euo pipefail
(cd cla-backend-go && go build -o bin/cla-fbsd main.go)
source setenv.sh
# source setenv-prod.sh.secret
GH_ORG_VALIDATION=false PORT=8080 ./cla-backend-go/bin/cla-fbsd
🧰 Tools
🪛 Shellcheck (0.11.0)

[warning] 5-5: Use 'cd ... || exit' or 'cd ... || return' in case cd fails.

(SC2164)


[info] 7-7: Use a ( subshell ) to avoid having to cd back.

(SC2103)


[info] 8-8: Not following: setenv.sh was not specified as input (see shellcheck -x).

(SC1091)

🤖 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 `@utils/local-fbsd-api.sh` around lines 5 - 10, Update the setup and build flow
in the local fBSD launch script to enable strict shell failure handling and run
the `cd cla-backend-go` plus `go build` commands in a subshell. Ensure any
failed directory change or build terminates the script before sourcing
environment files or executing the binary.

Source: Linters/SAST tools

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 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 `@specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md`:
- Line 20: Resolve the selection contract across FR-002, Acceptance Scenario 3,
and Open Question 1: choose either Self Serve narrowing the CLA Group to a
relevant org/repo or deferring that selection to Contributor Console. Update all
three references consistently so the requirement, scenario, and open question
describe the same behavior, and remove the ambiguity.
- Line 43: Update the scope wording to use the required “Approved List”
terminology, replacing the hyphenated “approval-list” reference while preserving
the existing M4 exclusion meaning. Do not introduce “whitelist” or alter any
other scope items.
- Line 9: Preserve the blockquote continuity at the blank line on line 9 of the
specification by prefixing that blank line with “>”, or remove the blank line
entirely so both notes render as one blockquote.
- Line 14: Standardize the corporate agreement terminology across the user
story, acceptance scenarios, and FR-004 by choosing one canonical sign-type
value consistent with project_ccla_enabled. If ECLA remains user-facing,
explicitly define its mapping to CCLA for the agreement picker and Contributor
Console hand-off, and apply that mapping consistently throughout the
specification.
- Line 33: Update FR-008 and the hand-off contract specification to require
server-side identity derivation from the Console session, never client-supplied
user IDs or query parameters. Define carried context such as CLA Group ID, sign
type, and org/repo as either session-bound through a server-validated one-time
signed token or non-identifying metadata, and verify the Console session model
enforces ownership before opening the signing flow.
🪄 Autofix

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: 396bf8f3-cc63-4e00-acd5-75c6e024fe95

📥 Commits

Reviewing files that changed from the base of the PR and between 4990e8e and b88e3ac.

📒 Files selected for processing (2)
  • specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md
  • specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md

Comment thread specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md
Comment thread specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md Outdated
Comment thread specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md Outdated
Comment thread specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md Outdated
Comment thread specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md Outdated
…ied facts

Verified against easycla-contributor-console and cla-backend-go:

- Hand-off reuses the Console's existing decision-screen deep link
  (/#/cla/project/{claGroupID}/user/{userID}, ?redirect= optional) — no new
  hand-off contract. The ICLA/ECLA choice, legal guidance, and flag gating
  stay in the Console; SS does not re-implement them.
- Drop org/repo selection: the signing unit is the CLA Group; org/repo names
  are at most search metadata in the picker.
- userID resolved server-side via existing GET /v4/user-from-token
  (lookup-or-create by LF username/email).

Adds constraints: simple/straightforward design, independently deliverable
in ~2 weeks.

Rewrites open questions from code evidence:
1. GitHub identity binding (primary) — proactive ICLA must land on a user
   record the PR check can match (GitHub ID → username → email); user-from-token
   records carry no GitHub identity. Recommended: require M1's GitHub-account
   linking for the ICLA path.
2. Proactive-ICLA active-signature gap — Console + backend assume PR-derived
   context on the GitHub ICLA path (active-signature record, repository_id/
   pull_request_id for the DocuSign callback); Gerrit path proves a no-PR
   shape works. ECLA path has no PR dependency.
3. CLA-Group listing endpoint (unchanged).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Michal Lehotsky <mlehotsky@linuxfoundation.org>
Copilot AI review requested due to automatic review settings August 4, 2026 14:04

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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)
specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md (1)

39-42: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Make the success criterion cover the signing outcome.

SC-002 measures only arrival at the decision screen. It can pass while Scenario 3 fails during identity binding or active-signature creation. Add a criterion for ICLA completion and PR-check matching, or state that M2 success stops at the hand-off.

🤖 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 `@specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md` around
lines 39 - 42, Update success criterion SC-002 in the Success Criteria section
to cover the full signing outcome, including successful ICLA completion and
matching PR checks after identity binding and active-signature creation;
alternatively, explicitly redefine M2 success as ending at the Contributor
Console hand-off.
🤖 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 `@specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md`:
- Line 19: Resolve the M2 specification inconsistency by either adding GitHub
identity binding and no-PR active-signature support to the M2 requirements and
flow, or removing/defering Scenario 3 and FR-008 with explicit interim behavior.
Update the affected sections consistently, including the contributor handoff
flow and all referenced requirements, so proactive ICLA signing can produce a
PR-matchable completed record.

---

Nitpick comments:
In `@specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md`:
- Around line 39-42: Update success criterion SC-002 in the Success Criteria
section to cover the full signing outcome, including successful ICLA completion
and matching PR checks after identity binding and active-signature creation;
alternatively, explicitly redefine M2 success as ending at the Contributor
Console hand-off.
🪄 Autofix

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: e83e6017-54a3-4ed6-a8ff-902e3a515d4b

📥 Commits

Reviewing files that changed from the base of the PR and between b88e3ac and f03162f.

📒 Files selected for processing (2)
  • specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md
  • specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md

Comment thread specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Suppressed comments (7)

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md:32

  • FR-002/FR-003 silently resolve two questions that the PR description and linked program PR #5132 still call unresolved: whether org/repo narrowing is required and what the no-PR Console hand-off contract is. The parent spec currently requires selection/pre-scoping by sign type and allows org/repo narrowing, whereas this slice removes both without recording a subsequent product decision. Keep these as clarification items, or update the PR description and parent artifacts with the decision that supersedes them before treating this as the extracted implementation contract.
- **FR-002**: The picker MUST let the user find and select a CLA Group — a simple searchable list (project/CLA Group names). No org/repo selection step: the signing unit is the CLA Group, and the Console hand-off URL carries only `claGroupID` + `userID`. Org/repo names MAY be used as search metadata to help users find the right CLA Group, nothing more.
- **FR-003**: On selection, Self Serve MUST hand off to the Contributor Console's existing decision-screen URL — `{console}/#/cla/project/{claGroupID}/user/{userID}` — the same shape the PR-check link uses, without the optional `?redirect=` param (there is no PR to return to). The ICLA/ECLA choice, its legal guidance text, and the `project_icla_enabled`/`project_ccla_enabled` gating stay in the Console; SS MUST NOT re-implement them.

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:63

  • These Angular paths describe the pre-implementation M1 plan rather than the current Self Serve layout. M1 landed as app/modules/profile/clas, registered in app/modules/profile/profile.routes.ts, with its feature-gated tab wired through the profile layout/sidebar; there is no app/modules/my-clas surface to mirror. Targeting main-layout.component.ts and top-level app.routes.ts would bypass the established Profile & Account integration, so update the structure after deciding whether Sign a CLA is a profile child or another current Me-lens surface.
├── app/modules/sign-cla/                        # NEW Me-lens page: searchable CLA Group list → hand-off
├── app/layouts/main-layout/main-layout.component.ts   # EDIT: add Me-lens entry behind flag
├── app/app.routes.ts                            # EDIT: register the route (lens: 'me', flag-guarded)

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:79

  • An ECLA-only fallback does not satisfy this PR's stated M2 scope. The spec's acceptance scenario 3 and FR-008 require proactive ICLA completion, and the PR description explicitly includes both ICLA and CCLA paths; deferring the no-PR ICLA change would ship a picker whose Individual path is known to fail. Make resolution of this gap a launch blocker, or formally revise the milestone scope and acceptance criteria with PM approval.
2. **Proactive-ICLA gap** (spec open question 2) — Console + backend delta; decide in/out of M2 at `/speckit.clarify`. ECLA works proactively with zero changes, so a worst-case fallback exists but weakens the milestone.

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:23

  • “No EasyCLA writes” is incompatible with FR-004 because user-from-token is explicitly lookup-or-create and may also refresh stored identity fields. Narrow this prohibition to signing-related writes while explicitly allowing the required provisioning operation.
**Constraints**: no EasyCLA writes and no signing-initiation calls from SS (FR-005); server-side identity derivation only (never trust client-supplied user IDs); PR-check remediation link untouched (FR-006); feature-flagged dark launch; 2-week delivery budget.

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:18

  • The storage statement conflicts with the required GET /v4/user-from-token lookup-or-create flow. That middleware updates an existing identity and calls CreateUser when no record exists (cla-backend-go/cmd/server.go:933-1068), so EasyCLA DynamoDB is not untouched. Distinguish “no SS-owned storage/schema” from the permitted EasyCLA user-provisioning write.
**Storage**: none in SS (stateless: list CLA Groups, resolve `userID`, redirect). EasyCLA DynamoDB + S3 untouched.

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:19

  • The current M1 CLA server service/controller tests use Vitest (cla.service.spec.ts and clas.controller.spec.ts), not Jest/Karma. Leaving the harness unresolved sends /speckit.tasks toward the wrong tooling even though the exact adjacent tests are already available; name Vitest for server tests and reference the current Angular harness separately.
**Testing**: lfx-self-serve conventions — Jest/Karma unit tests for services/components, server route tests, Cypress/E2E per repo norms (verify exact harness during implementation).

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:12

  • This says there are two clarification items, but the spec defines three and the plan later tracks CLA-Group listing as open question 3. Include the listing source here so the summary does not omit a required Phase 0 decision.
The SS-side build is deliberately thin: one page, one or two server routes, one redirect. The milestone's real design work is the two `/speckit.clarify` items in [spec.md](spec.md): **GitHub identity binding** (a proactive ICLA must land on a user record the PR check can match — recommended: require M1's GitHub-account linking for the ICLA path) and the **proactive-ICLA active-signature gap** (Console + backend assume PR-derived context on the GitHub ICLA path; the Gerrit path proves a no-PR shape already works). Both are scoped and evidenced in the spec's "Verified Console/backend facts" section; neither is resolved here.

Comment thread specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md Outdated
…count auth

Incorporate the M2 UI mockup (easyclav2-migration-planning, MyCLAs v8) as the
UI source of truth. M2 is now M1's My CLAs page extended with:

- "+ Sign a CLA" inline search (project / CLA group / repo source incl.
  GitLab & Gerrit) → hand-off to the Console decision screen, unchanged
  from the prior spec, plus a pre-hand-off account-authorization step
  (mockup's note) — adopted as the direction for the identity-binding
  question; per-platform mechanics remain open.
- CLA invalidation per row: ICLA via existing PUT /v4/cla-group/{id}/user/
  {id}/icla (verified: handler has no ownership check → SS enforces
  self-only); ECLA ("Confirm you no longer work at {company}?") needs a new
  backend endpoint — flagged as the main schedule risk.
- Invalidation blocked server-side during SS impersonation, reusing the
  existing impersonation-readonly middleware (FR-009).
- Status column (Valid / Needs attention / Invalidated) with
  "Request approval →" deep link into the Console; approval-criteria
  evaluation source is an open question.

Scope decision (2026-08-05): all mockup features are M2; schedule risks
noted in plan.md with independent degradation order if the 2-week budget
forces a cut.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Michal Lehotsky <mlehotsky@linuxfoundation.org>
Copilot AI review requested due to automatic review settings August 5, 2026 08:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Suppressed comments (4)

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md:9

  • This is not currently an extraction of the linked program-level M2 contract. PR #5132's revised User Story 2/FR-011–012 requires Self Serve to choose the sign type and, where applicable, org/repo before handing off, and it does not include invalidation or status. This slice instead delegates sign-type choice, excludes org/repo selection, and adds two substantial backend-backed features. Align the parent PR and this slice before Spec Kit consumes competing definitions of M2.
This is the extracted, implementable slice for Milestone 2. Program-wide context, assumptions, and resolved decisions live in the parent spec; this file is what `/speckit.plan`, `/speckit.tasks`, and `/speckit.implement` operate on.

> **Scope (revised 2026-08-04, per Heather/PM; UI per mockup v8)**: M2 extends M1's **My CLAs** page with three additions — a proactive "Sign a CLA" entry that hands off to the existing Contributor Console, per-CLA **invalidation**, and a richer **status** column. Self Serve never runs the DocuSign ceremony; nothing is cut over or retired; the PR-check remediation link is unchanged.

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md:42

  • GET /v4/user-from-token cannot by itself satisfy FR-004: it resolves only LF username/email and may create an LF-only record, while M1 deliberately aggregates platform-linked records because commit attribution uses GitHub/GitLab/Gerrit identities. Passing that LF-only user ID to the Console can bind the new signature to a record that contribution checks do not resolve. Make user-ID selection depend on the authorized platform identity/binding contract rather than mandating this endpoint before that contract is settled.
- **FR-003**: Self Serve MUST resolve the EasyCLA `userID` server-side from the session identity via the existing `GET /v4/user-from-token` endpoint (lookup-or-create). No client-supplied user IDs.

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md:85

  • The evaluation source is already implemented: GET /v4/my-clas evaluates each ECLA against its current CCLA and approval lists and returns valid (v2/my_clas/service.go:214-219,665-714). The actual missing contract is a backend-derived reason that distinguishes invalidation from an approval-criteria failure for the explanatory note. Reframe this question accordingly to avoid designing a duplicate evaluation endpoint.
3. **Status evaluation** — source for "no longer matches approval criteria": extend `GET /v4/my-clas` vs. a separate check; must not require SS to re-implement approval-list logic. Second schedule risk.

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:82

  • The proactive ICLA entry cannot ride existing endpoints: this spec establishes that the GitHub ICLA path requires a PR-created active-signature record and repository/PR metadata. Deferring the no-PR Console/backend delta while retaining sign entry would make the required ICLA path fail, so the fallback must treat that delta as mandatory or explicitly narrow the shipped hand-off to ECLA.
If the budget forces a cut, the mockup's pieces degrade independently: sign entry + ICLA invalidation ride existing endpoints; ECLA invalidation and status enrichment are the deferrable slices — decide at `/speckit.plan`.

@mlehotskylf

Copy link
Copy Markdown
Collaborator Author

Review Feedback Addressed (round 2)

Commit: d2c322f

All five copilot-pull-request-reviewer findings were verified against cla-backend-go and confirmed valid — this round materially improved the spec.

Changes Made

  • spec.md FR-003: userID resolution is now platform-aware per open question 1's binding order (selected/linked GitHub ID first → user-from-token fallback + enrichment; Gerrit uses user-from-token alone) — FR-003 had gone stale when open question 1 was resolved and contradicted it (per copilot-pull-request-reviewer, two threads)
  • spec.md FR-007 + verified facts: the existing invalidateICLA flow cannot be reused unchanged — verified: no ownership check; resolves (claGroupID, userID) and takes sigs[0] on multiple matches (now requires signature-ID targeting); manager-framed email + SignatureProjectInvalidatedEventData records a project-deletion reason (now requires actor-/reason-aware side effects) (per copilot-pull-request-reviewer)
  • plan.md: the GitHub sign entry requires the no-PR ICLA delta and is not budget-cuttable without narrowing to Gerrit-only; added complexity risk 2a for the ICLA-invalidation endpoint revision; corrected the degradation note (per copilot-pull-request-reviewer)

Threads Resolved

5 of 5 unresolved threads addressed in this iteration.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Suppressed comments (10)

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md:50

  • This invalidation is not durable while the Approved List remains unchanged. When auto_create_ecla is enabled, a later Approved List update processes every still-listed employee and calls ValidateProjectRecord for an unapproved ECLA (signatures/service.go:890-910), silently reactivating the record the contributor invalidated. Define either a durable self-opt-out honored by auto-create/authorization or removal of the matching Approved List entry; otherwise “ends their coverage” is not guaranteed.
- **FR-008**: Each valid ECLA row MUST offer Invalidate framed per the mockup — "This confirms you're no longer covered under {company}'s Corporate CLA (CCLA) for {project}, and marks your ECLA as invalid" (typed INVALIDATE confirmation). The company's Approved List is not mutated; the company's CLA managers are notified so they can update it. *([NEEDS CLARIFICATION]: no self-service ECLA-invalidation endpoint exists — new `cla-backend-go` work; see Open questions.)*

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:81

  • Status evaluation is not retired by exposing the existing boolean. covered=false also represents sanctioned companies and missing/inactive CCLAs, so the UI cannot derive the required explanatory note or decide whether “Request approval” applies. Keep this as backend work to expose a machine-readable coverage reason.
4. **Status evaluation** (open question 3) — retired: `GET /v4/my-clas` already computes the coverage evaluation per ECLA row; M2 exposes it as a status field instead of collapsing it into `Valid`.

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md:56

  • A single covered=false value cannot support these explanations. The current evaluator also returns false for a sanctioned company and for a missing/inactive CCLA (v2/my_clas/service.go:678-695), not only for an Approved List mismatch; mapping all of those cases to the same status would show misleading copy and remediation. Expose a reason code so the UI can distinguish the failure modes.
- **FR-010**: Each row MUST show a status: **Valid** (approved and covered); **Needs attention** with an explanatory note (ECLA signed but no longer matching the company's Approved List criteria — approved but not covered); **Invalidated** (not approved). Source: extend `GET /v4/my-clas`, which already computes the coverage evaluation per ECLA row and currently collapses it into a single boolean (resolved — see open question 3).

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md:57

  • The existing request-authorization route is not independently deep-linkable. Its component reads the selected company and project from Console browser storage; a user arriving directly from Self Serve has neither value, so managers are not loaded and submission dereferences missing state. Require a Console change or route through the dashboard with the needed context.
- **FR-011**: "Needs attention" ECLA rows MUST link "Request approval →" into the Contributor Console's existing request-authorization flow (deep link; no new SS flow).

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md:41

  • The linked parent milestone in PR #5132 still requires Self Serve to choose ICLA/ECLA and optionally select an org/repo before hand-off, while this requirement leaves both choices to the Console. Since both documents are presented as authoritative program context, they would give Spec Kit contradictory implementation scope. Update the parent milestone or explicitly mark its clauses as superseded before these links resolve.
- **FR-002**: On selection, Self Serve MUST hand off to the Contributor Console's existing decision-screen URL — `{console}/#/cla/project/{claGroupID}/user/{userID}` — without the optional `?redirect=` param. The ICLA/ECLA choice, legal guidance, and `project_icla_enabled`/`project_ccla_enabled` gating stay in the Console; SS MUST NOT re-implement them.

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md:4

  • The PR description identifies mockup v8 as the source of truth, but this file switches the acceptance baseline to Final/v16 and says v8 is superseded. Update the PR description or reconcile the document so reviewers and Spec Kit use the same UI contract.
**UI mockup (source of truth)**: [EasyCLA_MyCLAs_Full_Prototype_Final.html](https://github.com/linuxfoundation/easyclav2-migration-planning/blob/main/Mockups/M2/EasyCLA_MyCLAs_Full_Prototype_Final.html) (internally v16; supersedes v8/v10)

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md:94

  • This narrowed resolver only handles GitHub repository names/URLs, but FR-001 and the user story also promise Gerrit-backed repository search and pasted repository links. Without a Gerrit resolution path, that accepted input cannot select a Gerrit CLA Group. Define the Gerrit equivalent or explicitly narrow the earlier requirements to GitHub URLs.
5. **CLA-Group listing (narrowed 2026-08-08)** — search = CLA-group/project **names** from the existing project↔CLA-group mappings, plus an **exact repo-name/URL resolver**: the user types a repo name or pastes a GitHub URL, SS parses `owner/repo` and resolves it via the existing `RepositoryNameIndex` GSI (`GitHubGetRepositoryByName`) → `cla_group_id`. No fuzzy full-text search over repo names. Remaining question is only which existing listing endpoint to reuse for the name search.

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:8

  • This summary assumes the existing request-authorization flow can be deep-linked, but that route depends on company/project objects already stored by the Console's corporate dashboard. A direct Self Serve link starts without that browser state and cannot load managers or submit. Include the required Console routing/state-loading delta in the plan.
Extend M1's **My CLAs** page (Me lens) per the mockup: (1) a "Sign CLA" modal search (project / CLA group / repo source / pasted repo link) that resolves the user's EasyCLA `userID` server-side (existing `GET /v4/user-from-token`) and — after an account-authorization step for the platform they'll contribute with — redirects to the Contributor Console's existing decision-screen URL (`{console}/#/cla/project/{claGroupID}/user/{userID}`); (2) per-row **CLA invalidation** with confirmation modals (ICLA via a revised `invalidateICLA` flow — signature-ID-targeted, actor-aware side effects, SS-side ownership enforcement; ECLA needs a new backend endpoint), blocked server-side during impersonation via SS's existing impersonation-readonly middleware; (3) a **status** column (Valid / Needs attention / Invalidated) with a "Request approval →" deep link into the Console for ECLAs that no longer match Approved List criteria.

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:69

  • The status extension is no longer conditional: FR-010 requires a richer status and open question 3 is marked resolved. Leaving it under “Conditional” can omit the API field that the UI depends on. Move the GET /v4/my-clas status/reason extension into the mandatory backend work.

This issue also appears on line 81 of the same file.

Secondary repo: `linuxfoundation/easycla` `cla-backend-go` — new self-service ECLA-invalidation endpoint, swagger-first (`swagger/cla.v2.yaml` → `make swagger` → handler/service), no schema changes expected. Conditional (per clarify): status-evaluation extension to `GET /v4/my-clas`; no-PR ICLA request shape in `v2/sign` (+ matching `easycla-contributor-console` tweak).

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:77

  • These settled semantics do not prevent reactivation. With auto_create_ecla, a later Approved List update revalidates an unapproved employee signature that still matches the unchanged list (signatures/service.go:890-910). Track the durable self-opt-out/removal decision as a design risk; otherwise the new endpoint cannot guarantee that self-invalidation ends coverage.
1. **ECLA-invalidation endpoint** (spec open question 2) — the one guaranteed new backend piece; semantics settled (per-signature flag flip, no Approved List mutation, notifications, backend ownership check) — execution risk only.

Comment thread specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md Outdated
Comment thread specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md Outdated
…round-2 fixes

- spec.md + plan.md: delivery budget ~2 weeks -> ~3 weeks (per PM)
- spec.md acceptance scenario 3: signature-ID-targeted invalidation with
  self-invalidation event reason (was "existing endpoint" — contradicted
  the revised FR-007)
- plan.md: summary resolution wording matches platform-aware FR-003;
  constitution Simplicity gate reflects the ICLA-flow revision;
  open-question count references updated (1/3 resolved, 5 narrowed)

Signed-off-by: Michal Lehotsky <mlehotsky@linuxfoundation.org>
Copilot AI review requested due to automatic review settings August 7, 2026 15:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Suppressed comments (7)

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md:56

  • A single approved && !covered status cannot support the required explanatory note or decide whether “Request approval” is applicable. The current helper returns false not only for an Approved List mismatch, but also for a missing/sanctioned company and a missing signed CCLA (v2/my_clas/service.go:678-696); in those cases requesting approval cannot restore coverage. Require a machine-readable coverage reason/action (or distinct statuses) from /v4/my-clas rather than only exposing the collapsed boolean as Needs attention.
- **FR-010**: Each row MUST show a status: **Valid** (approved and covered); **Needs attention** with an explanatory note (ECLA signed but no longer matching the company's Approved List criteria — approved but not covered); **Invalidated** (not approved). Source: extend `GET /v4/my-clas`, which already computes the coverage evaluation per ECLA row and currently collapses it into a single boolean (resolved — see open question 3).

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:16

  • The status response extension and no-PR ICLA shape are not optional clarify outcomes for the stated scope. FR-010 requires the former, and the plan's own lines 78 and 83 confirm the latter is required for the GitHub acceptance path. Calling both “possibly” needed understates the required Go work.
**Language/Version**: TypeScript (Angular 20.3 frontend + Node 22 / Express 4 SSR server) in `lfx-self-serve`; Go 1.25 in `cla-backend-go` for the ECLA-invalidation endpoint (and possibly status evaluation / no-PR ICLA shape, per clarify outcomes).

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:24

  • This scope estimate still treats the status extension as optional and omits the required no-PR ICLA backend delta. Both are mandatory for FR-010 and the GitHub sign-entry acceptance path, so they need to be included in the backend scope rather than left to clarification.
**Scale/Scope**: all LFX contributors; extends 1 existing Me-lens page, ~3–4 server routes (CLA-Group search, `userID` resolution/hand-off, ICLA invalidate, ECLA invalidate), 1 new upstream endpoint (ECLA invalidation) + possibly a status/listing extension per clarify outcomes.

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:69

  • These two backend changes are mandatory, not conditional: FR-010 requires extending /v4/my-clas, and lines 78/83 establish that no-PR ICLA support is required for the in-scope GitHub path. Leaving them under “Conditional” contradicts the completed risk analysis and can cause the implementation plan to omit required work.
Secondary repo: `linuxfoundation/easycla` `cla-backend-go` — new self-service ECLA-invalidation endpoint, swagger-first (`swagger/cla.v2.yaml` → `make swagger` → handler/service), no schema changes expected. Conditional (per clarify): status-evaluation extension to `GET /v4/my-clas`; no-PR ICLA request shape in `v2/sign` (+ matching `easycla-contributor-console` tweak).

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md:86

  • This open question simultaneously says the per-platform mechanism is still to be clarified and then adopts that mechanism in the next bullet (M1 Auth0 linking for GitHub, none for Gerrit, configuration for GitLab). State only the remaining decision—such as the end-to-end binding contract and updateUser gateway authorization—so /speckit.clarify does not revisit an adopted matrix or mistakenly treat all mechanics as settled.
1. **Account-authorization mechanics (direction adopted)** — the mockup resolves the *what*: users authorize their GitHub/GitLab/Gerrit account before the Console opens. To clarify: the *how* per platform (Auth0 identity linking as in M1 vs. an OAuth step in the flow), and how the authorized identity gets bound to the EasyCLA user record used in the hand-off (existing GitHub-anchored record vs. enriching the LF-created one). Sub-decisions already reasoned through (2026-08-07):

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:36

  • Question 1 is not resolved: the spec still leaves the end-to-end account-binding mechanics and updateUser gateway authorization for clarification. Labeling it resolved here contradicts the open-question ledger and can let the post-design security re-check pass without that contract.
**Post-Phase-1 re-check**: pending — re-run after `/speckit.clarify` confirms the remaining open items in [spec.md](spec.md) (questions 1 and 3 resolved; 5 narrowed; 2 and 4 are execution deliverables).

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:80

  • Account authorization cannot be marked retired while the spec still requires clarification of the binding contract and verification that SS can call the authenticated v1 updateUser endpoint. Keep this as a narrowed risk until those mechanics are confirmed.
3. **Account-authorization mechanics** (open question 1) — retired: Gerrit needs no step (same LF SSO), GitLab is conditional on SS shipping GitLab linking (config flip, M2 doesn't block), GitHub reuses M1's linking + picker; first-timer enrichment is a call to the existing v1 `updateUser` API.

Comment thread specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md
Address unresolved review comments from copilot-pull-request-reviewer:

- spec.md FR-008: note the durability caveat — a per-signature flag flip
  is not durable while the user stays on the Approved List
  (auto_create_ecla re-approves via ValidateProjectRecord, signatures/
  service.go:895-900; ProcessEmployeeSignature re-authorizes, :1562-1588).
  The new ECLA endpoint must apply a persistent self-exclusion, not just
  flip signature_approved.
- spec.md FR-007(a): backend ownership enforcement is now mandatory on the
  revised ICLA endpoint (v4 is independently reachable); SS self-only check
  retained as defense in depth.
- plan.md Constitution Check (Security): make backend signature-ownership
  mandatory for both the revised ICLA and new ECLA endpoints, SS as
  defense in depth.
- spec.md: add supersession note — this slice is the authoritative M2
  contract where it diverges from the program-level M2 brief in #5132.

Resolves 4 review threads.

Signed-off-by: Michal Lehotsky <mlehotsky@linuxfoundation.org>
Copilot AI review requested due to automatic review settings August 7, 2026 15:49
@mlehotskylf

Copy link
Copy Markdown
Collaborator Author

Review Feedback Addressed

Commit: d5a115f

Changes Made

  • spec.md FR-008: added a verified durability caveat — a per-signature flag flip is not durable while the user stays on the Approved List (auto_create_ecla re-approves via ValidateProjectRecord, signatures/service.go:895-900; ProcessEmployeeSignature re-authorizes on UserIsApproved, :1562-1588). The new ECLA endpoint must apply a persistent self-exclusion, not just flip signature_approved. (flagged by copilot-pull-request-reviewer)
  • spec.md FR-007(a): backend ownership enforcement is now mandatory on the revised ICLA endpoint (v4 is independently reachable); SS self-only check retained as defense in depth. (flagged by copilot-pull-request-reviewer)
  • plan.md Constitution Check (Security): backend signature-ownership enforcement made mandatory for both the revised ICLA and new ECLA endpoints, SS as defense in depth. (flagged by copilot-pull-request-reviewer)
  • spec.md: added a supersession note — this slice is the authoritative M2 contract where it diverges from the program-level M2 brief in docs(spec): EasyCLA → Self Serve program spec + M1–M6 milestone docs #5132; docs(spec): EasyCLA → Self Serve program spec + M1–M6 milestone docs #5132's M2 material must be updated or marked superseded before Spec Kit consumes both. (flagged by copilot-pull-request-reviewer)

Threads Resolved

4 of 4 unresolved threads addressed in this iteration.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Suppressed comments (7)

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md:17

  • This constraint conflicts with FR-008. A plain signature_approved flip is revalidated, while Approved List mutation is prohibited, so durable self-invalidation needs persisted state unless an existing durable mechanism is identified. Permit the narrowly scoped self-exclusion state required by FR-008.
- **Simple and straightforward** — no new services, no new state, no bespoke contracts where an existing one works. Prefer reusing what the Console and EasyCLA backend already do.

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md:93

  • This “per-signature flag flip” contradicts FR-008, which correctly establishes that such a flip is automatically reversed while the employee remains listed. The settled semantics must include the persistent self-exclusion honored by revalidation and PR gating, or implementation may follow this stale requirement and fail to end coverage.
2. **ECLA invalidation endpoint** — new `cla-backend-go` API (swagger-first) for an employee ending their own CCLA coverage. Semantics settled per FR-008/FR-008a and the invalidation preamble: per-signature flag flip, record kept for audit, no Approved List mutation, CLA managers + user notified, backend-side ownership check (don't replicate `invalidateICLA`'s missing check). Main remaining backend deliverable alongside open question 4.

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md:94

  • The resolved formula repeats the status-classification bug in FR-010. Approved List removal currently flips signature_approved to false, so Invalidated = ¬approved cannot represent the required “Needs attention” state for a signed ECLA that no longer matches the list. The endpoint needs a distinct invalidation reason/marker rather than deriving all three statuses from these two booleans.
3. **Status evaluation — RESOLVED (2026-08-08)**: extend `GET /v4/my-clas`. The Approved List evaluation already runs there — `eclaCoveredByCurrentApprovalList` (mirroring the PR-gating `UserIsApproved`) is computed per ECLA row and then collapsed into `row.Valid = signature_approved && covered` (`v2/my_clas/service.go`). M2 exposes the distinction as a status field instead of collapsing it: **Valid** = approved ∧ covered; **Needs attention** = approved ∧ ¬covered (ECLA); **Invalidated** = ¬approved. Known carried-over caveat: GitLab group membership can't be evaluated (needs per-group OAuth) and defers to the flag — acceptable, and moot while GitLab is deferred.

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:69

  • “No schema changes expected” is incompatible with FR-008 unless an existing durable exclusion field is identified. Since signature_approved is revalidated and Approved List mutation is out of scope, the plan must account for a possible signature model/schema extension rather than excluding it.
Secondary repo: `linuxfoundation/easycla` `cla-backend-go` — new self-service ECLA-invalidation endpoint, swagger-first (`swagger/cla.v2.yaml` → `make swagger` → handler/service), no schema changes expected. Conditional (per clarify): status-evaluation extension to `GET /v4/my-clas`; no-PR ICLA request shape in `v2/sign` (+ matching `easycla-contributor-console` tweak).

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md:58

  • This status mapping does not satisfy acceptance scenario 6 for normal Approved List removals. That flow calls InvalidateProjectRecord and sets signature_approved=false (signatures/repository.go:4131-4178), so the proposed rule labels the row “Invalidated,” not “Needs attention.” Define status using an explicit invalidation/revocation reason or marker so loss of current coverage can be distinguished from user invalidation.

This issue also appears on line 94 of the same file.

- **FR-010**: Each row MUST show a status: **Valid** (approved and covered); **Needs attention** with an explanatory note (ECLA signed but no longer matching the company's Approved List criteria — approved but not covered); **Invalidated** (not approved). Source: extend `GET /v4/my-clas`, which already computes the coverage evaluation per ECLA row and currently collapses it into a single boolean (resolved — see open question 3).

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:36

  • The ledger incorrectly says question 1 is resolved. The spec still explicitly leaves per-platform mechanics for clarify (line 88) and gateway/ownership behavior for the enrichment call to clarify (line 92). Mark it as partially resolved so Spec Kit does not skip required identity-binding work.
**Post-Phase-1 re-check**: pending — re-run after `/speckit.clarify` confirms the remaining open items in [spec.md](spec.md) (questions 1 and 3 resolved; 5 narrowed; 2 and 4 are execution deliverables).

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md:96

  • This resolver only covers GitHub owner/repo names and URLs, but FR-001 and the user story also include Gerrit-backed repository sources and pasted repository links. A Gerrit URL cannot resolve through RepositoryNameIndex; define its resolver or explicitly restrict pasted-link support to GitHub throughout the scope.
5. **CLA-Group listing (narrowed 2026-08-08)** — search = CLA-group/project **names** from the existing project↔CLA-group mappings, plus an **exact repo-name/URL resolver**: the user types a repo name or pastes a GitHub URL, SS parses `owner/repo` and resolves it via the existing `RepositoryNameIndex` GSI (`GitHubGetRepositoryByName`) → `cla_group_id`. No fuzzy full-text search over repo names. Remaining question is only which existing listing endpoint to reuse for the name search.

Comment thread specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md Outdated
Comment thread specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md Outdated
Comment thread specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md Outdated
…dback

Address follow-up review comments from copilot-pull-request-reviewer:

- plan.md risk 1: replace the "per-signature flag flip" parenthetical with
  the durable self-exclusion mechanism, consistent with FR-008; make it part
  of the ECLA endpoint deliverable.
- plan.md risk 3 & spec.md account-auth sub-decision: retract the "first-timer
  enrichment is a call to existing v1 updateUser, no new backend work" claim.
  Verified against users/handlers.go:84-107 — the githubUsername branch returns
  400 for an unseen username and, when found, Saves without verifying the linked
  identity belongs to the caller. A small backend-owned, ownership-checked
  identity-binding operation is required.

Resolves 3 review threads.

Signed-off-by: Michal Lehotsky <mlehotsky@linuxfoundation.org>
Copilot AI review requested due to automatic review settings August 7, 2026 16:19
@mlehotskylf

Copy link
Copy Markdown
Collaborator Author

Review Feedback Addressed

Commit: 8059c4e

Changes Made

  • plan.md (risk 1): replaced the "per-signature flag flip" parenthetical with a durable self-exclusion the Approved-List re-validation and PR gating honor — consistent with FR-008; made the mechanism part of the ECLA endpoint deliverable. (flagged by copilot-pull-request-reviewer)
  • plan.md (risk 3) & spec.md (account-auth sub-decision): retracted the "first-timer enrichment = existing v1 updateUser, no new backend work" claim. Verified against users/handlers.go:84-107 — the githubUsername branch returns 400 for an unseen username and, when found, Saves without verifying the linked identity belongs to the caller. Both docs now require a small backend-owned, ownership-checked identity-binding operation (marked [NEEDS CLARIFICATION] for its shape and gateway auth). (flagged by copilot-pull-request-reviewer)

Threads Resolved

3 of 3 unresolved threads addressed in this iteration.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Suppressed comments (4)

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md:59

  • The status/action mapping loses required coverage reasons. The current evaluator also returns covered=false for a sanctioned or missing company and for a missing active CCLA (cla-backend-go/v2/my_clas/service.go:678-695), not only for an Approved List mismatch. Under FR-010 these rows become “Needs attention,” and FR-011 then shows a request-approval link that cannot fix them. Require a reason-aware API result and only show that link for approval-list failures.
- **FR-010**: Each row MUST show a status: **Valid** (approved and covered); **Needs attention** with an explanatory note (ECLA signed but no longer matching the company's Approved List criteria — approved but not covered); **Invalidated** (not approved). Source: extend `GET /v4/my-clas`, which already computes the coverage evaluation per ECLA row and currently collapses it into a single boolean (resolved — see open question 3).
- **FR-011**: "Needs attention" ECLA rows MUST link "Request approval →" into the Contributor Console's existing request-authorization flow (deep link; no new SS flow).

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md:93

  • This reintroduces the flag-only invalidation semantics that FR-008 explicitly rejects as non-durable. An Approved List update can reapprove a signature whose only change is signature_approved=false; the open-question contract must require the persistent self-exclusion described in FR-008.
2. **ECLA invalidation endpoint** — new `cla-backend-go` API (swagger-first) for an employee ending their own CCLA coverage. Semantics settled per FR-008/FR-008a and the invalidation preamble: per-signature flag flip, record kept for audit, no Approved List mutation, CLA managers + user notified, backend-side ownership check (don't replicate `invalidateICLA`'s missing check). Main remaining backend deliverable alongside open question 4.

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:36

  • Question 1 is not resolved: the spec still marks the identity-binding operation and its gateway authentication [NEEDS CLARIFICATION] on spec.md:92, and risk 3 below says it is “not fully retired.” Calling it resolved can let the post-Phase-1 gate pass before that security contract exists.
**Post-Phase-1 re-check**: pending — re-run after `/speckit.clarify` confirms the remaining open items in [spec.md](spec.md) (questions 1 and 3 resolved; 5 narrowed; 2 and 4 are execution deliverables).

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:69

  • These backend changes are not conditional under the stated M2 scope. FR-010 requires the GET /v4/my-clas extension, and the verified GitHub ICLA gap plus risk 2 require the no-PR request shape unless GitHub is explicitly removed from M2. Leaving both under “Conditional” can produce an implementation plan that cannot satisfy the acceptance scenarios.
Secondary repo: `linuxfoundation/easycla` `cla-backend-go` — new self-service ECLA-invalidation endpoint, swagger-first (`swagger/cla.v2.yaml` → `make swagger` → handler/service), no schema changes expected. Conditional (per clarify): status-evaluation extension to `GET /v4/my-clas`; no-PR ICLA request shape in `v2/sign` (+ matching `easycla-contributor-console` tweak).

Add FR-008b: both invalidation flows must record an invalidation
timestamp and a reason/actor value on the signature record, alongside
a contributor-framed signature.note.

date_modified is not a usable invalidation date -- any write bumps it
and InvalidateProjectRecord does not set it, so the invalidation date
cannot be reconstructed from the record today.

The reason/actor value should be the same field FR-008's durable
self-exclusion relies on; the Approved-List re-validation marker and
the audit signal are one concept, not two overlapping fields.

Correct plan.md's "no schema changes expected" claim: the signatures
table gains additive attributes (no migrations), read by v1/v2 paths,
so consumers must tolerate empty values on pre-M2 records.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Michal Lehotsky <mlehotsky@linuxfoundation.org>
Copilot AI review requested due to automatic review settings August 7, 2026 19:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Suppressed comments (6)

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md:94

  • This summary restores the flag-only behavior that FR-008 explicitly rejects. A per-signature flag flip can be undone by Approved List re-validation, so the settled semantics must include the persistent self-exclusion honored by both re-validation and PR gating.
2. **ECLA invalidation endpoint** — new `cla-backend-go` API (swagger-first) for an employee ending their own CCLA coverage. Semantics settled per FR-008/FR-008a and the invalidation preamble: per-signature flag flip, record kept for audit, no Approved List mutation, CLA managers + user notified, backend-side ownership check (don't replicate `invalidateICLA`'s missing check). Main remaining backend deliverable alongside open question 4.

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:32

  • This gate's “one genuinely new backend piece” claim conflicts with risk 3, which requires a new backend-owned identity-binding operation in addition to ECLA invalidation. Include that operation in the simplicity assessment rather than passing the gate on an incomplete backend scope.
- **Simplicity**: no new services, storage, or state; extends M1's page and module; hand-off is the Console's existing deep link; invalidation reuses the existing impersonation middleware and revises (not replaces) the existing ICLA flow. The ECLA endpoint is the one genuinely new backend piece; the ICLA revision and no-PR request shape are bounded deltas to existing flows. PASS.

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:36

  • Question 1 is not resolved: the spec still marks the identity-binding operation's shape and gateway authentication as needing clarification, and risk 3 calls the account mechanics “not fully retired.” Leaving this re-check list as “1 and 3 resolved” can cause the remaining security contract to be skipped.
**Post-Phase-1 re-check**: pending — re-run after `/speckit.clarify` confirms the remaining open items in [spec.md](spec.md) (questions 1 and 3 resolved; 5 narrowed; 2 and 4 are execution deliverables).

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:69

  • These backend changes are no longer conditional under the current spec. FR-010 requires extending GET /v4/my-clas, and risk 2 says the no-PR ICLA shape is required for the core GitHub sign-entry path. Mark both as required in the source structure so implementation planning cannot omit them during clarification.
Secondary repo: `linuxfoundation/easycla` `cla-backend-go` — new self-service ECLA-invalidation endpoint, swagger-first (`swagger/cla.v2.yaml` → `make swagger` → handler/service). Schema impact is limited to **additive** attributes on the signatures table per FR-008b (invalidation timestamp + reason/actor, the latter doubling as FR-008's durable self-exclusion marker) — no migrations, but the attributes are read by v1/v2 code paths, so consumers must tolerate empty values on pre-M2 records. Conditional (per clarify): status-evaluation extension to `GET /v4/my-clas`; no-PR ICLA request shape in `v2/sign` (+ matching `easycla-contributor-console` tweak).

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md:52

  • The invalidation eligibility is internally inconsistent. The user story says every signed CLA row offers Invalidate, while this requirement limits ECLAs to the Valid state; a signed, approved ECLA in the newly defined Needs attention state is therefore unspecified. State explicitly whether Needs attention ECLAs can be self-invalidated so the UI and endpoint authorization use the same rule.

This issue also appears on line 94 of the same file.

- **FR-008**: Each valid ECLA row MUST offer Invalidate framed per the mockup — "This confirms you're no longer covered under {company}'s Corporate CLA (CCLA) for {project}, and marks your ECLA as invalid" (typed INVALIDATE confirmation). The company's Approved List is not mutated; the company's CLA managers are notified so they can update it. **Durability caveat (verified):** a plain per-signature flag flip is not durable while the user stays on the Approved List — when `auto_create_ecla` is set, a later Approved List update re-processes every still-listed employee and calls `ValidateProjectRecord` to re-approve unapproved employee signatures (`signatures/service.go:895-900`), and `ProcessEmployeeSignature` re-authorizes any existing employee signature whenever `UserIsApproved` succeeds (`:1562-1588`). The new endpoint MUST therefore apply a **persistent self-exclusion** the Approved-List re-validation and PR gating both honor (or otherwise change eligibility), not merely flip `signature_approved`. *([NEEDS CLARIFICATION]: no self-service ECLA-invalidation endpoint exists — new `cla-backend-go` work; the durable self-exclusion mechanism is part of that work; see Open questions.)*

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:24

  • The scope count omits mandatory backend work already identified by this plan: risk 3 requires a backend-owned identity-binding operation, FR-007 requires the ICLA invalidation revision, and risk 2 makes the no-PR ICLA delta mandatory for GitHub. Counting only one upstream endpoint understates the cross-repository implementation scope and the three-week schedule risk.

This issue also appears in the following locations of the same file:

  • line 32
  • line 36
  • line 69
**Scale/Scope**: all LFX contributors; extends 1 existing Me-lens page, ~3–4 server routes (CLA-Group search, `userID` resolution/hand-off, ICLA invalidate, ECLA invalidate), 1 new upstream endpoint (ECLA invalidation) + possibly a status/listing extension per clarify outcomes.

- **Per-platform matrix**: *Gerrit* — no account step ever: Gerrit authenticates via the same LF SSO as Self Serve, so the LF identity from `user-from-token` already is the signing identity. *GitHub* — one-time Auth0 account link via M1's existing flow; resolved by GitHub ID. *GitLab* — **conditional** (decided 2026-08-07): the sign entry requires SS's GitLab account linking, which SS may ship before M2 completes. If it lands in time, GitLab enables in M2 by config (same pattern as GitHub; EasyCLA user records already carry `user_gitlab_id`/`user_gitlab_username`); if not, GitLab CLA groups (~2 projects) stay excluded from the "Sign CLA" entry and enable as a follow-up. Either way the account step MUST be platform-parametrized — GitLab is a config flip, never a redesign. M2 does not block on it.
- **Multiple linked GitHub accounts**: EasyCLA user records carry a *single* GitHub identity, so two linked GH accounts typically mean two EasyCLA records — an ICLA signed on one does **not** cover commits from the other by GitHub-ID match (only the fragile email fallback). When >1 GitHub account is linked, the sign flow MUST ask "which account will you contribute with?" and resolve the EasyCLA record by that GitHub ID; no silent auto-pick. Single-account users never see the picker.
- **No live platform session required at sign time**: a GitHub login is needed exactly once, to complete the Auth0 account link. The hand-off and signing work off the server-resolved `userID` — the Console decision screen is deep-linkable and the DocuSign ceremony needs nothing from GitHub. This is a constraint on open question 4: the no-PR ICLA request shape must accept the routed user without demanding a fresh GitHub OAuth, or it reintroduces the session dependency the proactive path removes.
- **Record binding + first-timer enrichment (resolved 2026-08-08)**: resolve the EasyCLA record by the (chosen) linked GitHub ID first; if none exists, fall back to `GET /v4/user-from-token` (lookup-or-create) and **enrich** the record with the linked GitHub ID/username so the PR check matches by GitHub ID rather than the fragile email fallback. The existing v1 `updateUser` API is **not** a usable enrichment primitive (verified): its `githubUsername` branch returns 400 for a previously-unseen username and, when a username exists, updates that record without verifying the linked identity belongs to the authenticated caller (`users/handlers.go:84-107`). Enrichment therefore requires a **small backend-owned, ownership-checked identity-binding operation** — not new business logic, but not zero backend work either. *([NEEDS CLARIFICATION]: shape of that binding operation and its gateway auth.)*
…tHub ID

PR #5144 review (copilot): the FR-003 fallback chain was internally
inconsistent. It asserts one-GitHub-identity-per-EasyCLA-record, then
prescribed enriching whatever record user-from-token returns.

createUserFromRequest (cmd/server.go:1004-1042) resolves by LF username
then LF email and returns any match -- including a record already bound
to a different GitHub identity. In the documented multi-account case
(A and B linked, record bound to A, user selects B), enrichment would
overwrite A's binding and silently break GitHub-ID matching for A's
existing commits.

Constrain enrichment to unbound fallback records; require a separate
record bound to the selected GitHub ID otherwise. This widens the
binding operation's scope to include create-record-bound-to-GitHub-ID,
since user-from-token's CreateUser path fires only when no record
matches LF username/email and so cannot produce the second record.

Also note refreshStoredUserIdentity touches only user_name/lf_email/
date_modified, so this is an M2-introduced risk rather than a
pre-existing overwrite bug.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Michal Lehotsky <mlehotsky@linuxfoundation.org>
Copilot AI review requested due to automatic review settings August 7, 2026 20:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Suppressed comments (7)

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:32

  • This PASS contradicts the plan's own required work: FR-008b adds persistent signature attributes, and line 80 requires a new backend-owned identity-binding operation. Reframe the gate as “no SS-owned state/new service” and assess the added EasyCLA state and backend operations rather than asserting there is no new state and only one new backend piece.
- **Simplicity**: no new services, storage, or state; extends M1's page and module; hand-off is the Console's existing deep link; invalidation reuses the existing impersonation middleware and revises (not replaces) the existing ICLA flow. The ECLA endpoint is the one genuinely new backend piece; the ICLA revision and no-PR request shape are bounded deltas to existing flows. PASS.

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:36

  • Question 1 is not resolved: spec line 98 still marks the identity-binding operation's create/enrich shape and gateway authentication as needing clarification. Calling it resolved can cause /speckit.clarify to skip a required security-sensitive contract.
**Post-Phase-1 re-check**: pending — re-run after `/speckit.clarify` confirms the remaining open items in [spec.md](spec.md) (questions 1 and 3 resolved; 5 narrowed; 2 and 4 are execution deliverables).

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md:98

  • The PR description still says fallback enrichment uses the existing v1 updateUser API and lists only questions 2 and 4 as remaining backend deliverables, while this requirement correctly establishes that updateUser is unusable and a new ownership-checked binding operation remains undefined. Update the PR description so reviewers and Spec Kit consumers do not miss this additional backend/security deliverable.
     The existing v1 `updateUser` API is **not** a usable enrichment primitive (verified): its `githubUsername` branch returns 400 for a previously-unseen username and, when a username exists, updates that record without verifying the linked identity belongs to the authenticated caller (`users/handlers.go:84-107`). Enrichment therefore requires a **backend-owned, ownership-checked identity-binding operation** covering both bind-to-unbound-record and create-record-bound-to-GitHub-ID. *([NEEDS CLARIFICATION]: shape of that binding operation, its create-vs-enrich branches, and its gateway auth.)*

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md:60

  • The three-state mapping cannot infer the stated reason from the existing boolean. eclaCoveredByCurrentApprovalList also returns false for a sanctioned/unknown company, a missing CCLA, and approval-evaluation errors (v2/my_clas/service.go:678-705); those rows would be mislabeled as an Approved List mismatch and incorrectly offered “Request approval.” Extend the API with a coverage reason (or an unavailable/error outcome) and expose the approval link only for an actual approval-list mismatch.
- **FR-010**: Each row MUST show a status: **Valid** (approved and covered); **Needs attention** with an explanatory note (ECLA signed but no longer matching the company's Approved List criteria — approved but not covered); **Invalidated** (not approved). Source: extend `GET /v4/my-clas`, which already computes the coverage evaluation per ECLA row and currently collapses it into a single boolean (resolved — see open question 3).
- **FR-011**: "Needs attention" ECLA rows MUST link "Request approval →" into the Contributor Console's existing request-authorization flow (deep link; no new SS flow).

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:81

  • Status evaluation is not retired: the current endpoint exposes only approved and aggregate valid, while covered=false conflates approval-list mismatch with sanctions, missing CCLA/company data, and evaluation failure. Keep this as backend work to return a reason/status that can drive the explanatory note and conditionally show “Request approval.”
4. **Status evaluation** (open question 3) — retired: `GET /v4/my-clas` already computes the coverage evaluation per ECLA row; M2 exposes it as a status field instead of collapsing it into `Valid`.

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/spec.md:103

  • This says the only unresolved item is the listing endpoint, but the resolver is defined only for GitHub URLs even though FR-001 accepts an unqualified pasted repo link and includes Gerrit-backed groups. Either restrict pasted links explicitly to GitHub or specify Gerrit URL resolution before marking the resolver decision narrowed.
5. **CLA-Group listing (narrowed 2026-08-08)** — search = CLA-group/project **names** from the existing project↔CLA-group mappings, plus an **exact repo-name/URL resolver**: the user types a repo name or pastes a GitHub URL, SS parses `owner/repo` and resolves it via the existing `RepositoryNameIndex` GSI (`GitHubGetRepositoryByName`) → `cla_group_id`. No fuzzy full-text search over repo names. Remaining question is only which existing listing endpoint to reuse for the name search.

specs/001-easycla-ss-integration-fable/m2-sign-cla-handoff/plan.md:16

  • The Go work is not merely “possibly” needed for the no-PR ICLA shape: risk 2 states it is required for the core GitHub sign-entry path. The technical context also omits the mandatory ICLA invalidation revision and identity-binding operation, materially understating the implementation scope.

This issue also appears in the following locations of the same file:

  • line 32
  • line 36
**Language/Version**: TypeScript (Angular 20.3 frontend + Node 22 / Express 4 SSR server) in `lfx-self-serve`; Go 1.25 in `cla-backend-go` for the ECLA-invalidation endpoint (and possibly status evaluation / no-PR ICLA shape, per clarify outcomes).


The existing v1 `updateUser` API is **not** a usable enrichment primitive (verified): its `githubUsername` branch returns 400 for a previously-unseen username and, when a username exists, updates that record without verifying the linked identity belongs to the authenticated caller (`users/handlers.go:84-107`). Enrichment therefore requires a **backend-owned, ownership-checked identity-binding operation** covering both bind-to-unbound-record and create-record-bound-to-GitHub-ID. *([NEEDS CLARIFICATION]: shape of that binding operation, its create-vs-enrich branches, and its gateway auth.)*
> Note: `refreshStoredUserIdentity` (called on both fallback hit paths) touches only `user_name`/`lf_email`/`date_modified` — it does not mutate GitHub fields, so this is an M2-introduced risk, not a pre-existing overwrite bug.
2. **ECLA invalidation endpoint** — new `cla-backend-go` API (swagger-first) for an employee ending their own CCLA coverage. Semantics settled per FR-008/FR-008a and the invalidation preamble: per-signature flag flip, record kept for audit, no Approved List mutation, CLA managers + user notified, backend-side ownership check (don't replicate `invalidateICLA`'s missing check). Main remaining backend deliverable alongside open question 4.
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.

2 participants