Skip to content

Commit 16f611c

Browse files
build(deps): migrate @casl/ability v6 → v7 (#3696)
* build(deps): migrate @casl/ability v6 → v7 v7 renames PureAbility to Ability and drops the default conditions matcher; the historical MongoDB-matching Ability class no longer exists. Build abilities via createMongoAbility so condition rules ({ _id }, { organizationId }) keep matching — without this, authorization silently denies and endpoints return 403/422. Supersedes dependabot #3693. See MIGRATIONS.md for downstream notes. * test(policy): guard createMongoAbility builder + clarify v7 migration note Review (Copilot): the policy unit-test mock did not export createMongoAbility nor assert the builder is seeded with it, so the #3693 auth regression could silently return. Mock createMongoAbility + assert new AbilityBuilder(createMongoAbility). Also corrects the MIGRATIONS wording: v7 renames PureAbility→Ability (the class isn't removed); only its default conditions matcher is dropped. * docs(migrations): blank lines around CASL code fence (markdownlint MD031)
1 parent 1b9f7a6 commit 16f611c

6 files changed

Lines changed: 71 additions & 27 deletions

File tree

MIGRATIONS.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,39 @@ Breaking changes and upgrade notes for downstream projects.
44

55
---
66

7+
## @casl/ability v6 → v7 (2026-05-22)
8+
9+
`@casl/ability` upgraded from `^6.8.1` to `^7.0.0`.
10+
11+
### What changed (this repo)
12+
13+
- **`lib/middlewares/policy.js`** — v7 renames `PureAbility` to `Ability` and **drops its default conditions matcher**, so the `Ability` export no longer does MongoDB-style condition matching out of the box (`createMongoAbility` is the replacement for the old behavior). `defineAbilityFor()` now builds via `createMongoAbility`:
14+
15+
```js
16+
// before (v6)
17+
const { AbilityBuilder, Ability } = await import('@casl/ability');
18+
const { can, cannot, build } = new AbilityBuilder(Ability);
19+
// after (v7)
20+
const { AbilityBuilder, createMongoAbility } = await import('@casl/ability');
21+
const { can, cannot, build } = new AbilityBuilder(createMongoAbility);
22+
```
23+
24+
Without this, conditions like `can('manage', 'Organization', { _id })` stop matching → authorization silently denies → endpoints return 403/422.
25+
- **JSDoc type refs** `import('@casl/ability').Ability``MongoAbility` (`lib/middlewares/policy.js`, `lib/helpers/abilities.js`).
26+
- **`package.json`**`@casl/ability` `^6.8.1``^7.0.0`.
27+
28+
### Downstream action required
29+
30+
The `policy.js` fix is a devkit-owned file → it arrives via `/update-stack` (`--theirs`). The **dependency bump does not auto-propagate** (`package.json` is `--ours`):
31+
32+
1. Bump `@casl/ability` to `^7.0.0` in `package.json` and reinstall.
33+
2. After `/update-stack`, verify `lib/middlewares/policy.js` (~line 95) reads `new AbilityBuilder(createMongoAbility)`.
34+
3. **Module policy files need no change** — they use `can`/`cannot` closures, never the `Ability` class.
35+
4. The serialized rules format is **unchanged** (`createMongoAbility` keeps the MongoQuery rule shape), so Node→client rule packing stays compatible.
36+
5. Run unit + integration + e2e to confirm authorization paths still pass.
37+
38+
---
39+
740
## Sentry removed — PostHog Error Tracking is now sole source (2026-05-10)
841

942
The `@sentry/node` integration shipped in 2026-03-26 (still documented below as **PostHog Analytics (2026-03-26)** + the now-removed Sentry monitoring section) is dropped. Error capture moves entirely to PostHog Error Tracking via `posthog.capture('$exception', ...)`.

lib/helpers/abilities.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* Serialize CASL abilities to a JSON-safe array compatible with createMongoAbility().
3-
* @param {import('@casl/ability').Ability} ability - The CASL ability instance
3+
* @param {import('@casl/ability').MongoAbility} ability - The CASL ability instance
44
* @returns {Array<{action: string, subject: string, conditions?: Object, inverted?: boolean}>} Array of serialized rules
55
*/
66
const serializeAbilities = (ability) =>

lib/middlewares/policy.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,13 @@ const registerAbilities = (entry) => {
8888
* Iterates over all registered ability builder functions from module policy files.
8989
* @param {Object|null} user - The authenticated user, or null/undefined for guests
9090
* @param {Object|null} [membership] - Optional organization membership (reserved for future use)
91-
* @returns {Promise<import('@casl/ability').Ability>} CASL ability instance
91+
* @returns {Promise<import('@casl/ability').MongoAbility>} CASL ability instance
9292
*/
9393
const defineAbilityFor = async (user, membership) => {
94-
const { AbilityBuilder, Ability } = await loadCasl();
95-
const { can, cannot, build } = new AbilityBuilder(Ability);
94+
// v7: the `Ability` export is now PureAbility (no default conditions matcher).
95+
// createMongoAbility restores MongoDB-style condition matching ({ _id }, { organizationId }).
96+
const { AbilityBuilder, createMongoAbility } = await loadCasl();
97+
const { can, cannot, build } = new AbilityBuilder(createMongoAbility);
9698

9799
// Normalize Mongoose membership to a plain object so all fields (role, status, etc.)
98100
// are accessible, and flatten populated organizationId to a plain string ID.

lib/middlewares/tests/policy.unit.tests.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ jest.unstable_mockModule('@casl/ability', () => ({
2424
build: jest.fn().mockReturnValue({ can: jest.fn().mockReturnValue(true) }),
2525
})),
2626
Ability: jest.fn(),
27+
createMongoAbility: jest.fn(),
2728
subject: jest.fn((type, doc) => doc),
2829
}));
2930

@@ -69,4 +70,12 @@ describe('policy discoverPolicies unit tests:', () => {
6970
expect.stringContaining('exports abilities/guestAbilities but no SubjectRegistration'),
7071
);
7172
});
73+
74+
test('defineAbilityFor builds via createMongoAbility (v7 — guards the #3693 auth regression)', async () => {
75+
// v7 drops the conditions matcher from the `Ability` export; the builder must be
76+
// seeded with createMongoAbility or Mongo-style conditions ({ _id }, ...) stop matching.
77+
const { AbilityBuilder, createMongoAbility } = await import('@casl/ability');
78+
await policy.defineAbilityFor({ _id: 'u1', roles: ['user'] }, null);
79+
expect(AbilityBuilder).toHaveBeenCalledWith(createMongoAbility);
80+
});
7281
});

package-lock.json

Lines changed: 22 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"release:auto": "npx semantic-release"
5050
},
5151
"dependencies": {
52-
"@casl/ability": "^6.8.1",
52+
"@casl/ability": "^7.0.0",
5353
"@jest/globals": "^30.4.1",
5454
"axios": "^1.16.1",
5555
"bcrypt": "^6.0.0",

0 commit comments

Comments
 (0)