Skip to content

Statement-anchored shared responsibility: by-component reads, Inherited/Satisfied CRUD, control-centric queries#456

Merged
gusfcarvalho merged 13 commits into
mainfrom
lisa/fix/shared-responsibility-fixes
Jul 21, 2026
Merged

Statement-anchored shared responsibility: by-component reads, Inherited/Satisfied CRUD, control-centric queries#456
gusfcarvalho merged 13 commits into
mainfrom
lisa/fix/shared-responsibility-fixes

Conversation

@ccf-lisa

@ccf-lisa ccf-lisa Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Makes the statement the canonical anchor for everything carrying a sense of shared responsibility. Every export, inherited and satisfied now hangs off a statement-level by-component. Requirement-level by-components become legacy: readable, deletable, migratable — never newly creatable.

A control is too coarse to attribute responsibility against. Anchoring on the statement lets us say exactly which clause the upstream discharges and which the downstream must — the whole point of the export → inherit → satisfy loop.

Part 1 — The statement is the anchor

  • statementId is required on offering item create/update (400 with a message that explains why).
  • Coherence validation, which nothing did before: the (controlId, statementId, componentUuid, providedUuid) tuple must actually resolve to one real statement-anchored by-component inside this SSPProvided → Export → ByComponent(parent_type="statements") → Statement → ImplementedRequirement → ControlImplementation. Incoherent tuples → 400.
  • Backfill (migrateBackfillOfferingItemStatementIDs): walks provided_uuid → export → by_component for legacy statement_id IS NULL rows and derives the statement where it can. Requirement-anchored and dangling rows keep NULL — both counts are logged. The column stays nullable: statement-anchoring is a write-path constraint, not a schema change, so legacy rows stay readable and deletable.
  • Subscribe always statement-anchors. A legacy NULL-statement item → 422 naming the item, instead of silently falling back to requirement-anchoring (which is what produced by-component rows the API could never delete).
  • Subscribe reports what it materialized, in the response envelope's existing meta fielddata stays exactly the SSPLeverageLink[] every current caller reads:
"meta": { "created": {
  "implementedRequirements": [{ "uuid": "", "controlId": "AC-2", "created": true }],
  "statements":              [{ "uuid": "", "statementId": "AC-2_smt.a", "implementedRequirementUuid": "", "created": true }],
  "byComponents":            [{ "uuid": "", "statementUuid": "", "componentUuid": "", "created": false }]
}}

Part 2 — By-component reads (there were none, at either level)

GET for a single by-component (both levels), the list on a statement, and the export/provided + export/responsibilities collections. The single-by-component GET is deep-preloaded — export (provided + responsibilities), inherited, satisfied, responsible-roles, implementation-status — so the UI can refetch one by-component after editing a sub-resource.

Added the missing composite index on by_components(parent_id, parent_type). Every by-component resolution was an unindexed scan, and the new read models hammer it.

Part 3 — Inherited / Satisfied CRUD (the biggest hole)

Statement-level only. These had zero handlers — they were written only by Subscribe, so a downstream author could not mark a responsibility satisfied later, edit a description, attach responsible-roles, or drop a stale inherited entry without re-subscribing.

Leverage bookkeeping is kept honest:

  • DELETE .../inherited/:id where an SSPLeverageLink references it → 409 (owned by its subscription; unsubscribe instead). Hand-authored entries delete freely.
  • POST/DELETE .../satisfied re-derive the owning link's Satisfaction via the existing deriveSatisfaction, in the same transaction — so partial ⇄ full flips atomically with the write that causes it and the drift detector never reads stale bookkeeping.
  • PUT on either is metadata-only: providedUuid / responsibilityUuid are immutable (400) — they are the identities the link and drift detector join on.
  • POST .../satisfied validates the responsibility-uuid resolves to a responsibility on an export this by-component actually inherits from, scoped by (export_id, provided_uuid).

Part 4 — Close the requirement-level hole without blessing it

Added DELETE .../implemented-requirements/:reqId/by-components/:bcId (full deleteByComponentCascade) purely so legacy rows can be wound down. No requirement-level POST — pinned by a test asserting the route does not exist. All 15 requirement-level operations are marked @Deprecated in swagger, pointing at the statement-level equivalents.

Part 5 — Control-centric queries

  • GET /oscal/ssp-export-offerings/by-control/:controlId → every published offering item for a control, with offering / upstream SSP / component / provided / responsibilities all resolved server-side. Honours the same trust boundary as the flat catalog: ssp-export-offering:read only, never ssp:read on the upstream. Optional ?downstreamSspId= filters to offerings that SSP may actually subscribe to.
  • GET /oscal/system-security-plans/:id/shared-responsibilityprovides / inherits / satisfies / legacy, flattened and statement-keyed. Optional ?controlId=.

The inherits arm reuses the LeveragedControls projection (extracted to projectLeveragedControls), so satisfaction is derived in exactly one place and the two surfaces cannot disagree — a fixed ~6 queries regardless of link count, not an N+1.

No new authz actions.

