0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-11 02:12:21 -05:00

Permission restrictions for post.visibility modifications (#11213)

no issue

- Limited posts visibility field permissions to Editor-Up + Admin Integrations
- We don't want contributors or other roles lower than Editor to be able to modify content gating attribute
This commit is contained in:
Naz Gargol 2019-10-08 15:44:27 +02:00 committed by GitHub
parent da45881719
commit daa77c5c00
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 105 additions and 5 deletions

View file

@ -2,7 +2,7 @@ const models = require('../../models');
const common = require('../../lib/common');
const urlUtils = require('../../lib/url-utils');
const ALLOWED_INCLUDES = ['tags', 'authors', 'authors.roles'];
const UNSAFE_ATTRS = ['status', 'authors'];
const UNSAFE_ATTRS = ['status', 'authors', 'visibility'];
module.exports = {
docName: 'pages',

View file

@ -2,7 +2,7 @@ const models = require('../../models');
const common = require('../../lib/common');
const urlUtils = require('../../lib/url-utils');
const allowedIncludes = ['tags', 'authors', 'authors.roles'];
const unsafeAttrs = ['status', 'authors'];
const unsafeAttrs = ['status', 'authors', 'visibility'];
module.exports = {
docName: 'posts',

View file

@ -2,7 +2,7 @@ const models = require('../../models');
const common = require('../../lib/common');
const urlUtils = require('../../lib/url-utils');
const ALLOWED_INCLUDES = ['tags', 'authors', 'authors.roles'];
const UNSAFE_ATTRS = ['status', 'authors'];
const UNSAFE_ATTRS = ['status', 'authors', 'visibility'];
module.exports = {
docName: 'pages',

View file

@ -2,7 +2,7 @@ const models = require('../../models');
const common = require('../../lib/common');
const urlUtils = require('../../lib/url-utils');
const allowedIncludes = ['tags', 'authors', 'authors.roles'];
const unsafeAttrs = ['status', 'authors'];
const unsafeAttrs = ['status', 'authors', 'visibility'];
module.exports = {
docName: 'posts',

View file

@ -901,7 +901,14 @@ Post = ghostBookshelf.Model.extend({
// NOTE: the `authors` extension is the parent of the post model. It also has a permissible function.
permissible: function permissible(postModel, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasAppPermission, hasApiKeyPermission) {
let isContributor, isEdit, isAdd, isDestroy;
let isContributor;
let isOwner;
let isAdmin;
let isEditor;
let isIntegration;
let isEdit;
let isAdd;
let isDestroy;
function isChanging(attr) {
return unsafeAttrs[attr] && unsafeAttrs[attr] !== postModel.get(attr);
@ -916,6 +923,11 @@ Post = ghostBookshelf.Model.extend({
}
isContributor = loadedPermissions.user && _.some(loadedPermissions.user.roles, {name: 'Contributor'});
isOwner = loadedPermissions.user && _.some(loadedPermissions.user.roles, {name: 'Owner'});
isAdmin = loadedPermissions.user && _.some(loadedPermissions.user.roles, {name: 'Admin'});
isEditor = loadedPermissions.user && _.some(loadedPermissions.user.roles, {name: 'Editor'});
isIntegration = loadedPermissions.apiKey && _.some(loadedPermissions.apiKey.roles, {name: 'Admin Integration'});
isEdit = (action === 'edit');
isAdd = (action === 'add');
isDestroy = (action === 'destroy');
@ -929,6 +941,8 @@ Post = ghostBookshelf.Model.extend({
} else if (isContributor && isDestroy) {
// If destroying, only allow contributor to destroy their own draft posts
hasUserPermission = isDraft();
} else if (!(isOwner || isAdmin || isEditor || isIntegration)) {
hasUserPermission = !isChanging('visibility');
}
const excludedAttrs = [];

View file

@ -397,6 +397,36 @@ describe('Unit: models/post: uses database (@TODO: fix me)', function () {
});
});
it('rejects if changing visibility', function (done) {
var mockPostObj = {
get: sinon.stub(),
related: sinon.stub()
},
context = {user: 1},
unsafeAttrs = {visibility: 'public'};
mockPostObj.get.withArgs('visibility').returns('paid');
mockPostObj.related.withArgs('authors').returns({models: [{id: 1}]});
models.Post.permissible(
mockPostObj,
'edit',
context,
unsafeAttrs,
testUtils.permissions.contributor,
false,
false,
true
).then(() => {
done(new Error('Permissible function should have rejected.'));
}).catch((error) => {
error.should.be.an.instanceof(common.errors.NoPermissionError);
should(mockPostObj.get.called).be.false();
should(mockPostObj.related.calledOnce).be.true();
done();
});
});
it('rejects if changing author id', function (done) {
var mockPostObj = {
get: sinon.stub(),
@ -869,6 +899,36 @@ describe('Unit: models/post: uses database (@TODO: fix me)', function () {
});
});
it('rejects if changing visibility', function (done) {
var mockPostObj = {
get: sinon.stub(),
related: sinon.stub()
},
context = {user: 1},
unsafeAttrs = {visibility: 'public'};
mockPostObj.get.withArgs('visibility').returns('paid');
mockPostObj.related.withArgs('authors').returns({models: [{id: 1}]});
models.Post.permissible(
mockPostObj,
'edit',
context,
unsafeAttrs,
testUtils.permissions.author,
false,
false,
true
).then(() => {
done(new Error('Permissible function should have rejected.'));
}).catch((error) => {
error.should.be.an.instanceof(common.errors.NoPermissionError);
should(mockPostObj.get.called).be.false();
should(mockPostObj.related.calledOnce).be.true();
done();
});
});
it('rejects if editing another\'s post (using `authors`)', function (done) {
var mockPostObj = {
get: sinon.stub(),
@ -1174,6 +1234,32 @@ describe('Unit: models/post: uses database (@TODO: fix me)', function () {
should(mockPostObj.get.called).be.false();
});
});
it('resolves if changing visibility', function () {
var mockPostObj = {
get: sinon.stub(),
related: sinon.stub()
},
context = {user: 1},
unsafeAttrs = {visibility: 'public'};
mockPostObj.get.withArgs('visibility').returns('paid');
mockPostObj.related.withArgs('authors').returns({models: [{id: 1}]});
models.Post.permissible(
mockPostObj,
'edit',
context,
unsafeAttrs,
testUtils.permissions.editor,
false,
true,
true
).then(() => {
should(mockPostObj.get.called).be.false();
should(mockPostObj.related.calledOnce).be.true();
});
});
});
});
});