diff --git a/MIGRATIONS.md b/MIGRATIONS.md index 1817ebcb9..9ce72dbe6 100644 --- a/MIGRATIONS.md +++ b/MIGRATIONS.md @@ -4,6 +4,39 @@ Breaking changes and upgrade notes for downstream projects. --- +## @casl/ability v6 → v7 (2026-05-22) + +`@casl/ability` upgraded from `^6.8.1` to `^7.0.0`. + +### What changed (this repo) + +- **`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`: + + ```js + // before (v6) + const { AbilityBuilder, Ability } = await import('@casl/ability'); + const { can, cannot, build } = new AbilityBuilder(Ability); + // after (v7) + const { AbilityBuilder, createMongoAbility } = await import('@casl/ability'); + const { can, cannot, build } = new AbilityBuilder(createMongoAbility); + ``` + + Without this, conditions like `can('manage', 'Organization', { _id })` stop matching → authorization silently denies → endpoints return 403/422. +- **JSDoc type refs** `import('@casl/ability').Ability` → `MongoAbility` (`lib/middlewares/policy.js`, `lib/helpers/abilities.js`). +- **`package.json`** — `@casl/ability` `^6.8.1` → `^7.0.0`. + +### Downstream action required + +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`): + +1. Bump `@casl/ability` to `^7.0.0` in `package.json` and reinstall. +2. After `/update-stack`, verify `lib/middlewares/policy.js` (~line 95) reads `new AbilityBuilder(createMongoAbility)`. +3. **Module policy files need no change** — they use `can`/`cannot` closures, never the `Ability` class. +4. The serialized rules format is **unchanged** (`createMongoAbility` keeps the MongoQuery rule shape), so Node→client rule packing stays compatible. +5. Run unit + integration + e2e to confirm authorization paths still pass. + +--- + ## Sentry removed — PostHog Error Tracking is now sole source (2026-05-10) 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', ...)`. diff --git a/lib/helpers/abilities.js b/lib/helpers/abilities.js index 5d40e663b..99c2520bc 100644 --- a/lib/helpers/abilities.js +++ b/lib/helpers/abilities.js @@ -1,6 +1,6 @@ /** * Serialize CASL abilities to a JSON-safe array compatible with createMongoAbility(). - * @param {import('@casl/ability').Ability} ability - The CASL ability instance + * @param {import('@casl/ability').MongoAbility} ability - The CASL ability instance * @returns {Array<{action: string, subject: string, conditions?: Object, inverted?: boolean}>} Array of serialized rules */ const serializeAbilities = (ability) => diff --git a/lib/middlewares/policy.js b/lib/middlewares/policy.js index 3ad2be908..d95dbcd44 100644 --- a/lib/middlewares/policy.js +++ b/lib/middlewares/policy.js @@ -88,11 +88,13 @@ const registerAbilities = (entry) => { * Iterates over all registered ability builder functions from module policy files. * @param {Object|null} user - The authenticated user, or null/undefined for guests * @param {Object|null} [membership] - Optional organization membership (reserved for future use) - * @returns {Promise} CASL ability instance + * @returns {Promise} CASL ability instance */ const defineAbilityFor = async (user, membership) => { - const { AbilityBuilder, Ability } = await loadCasl(); - const { can, cannot, build } = new AbilityBuilder(Ability); + // v7: the `Ability` export is now PureAbility (no default conditions matcher). + // createMongoAbility restores MongoDB-style condition matching ({ _id }, { organizationId }). + const { AbilityBuilder, createMongoAbility } = await loadCasl(); + const { can, cannot, build } = new AbilityBuilder(createMongoAbility); // Normalize Mongoose membership to a plain object so all fields (role, status, etc.) // are accessible, and flatten populated organizationId to a plain string ID. diff --git a/lib/middlewares/tests/policy.unit.tests.js b/lib/middlewares/tests/policy.unit.tests.js index ec6fc1681..6afd58bea 100644 --- a/lib/middlewares/tests/policy.unit.tests.js +++ b/lib/middlewares/tests/policy.unit.tests.js @@ -24,6 +24,7 @@ jest.unstable_mockModule('@casl/ability', () => ({ build: jest.fn().mockReturnValue({ can: jest.fn().mockReturnValue(true) }), })), Ability: jest.fn(), + createMongoAbility: jest.fn(), subject: jest.fn((type, doc) => doc), })); @@ -69,4 +70,12 @@ describe('policy discoverPolicies unit tests:', () => { expect.stringContaining('exports abilities/guestAbilities but no SubjectRegistration'), ); }); + + test('defineAbilityFor builds via createMongoAbility (v7 — guards the #3693 auth regression)', async () => { + // v7 drops the conditions matcher from the `Ability` export; the builder must be + // seeded with createMongoAbility or Mongo-style conditions ({ _id }, ...) stop matching. + const { AbilityBuilder, createMongoAbility } = await import('@casl/ability'); + await policy.defineAbilityFor({ _id: 'u1', roles: ['user'] }, null); + expect(AbilityBuilder).toHaveBeenCalledWith(createMongoAbility); + }); }); diff --git a/package-lock.json b/package-lock.json index 678e4853d..57de51d83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.4.0", "license": "MIT", "dependencies": { - "@casl/ability": "^6.8.1", + "@casl/ability": "^7.0.0", "@jest/globals": "^30.4.1", "axios": "^1.16.1", "bcrypt": "^6.0.0", @@ -625,12 +625,12 @@ "license": "MIT" }, "node_modules/@casl/ability": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.8.1.tgz", - "integrity": "sha512-VX5DD1JbSP/DdewZnNwXXaCzve+0pLe14mcUj2l93CdOFAQUT/ylAptNqxf3Wc/jlsuSanAgXza4Z1Iq23dzpQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@casl/ability/-/ability-7.0.0.tgz", + "integrity": "sha512-QhwRflkTucpdS2uw1XScrzLWbgLYJGvPoq2Xm5OjeRci3dwtPixxnjUKJ04Ss1ivNS9tZQ8y4sjWeelWsrwo4g==", "license": "MIT", "dependencies": { - "@ucast/mongo2js": "^1.3.0" + "@ucast/mongo2js": "^2.0.0" }, "funding": { "url": "https://github.com/stalniy/casl/blob/master/BACKERS.md" @@ -4968,38 +4968,38 @@ "license": "MIT" }, "node_modules/@ucast/core": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/@ucast/core/-/core-1.10.2.tgz", - "integrity": "sha512-ons5CwXZ/51wrUPfoduC+cO7AS1/wRb0ybpQJ9RrssossDxVy4t49QxWoWgfBDvVKsz9VXzBk9z0wqTdZ+Cq8g==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ucast/core/-/core-2.0.0.tgz", + "integrity": "sha512-4XVx6LzPXZGvnZO5jp39cm/G4UvuwvEdtmg+9+4+zl6uFkCcB7UJacvtMYeBE56GJVT99Zqy6Pii7dGJq3Kz9Q==", "license": "Apache-2.0" }, "node_modules/@ucast/js": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@ucast/js/-/js-3.1.0.tgz", - "integrity": "sha512-eJ7yQeYtMK85UZjxoxBEbTWx6UMxEXKbjVyp+NlzrT5oMKV5Gpo/9bjTl3r7msaXTVC8iD9NJacqJ8yp7joX+Q==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@ucast/js/-/js-4.0.1.tgz", + "integrity": "sha512-9O5xPBvwEWQk2WvO69Eh2WJB8QljVZ2vRVdFvfnKjlZwWXcYxp1lqLBhwXBU1AtuSgCvKhJPkXdjKJggUmAmQQ==", "license": "Apache-2.0", "dependencies": { - "@ucast/core": "1.10.2" + "@ucast/core": "2.0.0" } }, "node_modules/@ucast/mongo": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/@ucast/mongo/-/mongo-2.4.3.tgz", - "integrity": "sha512-XcI8LclrHWP83H+7H2anGCEeDq0n+12FU2mXCTz6/Tva9/9ddK/iacvvhCyW6cijAAOILmt0tWplRyRhVyZLsA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@ucast/mongo/-/mongo-3.0.0.tgz", + "integrity": "sha512-kwuSH+kdB4GCR0LGhy/PEDm4PCflur89AlK82kNiYD0FvsA8A/p+0sx7m+/R8mMFAlmlkAd3VXp7sM/cLLYWYg==", "license": "Apache-2.0", "dependencies": { - "@ucast/core": "^1.4.1" + "@ucast/core": "2.0.0" } }, "node_modules/@ucast/mongo2js": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@ucast/mongo2js/-/mongo2js-1.4.1.tgz", - "integrity": "sha512-9aeg5cmqwRQnKCXHN6I17wk83Rcm487bHelaG8T4vfpWneAI469wSI3Srnbu+PuZ5znWRbnwtVq9RgPL+bN6CA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ucast/mongo2js/-/mongo2js-2.0.0.tgz", + "integrity": "sha512-vNBZzRnsfLr/TSxEoxz6W6hHQ5tmWsfEeC0nCq5z8RezC1AqIRy3cfHm8AGvlGtcn+cTSFQcZremfqnz6wm+nQ==", "license": "Apache-2.0", "dependencies": { - "@ucast/core": "1.10.2", - "@ucast/js": "3.1.0", - "@ucast/mongo": "2.4.3" + "@ucast/core": "2.0.0", + "@ucast/js": "4.0.1", + "@ucast/mongo": "3.0.0" } }, "node_modules/@ungap/structured-clone": { diff --git a/package.json b/package.json index b554cd4ec..a711d16f4 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "release:auto": "npx semantic-release" }, "dependencies": { - "@casl/ability": "^6.8.1", + "@casl/ability": "^7.0.0", "@jest/globals": "^30.4.1", "axios": "^1.16.1", "bcrypt": "^6.0.0",