Two latent bugs fixed

  1. Blind Save in both by-component PUTs. They db.Save()-ed a struct rebuilt by UnmarshalOscal from the request body: every omitted field was zeroed, and any nested export/inherited/satisfied in the payload was upserted as a GORM association with no cascade cleanup, diverging from deleteByComponentCascade and bypassing the leverage bookkeeping the sub-resource routes enforce. Now metadata-only; nested subtrees in the body are ignored (not rejected — a client round-tripping a GET into a PUT must not start failing).
  2. POST /api/oscal/import bypassed validation and lied about no-ops. It never called validateByComponentImplementationStatus, so a bad implementation-status sailed into the tree; and FirstOrCreate keyed on the OSCAL UUID silently writes nothing when re-importing a changed SSP under an existing UUID, while reporting "Successfully imported". Now validated at both anchoring levels, and the no-op says so plainly. (Success stays true — flipping it would turn an idempotent re-import into a 400.)

A third bug, caught by self-review

Writing the responsible-roles test the spec asked for exposed a live bug in this PR's own code: Association("ResponsibleRoles").Append(roles) writes the role rows but does not cascade into each role's Parties many2many — every responsible_role_parties join row was silently never written. Switched to tx.Create(&roles) with an explicit parent_id/parent_type (derived from the parsed GORM schema, not hardcoded), the same cascade path the nested by-component create already relies on.

Tests

~30 new tests. Statement anchoring (missing statementId → 400; each incoherent tuple → 400; NULL-statement subscribe → 422; backfill derives / leaves NULL / is idempotent). Subscribe's created block (inserted vs reused rows; tree is never requirement-anchored). By-component reads. The blind-Save regression (a PUT omitting the subtrees leaves them intact; a PUT carrying a nested export does not upsert it). Inherited/Satisfied round-trips, immutable uuids, 409 on subscription-owned inherited, satisfaction re-derivation partial ⇄ full. Responsible-roles + their m2m join rows across create/update/delete, with the shared Party surviving. Requirement-level cascade delete; no requirement-level POST route. Import validation + honest no-op. Both new queries, incl. case-folded control matching, allow-list filtering, and a router-level test that by-control isn't shadowed by /:id.

Existing leverage/offering fixtures were migrated from requirement-anchored to statement-anchored exports — not incidental churn: an offering item whose providedUuid was invented out of thin air is exactly the incoherent tuple the new coherence check rejects.

Verification

go build ./... · go vet ./... and go vet -tags integration ./internal/... · make test · full make test-integration (incl. TestSSPLeverageIntegrationSuite, TestSystemSecurityPlanApiIntegrationSuite, TestSubscribeAllowlistIntegrationSuite, TestLeverageDriftIntegrationSuite, and a new TestImportApiIntegrationSuite) · golangci-lint run0 issues · swagger regenerated (docs/ committed). All green.

internal/authz/cedar_test.go is gofmt-dirty on main already; untouched here.

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

This PR re-anchors “shared responsibility” data on statement-level by-components (instead of requirement-level), adds the missing read + CRUD surfaces to manage inherited/satisfied responsibilities, and introduces control-centric projections to support UI/query use-cases without upstream ssp:read.

Changes:

  • Enforce statementId on export-offering item writes, validate tuple coherence, and backfill legacy statement_id where derivable.
  • Add by-component GETs, statement-level Inherited/Satisfied CRUD, and a shared responsibility rollup endpoint.
  • Add a by-control catalog endpoint and refactor leveraged-controls logic into a shared batched projection; tighten import validation and messaging.

Reviewed changes

Copilot reviewed 18 out of 20 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
internal/service/relational/system_security_plan.go Adds composite index for by-component parent lookup (parent_id,parent_type).
internal/service/migrator.go Adds backfill migration for legacy offering items missing statement_id.
internal/service/migrator_test.go Tests backfill behavior (derivable vs underivable vs dangling; idempotency).
internal/api/handler/oscal/system_security_plans.go Adds/rewires SSP routes; replaces blind Save() with metadata-only updates; adds advisory lock scope and responsible-role handling utilities.
internal/api/handler/oscal/ssp_by_components.go New by-component read surface + statement-level Inherited/Satisfied CRUD and leverage satisfaction resync.
internal/api/handler/oscal/ssp_shared_responsibility.go New /shared-responsibility rollup endpoint (provides/inherits/satisfies/legacy).
internal/api/handler/oscal/ssp_shared_responsibility_test.go Extensive unit tests covering anchoring, reads, CRUD, rollup, by-control catalog behaviors, and responsible-role m2m correctness.
internal/api/handler/oscal/ssp_leverage.go Adds batched allow-list resolution, subscribe meta reporting, statement-anchored subscribe enforcement, and shared leveraged-controls projection.
internal/api/handler/oscal/ssp_leverage_unit_test.go Updates unit fixtures and adds tests for created-flags and statement creation helpers.
internal/api/handler/oscal/ssp_leverage_test.go Updates integration fixtures and assertions for statement-anchored subscribe materialization.
internal/api/handler/oscal/ssp_leverage_drift_integration_test.go Updates drift fixture to use statement-anchored export + offering items.
internal/api/handler/oscal/ssp_leverage_allowlist_integration_test.go Updates allowlist integration fixture to use coherent statement-anchored offering items.
internal/api/handler/oscal/ssp_export_offerings.go Requires statementId on item writes, adds coherence validation, and adds GET /by-control/:controlId catalog projection.
internal/api/handler/oscal/ssp_export_offerings_test.go Updates offering fixtures to seed coherent statement-anchored exports and item bodies.
internal/api/handler/oscal/import.go Validates by-component implementation-status on import; clarifies no-op re-import messaging.
internal/api/handler/oscal/import_unit_test.go Tests import validation and SSP route presence/absence contracts (no requirement-level POST).
internal/api/handler/oscal/import_integration_test.go Integration tests for import rejecting invalid status and reporting no-op on existing UUID.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/api/handler/oscal/system_security_plans.go Outdated
Comment thread internal/api/handler/oscal/system_security_plans.go
Comment thread internal/api/handler/oscal/ssp_shared_responsibility.go Outdated
Comment thread internal/api/handler/oscal/system_security_plans.go Outdated

