From 297993803a085f0ce916dbc5d395d457f67970ff Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Thu, 25 Jun 2026 17:45:05 +0100 Subject: [PATCH 1/2] Removed gift links permission from the Author role MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ref https://linear.app/ghost/issue/BER-3748/ - the fixtures granted Authors the "Manage gift links" permission, but Authors can't change post visibility, so they shouldn't be able to manage gift links either — the original grant was too broad - updated the fixtures and added a migration to drop the existing Author grant for sites that already ran the gift links permission migration --- .../data/migrations/utils/permissions.js | 1 + ...emove-gift-links-permission-from-author.js | 8 ++++++ .../server/data/schema/fixtures/fixtures.json | 3 +-- .../test/e2e-api/admin/gift-links.test.ts | 25 ++++++++++++++----- .../integration/migrations/migration.test.js | 2 +- ghost/core/test/utils/fixtures/fixtures.json | 3 +-- 6 files changed, 31 insertions(+), 11 deletions(-) create mode 100644 ghost/core/core/server/data/migrations/versions/6.48/2026-06-25-00-00-00-remove-gift-links-permission-from-author.js diff --git a/ghost/core/core/server/data/migrations/utils/permissions.js b/ghost/core/core/server/data/migrations/utils/permissions.js index e0e518ae3fc..af6dcd5bfcd 100644 --- a/ghost/core/core/server/data/migrations/utils/permissions.js +++ b/ghost/core/core/server/data/migrations/utils/permissions.js @@ -272,6 +272,7 @@ function createRemovePermissionMigration(config, roles) { module.exports = { addPermission, addPermissionToRole, + removePermissionFromRole, addPermissionWithRoles, createRemovePermissionMigration }; diff --git a/ghost/core/core/server/data/migrations/versions/6.48/2026-06-25-00-00-00-remove-gift-links-permission-from-author.js b/ghost/core/core/server/data/migrations/versions/6.48/2026-06-25-00-00-00-remove-gift-links-permission-from-author.js new file mode 100644 index 00000000000..0147d53549c --- /dev/null +++ b/ghost/core/core/server/data/migrations/versions/6.48/2026-06-25-00-00-00-remove-gift-links-permission-from-author.js @@ -0,0 +1,8 @@ +const {removePermissionFromRole} = require('../../utils'); + +// Authors cannot change post visibility, so they should not be able to manage gift +// links either. Drop the over-broad grant added in the original gift links rollout. +module.exports = removePermissionFromRole({ + permission: 'Manage gift links', + role: 'Author' +}); diff --git a/ghost/core/core/server/data/schema/fixtures/fixtures.json b/ghost/core/core/server/data/schema/fixtures/fixtures.json index deb3949b6f2..fc700dca5df 100644 --- a/ghost/core/core/server/data/schema/fixtures/fixtures.json +++ b/ghost/core/core/server/data/schema/fixtures/fixtures.json @@ -1090,8 +1090,7 @@ "product": ["browse", "read"], "newsletter": ["browse", "read"], "collection": ["browse", "read", "add"], - "recommendation": ["browse", "read"], - "gift_link": "manage" + "recommendation": ["browse", "read"] }, "Contributor": { "post": ["browse", "read", "edit", "add", "destroy"], diff --git a/ghost/core/test/e2e-api/admin/gift-links.test.ts b/ghost/core/test/e2e-api/admin/gift-links.test.ts index 508c94f8a63..6f0273906c2 100644 --- a/ghost/core/test/e2e-api/admin/gift-links.test.ts +++ b/ghost/core/test/e2e-api/admin/gift-links.test.ts @@ -9,6 +9,7 @@ describe('Gift Links Admin API', function () { put: (_url: string) => any; post: (_url: string) => any; loginAsOwner: () => Promise; + loginAsAuthor: () => Promise; loginAsContributor: () => Promise; }; let postId: string; @@ -64,12 +65,6 @@ describe('Gift Links Admin API', function () { assert.notEqual(body.gift_links[0].token, first); }); - it('403s for a role without gift-link permission', async function () { - await agent.loginAsContributor(); - await agent.get(`${name}/${id()}/gift_links/`).expectStatus(403); - await agent.loginAsOwner(); - }); - it('supports the full lifecycle', async function () { // empty let body = (await agent.get(`${name}/${id()}/gift_links/`).expectStatus(200)).body; @@ -95,6 +90,24 @@ describe('Gift Links Admin API', function () { }); }); + // Permission is granted at the role level, independent of the post/page, so these + // run once rather than per-entity. + describe('without gift-link permission', function () { + afterEach(async function () { + await agent.loginAsOwner(); + }); + + it('403s for an Author', async function () { + await agent.loginAsAuthor(); + await agent.get(`posts/${postId}/gift_links/`).expectStatus(403); + }); + + it('403s for a Contributor', async function () { + await agent.loginAsContributor(); + await agent.get(`posts/${postId}/gift_links/`).expectStatus(403); + }); + }); + describe('remove_all', function () { it('returns the count in a meta block, not as a resource', async function () { await agent.put(`posts/${postId}/gift_links/`).expectStatus(200); diff --git a/ghost/core/test/integration/migrations/migration.test.js b/ghost/core/test/integration/migrations/migration.test.js index 4f3e8a19960..e107ec8a4c5 100644 --- a/ghost/core/test/integration/migrations/migration.test.js +++ b/ghost/core/test/integration/migrations/migration.test.js @@ -111,7 +111,7 @@ describe('Migrations', function () { assertHavePermission(permissions, 'Delete posts', ['Administrator', 'Editor', 'Author', 'Contributor', 'Admin Integration', 'Super Editor']); assertHavePermission(permissions, 'Publish posts', ['Administrator', 'Editor', 'Admin Integration', 'Scheduler Integration', 'Super Editor']); assertHavePermission(permissions, 'Flush gift reminders', ['Scheduler Integration']); - assertHavePermission(permissions, 'Manage gift links', ['Administrator', 'Editor', 'Author', 'Admin Integration', 'Super Editor']); + assertHavePermission(permissions, 'Manage gift links', ['Administrator', 'Editor', 'Admin Integration', 'Super Editor']); assertHavePermission(permissions, 'Remove all gift links', ['Administrator']); assertHavePermission(permissions, 'Browse settings', ['Administrator', 'Editor', 'Author', 'Contributor', 'Admin Integration', 'Super Editor']); diff --git a/ghost/core/test/utils/fixtures/fixtures.json b/ghost/core/test/utils/fixtures/fixtures.json index 7ac15c77678..2f7972ec337 100644 --- a/ghost/core/test/utils/fixtures/fixtures.json +++ b/ghost/core/test/utils/fixtures/fixtures.json @@ -1220,8 +1220,7 @@ "product": ["browse", "read"], "newsletter": ["browse", "read"], "collection": ["browse", "read", "add"], - "recommendation": ["browse", "read"], - "gift_link": "manage" + "recommendation": ["browse", "read"] }, "Super Editor": { "notification": "all", From 5f7f4a64c7dfa6b83794715043b3e3ba83c9cc52 Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Thu, 25 Jun 2026 19:01:37 +0100 Subject: [PATCH 2/2] Updated fixture integrity hash and permission count ref https://linear.app/ghost/issue/BER-3748/ - removing the Author gift links permission changed the fixtures, so the integrity hash and the permissions-roles relation count in the fixture tests needed updating to match --- .../unit/server/data/schema/fixtures/fixture-manager.test.js | 2 +- ghost/core/test/unit/server/data/schema/integrity.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ghost/core/test/unit/server/data/schema/fixtures/fixture-manager.test.js b/ghost/core/test/unit/server/data/schema/fixtures/fixture-manager.test.js index 294718bb8e8..c68e24fcd13 100644 --- a/ghost/core/test/unit/server/data/schema/fixtures/fixture-manager.test.js +++ b/ghost/core/test/unit/server/data/schema/fixtures/fixture-manager.test.js @@ -398,7 +398,7 @@ describe('Migration Fixture Utils', function () { const rolesAllStub = sinon.stub(models.Role, 'findAll').returns(Promise.resolve(dataMethodStub)); const result = await fixtureManager.addFixturesForRelation(fixtures.relations[0]); - const FIXTURE_COUNT = 148; + const FIXTURE_COUNT = 147; assertExists(result); assert(_.isPlainObject(result)); assert.equal(result.expected, FIXTURE_COUNT); diff --git a/ghost/core/test/unit/server/data/schema/integrity.test.js b/ghost/core/test/unit/server/data/schema/integrity.test.js index eb62c605995..51507fbe0f5 100644 --- a/ghost/core/test/unit/server/data/schema/integrity.test.js +++ b/ghost/core/test/unit/server/data/schema/integrity.test.js @@ -36,7 +36,7 @@ const validateRouteSettings = require('../../../../../core/server/services/route describe('DB version integrity', function () { // Only these variables should need updating const currentSchemaHash = '843875f56844de5c63752e18ceaeb095'; - const currentFixturesHash = 'f4941d9d92b59075e0c2a1cc3fc4c44a'; + const currentFixturesHash = '16c0d239e8d04682ccb1894124179289'; const currentSettingsHash = '397be8628c753b1959b8954d5610f83f'; const currentRoutesHash = '3d180d52c663d173a6be791ef411ed01';