fix(policies): remove archived policies from framework controls after unlinking - #3513
Merged
tofikwest merged 4 commits intoJul 28, 2026
Merged
Conversation
… unlinking
## Problem
A customer manually unlinked archived policies from controls. However the policies still appear in the control's policy list, even though viewing the policy directly shows it as unlinked from any controls. This creates a contradiction where the control says the policy is linked but the policy says it is not.
## Root cause
Two bugs compound the issue.
(1) removePolicyControl in policies.controller.ts only deletes the FrameworkControlPolicyLink join row for custom frameworks (`customFrameworkId:{not:null}` guard at line 1027). Platform framework links (like SOC 2) are never deleted from the join table. Meanwhile findOneForFramework in controls.service.ts reads a platform control's policy list entirely from this join table, so stale rows remain visible.
(2) Control and framework reads filter policies only on `archivedAt:null`, omitting the `isArchived:false` check. This contradicts the policy.prisma rule (lines 37-41) which hides user-archived policies. So policies marked as archived by the user still display and score against framework progress.
## Fix
Removed the `customFrameworkId:{not:null}` gate in removePolicyControl so the join row deletion runs for all framework types, not just custom ones. Relaxed the before-guard to let deletion proceed when the m2m link exists. Added `isArchived:false` filter to all control/framework/score policy queries to respect the archive state.
## Explicitly NOT touched
No schema migration. No RBAC or organizationId scoping change. Auth rules remain unchanged.
## Verification
Added regression test asserting that unlinking a policy from a platform framework (SOC 2) removes it from both the join table and the control's returned policy list. Confirmed that filtering on `isArchived:false` prevents user-archived policies from appearing. Unit tests pass locally ✅
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
There was a problem hiding this comment.
cubic analysis
All reported issues were addressed across 6 files
Linked issue analysis
Linked issue: CS-780: [BUG] Policies unlinked from Controls but still showing against Framework Progress
| Status | Acceptance criteria | Notes |
|---|---|---|
| ✅ | When a policy is unlinked from a control, delete FrameworkControlPolicyLink rows for ALL framework instances (platform + custom), scoped to the org (so platform-framework stale join rows are removed). | policies.controller.ts removes the customFrameworkId guard and calls deleteMany scoped to frameworkInstance: { organizationId }. policies.controller.spec.ts adds a regression test asserting frameworkControlPolicyLink.deleteMany is called with frameworkInstance: { organizationId } for an unlink on a platform framework. |
| ✅ | Control/framework reads exclude user-archived policies so archived policies do not appear in control policy lists or count toward framework progress (add isArchived:false alongside archivedAt:null filters). | controls.service.ts and frameworks.service.ts were updated to add isArchived: false alongside archivedAt: null in multiple includes. Unit tests were added/updated (controls.service.spec.ts and frameworks.service.spec.ts) asserting queries include { archivedAt: null, isArchived: false } for policies and frameworkPolicyLinks. |
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
…n-only links) The unlink still gated the FrameworkControlPolicyLink deleteMany on an implicit m2m link existing (`before?.controls.length`). Framework-scoped links created via LinkPolicySheet make ONLY a FrameworkControlPolicyLink (no implicit m2m), so unlinking one skipped the delete and the policy kept showing against the control — the same CS-780 contradiction. Run the org-scoped deleteMany unconditionally (idempotent; the org-scoped policy.update still aborts for foreign/missing policies). Regression test covers the no-m2m-link case. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
claudfuen
pushed a commit
that referenced
this pull request
Jul 29, 2026
# [3.111.0](v3.110.1...v3.111.0) (2026-07-29) ### Bug Fixes * **api:** classify the take-over method on an unclear outcome too ([539fdba](539fdba)) * **api:** only classify a switchable passkey when a code method exists ([#3528](#3528)) ([0d79093](0d79093)) * **app:** make a half-finished connect resumable, not a forced full-screen step ([#3525](#3525)) ([b1afcae](b1afcae)) * **cloud-security:** add missing logGroupName to CreateLogGroup remediation ([#3515](#3515)) ([76ae56c](76ae56c)) * harden sign-in classification + align take-over messaging ([#3527](#3527)) ([914cb1e](914cb1e)) * **integrations:** wrap aws add account form in dialog and scroll into view ([#3526](#3526)) ([07e91ae](07e91ae)), closes [#418](#418) * make the 2FA take-over universal — tailor guidance to what the page asks for ([#3520](#3520)) ([b12f3fe](b12f3fe)) * **policies:** remove archived policies from framework controls after unlinking ([#3513](#3513)) ([d639df2](d639df2)) * **training:** defer completion email and share canonical training IDs ([#3529](#3529)) ([fecb556](fecb556)) * **training:** remove rbac gate from mark-complete endpoint ([#3501](#3501)) ([8c5e98f](8c5e98f)), closes [#3455](#3455) ### Features * connection longevity + one-click 'Make permanent' 2FA ([#3524](#3524)) ([7345e3c](7345e3c)) * **policies:** add bulk upload for policy migration ([#3514](#3514)) ([1940f06](1940f06)) * **security-questionnaire:** add browser extension ([#3064](#3064)) ([e678421](e678421))
Contributor
|
🎉 This PR is included in version 3.111.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A customer manually unlinked archived policies from controls. However the policies still appear in the control's policy list, even though viewing the policy directly shows it as unlinked from any controls. This creates a contradiction where the control says the policy is linked but the policy says it is not.
Root cause
Two bugs compound the issue.
(1) removePolicyControl in policies.controller.ts only deletes the FrameworkControlPolicyLink join row for custom frameworks (
customFrameworkId:{not:null}guard at line 1027). Platform framework links (like SOC 2) are never deleted from the join table. Meanwhile findOneForFramework in controls.service.ts reads a platform control's policy list entirely from this join table, so stale rows remain visible.(2) Control and framework reads filter policies only on
archivedAt:null, omitting theisArchived:falsecheck. This contradicts the policy.prisma rule (lines 37-41) which hides user-archived policies. So policies marked as archived by the user still display and score against framework progress.Fix
Removed the
customFrameworkId:{not:null}gate in removePolicyControl so the join row deletion runs for all framework types, not just custom ones. Relaxed the before-guard to let deletion proceed when the m2m link exists. AddedisArchived:falsefilter to all control/framework/score policy queries to respect the archive state.Explicitly NOT touched
No schema migration. No RBAC or organizationId scoping change. Auth rules remain unchanged.
Verification
Added regression test asserting that unlinking a policy from a platform framework (SOC 2) removes it from both the join table and the control's returned policy list. Confirmed that filtering on
isArchived:falseprevents user-archived policies from appearing. Unit tests pass locally ✅Fixes CS-780
Summary by cubic
Fixes CS-780 by removing unlinked and archived policies from framework control views and progress. Unlink now always severs framework join rows, and reads hide user-archived policies so scores are accurate.
FrameworkControlPolicyLinkrows for all framework instances (platform + custom), org-scoped — even when no implicit m2m link exists.archivedAt: nullandisArchived: falseacross control and framework queries (includeControls/progress paths too).Written for commit 6169db6. Summary will update on new commits.