@gusfcarvalho gusfcarvalho 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.

Findings inline (9). Verified first: CI is green 4/4, and go build / go vet / unit tests pass locally — nothing here is a ship-blocker, and I confirmed the cedar_test.go gofmt note is genuinely pre-existing on main.

Headline: the statement-anchoring work is sound and the batching discipline is genuinely good, but the PR asserts two invariants it does not fully enforce. (1) "an SSPLeverageLink never points at nothing" is enforced with a 409 on the inherited sub-resource DELETE, while deleteByComponentCascade — reached by three routes, one of them added here — deletes those same rows unconditionally, with no FK to stop it; the result is links that silently report partial and 500 on re-attest. (2) the new offering-item coherence check validates control-id case-insensitively but stores the client's casing, and Subscribe matches it exactly — so a mixed-case controlId forks a duplicate ImplementedRequirement in the downstream tree.

Two Mediums: the satisfied DELETE skips the advisory lock its CREATE takes (lost update on the link's cached Satisfaction, which the drift detector reads), and the rollup's legacy arm reports every requirement-anchored by-component rather than only those carrying shared responsibility — so any imported OSCAL SSP has its whole control implementation listed as debt. Worth reading that one alongside its test: the fixture seeds a by-component with no export and asserts the reason "requirement-anchored export", which pins the bug rather than catching it.

Rest are Low: a third satisfaction-derivation input scope in Subscribe, an unvalidated provided-uuid on the inherited POST, unauthorized downstreamSspId on ByControl, and two test gaps where assertions can't fail.

Credit where due — bulkResolveUpstreamResponsibilities scoping by (export_id, provided_uuid) rather than provided_uuid alone is a subtle correctness point handled correctly, the blind-Save and Association().Append m2m fixes are both real bugs you caught yourself, and the git notes are the best design record I've seen on a PR in this repo — several of them pre-empted questions I was about to raise.

Comment thread internal/api/handler/oscal/ssp_by_components.go
Comment thread internal/api/handler/oscal/ssp_export_offerings.go
Comment thread internal/api/handler/oscal/ssp_by_components.go
Comment thread internal/api/handler/oscal/ssp_shared_responsibility.go Outdated
Comment thread internal/api/handler/oscal/ssp_leverage.go
Comment thread internal/api/handler/oscal/ssp_by_components.go Outdated
Comment thread internal/api/handler/oscal/ssp_export_offerings.go Outdated
Comment thread internal/api/handler/oscal/ssp_shared_responsibility_test.go
Comment thread internal/api/handler/oscal/import_unit_test.go Outdated
@ccf-lisa

ccf-lisa Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the three review findings in ff12cb4. All were valid; two were live correctness bugs.

[High] deleteByComponentCascade orphaned leverage links. You were right that the 409 was bypassable: the guard only sat on the inherited sub-resource DELETE, while three routes reach the cascade and destroy Inherited rows unconditionally — including the statement-level by-component DELETE, which targets the very by-component Subscribe always creates. Hoisted the check into deleteByComponentCascade as you suggested, so all three paths inherit one definition of "owned by a subscription"; each handler maps it to 409. Confirmed DELETE /system-security-plans/:id does not route through the cascade (it's a plain db.Delete), so deleting an SSP is unaffected.

The failure mode you described is now covered by a test that asserts the exact consequences: the by-component and requirement deletes both 409, and projectLeveragedControls still resolves the link to a live inherited row (rather than falling back to the zero by-component id and reporting partial with everything outstanding). Plus a control test that a hand-authored inherited entry — no link — still deletes freely, so the cascade isn't bricked.

[High] control-id casing split the downstream tree. Confirmed and fixed at the validator, as you suggested: it now adopts the requirement's canonical casing (req.ControlID = requirement.ControlId, req taken by pointer), so what the item stores is what findOrCreateImplementedRequirement later matches on exactly. The end-to-end test seeds a downstream that already implements ac-2, POSTs an item as "AC-2", subscribes, and asserts the downstream still has one requirement row — it would have had two before. Upholds the repo's invariant: store catalog-canonical casing, fold only when matching. Left the exact-match in findOrCreateImplementedRequirement alone, since as you note it predates this PR.

Also made the statement-id/control-id asymmetry deliberate rather than accidental: statement-ids are author-coined with no canonical form and findOrCreateStatement matches them exactly, so folding case there would create precisely the split we're preventing for control-ids. That reasoning is now in the code.

[Medium] satisfied DELETE skipped the advisory lock. Correct — with only the create side locking, the lock serialized create-vs-create and nothing else, and the SET value being computed in Go makes the concurrent create/delete case a plain lost update. Took the same lock at the top of the delete transaction. Also renamed lockByComponentSubtreeCreatelockByComponentSubtreeWrite, since the create-only name is exactly why this got missed. The advisory-lock key string deliberately stays "export-create:" — it's only meaningful as a value all writers agree on, and changing it would stop old and new pods serializing against each other mid-rolling-deploy.

Verification: go build, go vet (both tags), unit, full integration suite, golangci-lint (0 issues), swagger regenerated.

Note: these came in as a pending (unsubmitted) review, so the inline threads aren't visible to the bot and I couldn't reply on them directly or resolve them. If you submit the review, they'll thread properly.

@gusfcarvalho gusfcarvalho 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.

Round 2 on ff12cb4. Verified: go build, go vet, unit tests pass locally; CI green (integration still running at time of writing).

Confirmed fixed — all three verified against the code, not just the reply:

  • deleteByComponentCascade orphaning links (High): guard hoisted into the cascade itself, placed before any destructive work, and all three handlers map it to 409. This is the right shape — one definition of "owned by a subscription", inherited by every path. Nice catch on checking that DELETE /system-security-plans/:id doesn't route through the cascade.
  • Control-id casing (High): req.ControlID = requirement.ControlId after the fold-compare, req taken by pointer, so CreateItem/UpdateItem both persist canonical bytes. TestOfferingItemStoresCanonicalControlIdCasing drives it end-to-end — seeds a downstream that already implements ac-2, POSTs "AC-2", subscribes, asserts exactly one requirement row. That is the test I would have written.
  • Satisfied DELETE lock (Medium): lock taken, and the rename to lockByComponentSubtreeWrite is the better fix — you're right that the create-only name is why it got missed.

I also want to call out TestDeletingByComponentWithHandAuthoredInheritedSucceeds: adding the negative control (the guard keys on a real link, so a hand-authored inherited still deletes freely) is what makes the 409 test meaningful rather than something that would pass if the cascade were simply broken. That is the habit that catches the next bug.

One new finding (Medium), inline: ReAttest is a third read-modify-write over the same subtree and doesn't take the lock the new doc comment now says every such writer must. It's in scope precisely because of this PR — before the satisfied CRUD landed there was no concurrent writer for it to race. Same lost-update, one line to fix.

Still open from round 1 (6): the legacy arm over-reporting (Medium — this one also needs its test fixture changed, since the fixture currently pins the bug), Subscribe's third satisfaction-derivation scope, the unvalidated provided-uuid on the inherited POST, ByControl's unauthorized downstreamSspId, and the two tests with assertions that can't fail. Happy to take a decision-and-a-doc-line on the ByControl one rather than code — I flagged it so it wouldn't be settled by omission, not because I think it must change.

Comment thread internal/api/handler/oscal/system_security_plans.go Outdated

@gusfcarvalho gusfcarvalho 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.

Round 3 on 58a8d91. One finding, inline — and it is the only thing standing between this PR and an approve.

Subscribe still does not take lockByComponentSubtreeWrite. 58a8d91 fixed ReAttest and rewrote the lock doc to enumerate all four read-modify-write writers — explicitly naming Subscribe (via resyncLeverageSatisfaction) and warning that "partial adoption is worse than useless: it serializes only the writers that opted in, while reading as though the subtree were safe." But the lock call was never added to Subscribe. lockByComponentSubtreeWrite appears exactly once in ssp_leverage.go, in ReAttest. So the code is now in precisely the state its own new comment warns about, and the thread was resolved as though both had landed.

I want to be clear that I am not re-litigating a design decision — you agreed with this one and documented it; the call site is just missing. One line after findOrCreateByComponent, as in the thread.

Everything else across all three rounds is confirmed fixed, re-read in the code rather than taken from the replies: the cascade guard and its 409s, control-id canonicalization, the satisfied CREATE/DELETE locks, the legacy arm (with its fixture corrected — that one had been pinning the bug), Subscribe's satisfaction derivation collapsing to a single scope, the inherited provided-uuid resolution, the ByControl trade-off written out as an explicit decision, and both weak tests.

Worth saying plainly: the rewritten lock doc is better than what I asked for. Enumerating every writer and naming partial adoption as the hazard is what stops the next person reintroducing this — it just needs the fourth writer to comply with it. Approving the moment that lands. Local build/vet/unit are green; CI still running.

Comment thread internal/api/handler/oscal/ssp_leverage.go

@gusfcarvalho gusfcarvalho 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.

Approving 8f45bcf. All 10 findings across four rounds are fixed, and I re-read the code for each rather than trusting the replies — which mattered, since one round resolved a thread with only half the fix landed.

Verified green: go build, go vet, gofmt, unit tests locally; CI 4/4 including the full 7-minute integration suite. (The one gofmt complaint, internal/authz/cedar_test.go, I confirmed is already dirty on main and untouched here.)

The last fix is better than what I asked for. I asked for one line in Subscribe. You added it — and then made resyncLeverageSatisfaction take the lock itself, so the invariant is enforced by construction on the path three of the four writers use, rather than by everyone remembering. That is the right instinct: the reason this was half-missed twice is that "every writer must remember X" is not an invariant, it is a hope. Moving it into the function that is the read-modify-write closes the class, not just the instance.

TestEveryLeverageSatisfactionWriterTakesTheSubtreeLock is the part I want to single out. Asserting a concurrency invariant structurally, via the AST, because the advisory lock is a no-op under the sqlite unit driver and no unit test could ever observe the race — that is a genuinely good answer to "this is untestable." The failure message names the writers and restates why partial adoption is the hazard, so the next person to trip it gets the reasoning, not just a red X. I would like to see this pattern used elsewhere in the codebase.

Trajectory across the review: two Highs (a 409 guard bypassable by deleting the parent; control-id casing forking a duplicate requirement in the downstream tree), three Mediums (three separate unlocked read-modify-writes on link.Satisfaction; the legacy arm reporting every ordinary requirement-anchored by-component as debt), and five Lows. Two of those were only findable because the tests pinned the bug rather than catching it — the legacy fixture asserting a "requirement-anchored export" on a by-component with no export, and the filter test whose helper dropped the status assertion so its negative cases passed on a 500. You fixed the fixtures, not just the code, which is the harder and more honest call.

The design record in the git notes did real work here: several times it pre-empted a finding I was about to raise, and the pushback on PUT-vs-PATCH semantics was correct and well-argued. Nice piece of engineering.

@ccf-lisa

ccf-lisa Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

PR approved. Marking ready for e2e.

Signed-off-by: Gustavo Carvalho <gustavo.carvalho@container-solutions.com>

@gusfcarvalho gusfcarvalho 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.

Findings inline (21 threads).

Headline: 1603700 broke the integration build, and CI agrees — internal/service/worker no longer compiles under the integration tag because one LeveragedAuthUUID assignment site was missed. One-line fix, but make test-integration cannot pass until it lands.

This is a re-review of the commits pushed after the approval at 8f45bcf, plus a full pass over the implementation. Scope note: the PR body still claims full make test-integration green and 0 lint issues — both were true at 8f45bcf and were invalidated by 1603700.

Beyond the blocker, three findings I would want addressed before this merges:

  • resyncLeverageSatisfaction can overwrite the wrong link's Satisfaction — it derives from one by-component's satisfied set but finds the link by (downstream_ssp_id, provided_uuid). That unique index constrains links, not inherited rows, and nothing stops a second by-component in the same SSP inheriting the same provided-uuid. Silent corruption on the drift path, bidirectional.
  • deleteByComponentCascade's new subscription guard is TOCTOU-racy — it checks a preloaded id set but bulk-deletes by by_component_id, and never takes lockByComponentSubtreeWrite. A concurrent Subscribe gets its inherited row deleted anyway, dangling SSPLeverageLink.InheritedUUID with no FK to catch it. This is the "partial adoption is worse than useless" case the lock's own comment argues against.
  • The new filter↔responsibility API ignores Control's composite (catalog_id, id) PK in three places, so with two catalogs defining the same control id it links the wrong catalog's control and fails to unwind the link on detach. resolveCatalogIDForControl already exists for exactly this, and the handler has the SSP id in hand.

Two of the PR's own claims do not hold up, which is why I am flagging them rather than trusting the descriptions:

  • updateByComponentMetadata still zeroes omitted scalar fields. Map-form Updates writes every key, so the fix narrowed the blast radius from all columns to six — within those six, omitted still means zeroed. The inline comment frames that as the bug being fixed, and the swagger now publishes the stronger claim. TestUpdateByComponentDoesNotClobberSubtreesOrOmittedFields does not catch it: despite the name it only asserts the omitted subtrees survive.
  • TestSubscribeReportsReusedRowsAsNotCreated asserts Created == true. Statements[].Created == false and ByComponents[].Created == false are asserted nowhere in the PR, so a regression hardcoding created: true would pass the suite.

subtree_lock_invariant_test.go deserves a second look too: as a hand-maintained allowlist it cannot fail for the sixth-writer regression it exists to catch, and the "no test can observe the lock" justification does not hold — there is a real Postgres integration suite here, and nothing drives concurrent writes against the same by-component. As it stands the invariant would still be green if the lock body were return nil.

Verified rather than assumed: go build, go vet, go test, and go test -race all pass locally; the integration break reproduces locally and matches CI's log verbatim; the gofmt regression is clean at main/8f45bcf and dirty at 1603700; the FirstOrCreate fix was probed empirically against this repo's GORM version and is correct.

What is genuinely good: the trust boundary holds throughout — ByControl evaluates ssp-export-offering:read only and never ssp:read on the upstream. No blind Save() survives in the new code. The LeveragedAuthUUID nullability change is otherwise clean and its migration is correctly ordered and idempotent. Swagger is properly regenerated. projectLeveragedControls reuse genuinely keeps the two surfaces consistent at a fixed query count. And filter_responsibilities_test.go is the cleanest test file in the PR — every zero-count assertion has a proven non-zero precondition, which is exactly the discipline the two tests above are missing.

Comment thread internal/service/relational/ssp_leverage.go
Comment thread internal/api/handler/oscal/ssp_by_components.go Outdated
Comment thread internal/api/handler/oscal/system_security_plans.go Outdated
Comment thread internal/api/handler/filter.go Outdated
Comment thread internal/api/handler/filter.go Outdated
Comment thread internal/api/handler/oscal/ssp_by_components.go Outdated
Comment thread internal/api/handler/oscal/ssp_shared_responsibility_test.go
Comment thread internal/api/handler/oscal/subtree_lock_invariant_test.go Outdated
Comment thread internal/api/handler/oscal/ssp_leverage_drift_test.go Outdated
Comment thread internal/api/handler/oscal/system_security_plans.go Outdated
@ccf-lisa

ccf-lisa Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Round 5 addressed in 4f43822 — all 21 threads, each fixed and resolved with per-thread notes. Highlights and the places I did something other than what was suggested:

Blocker. Confirmed and fixed: risk_evidence_worker_test_helpers_test.go:49 dropped the field (optional now, never read). go vet -tags integration ./... is clean. You are right that the PR body claim was invalidated by 1603700 — corrected below.

resyncLeverageSatisfaction (:998). Your mechanism is exactly right, and worth restating because it inverts the doc I had written: idx_ssp_leverage_links_unique is what enables the bug, not what prevents it. It guarantees one link per (ssp, provided_uuid) — but InheritedControlImplementation has no unique index at all, so two by-components can each inherit the same provided-uuid while only one link exists, and resync for B rewrites As link from Bs satisfied set. Now keyed on inherited_uuid IN (this bcs inherited ids), one query, N+1 gone. I also added the 409 you suggested on a provided-uuid already inherited in the same SSP.

Two corrections, for the record:

  1. len(bc.Inherited) > 0 (:5106) is not an independent hole. With an empty slice, inherited_uuid IN ? renders as a no-match, so linkCount is 0 and the guard passes either way — it is an optimization, not a bypass. The real defect is the one you led with (missing lock + fixed-set-vs-bulk-delete), and it applies at any length. Both fixed; deletes are now scoped to the ids actually checked, which also closes the orphaned responsible_roles.
  2. The lock-invariant comment (:18-22) says no unit test can observe the lock, which is literally true — it no-ops off Postgres. But your substantive point stood, so the structural test is now discovery-based (AST-walks the package for anything calling resyncLeverageSatisfaction or writing SSPLeverageLink.Satisfaction), and I verified it fails both when the lock is removed from resync and for a newly-added unlocked writer — the exact regression the allowlist could not catch. Plus subtree_lock_concurrency_integration_test.go, real Postgres, two concurrent goroutines, as you asked.

One deliberate departure — filter catalog scoping (:522). I did not make a profile miss a 404. resolveCatalogIDForControls own doc says a downstream need not have the leveraged control in its profile, and the existing fixture seeds no ssp_profiles, so a hard 404 would reject legitimate use. Instead: resolve via the SSPs profiles (authoritative); if they dont import it, fall back to the catalogs defining it — exactly one resolves, more than one is a 400. No arbitrary choice is ever made, which was the actual bug. The join carries no CAST (the CAST AS uuid in the oscal helper would break the sqlite unit suites; this follows system_component_suggestions.gos cast-free precedent). Happy to switch to a strict 404 if youd rather.

By-component PUT (:4448). Went with merge semantics rather than weakening the docs, since the swagger claim is already published — the update map is built from the keys actually present in the raw body. Note responsible-roles deliberately stays replace: TestUpdateByComponentReplacesResponsibleRoles pins that an omitted list clears them. I had gated that too at first and the test correctly caught it as scope creep.

Tests. TestSubscribeReportsReusedRowsAsNotCreated now makes a second subscribe call and asserts Created == false for requirement, statement and by-component — you were right that this was asserted nowhere in the repo. Dedup assertions moved to their own test under a name that says what they do. The PUT fixture now populates props/links/set-parameters (and it already set remarks, so the wipe was live and simply unasserted, as you spotted). Added TestDetachUnwindsLinkDespiteUnowningSibling and TestAttachRejectsAmbiguousControlAcrossCatalogs.

Verification. go build ./... clean; go vet -tags integration ./... clean; full unit suite 37/37 green; gofmt clean except cedar_test.go (pre-existing on main, as you noted); swagger regenerated so check-diff stays clean.

One gap, stated plainly: I could not run the integration suites locally — no Docker daemon on this machine. They compile under -tags integration but the two new concurrency tests are unexercised at runtime; CI needs to confirm them. I am not claiming make test-integration is green this time.

@ccf-lisa

ccf-lisa Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

CI is fully green on 2fd340d — lint, check-diff, unit-tests and integration-tests all pass.

The last outstanding failure was a staticcheck SA1019 (parser.ParseDir deprecated since Go 1.25) in internal/api/handler/oscal/subtree_lock_invariant_test.go. parsePackageFiles now enumerates with os.ReadDir and parses each non-test .go file via parser.ParseFile, filtering on the package name to preserve the previous pkgs["oscal"] behaviour. I deliberately did not migrate to golang.org/x/tools/go/packages as the deprecation suggests — it would add a full package load and build-tag resolution to a test that only needs this one directory's syntax trees, and build tags are irrelevant here since the invariant walk wants every file in the package regardless.

There are no open review threads and nothing further is actionable on my side. The PR still carries a CHANGES_REQUESTED decision from the earlier review round; this needs a human pass to clear it.

@ccf-lisa

ccf-lisa Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

Closing the verification gap from my last comment: Docker became available, so I ran what I previously could not. make test-integration is green on 4f43822 — every package, including the two suites I could only compile before.

The new/changed suites specifically:

--- PASS: TestSubtreeLockConcurrencySuite (2.21s)
    --- PASS: TestConcurrentSatisfiedWritesConvergeOnProjection (0.37s)
    --- PASS: TestConcurrentSatisfiedWriteAndInheritedDeleteDoNotDangle (0.36s)
--- PASS: TestFilterResponsibilityIntegrationSuite (4.41s)
    --- PASS: TestAttachRejectsAmbiguousControlAcrossCatalogs (0.37s)
    --- PASS: TestDetachUnwindsLinkDespiteUnowningSibling (0.35s)
    ... 5 pre-existing cases also pass
--- PASS: TestLeverageDriftIntegrationSuite (2.63s)

More importantly, I verified the concurrency test is not vacuous — which was the substance of your point about the structural test. You wrote that the whole invariant would still be green if lockByComponentSubtreeWrites body were replaced with return nil. So I did exactly that (forced the early return nil, defeating the tx.Name() != "postgres" guard) and re-ran:

--- FAIL: TestSubtreeLockConcurrencySuite/TestConcurrentSatisfiedWritesConvergeOnProjection
    Error: Not equal:
    Messages: both responsibilities are satisfied, so the stored satisfaction must be full —
              `partial` means one writers derivation overwrote the others with a stale value

That is the lost update itself, reproduced against real Postgres: two concurrent satisfied-writes, both deriving partial from their own single row, second clobbering the first. Restored the lock, back to green, git diff clean. So the lock is now covered by a test that genuinely goes red when it stops working, rather than only by structure.

For completeness, the discovery-based structural test was verified the same way earlier: it fails when the lock is removed from resyncLeverageSatisfaction, and when a new unlocked writer is added — the regression the old allowlist could not catch.

No code changed in this pass; this is verification only, so 4f43822 still stands. Sorry for the earlier hedge — it was accurate at the time, but it is now simply green.

@gusfcarvalho gusfcarvalho 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.

Findings inline (14). No blockers — verified at 2fd340d: go build, go vet, go test ./internal/... and gofmt all clean, CI 4/4 green.

Headline: one HighAttachResponsibility never checks that the filter belongs to the SSP it is attached for, and because the route guards on the filter id, no permission is evaluated against the body-supplied sspId at all. The rest are Mediums around a genuine gap in the otherwise-solid lock story (the new Postgres concurrency suite does not currently fail if the lock is removed), the duplicate-inherited invariant being enforced on the hand-authored path but not on Subscribe, an N+1 hiding under the "fixed six queries" comment, and UpdateItem shipping untested.

Credit where due: I enumerated all four SSPLeverageLink.Satisfaction writers and each now takes lockByComponentSubtreeWrite inside its own transaction, with resyncLeverageSatisfaction and deleteByComponentCascade both self-locking — that invariant is complete. All three dangling-InheritedUUID delete paths are guarded, every new route carries an authz guard, and I found no nil-deref panics or IDOR. The FindInBatches rework is correct specifically because gorm uses keyset pagination there. The git notes design record made this reviewable — several things I would otherwise have flagged were already answered there, and I left them alone.

Two notes outside the diff, for information only, not asks:

  • internal/service/scheduler trips the race detector under go test -race. Pre-existing and untouched by this PR — out of scope here, but worth a ticket.
  • I initially reviewed two commits behind: git fetch fails silently against the SSH remote, so 4f43822 and 2fd340d were invisible. Four findings I had drafted were already fixed in those commits and were dropped before this review — flagging in case the same stale-tree trap is hitting other reviewers or tooling.

Comment thread internal/api/handler/filter.go
Comment thread internal/api/handler/oscal/ssp_leverage.go
Comment thread internal/api/handler/oscal/subtree_lock_concurrency_integration_test.go Outdated
Comment thread internal/api/handler/oscal/subtree_lock_concurrency_integration_test.go Outdated
Comment thread internal/api/handler/oscal/subtree_lock_concurrency_integration_test.go Outdated
Comment thread internal/api/handler/oscal/ssp_export_offerings.go
Comment thread internal/api/handler/oscal/ssp_by_components.go Outdated
Comment thread internal/api/handler/oscal/ssp_by_components.go Outdated
Comment thread internal/api/handler/oscal/ssp_export_offerings.go
Comment thread internal/api/handler/oscal/ssp_by_components.go

@gusfcarvalho gusfcarvalho 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.

Round 6 re-review at 5893c06. All 14 findings from the last round are genuinely fixed — I verified each against the code rather than taking the note at its word. One new Medium inline, introduced by the Subscribe fix itself.

Verified at this head: go build, go vet, full unit suite, and gofmt all clean (only the pre-existing internal/authz/cedar_test.go). CI: check-diff, lint and unit-tests green; integration-tests still running at the time of writing.

The one new issue: ensureProvidedUUIDNotAlreadyInherited is correct and correctly shared across both creation paths, but Subscribe calls it once per item, and the helper re-derives every requirement / statement / by-component id in the SSP each time — ~4N queries for an N-item subscribe, plus unbounded IN lists that run into the same 65535 bind ceiling this commit reasons about for allowedDownstreamSQL. A single correlated EXISTS fixes both; details and a suggested query are in the thread. Keeping the call inside the loop is right — hoisting it would miss two items in one request sharing a provided_uuid.

Things I want to call out positively, because they are the difference between a fix and a fixed-and-proven fix:

  • The lock is now proven, not just asserted. Neutering lockByComponentSubtreeWrite and confirming TestConcurrentSatisfiedWritesConvergeOnProjection fails with expected full / actual partial is exactly the evidence that finding asked for. Placing the rendezvous before the lock rather than after — because after would self-deadlock — is the correct reading of why the naive fix does not work.
  • The honest limit on the dangle test. Stating plainly that it is no longer vacuous but still not lock-discriminating, and pointing its doc comment at the test that is, is a better outcome than quietly claiming coverage that is not there.
  • Two deviations from the guidance, both right. Ordering the ownership check after the SSP-existence check (a hoist turns a 404 into a misleading 400) and dropping the unused excludeByComponentID parameter. Catching the 404→400 regression by running the suite, then recording why the order matters in a comment, is the right instinct.
  • The out-of-scope catch was surfaced, not smuggled. The satisfied create had the same post-commit 5xx defect as the reload at :601; fixing both and flagging explicitly that the second was not in any thread is the right way to handle it.
  • allowedDownstreamSQL is a faithful translation of isDownstreamAllowed, and the reasoning for correlated EXISTS over WHERE id IN (?) is sound — which is precisely why the Subscribe path above should follow the same principle.

Nothing else outstanding. Once the EXISTS rewrite lands and integration-tests are green, this is good to go from my side.

Comment thread internal/api/handler/oscal/ssp_leverage.go

@gusfcarvalho gusfcarvalho 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.

Approving at 07f1f13. All 15 findings across rounds 6 and 7 are fixed and verified; every review thread is resolved.

Verified at this head, not taken on trust: go build -mod=readonly ./..., go vet ./..., the full unit suite, and gofmt all clean (the only gofmt-dirty file is internal/authz/cedar_test.go, which is dirty on main already and untouched here). CI 4/4 green: check-diff, lint, unit-tests, integration-tests.

The final fix is exactly right: ensureProvidedUUIDNotAlreadyInherited now walks control_implementations → implemented_requirements → statements → by_components → inherited_control_implementations in one correlated query with two bind parameters, independent of SSP size, with no CAST (the join columns are uuid on both sides, and casting would break the sqlite unit suites) and parent_type literals matching the previous walker. The call stayed inside the item loop, and byComponentIDsForSSP was moved to the test file rather than left as dead production code or silenced with a lint exclusion.

One correction I want to record, because it went the right way: I justified keeping that call inside the loop by claiming per-item evaluation is what catches two items in one request sharing a provided_uuid. That premise was wrong — Subscribe already rejects that at request level with a 400 via the seenProvidedUUIDs check (ssp_leverage.go:637), in a validation pass that runs before the item loop, and that guard predates the per-SSP invariant. The author found this by writing the test I asked for, watching it fail with 400 instead of 409, and then asserting the actual behaviour and documenting which guard catches it. Correcting a reviewer's stated reasoning rather than quietly implementing around it is the better outcome, and the resulting test is more useful than the one I asked for.

What this PR does well, across the whole review:

  • Invariants are proven, not asserted. Neutering lockByComponentSubtreeWrite and confirming the lost-update test fails with expected full / actual partial is the evidence that finding needed. Placing the rendezvous before the lock — because after would self-deadlock — is the correct reading of why the naive fix fails.
  • Honest about limits. Stating plainly that the dangle test is no longer vacuous but still not lock-discriminating, and pointing its doc comment at the test that is, beats quietly claiming coverage that is not there.
  • Deviations from guidance were reasoned and recorded. Ordering the ownership check after the SSP-existence check (hoisting turns a 404 into a misleading 400, caught by running the suite) and dropping an unused parameter.
  • Out-of-scope catches were surfaced, not smuggled. The satisfied create carrying the same post-commit 5xx defect as the reload was fixed and explicitly flagged as not having been in any thread.
  • The git notes design record made this reviewable at a scale — 12.6k additions — where it otherwise would not have been.

Nothing outstanding from my side.

@ccf-lisa

ccf-lisa Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

PR approved. Marking ready for e2e.

@gusfcarvalho
gusfcarvalho merged commit ae6434a into main Jul 21, 2026
4 checks passed
@gusfcarvalho
gusfcarvalho deleted the lisa/fix/shared-responsibility-fixes branch July 21, 2026 13:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants