From 2786c103ac948aefcaf5863b36f12dfb0d46f0cc Mon Sep 17 00:00:00 2001 From: Jacob Gable Date: Tue, 4 Jun 2013 22:47:11 -0500 Subject: [PATCH] Permissions / ACL - Created Role model - Created Permission model - Linked Users->Roles with a belongsToMany relationship - Linked Permissions to Users and Roles with a belongsToMany relationship - Created permissions helper with functions for initializing and checking permissions (canThis) - Unit tests for lots of things --- core/shared/data/fixtures/001.js | 66 +++++++ core/shared/data/migration/001.js | 49 ++++- core/shared/errorHandling.js | 1 + core/shared/models/index.js | 2 + core/shared/models/permission.js | 31 ++++ core/shared/models/role.js | 32 ++++ core/shared/models/user.js | 41 ++++- core/shared/permissions/index.js | 160 +++++++++++++++++ core/test/ghost/api_permissions_spec.js | 171 ++++++++++++++++++ core/test/ghost/api_posts_spec.js | 2 +- core/test/ghost/api_settings_spec.js | 2 +- core/test/ghost/api_users_spec.js | 13 +- core/test/ghost/ghost_spec.js | 4 +- core/test/ghost/permissions_spec.js | 226 ++++++++++++++++++++++++ 14 files changed, 793 insertions(+), 7 deletions(-) create mode 100644 core/shared/models/permission.js create mode 100644 core/shared/models/role.js create mode 100644 core/shared/permissions/index.js create mode 100644 core/test/ghost/api_permissions_spec.js create mode 100644 core/test/ghost/permissions_spec.js diff --git a/core/shared/data/fixtures/001.js b/core/shared/data/fixtures/001.js index 849f92a6ae2c..c832ae3b29bd 100644 --- a/core/shared/data/fixtures/001.js +++ b/core/shared/data/fixtures/001.js @@ -63,5 +63,71 @@ module.exports = { "created_by": 1, "updated_by": 1 } + ], + + roles: [ + { + "id": 1, + "name": "Administrator", + "description": "Administrators" + }, + { + "id": 2, + "name": "Editor", + "description": "Editors" + }, + { + "id": 3, + "name": "Author", + "description": "Authors" + } + ], + + roles_users: [ + { + "id": 1, + "role_id": 1, + "user_id": 1 + } + ], + + permissions: [ + { + "id": 1, + "name": "Edit posts", + "action_type": "edit", + "object_type": "post" + }, + { + "id": 2, + "name": "Remove posts", + "action_type": "remove", + "object_type": "post" + }, + { + "id": 3, + "name": "Create posts", + "action_type": "create", + "object_type": "post" + } + ], + + permissions_roles: [ + { + "id": 1, + "permission_id": 1, + "role_id": 1 + }, + { + "id": 2, + "permission_id": 2, + "role_id": 1 + }, + { + "id": 3, + "permission_id": 3, + "role_id": 1 + } ] + }; diff --git a/core/shared/data/migration/001.js b/core/shared/data/migration/001.js index 5e01bb5fac46..8717c49c3f21 100644 --- a/core/shared/data/migration/001.js +++ b/core/shared/data/migration/001.js @@ -47,6 +47,38 @@ t.integer('updated_by'); }), + knex.Schema.createTable('roles', function (t) { + t.increments().primary(); + t.string('name'); + t.string('description'); + }), + + knex.Schema.createTable('roles_users', function (t) { + t.increments().primary(); + t.integer('role_id'); + t.integer('user_id'); + }), + + knex.Schema.createTable('permissions', function (t) { + t.increments().primary(); + t.string('name'); + t.string('object_type'); + t.string('action_type'); + t.integer('object_id'); + }), + + knex.Schema.createTable('permissions_users', function (t) { + t.increments().primary(); + t.integer('user_id'); + t.integer('permission_id'); + }), + + knex.Schema.createTable('permissions_roles', function(t) { + t.increments().primary(); + t.integer('role_id'); + t.integer('permission_id'); + }), + knex.Schema.createTable('settings', function (t) { t.increments().primary(); t.string('key'); @@ -63,6 +95,10 @@ return when.all([ knex('posts').insert(fixtures.posts), knex('users').insert(fixtures.users), + knex('roles').insert(fixtures.roles), + knex('roles_users').insert(fixtures.roles_users), + knex('permissions').insert(fixtures.permissions), + knex('permissions_roles').insert(fixtures.permissions_roles), knex('settings').insert(fixtures.settings) ]); @@ -74,8 +110,17 @@ return when.all([ knex.Schema.dropTableIfExists("posts"), knex.Schema.dropTableIfExists("users"), - knex.Schema.dropTableIfExists("settings") - ]); + knex.Schema.dropTableIfExists("roles"), + knex.Schema.dropTableIfExists("settings"), + knex.Schema.dropTableIfExists("permissions") + ]).then(function() { + // Drop the relation tables after the model tables? + return when.all([ + knex.Schema.dropTableIfExists("roles_users"), + knex.Schema.dropTableIfExists("permissions_users"), + knex.Schema.dropTableIfExists("permissions_roles") + ]); + }); }; exports.up = up; diff --git a/core/shared/errorHandling.js b/core/shared/errorHandling.js index af678453aed9..ff0aaf8efa47 100644 --- a/core/shared/errorHandling.js +++ b/core/shared/errorHandling.js @@ -21,6 +21,7 @@ }, logError: function (err) { + err = err || "Unknown"; // TODO: Logging framework hookup console.log("Error occurred: ", err.message || err); }, diff --git a/core/shared/models/index.js b/core/shared/models/index.js index f52150cb8e53..ec9afd8da8a8 100644 --- a/core/shared/models/index.js +++ b/core/shared/models/index.js @@ -9,6 +9,8 @@ module.exports = { Post: require('./post').Post, User: require('./user').User, + Role: require('./role').Role, + Permission: require('./permission').Permission, Setting: require('./setting').Setting, init: function () { return knex.Schema.hasTable('posts').then(null, function () { diff --git a/core/shared/models/permission.js b/core/shared/models/permission.js new file mode 100644 index 000000000000..066e3075e3f4 --- /dev/null +++ b/core/shared/models/permission.js @@ -0,0 +1,31 @@ +(function () { + "use strict"; + + var GhostBookshelf = require('./base'), + User = require('./user').User, + Role = require('./role').Role, + Permission, + Permissions; + + Permission = GhostBookshelf.Model.extend({ + tableName: 'permissions', + + roles: function () { + return this.belongsToMany(Role); + }, + + users: function () { + return this.belongsToMany(User); + } + }); + + Permissions = GhostBookshelf.Collection.extend({ + model: Permission + }); + + module.exports = { + Permission: Permission, + Permissions: Permissions + }; + +}()); \ No newline at end of file diff --git a/core/shared/models/role.js b/core/shared/models/role.js new file mode 100644 index 000000000000..ad5f08b4f4d5 --- /dev/null +++ b/core/shared/models/role.js @@ -0,0 +1,32 @@ +(function () { + + "use strict"; + + var User = require('./user').User, + Permission = require('./permission').Permission, + GhostBookshelf = require('./base'), + Role, + Roles; + + Role = GhostBookshelf.Model.extend({ + tableName: 'roles', + + users: function () { + return this.belongsToMany(User); + }, + + permissions: function () { + return this.belongsToMany(Permission); + } + }); + + Roles = GhostBookshelf.Collection.extend({ + model: Role + }); + + module.exports = { + Role: Role, + Roles: Roles + }; + +}()); \ No newline at end of file diff --git a/core/shared/models/user.js b/core/shared/models/user.js index ed477d241edd..356859b44be4 100644 --- a/core/shared/models/user.js +++ b/core/shared/models/user.js @@ -8,7 +8,9 @@ nodefn = require('when/node/function'), bcrypt = require('bcrypt-nodejs'), Posts = require('./post').Posts, - GhostBookshelf = require('./base'); + GhostBookshelf = require('./base'), + Role = require('./role').Role, + Permission = require('./permission').Permission; User = GhostBookshelf.Model.extend({ @@ -18,6 +20,14 @@ posts: function () { return this.hasMany(Posts, 'created_by'); + }, + + roles: function () { + return this.belongsToMany(Role); + }, + + permissions: function () { + return this.belongsToMany(Permission); } }, { @@ -62,6 +72,35 @@ return user; }); }); + }, + + effectivePermissions: function (id) { + return this.read({id: id}, { withRelated: ['permissions', 'roles.permissions'] }) + .then(function (foundUser) { + var seenPerms = {}, + rolePerms = _.map(foundUser.related('roles').models, function (role) { + return role.related('permissions').models; + }), + allPerms = []; + + rolePerms.push(foundUser.related('permissions').models); + + _.each(rolePerms, function (rolePermGroup) { + _.each(rolePermGroup, function (perm) { + var key = perm.get('action_type') + '-' + perm.get('object_type') + '-' + perm.get('object_id'); + + // Only add perms once + if (seenPerms[key]) { + return; + } + + allPerms.push(perm); + seenPerms[key] = true; + }); + }); + + return when.resolve(allPerms); + }); } }); diff --git a/core/shared/permissions/index.js b/core/shared/permissions/index.js new file mode 100644 index 000000000000..422138bbede3 --- /dev/null +++ b/core/shared/permissions/index.js @@ -0,0 +1,160 @@ +(function () { + "use strict"; + + // canThis(someUser).edit.posts([id]|[[ids]]) + // canThis(someUser).edit.post(somePost|somePostId) + + var _ = require('underscore'), + when = require('when'), + Models = require('../models'), + UserProvider = Models.User, + PermissionsProvider = Models.Permission, + init, + refresh, + canThis, + CanThisResult, + exported; + + // Base class for canThis call results + CanThisResult = function () { + this.userPermissionLoad = false; + }; + + CanThisResult.prototype.buildObjectTypeHandlers = function (obj_types, act_type) { + var self = this, + obj_type_handlers = {}; + + // Iterate through the object types, i.e. ['post', 'tag', 'user'] + _.each(obj_types, function (obj_type) { + + // Create the 'handler' for the object type; + // the '.post()' in canThis(user).edit.post() + obj_type_handlers[obj_type] = function (modelOrId) { + var modelId; + + if (_.isNumber(modelOrId) || _.isString(modelOrId)) { + // It's an id already, do nothing + modelId = modelOrId; + } else if (modelOrId) { + // It's a model, get the id + modelId = modelOrId.id; + } + + // Wait for the user loading to finish + return self.userPermissionLoad.then(function (userPermissions) { + + // Iterate through the user permissions looking for an affirmation + var hasPermission = _.any(userPermissions, function (userPermission) { + var permObjId; + + // Look for a matching action type and object type first + if (userPermission.get('action_type') !== act_type || userPermission.get('object_type') !== obj_type) { + return false; + } + + // Grab the object id (if specified, could be null) + permObjId = userPermission.get('object_id'); + + // If we didn't specify a model (any thing) + // or the permission didn't have an id scope set + // then the user has permission + if (!modelId || !permObjId) { + return true; + } + + // Otherwise, check if the id's match + // TODO: String vs Int comparison possibility here? + return modelId === permObjId; + }); + + if (hasPermission) { + return when.resolve(); + } + + return when.reject(); + }); + }; + }); + + return obj_type_handlers; + }; + + CanThisResult.prototype.beginCheck = function (user) { + var self = this; + + // TODO: Switch logic based on object type; user, role, post. + + // Kick off the fetching of the user data + this.userPermissionLoad = UserProvider.effectivePermissions(user.id || user); + + // Iterate through the actions and their related object types + // We should have loaded these through a permissions.init() call previously + // TODO: Throw error if not init() yet? + _.each(exported.actionsMap, function (obj_types, act_type) { + // Build up the object type handlers; + // the '.post()' parts in canThis(user).edit.post() + var obj_type_handlers = self.buildObjectTypeHandlers(obj_types, act_type); + + // Define a property for the action on the result; + // the '.edit' in canThis(user).edit.post() + Object.defineProperty(self, act_type, { + writable: false, + enumerable: false, + configurable: false, + value: obj_type_handlers + }); + }); + + // Return this for chaining + return this; + }; + + canThis = function (user) { + var result = new CanThisResult(); + + return result.beginCheck(user); + }; + + init = refresh = function () { + // Load all the permissions + return PermissionsProvider.browse().then(function (perms) { + var seenActions = {}; + + exported.actionsMap = {}; + + // Build a hash map of the actions on objects, i.e + /* + { + 'edit': ['post', 'tag', 'user', 'page'], + 'delete': ['post', 'user'], + 'create': ['post', 'user', 'page'] + } + */ + _.each(perms.models, function (perm) { + var action_type = perm.get('action_type'), + object_type = perm.get('object_type'); + + exported.actionsMap[action_type] = exported.actionsMap[action_type] || []; + seenActions[action_type] = seenActions[action_type] || {}; + + // Check if we've already seen this action -> object combo + if (seenActions[action_type][object_type]) { + return; + } + + exported.actionsMap[action_type].push(object_type); + seenActions[action_type][object_type] = true; + }); + + return when(exported.actionsMap); + }); + }; + + module.exports = exported = { + init: init, + refresh: refresh, + canThis: canThis, + actionsMap: {} + }; + +}()); \ No newline at end of file diff --git a/core/test/ghost/api_permissions_spec.js b/core/test/ghost/api_permissions_spec.js new file mode 100644 index 000000000000..0f85b159eded --- /dev/null +++ b/core/test/ghost/api_permissions_spec.js @@ -0,0 +1,171 @@ +/*globals describe, beforeEach, it*/ + +(function () { + "use strict"; + + var should = require('should'), + helpers = require('./helpers'), + errors = require('../../shared/errorHandling'), + Models = require('../../shared/models'); + + describe("Role Model", function () { + + var RoleModel = Models.Role; + + should.exist(RoleModel); + + beforeEach(function(done) { + helpers.resetData().then(function() { + done(); + }, done); + }); + + it("can browse roles", function (done) { + RoleModel.browse().then(function (foundRoles) { + should.exist(foundRoles); + + foundRoles.models.length.should.be.above(0); + + done(); + }, errors.logError); + }); + + it("can read roles", function (done) { + RoleModel.read({id: 1}).then(function (foundRole) { + should.exist(foundRole); + + done(); + }, errors.logError); + }); + + it("can edit roles", function (done) { + RoleModel.read({id: 1}).then(function (foundRole) { + should.exist(foundRole); + + return foundRole.set({name: "updated"}).save(); + }).then(function () { + return RoleModel.read({id: 1}); + }).then(function (updatedRole) { + should.exist(updatedRole); + + updatedRole.get("name").should.equal("updated"); + + done(); + }, errors.logError); + }); + + it("can add roles", function (done) { + var newRole = { + name: "test1", + description: "test1 description" + }; + + RoleModel.add(newRole).then(function (createdRole) { + should.exist(createdRole); + + createdRole.attributes.name.should.equal(newRole.name); + createdRole.attributes.description.should.equal(newRole.description); + + done(); + }, done); + }); + + it("can delete roles", function (done) { + RoleModel.read({id: 1}).then(function (foundRole) { + should.exist(foundRole); + + return RoleModel['delete'](1); + }).then(function () { + return RoleModel.browse(); + }).then(function (foundRoles) { + var hasRemovedId = foundRoles.any(function(role) { + return role.id === 1; + }); + + hasRemovedId.should.equal(false); + + done(); + }, errors.logError); + }); + }); + + describe("Permission Model", function () { + + var PermissionModel = Models.Permission; + + should.exist(PermissionModel); + + beforeEach(function(done) { + helpers.resetData().then(function() { + done(); + }, done); + }); + + it("can browse permissions", function (done) { + PermissionModel.browse().then(function (foundPermissions) { + should.exist(foundPermissions); + + foundPermissions.models.length.should.be.above(0); + + done(); + }, errors.logError); + }); + + it("can read permissions", function (done) { + PermissionModel.read({id: 1}).then(function (foundPermission) { + should.exist(foundPermission); + + done(); + }, errors.logError); + }); + + it("can edit permissions", function (done) { + PermissionModel.read({id: 1}).then(function (foundPermission) { + should.exist(foundPermission); + + return foundPermission.set({name: "updated"}).save(); + }).then(function () { + return PermissionModel.read({id: 1}); + }).then(function (updatedPermission) { + should.exist(updatedPermission); + + updatedPermission.get("name").should.equal("updated"); + + done(); + }, errors.logError); + }); + + it("can add permissions", function (done) { + var newPerm = { + name: "testperm1" + }; + + PermissionModel.add(newPerm).then(function (createdPerm) { + should.exist(createdPerm); + + createdPerm.attributes.name.should.equal(newPerm.name); + + done(); + }, done); + }); + + it("can delete permissions", function (done) { + PermissionModel.read({id: 1}).then(function (foundPermission) { + should.exist(foundPermission); + + return PermissionModel['delete'](1); + }).then(function () { + return PermissionModel.browse(); + }).then(function (foundPermissions) { + var hasRemovedId = foundPermissions.any(function(permission) { + return permission.id === 1; + }); + + hasRemovedId.should.equal(false); + + done(); + }, errors.logError); + }); + }); + +}()); \ No newline at end of file diff --git a/core/test/ghost/api_posts_spec.js b/core/test/ghost/api_posts_spec.js index 9ee920ef665b..5028bf08ed5a 100644 --- a/core/test/ghost/api_posts_spec.js +++ b/core/test/ghost/api_posts_spec.js @@ -8,7 +8,7 @@ helpers = require('./helpers'), Models = require('../../shared/models'); - describe('Bookshelf Post Model', function () { + describe('Post Model', function () { var PostModel = Models.Post; diff --git a/core/test/ghost/api_settings_spec.js b/core/test/ghost/api_settings_spec.js index 8f6de03be194..082aeac61ccc 100644 --- a/core/test/ghost/api_settings_spec.js +++ b/core/test/ghost/api_settings_spec.js @@ -8,7 +8,7 @@ helpers = require('./helpers'), Models = require('../../shared/models'); - describe('Bookshelf Setting Model', function () { + describe('Setting Model', function () { var SettingModel = Models.Setting; diff --git a/core/test/ghost/api_users_spec.js b/core/test/ghost/api_users_spec.js index 32be67ed5e2b..fa5b4a98dd1f 100644 --- a/core/test/ghost/api_users_spec.js +++ b/core/test/ghost/api_users_spec.js @@ -6,9 +6,10 @@ var _ = require('underscore'), should = require('should'), helpers = require('./helpers'), + errors = require('../../shared/errorHandling'), Models = require('../../shared/models'); - describe('Bookshelf User Model', function () { + describe('User Model', function () { var UserModel = Models.User; @@ -134,6 +135,16 @@ }).then(null, done); }); + + it("can get effective permissions", function (done) { + UserModel.effectivePermissions(1).then(function (effectivePermissions) { + should.exist(effectivePermissions); + + effectivePermissions.length.should.be.above(0); + + done(); + }, errors.logError); + }); }); }()); \ No newline at end of file diff --git a/core/test/ghost/ghost_spec.js b/core/test/ghost/ghost_spec.js index 88856529b197..22d7f2e77fb1 100644 --- a/core/test/ghost/ghost_spec.js +++ b/core/test/ghost/ghost_spec.js @@ -11,10 +11,12 @@ describe("Ghost API", function () { it("is a singleton", function () { - var ghost1 = new Ghost(), + var logStub = sinon.stub(console, "log"), + ghost1 = new Ghost(), ghost2 = new Ghost(); should.strictEqual(ghost1, ghost2); + logStub.restore(); }); it("uses init() to initialize", function (done) { diff --git a/core/test/ghost/permissions_spec.js b/core/test/ghost/permissions_spec.js new file mode 100644 index 000000000000..4fa6c18b54ec --- /dev/null +++ b/core/test/ghost/permissions_spec.js @@ -0,0 +1,226 @@ +/*globals describe, beforeEach, it*/ + +(function () { + "use strict"; + + var _ = require("underscore"), + when = require('when'), + should = require('should'), + errors = require('../../shared/errorHandling'), + helpers = require('./helpers'), + permissions = require('../../shared/permissions'), + Models = require('../../shared/models'), + UserProvider = Models.User, + PermissionsProvider = Models.Permission; + + describe('permissions', function () { + + should.exist(permissions); + + beforeEach(function (done) { + helpers.resetData().then(function () { done(); }, errors.throwError); + }); + + var testPerms = [ + { act: "edit", obj: "post" }, + { act: "edit", obj: "tag" }, + { act: "edit", obj: "user" }, + { act: "edit", obj: "page" }, + { act: "add", obj: "post" }, + { act: "add", obj: "user" }, + { act: "add", obj: "page" }, + { act: "remove", obj: "post" }, + { act: "remove", obj: "user" } + ], + currTestPermId = 1, + createPermission = function (name, act, obj) { + if (!name) { + currTestPermId += 1; + name = "test" + currTestPermId; + } + + var newPerm = { + name: name, + action_type: act, + object_type: obj + }; + + return PermissionsProvider.add(newPerm); + }, + createTestPermissions = function() { + var createActions = _.map(testPerms, function (testPerm) { + return createPermission(null, testPerm.act, testPerm.obj); + }); + + return when.all(createActions); + }; + + it('can load an actions map from existing permissions', function (done) { + + createTestPermissions() + .then(permissions.init) + .then(function (actionsMap) { + should.exist(actionsMap); + + actionsMap.edit.should.eql(['post', 'tag', 'user', 'page']); + + actionsMap.should.equal(permissions.actionsMap); + + done(); + }, errors.throwError); + }); + + it('can add user to role', function (done) { + var existingUserRoles; + + UserProvider.read({id: 1}, { withRelated: ['roles'] }).then(function (foundUser) { + var testRole = new Models.Role({ + name: 'testrole1', + description: 'testrole1 description' + }); + + should.exist(foundUser); + + should.exist(foundUser.roles()); + + existingUserRoles = foundUser.related('roles').length; + + return testRole.save().then(function () { + return foundUser.roles().attach(testRole); + }); + }).then(function () { + return UserProvider.read({id: 1}, { withRelated: ['roles'] }); + }).then(function (updatedUser) { + should.exist(updatedUser); + + updatedUser.related('roles').length.should.equal(existingUserRoles + 1); + + done(); + }); + }); + + it('can add user permissions', function (done) { + Models.User.read({id: 1}, { withRelated: ['permissions']}).then(function (testUser) { + var testPermission = new Models.Permission({ + name: "test edit posts", + action_type: 'edit', + object_type: 'post' + }); + + testUser.related('permissions').length.should.equal(0); + + return testPermission.save().then(function () { + return testUser.permissions().attach(testPermission); + }); + }).then(function () { + return Models.User.read({id: 1}, { withRelated: ['permissions']}); + }).then(function (updatedUser) { + should.exist(updatedUser); + + updatedUser.related('permissions').length.should.equal(1); + + done(); + }); + }); + + it('can add role permissions', function (done) { + var testRole = new Models.Role({ + name: "test2", + description: "test2 description" + }); + + testRole.save().then(function () { + return testRole.load('permissions'); + }).then(function () { + var rolePermission = new Models.Permission({ + name: "test edit posts", + action_type: 'edit', + object_type: 'post' + }); + + testRole.related('permissions').length.should.equal(0); + + return rolePermission.save().then(function () { + return testRole.permissions().attach(rolePermission); + }); + }).then(function () { + return Models.Role.read({id: testRole.id}, { withRelated: ['permissions']}); + }).then(function (updatedRole) { + should.exist(updatedRole); + + updatedRole.related('permissions').length.should.equal(1); + + done(); + }); + }); + + it('does not allow edit post without permission', function (done) { + var fakePage = { + id: 1 + }; + + createTestPermissions() + .then(permissions.init) + .then(function () { + return Models.User.read({id: 1}); + }) + .then(function (foundUser) { + var canThisResult = permissions.canThis(foundUser); + + should.exist(canThisResult.edit); + should.exist(canThisResult.edit.post); + + return canThisResult.edit.page(fakePage); + }) + .then(function () { + errors.logError(new Error("Allowed edit post without permission")); + }, function () { + done(); + }); + }); + + it('allows edit post with permission', function (done) { + var fakePost = { + id: "1" + }; + + createTestPermissions() + .then(permissions.init) + .then(function () { + return Models.User.read({id: 1}); + }) + .then(function (foundUser) { + var newPerm = new Models.Permission({ + name: "test3 edit post", + action_type: "edit", + object_type: "post" + }); + + return newPerm.save().then(function () { + return foundUser.permissions().attach(newPerm); + }); + }) + .then(function () { + return Models.User.read({id: 1}, { withRelated: ['permissions']}); + }) + .then(function (updatedUser) { + + // TODO: Verify updatedUser.related('permissions') has the permission? + + var canThisResult = permissions.canThis(updatedUser); + + should.exist(canThisResult.edit); + should.exist(canThisResult.edit.post); + + return canThisResult.edit.post(fakePost); + }) + .then(function () { + done(); + }, function () { + errors.logError(new Error("Did not allow edit post with permission")); + }); + }); + + }); + +}()); \ No newline at end of file