0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-10 23:36:14 -05:00

Added members permission system

refs https://github.com/TryGhost/Team/issues/1664

- the new member comments API needs members to have permissions to edit and delete their own posts
- added members as a provider, and then wired up permissible logic at the model level
This commit is contained in:
Hannah Wolfe 2022-07-05 11:16:14 +02:00 committed by Simon Backx
parent 00110e541e
commit 42fc272433
7 changed files with 94 additions and 12 deletions

View file

@ -92,7 +92,7 @@ module.exports = {
frame.options.context = permissions.parseContext(frame.options.context); frame.options.context = permissions.parseContext(frame.options.context);
// CASE: Content API access // CASE: Content API access
if (frame.options.context.public) { if (frame.options.context.public && frame.apiType !== 'comments') {
debug('check content permissions'); debug('check content permissions');
// @TODO: Remove when we drop v0.1 // @TODO: Remove when we drop v0.1

View file

@ -1,4 +1,12 @@
const ghostBookshelf = require('./base'); const ghostBookshelf = require('./base');
const _ = require('lodash');
const errors = require('@tryghost/errors');
const tpl = require('@tryghost/tpl');
const messages = {
commentNotFound: 'Comment could not be found',
notYourComment: 'You may only edit your own comments'
};
const Comment = ghostBookshelf.Model.extend({ const Comment = ghostBookshelf.Model.extend({
tableName: 'comments', tableName: 'comments',
@ -32,9 +40,37 @@ const Comment = ghostBookshelf.Model.extend({
model.emitChange('added', options); model.emitChange('added', options);
} }
}, { }, {
async permissible(model, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasApiKeyPermission, hasMemberPermission) { async permissible(commentModelOrId, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasApiKeyPermission, hasMemberPermission) {
console.log('checking permissions', hasMemberPermission); const self = this;
return true;
if (_.isString(commentModelOrId)) {
// Grab the original args without the first one
const origArgs = _.toArray(arguments).slice(1);
// Get the actual comment model
return this.findOne({
id: commentModelOrId
}).then(function then(foundCommentModel) {
if (!foundCommentModel) {
throw new errors.NotFoundError({
message: tpl(messages.commentNotFound)
});
}
// Build up the original args but substitute with actual model
const newArgs = [foundCommentModel].concat(origArgs);
return self.permissible.apply(self, newArgs);
});
}
if ((action === 'edit' || action === 'destroy') && commentModelOrId.get('member_id') !== context.member.id) {
return Promise.reject(new errors.NoPermissionError({
message: tpl(messages.notYourComment)
}));
}
return hasMemberPermission;
} }
}); });

View file

@ -27,7 +27,8 @@ CanThisResult.prototype.buildObjectTypeHandlers = function (objTypes, actType, c
permission: models.Permission, permission: models.Permission,
setting: models.Settings, setting: models.Settings,
invite: models.Invite, invite: models.Invite,
integration: models.Integration integration: models.Integration,
comment: models.Comment
}; };
// Iterate through the object types, i.e. ['post', 'tag', 'user'] // Iterate through the object types, i.e. ['post', 'tag', 'user']
@ -57,10 +58,12 @@ CanThisResult.prototype.buildObjectTypeHandlers = function (objTypes, actType, c
return permissionLoad.then(function (loadedPermissions) { return permissionLoad.then(function (loadedPermissions) {
// Iterate through the user permissions looking for an affirmation // Iterate through the user permissions looking for an affirmation
const userPermissions = loadedPermissions.user ? loadedPermissions.user.permissions : null; const userPermissions = loadedPermissions.user ? loadedPermissions.user.permissions : null;
const apiKeyPermissions = loadedPermissions.apiKey ? loadedPermissions.apiKey.permissions : null; const apiKeyPermissions = loadedPermissions.apiKey ? loadedPermissions.apiKey.permissions : null;
const memberPermissions = loadedPermissions.member ? loadedPermissions.member.permissions : null;
let hasUserPermission; let hasUserPermission;
let hasApiKeyPermission; let hasApiKeyPermission;
let hasMemberPermission = false;
const checkPermission = function (perm) { const checkPermission = function (perm) {
let permObjId; let permObjId;
@ -91,6 +94,10 @@ CanThisResult.prototype.buildObjectTypeHandlers = function (objTypes, actType, c
hasUserPermission = _.some(userPermissions, checkPermission); hasUserPermission = _.some(userPermissions, checkPermission);
} }
if (loadedPermissions.member) {
hasMemberPermission = _.some(memberPermissions, checkPermission);
}
// Check api key permissions if they were passed // Check api key permissions if they were passed
hasApiKeyPermission = true; hasApiKeyPermission = true;
if (!_.isNull(apiKeyPermissions)) { if (!_.isNull(apiKeyPermissions)) {
@ -102,7 +109,7 @@ CanThisResult.prototype.buildObjectTypeHandlers = function (objTypes, actType, c
// Offer a chance for the TargetModel to override the results // Offer a chance for the TargetModel to override the results
if (TargetModel && _.isFunction(TargetModel.permissible)) { if (TargetModel && _.isFunction(TargetModel.permissible)) {
return TargetModel.permissible( return TargetModel.permissible(
modelId, actType, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasApiKeyPermission modelId, actType, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasApiKeyPermission, hasMemberPermission
); );
} }
@ -122,6 +129,7 @@ CanThisResult.prototype.beginCheck = function (context) {
const self = this; const self = this;
let userPermissionLoad; let userPermissionLoad;
let apiKeyPermissionLoad; let apiKeyPermissionLoad;
let memberPermissionLoad;
let permissionsLoad; let permissionsLoad;
// Get context.user, context.api_key and context.app // Get context.user, context.api_key and context.app
@ -147,11 +155,16 @@ CanThisResult.prototype.beginCheck = function (context) {
apiKeyPermissionLoad = Promise.resolve(null); apiKeyPermissionLoad = Promise.resolve(null);
} }
if (context.member) {
memberPermissionLoad = providers.member(context.member.id);
}
// Wait for both user and app permissions to load // Wait for both user and app permissions to load
permissionsLoad = Promise.all([userPermissionLoad, apiKeyPermissionLoad]).then(function (result) { permissionsLoad = Promise.all([userPermissionLoad, apiKeyPermissionLoad, memberPermissionLoad]).then(function (result) {
return { return {
user: result[0], user: result[0],
apiKey: result[1] apiKey: result[1],
member: result[2]
}; };
}); });

View file

@ -12,6 +12,7 @@ module.exports = function parseContext(context) {
user: null, user: null,
api_key: null, api_key: null,
integration: null, integration: null,
member: null,
public: true public: true
}; };
@ -37,5 +38,9 @@ module.exports = function parseContext(context) {
parsed.public = (context.api_key.type === 'content'); parsed.public = (context.api_key.type === 'content');
} }
if (context && context.member) {
parsed.member = context.member;
}
return parsed; return parsed;
}; };

View file

@ -6,7 +6,8 @@ const tpl = require('@tryghost/tpl');
const messages = { const messages = {
userNotFound: 'User not found', userNotFound: 'User not found',
apiKeyNotFound: 'API Key not found' apiKeyNotFound: 'API Key not found',
memberNotFound: 'Member not found'
}; };
module.exports = { module.exports = {
@ -72,5 +73,20 @@ module.exports = {
return {permissions, roles}; return {permissions, roles};
}); });
},
async member(id) {
const foundMember = await models.Member.findOne({id});
if (!foundMember) {
throw new errors.NotFoundError({
message: tpl(messages.memberNotFound)
});
}
// @TODO: figure out how we want to associate members with permissions
// Dirty code to load all comment permissions except moderation for members
const permissions = await models.Permission.findAll({filter: 'object_type:comment+action_type:-moderate'});
return {permissions: permissions.models};
} }
}; };

View file

@ -474,7 +474,7 @@ describe('Permissions', function () {
}) })
.catch(function (err) { .catch(function (err) {
permissibleStub.callCount.should.eql(1); permissibleStub.callCount.should.eql(1);
permissibleStub.firstCall.args.should.have.lengthOf(7); permissibleStub.firstCall.args.should.have.lengthOf(8);
permissibleStub.firstCall.args[0].should.eql(1); permissibleStub.firstCall.args[0].should.eql(1);
permissibleStub.firstCall.args[1].should.eql('edit'); permissibleStub.firstCall.args[1].should.eql('edit');
@ -509,7 +509,7 @@ describe('Permissions', function () {
.post({id: 1}) // tag id in model syntax .post({id: 1}) // tag id in model syntax
.then(function (res) { .then(function (res) {
permissibleStub.callCount.should.eql(1); permissibleStub.callCount.should.eql(1);
permissibleStub.firstCall.args.should.have.lengthOf(7); permissibleStub.firstCall.args.should.have.lengthOf(8);
permissibleStub.firstCall.args[0].should.eql(1); permissibleStub.firstCall.args[0].should.eql(1);
permissibleStub.firstCall.args[1].should.eql('edit'); permissibleStub.firstCall.args[1].should.eql('edit');
permissibleStub.firstCall.args[2].should.be.an.Object(); permissibleStub.firstCall.args[2].should.be.an.Object();
@ -517,6 +517,7 @@ describe('Permissions', function () {
permissibleStub.firstCall.args[4].should.be.an.Object(); permissibleStub.firstCall.args[4].should.be.an.Object();
permissibleStub.firstCall.args[5].should.be.true(); permissibleStub.firstCall.args[5].should.be.true();
permissibleStub.firstCall.args[6].should.be.true(); permissibleStub.firstCall.args[6].should.be.true();
permissibleStub.firstCall.args[7].should.be.false();
userProviderStub.callCount.should.eql(1); userProviderStub.callCount.should.eql(1);
should.not.exist(res); should.not.exist(res);

View file

@ -9,6 +9,7 @@ describe('Permissions', function () {
external: false, external: false,
user: null, user: null,
api_key: null, api_key: null,
member: null,
public: true, public: true,
integration: null integration: null
}); });
@ -17,6 +18,7 @@ describe('Permissions', function () {
external: false, external: false,
user: null, user: null,
api_key: null, api_key: null,
member: null,
public: true, public: true,
integration: null integration: null
}); });
@ -28,6 +30,7 @@ describe('Permissions', function () {
external: false, external: false,
user: null, user: null,
api_key: null, api_key: null,
member: null,
public: true, public: true,
integration: null integration: null
}); });
@ -36,6 +39,7 @@ describe('Permissions', function () {
external: false, external: false,
user: null, user: null,
api_key: null, api_key: null,
member: null,
public: true, public: true,
integration: null integration: null
}); });
@ -47,6 +51,7 @@ describe('Permissions', function () {
external: false, external: false,
user: 1, user: 1,
api_key: null, api_key: null,
member: null,
public: false, public: false,
integration: null integration: null
}); });
@ -64,6 +69,7 @@ describe('Permissions', function () {
id: 1, id: 1,
type: 'content' type: 'content'
}, },
member: null,
public: true, public: true,
integration: {id: 2} integration: {id: 2}
}); });
@ -81,6 +87,7 @@ describe('Permissions', function () {
id: 1, id: 1,
type: 'admin' type: 'admin'
}, },
member: null,
public: false, public: false,
integration: {id: 3} integration: {id: 3}
}); });
@ -92,6 +99,7 @@ describe('Permissions', function () {
external: false, external: false,
user: null, user: null,
api_key: null, api_key: null,
member: null,
public: false, public: false,
integration: null integration: null
}); });
@ -101,6 +109,7 @@ describe('Permissions', function () {
external: false, external: false,
user: null, user: null,
api_key: null, api_key: null,
member: null,
public: false, public: false,
integration: null integration: null
}); });
@ -112,6 +121,7 @@ describe('Permissions', function () {
external: true, external: true,
user: null, user: null,
api_key: null, api_key: null,
member: null,
public: false, public: false,
integration: null integration: null
}); });
@ -121,6 +131,7 @@ describe('Permissions', function () {
external: true, external: true,
user: null, user: null,
api_key: null, api_key: null,
member: null,
public: false, public: false,
integration: null integration: null
}); });