Skip to content

fix(policies): remove archived policies from framework controls after unlinking - #3513

Merged
tofikwest merged 4 commits into
mainfrom
tofik/cs-780-bug-policies-unlinked-from-controls
Jul 28, 2026
Merged

fix(policies): remove archived policies from framework controls after unlinking#3513
tofikwest merged 4 commits into
mainfrom
tofik/cs-780-bug-policies-unlinked-from-controls

Conversation

@tofikwest

@tofikwest tofikwest commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

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 ✅

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.

  • Bug Fixes
    • On unlink, unconditionally delete FrameworkControlPolicyLink rows for all framework instances (platform + custom), org-scoped — even when no implicit m2m link exists.
    • Exclude user-archived policies in reads: use archivedAt: null and isArchived: false across control and framework queries (includeControls/progress paths too).
    • Added regression tests for join-only unlink, platform unlink behavior, and archive visibility.

Written for commit 6169db6. Summary will update on new commits.

Review in cubic

… 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 ✅
@linear

linear Bot commented Jul 28, 2026

Copy link
Copy Markdown

CS-780

@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
comp-framework-editor Ready Ready Preview, Comment Jul 28, 2026 7:33pm
portal Ready Ready Preview, Comment Jul 28, 2026 7:33pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
app Skipped Skipped Jul 28, 2026 7:33pm

Request Review

@cubic-dev-ai cubic-dev-ai Bot 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.

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

Comment thread apps/api/src/policies/policies.controller.ts Outdated
…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>
@vercel
vercel Bot temporarily deployed to Preview – app July 28, 2026 19:29 Inactive
@tofikwest
tofikwest merged commit d639df2 into main Jul 28, 2026
9 of 11 checks passed
@tofikwest
tofikwest deleted the tofik/cs-780-bug-policies-unlinked-from-controls branch July 28, 2026 19:30
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))
@claudfuen

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 3.111.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants