diff --git a/ghost/admin/app/services/members-utils.js b/ghost/admin/app/services/members-utils.js
index 7a659f63a7..456f8f461b 100644
--- a/ghost/admin/app/services/members-utils.js
+++ b/ghost/admin/app/services/members-utils.js
@@ -4,6 +4,7 @@ export default class MembersUtilsService extends Service {
@service config;
@service settings;
@service feature;
+ @service session;
@service store;
paidTiers = null;
@@ -29,15 +30,21 @@ export default class MembersUtilsService extends Service {
return;
}
- return this.store.query('tier', {filter: 'type:paid+active:true', limit: 'all'}).then((tiers) => {
- this.paidTiers = tiers;
- });
+ // contributors don't have permissions to fetch tiers
+ if (this.session.user && !this.session.user.isContributor) {
+ return this.store.query('tier', {filter: 'type:paid+active:true', limit: 'all'}).then((tiers) => {
+ this.paidTiers = tiers;
+ });
+ }
}
async reload() {
- return this.store.query('tier', {filter: 'type:paid+active:true', limit: 'all'}).then((tiers) => {
- this.paidTiers = tiers;
- });
+ // contributors don't have permissions to fetch tiers
+ if (this.session.user && !this.session.user.isContributor) {
+ return this.store.query('tier', {filter: 'type:paid+active:true', limit: 'all'}).then((tiers) => {
+ this.paidTiers = tiers;
+ });
+ }
}
/**
diff --git a/ghost/admin/app/templates/posts.hbs b/ghost/admin/app/templates/posts.hbs
index dcc25abf98..dbd8463997 100644
--- a/ghost/admin/app/templates/posts.hbs
+++ b/ghost/admin/app/templates/posts.hbs
@@ -35,22 +35,22 @@
data-test-post-id={{post.id}}
/>
{{else}}
-
-
- {{#if this.showingAll}}
- {{svg-jar "posts-placeholder" class="gh-posts-placeholder"}}
-
Start creating content.
-
- Write a new post
-
- {{else}}
- No posts match the current filter
-
- Show all posts
-
- {{/if}}
-
-
+
+
+ {{#if this.showingAll}}
+ {{svg-jar "posts-placeholder" class="gh-posts-placeholder"}}
+
Start creating content.
+
+ Write a new post
+
+ {{else}}
+ No posts match the current filter
+
+ Show all posts
+
+ {{/if}}
+
+
{{/each}}
diff --git a/ghost/admin/mirage/config/members.js b/ghost/admin/mirage/config/members.js
index 8bc2ac38ab..9ebae0853f 100644
--- a/ghost/admin/mirage/config/members.js
+++ b/ghost/admin/mirage/config/members.js
@@ -2,43 +2,20 @@ import faker from 'faker';
import moment from 'moment-timezone';
import nql from '@tryghost/nql';
import {Response} from 'miragejs';
-import {extractFilterParam, paginateModelCollection} from '../utils';
+import {
+ extractFilterParam,
+ paginateModelCollection,
+ withPermissionsCheck
+} from '../utils';
import {underscore} from '@ember/string';
-function hasInvalidPermissions() {
- const {schema, request} = this;
-
- // always allow dev requests through - the logged in user will be real so
- // we can't check against it in the mocked db
- if (!request.requestHeaders['X-Test-User']) {
- return false;
- }
-
- const invalidPermsResponse = new Response(403, {}, {
- errors: [{
- type: 'NoPermissionError',
- message: 'You do not have permission to perform this action'
- }]
- });
-
- const user = schema.users.find(request.requestHeaders['X-Test-User']);
- const adminRoles = user.roles.filter(role => ['Owner', 'Administrator'].includes(role.name));
-
- if (adminRoles.length === 0) {
- return invalidPermsResponse;
- }
-}
-
-function withPermissionsCheck(fn) {
- return function () {
- const boundPermsCheck = hasInvalidPermissions.bind(this);
- const boundFn = fn.bind(this);
- return boundPermsCheck() || boundFn(...arguments);
- };
-}
+const ALLOWED_ROLES = [
+ 'Owner',
+ 'Administrator'
+];
export function mockMembersStats(server) {
- server.get('/members/stats/count', withPermissionsCheck(function (db, {queryParams}) {
+ server.get('/members/stats/count', withPermissionsCheck(ALLOWED_ROLES, function (db, {queryParams}) {
let {days} = queryParams;
let firstSubscriberDays = faker.datatype.number({min: 30, max: 600});
@@ -94,12 +71,12 @@ export function mockMembersStats(server) {
}
export default function mockMembers(server) {
- server.post('/members/', withPermissionsCheck(function ({members}) {
+ server.post('/members/', withPermissionsCheck(ALLOWED_ROLES, function ({members}) {
const attrs = this.normalizedRequestAttrs();
return members.create(attrs);
}));
- server.get('/members/', withPermissionsCheck(function ({members}, {queryParams}) {
+ server.get('/members/', withPermissionsCheck(ALLOWED_ROLES, function ({members}, {queryParams}) {
let {filter, search, page, limit} = queryParams;
page = +page || 1;
@@ -164,7 +141,7 @@ export default function mockMembers(server) {
return paginateModelCollection('members', collection, page, limit);
}));
- server.del('/members/', withPermissionsCheck(function ({members}, {queryParams}) {
+ server.del('/members/', withPermissionsCheck(ALLOWED_ROLES, function ({members}, {queryParams}) {
if (!queryParams.filter && !queryParams.search && queryParams.all !== 'true') {
return new Response(422, {}, {errors: [{
type: 'IncorrectUsageError',
@@ -200,7 +177,7 @@ export default function mockMembers(server) {
};
}));
- server.get('/members/:id/', withPermissionsCheck(function ({members}, {params}) {
+ server.get('/members/:id/', withPermissionsCheck(ALLOWED_ROLES, function ({members}, {params}) {
let {id} = params;
let member = members.find(id);
@@ -212,7 +189,7 @@ export default function mockMembers(server) {
});
}));
- server.put('/members/:id/', withPermissionsCheck(function ({members, tiers, subscriptions}, {params}) {
+ server.put('/members/:id/', withPermissionsCheck(ALLOWED_ROLES, function ({members, tiers, subscriptions}, {params}) {
const attrs = this.normalizedRequestAttrs();
const member = members.find(params.id);
@@ -282,12 +259,12 @@ export default function mockMembers(server) {
return member.update(attrs);
}));
- server.del('/members/:id/', withPermissionsCheck(function ({members}, request) {
+ server.del('/members/:id/', withPermissionsCheck(ALLOWED_ROLES, function ({members}, request) {
const id = request.params.id;
members.find(id).destroy();
}));
- server.get('/members/upload/', withPermissionsCheck(function () {
+ server.get('/members/upload/', withPermissionsCheck(ALLOWED_ROLES, function () {
return new Response(200, {
'Content-Disposition': 'attachment',
filename: `members.${moment().format('YYYY-MM-DD')}.csv`,
@@ -295,7 +272,7 @@ export default function mockMembers(server) {
}, '');
}));
- server.post('/members/upload/', withPermissionsCheck(function ({labels}, request) {
+ server.post('/members/upload/', withPermissionsCheck(ALLOWED_ROLES, function ({labels}, request) {
const label = labels.create();
// TODO: parse CSV and create member records
@@ -312,7 +289,7 @@ export default function mockMembers(server) {
});
}));
- server.get('/members/events/', withPermissionsCheck(function ({memberActivityEvents}, {queryParams}) {
+ server.get('/members/events/', withPermissionsCheck(ALLOWED_ROLES, function ({memberActivityEvents}, {queryParams}) {
let {limit} = queryParams;
limit = +limit || 15;
diff --git a/ghost/admin/mirage/config/tiers.js b/ghost/admin/mirage/config/tiers.js
index 37c24eb9e6..99ab53368e 100644
--- a/ghost/admin/mirage/config/tiers.js
+++ b/ghost/admin/mirage/config/tiers.js
@@ -1,11 +1,27 @@
-import {paginatedResponse} from '../utils';
+import {paginatedResponse, withPermissionsCheck} from '../utils';
+
+const ALLOWED_WRITE_ROLES = [
+ 'Owner',
+ 'Administrator'
+];
+const ALLOWED_READ_ROLES = [
+ 'Owner',
+ 'Administrator',
+ 'Editor',
+ 'Author'
+];
export default function mockTiers(server) {
- server.post('/tiers/');
+ // CREATE
+ server.post('/tiers/', withPermissionsCheck(ALLOWED_WRITE_ROLES, function ({tiers}) {
+ const attrs = this.normalizedRequestAttrs();
+ return tiers.create(attrs);
+ }));
- server.get('/tiers/', paginatedResponse('tiers'));
+ // READ
+ server.get('/tiers/', withPermissionsCheck(ALLOWED_READ_ROLES, paginatedResponse('tiers')));
- server.get('/tiers/:id/', function ({tiers}, {params}) {
+ server.get('/tiers/:id/', withPermissionsCheck(ALLOWED_READ_ROLES, function ({tiers}, {params}) {
let {id} = params;
let tier = tiers.find(id);
@@ -15,16 +31,21 @@ export default function mockTiers(server) {
message: 'Tier not found.'
}]
});
- });
+ }));
- server.put('/tiers/:id/', function ({tiers}, {params}) {
+ // UPDATE
+ server.put('/tiers/:id/', withPermissionsCheck(ALLOWED_WRITE_ROLES, function ({tiers}, {params}) {
const attrs = this.normalizedRequestAttrs();
const tier = tiers.find(params.id);
tier.update(attrs);
return tier.save();
- });
+ }));
- server.del('/tiers/:id/');
+ // DELETE
+ server.del('/tiers/:id/', withPermissionsCheck(ALLOWED_WRITE_ROLES, function (schema, request) {
+ const id = request.params.id;
+ schema.tiers.find(id).destroy();
+ }));
}
diff --git a/ghost/admin/mirage/utils.js b/ghost/admin/mirage/utils.js
index b22167cdb4..eef745cf0e 100644
--- a/ghost/admin/mirage/utils.js
+++ b/ghost/admin/mirage/utils.js
@@ -122,3 +122,35 @@ export function extractFilterParam(param, filter = '') {
return normalizeBooleanParams(normalizeStringParams(match));
}
+
+export function hasInvalidPermissions(allowedRoles) {
+ const {schema, request} = this;
+
+ // always allow dev requests through - the logged in user will be real so
+ // we can't check against it in the mocked db
+ if (!request.requestHeaders['X-Test-User']) {
+ return false;
+ }
+
+ const invalidPermsResponse = new Response(403, {}, {
+ errors: [{
+ type: 'NoPermissionError',
+ message: 'You do not have permission to perform this action'
+ }]
+ });
+
+ const user = schema.users.find(request.requestHeaders['X-Test-User']);
+ const adminRoles = user.roles.filter(role => allowedRoles.includes(role.name));
+
+ if (adminRoles.length === 0) {
+ return invalidPermsResponse;
+ }
+}
+
+export function withPermissionsCheck(allowedRoles, fn) {
+ return function () {
+ const boundPermsCheck = hasInvalidPermissions.bind(this);
+ const boundFn = fn.bind(this);
+ return boundPermsCheck(allowedRoles) || boundFn(...arguments);
+ };
+}
diff --git a/ghost/admin/package.json b/ghost/admin/package.json
index 1c4f7e96f3..14515c4ad6 100644
--- a/ghost/admin/package.json
+++ b/ghost/admin/package.json
@@ -1,6 +1,6 @@
{
"name": "ghost-admin",
- "version": "5.22.0",
+ "version": "5.22.1",
"description": "Ember.js admin client for Ghost",
"author": "Ghost Foundation",
"homepage": "http://ghost.org",
diff --git a/ghost/admin/tests/acceptance/content-test.js b/ghost/admin/tests/acceptance/content-test.js
index 61c9f3a7d9..7eea8bd7f3 100644
--- a/ghost/admin/tests/acceptance/content-test.js
+++ b/ghost/admin/tests/acceptance/content-test.js
@@ -1,6 +1,6 @@
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
import {beforeEach, describe, it} from 'mocha';
-import {click, currentURL, fillIn, find, findAll, settled, visit} from '@ember/test-helpers';
+import {blur, click, currentURL, fillIn, find, findAll, settled, visit} from '@ember/test-helpers';
import {clickTrigger, selectChoose} from 'ember-power-select/test-support/helpers';
import {expect} from 'chai';
import {setupApplicationTest} from 'ember-mocha';
@@ -247,13 +247,33 @@ describe('Acceptance: Content', function () {
describe('as contributor', function () {
beforeEach(async function () {
- let adminRole = this.server.create('role', {name: 'Administrator'});
- let admin = this.server.create('user', {roles: [adminRole]});
-
- // Create posts
- this.server.create('post', {authors: [admin], status: 'scheduled', title: 'Admin Post'});
+ let contributorRole = this.server.create('role', {name: 'Contributor'});
+ this.server.create('user', {roles: [contributorRole]});
return await authenticateSession();
});
+
+ it('shows posts list and allows post creation', async function () {
+ await visit('/posts');
+
+ // has an empty state
+ expect(findAll('[data-test-post-id]')).to.have.length(0);
+ expect(find('[data-test-no-posts-box]')).to.exist;
+ expect(find('[data-test-link="write-a-new-post"]')).to.exist;
+
+ await click('[data-test-link="write-a-new-post"]');
+
+ expect(currentURL()).to.equal('/editor/post');
+
+ await fillIn('[data-test-editor-title-input]', 'First contributor post');
+ await blur('[data-test-editor-title-input]');
+
+ expect(currentURL()).to.equal('/editor/post/1');
+
+ await click('[data-test-link="posts"]');
+
+ expect(findAll('[data-test-post-id]')).to.have.length(1);
+ expect(find('[data-test-no-posts-box]')).to.not.exist;
+ });
});
});
diff --git a/ghost/core/package.json b/ghost/core/package.json
index bc92fe9f6a..a50caa2e27 100644
--- a/ghost/core/package.json
+++ b/ghost/core/package.json
@@ -1,6 +1,6 @@
{
"name": "ghost",
- "version": "5.22.0",
+ "version": "5.22.1",
"description": "The professional publishing platform",
"author": "Ghost Foundation",
"homepage": "https://ghost.org",