[WIP] Read and update roles & permissions using database

This commit is contained in:
Aurelsicoko 2018-01-17 18:50:12 +01:00
parent b04a495bc4
commit 4aa28a196e
14 changed files with 373 additions and 133 deletions

View File

@ -27,10 +27,10 @@ class ListRow extends React.Component { // eslint-disable-line react/prefer-stat
// Roles that can't be deleted && modified
// Don't delete this line
protectedRoleIDs = ['0'];
protectedRoleIDs = ['root'];
// Roles that can't be deleted;
undeletableIDs = ['1'];
undeletableIDs = ['guest'];
generateContent = () => {
let icons = [
@ -46,11 +46,11 @@ class ListRow extends React.Component { // eslint-disable-line react/prefer-stat
switch (this.props.settingType) {
case 'roles':
if (includes(this.protectedRoleIDs, get(this.props.item, 'id').toString())) {
if (includes(this.protectedRoleIDs, get(this.props.item, 'type').toString())) {
icons = [];
}
if (includes(this.undeletableIDs, get(this.props.item, 'id').toString())) {
if (includes(this.undeletableIDs, get(this.props.item, 'type').toString())) {
icons = [{ icoType: 'pencil', onClick: this.handleClick }];
}
@ -135,7 +135,7 @@ class ListRow extends React.Component { // eslint-disable-line react/prefer-stat
handleClick = () => {
switch (this.props.settingType) {
case 'roles': {
if (!includes(this.protectedRoleIDs, get(this.props.item, 'id').toString())) {
if (!includes(this.protectedRoleIDs, get(this.props.item, 'type').toString())) {
return router.push(`${router.location.pathname}/edit/${this.props.item.id}`);
}
return;

View File

@ -0,0 +1 @@
{"actions":["content-manager.contentmanager.models","content-manager.contentmanager.find","content-manager.contentmanager.count","content-manager.contentmanager.findOne","content-manager.contentmanager.create","content-manager.contentmanager.update","content-manager.contentmanager.delete","content-manager.contentmanager.identity","content-type-builder.contenttypebuilder.getModels","content-type-builder.contenttypebuilder.getModel","content-type-builder.contenttypebuilder.getConnections","content-type-builder.contenttypebuilder.createModel","content-type-builder.contenttypebuilder.updateModel","content-type-builder.contenttypebuilder.deleteModel","content-type-builder.contenttypebuilder.autoReload","content-type-builder.contenttypebuilder.checkTableExists","content-type-builder.contenttypebuilder.identity","settings-manager.settingsmanager.menu","settings-manager.settingsmanager.environments","settings-manager.settingsmanager.languages","settings-manager.settingsmanager.databases","settings-manager.settingsmanager.database","settings-manager.settingsmanager.databaseModel","settings-manager.settingsmanager.get","settings-manager.settingsmanager.update","settings-manager.settingsmanager.createLanguage","settings-manager.settingsmanager.deleteLanguage","settings-manager.settingsmanager.createDatabase","settings-manager.settingsmanager.updateDatabase","settings-manager.settingsmanager.deleteDatabase","settings-manager.settingsmanager.autoReload","settings-manager.settingsmanager.identity","users-permissions.auth.callback","users-permissions.auth.changePassword","users-permissions.auth.forgotPassword","users-permissions.auth.register","users-permissions.auth.identity","users-permissions.user.find","users-permissions.user.me","users-permissions.user.findOne","users-permissions.user.create","users-permissions.user.update","users-permissions.user.destroy","users-permissions.user.identity","users-permissions.userspermissions.createRole","users-permissions.userspermissions.deleteProvider","users-permissions.userspermissions.deleteRole","users-permissions.userspermissions.getPermissions","users-permissions.userspermissions.getPolicies","users-permissions.userspermissions.getRole","users-permissions.userspermissions.getRoles","users-permissions.userspermissions.getRoutes","users-permissions.userspermissions.index","users-permissions.userspermissions.init","users-permissions.userspermissions.searchUsers","users-permissions.userspermissions.updateRole","users-permissions.userspermissions.identity"]}

View File

@ -17,6 +17,7 @@ module.exports = cb => {
if (!_.get(strapi.plugins['users-permissions'], 'config.jwtSecret')) {
try {
const jwtSecret = uuid();
fs.writeFileSync(path.join(strapi.config.appPath, 'plugins', 'users-permissions', 'config', 'jwt.json'), JSON.stringify({
jwtSecret
}, null, 2), 'utf8');
@ -28,6 +29,6 @@ module.exports = cb => {
}
strapi.plugins['users-permissions'].services.userspermissions.syncSchema(() => {
strapi.plugins['users-permissions'].services.userspermissions.updatePermissions(cb);
strapi.plugins['users-permissions'].services.userspermissions.initialize(cb);
});
};

View File

@ -1,6 +1,6 @@
module.exports = async (ctx, next) => {
if (!ctx.state.user) {
throw new Error('Authentication required.');
return ctx.unauthorized();
}
await next();

View File

@ -1,45 +1,54 @@
const _ = require('lodash');
module.exports = async (ctx, next) => {
const route = ctx.request.route;
let role = '1';
let role;
if (ctx.request && ctx.request.header && ctx.request.header.authorization) {
try {
const tokenUser = await strapi.plugins['users-permissions'].services.jwt.getToken(ctx);
ctx.state.user = await strapi.plugins['users-permissions'].services.user.fetch(_.pick(tokenUser, ['_id', 'id']));
const token = await strapi.plugins['users-permissions'].services.jwt.getToken(ctx);
ctx.state.user = await strapi.query('user', 'users-permissions').findOne(_.pick(token, ['_id', 'id']), ['role'])
} catch (err) {
return ctx.unauthorized(err);
}
if (!ctx.state.user) {
return ctx.unauthorized('This user doesn\'t exit.');
return ctx.unauthorized(`User Not Found.`);
}
role = ctx.state.user.role;
if (role.toString() === '0') {
if (role.type === 'root') {
return await next();
}
}
const permission = _.get(strapi.plugins['users-permissions'].config, ['roles', role.toString(), 'permissions', route.plugin || 'application', 'controllers', route.controller, route.action]);
// Retrieve `guest` role.
if (!role) {
role = await strapi.query('role', 'users-permissions').findOne({ type: 'guest' }, []);
}
const route = ctx.request.route;
const permission = await strapi.query('permission', 'users-permissions').findOne({
role: role._id || role.id,
type: route.plugin || 'application',
controller: route.controller,
action: route.action,
enabled: true
}, []);
if (!permission) {
return await next();
return ctx.unauthorized();
}
if (permission.enabled && permission.policy) {
try {
await strapi.plugins['users-permissions'].config.policies[permission.policy](ctx, next);
} catch (err) {
ctx.unauthorized(err);
}
} else if (permission.enabled) {
await next();
} else {
ctx.unauthorized('Access restricted for this action.');
// Execute the policies.
if (permission.policy) {
await strapi.plugins['users-permissions'].config.policies[permission.policy](ctx, next);
}
console.log("OKAY");
// Execute the action.
await next();
};

View File

@ -1,21 +1,22 @@
const _ = require('lodash');
module.exports = {
find: async function (params) {
find: async function (params = {}, populate) {
return this
.find(params.where)
.limit(Number(params.limit))
.sort(params.sort)
.skip(Number(params.skip))
.populate(this.associations.map(x => x.alias).join(' '));
.populate(populate || this.associations.map(x => x.alias).join(' '))
.lean();
},
count: async function (params) {
count: async function (params = {}) {
return Number(await this
.count());
.count(params));
},
findOne: async function (params) {
findOne: async function (params, populate) {
if (!params[this.primaryKey] && params.id) {
params[this.primaryKey] = params.id;
delete params.id;
@ -25,12 +26,12 @@ module.exports = {
return this
.findOne(params)
.populate(this.associations.map(x => x.alias).join(' '));
.populate(populate || this.associations.map(x => x.alias).join(' '));
},
create: async function (params) {
return this.create(Object.keys(params).reduce((acc, current) => {
if (_.get(this._attributes, [current, 'type'])) {
if (_.get(this._attributes, [current, 'type']) || _.get(this._attributes, [current, 'model'])) {
acc[current] = params[current];
}
@ -87,5 +88,20 @@ module.exports = {
}
}
]);
},
createRole: async function (params) {
return this.
create(params);
},
addPermission: async function (params) {
return this
.create(params);
},
removePermission: async function (params) {
return this
.remove(params);
}
};

View File

@ -159,15 +159,18 @@ module.exports = {
}
// First, check if the user is the first one to register as admin.
const adminUsers = await strapi.query('user', 'users-permissions').find(strapi.utils.models.convertParams('user', { role: '0' }));
const hasAdmin = await strapi.query('user', 'users-permissions').count(strapi.utils.models.convertParams('user', { type: 'root' }));
// Check if the user is the first to register
if (adminUsers.length === 0) {
params.role = '0';
} else {
params.role = '1';
const role = hasAdmin < 1 ?
await strapi.query('role', 'users-permissions').findOne({ type: 'root' }, []):
await strapi.query('role', 'users-permissions').findOne({ type: 'guest' }, []);
if (!role) {
return ctx.badRequest(null, ctx.request.admin ? [{ messages: [{ id: 'Auth.form.error.role.notFound' }] }] : 'Impossible to find the root role.');
}
params.role = role._id || role.id;
params.password = await strapi.plugins['users-permissions'].services.user.hashPassword(params);
try {

View File

@ -90,10 +90,12 @@ module.exports = {
getRoles: async (ctx) => {
try {
console.log("coucou");
const roles = await strapi.plugins['users-permissions'].services.userspermissions.getRoles();
ctx.send({ roles });
} catch(err) {
console.log(err);
ctx.badRequest(null, [{ messages: [{ id: 'Not found' }] }]);
}
},
@ -118,7 +120,7 @@ module.exports = {
},
init: async (ctx) => {
const hasAdmin = await strapi.query('user', 'users-permissions').find(strapi.utils.models.convertParams('user', { role: '0' }));
const hasAdmin = await strapi.query('user', 'users-permissions').find(strapi.utils.models.convertParams('user', { type: 'guest' }));
ctx.send({ hasAdmin: hasAdmin.length > 0 });
},

View File

@ -0,0 +1,54 @@
'use strict';
/**
* Lifecycle callbacks for the `User` model.
*/
module.exports = {
// Before saving a value.
// Fired before an `insert` or `update` query.
// beforeSave: async (model) => {},
// After saving a value.
// Fired after an `insert` or `update` query.
// afterSave: async (model, result) => {},
// Before fetching all values.
// Fired before a `fetchAll` operation.
// beforeFetchAll: async (model) => {},
// After fetching all values.
// Fired after a `fetchAll` operation.
// afterFetchAll: async (model, results) => {},
// Fired before a `fetch` operation.
// beforeFetch: async (model) => {},
// After fetching a value.
// Fired after a `fetch` operation.
// afterFetch: async (model, result) => {},
// Before creating a value.
// Fired before `insert` query.
// beforeCreate: async (model) => {},
// After creating a value.
// Fired after `insert` query.
// afterCreate: async (model, result) => {},
// Before updating a value.
// Fired before an `update` query.
// beforeUpdate: async (model) => {},
// After updating a value.
// Fired after an `update` query.
// afterUpdate: async (model, result) => {},
// Before destroying a value.
// Fired before a `delete` query.
// beforeDestroy: async (model) => {},
// After destroying a value.
// Fired after a `delete` query.
// afterDestroy: async (model, result) => {}
};

View File

@ -0,0 +1,34 @@
{
"connection": "default",
"info": {
"name": "permission",
"description": ""
},
"attributes": {
"type": {
"type": "string",
"required": true
},
"controller": {
"type": "string",
"required": true
},
"action": {
"type": "string",
"required": true
},
"enabled": {
"type": "boolean",
"required": true
},
"policy": {
"type": "json",
"required": true
},
"role": {
"model": "role",
"via": "permissions",
"plugin": "users-permissions"
}
}
}

View File

@ -0,0 +1,54 @@
'use strict';
/**
* Lifecycle callbacks for the `User` model.
*/
module.exports = {
// Before saving a value.
// Fired before an `insert` or `update` query.
// beforeSave: async (model) => {},
// After saving a value.
// Fired after an `insert` or `update` query.
// afterSave: async (model, result) => {},
// Before fetching all values.
// Fired before a `fetchAll` operation.
// beforeFetchAll: async (model) => {},
// After fetching all values.
// Fired after a `fetchAll` operation.
// afterFetchAll: async (model, results) => {},
// Fired before a `fetch` operation.
// beforeFetch: async (model) => {},
// After fetching a value.
// Fired after a `fetch` operation.
// afterFetch: async (model, result) => {},
// Before creating a value.
// Fired before `insert` query.
// beforeCreate: async (model) => {},
// After creating a value.
// Fired after `insert` query.
// afterCreate: async (model, result) => {},
// Before updating a value.
// Fired before an `update` query.
// beforeUpdate: async (model) => {},
// After updating a value.
// Fired after an `update` query.
// afterUpdate: async (model, result) => {},
// Before destroying a value.
// Fired before a `delete` query.
// beforeDestroy: async (model) => {},
// After destroying a value.
// Fired after a `delete` query.
// afterDestroy: async (model, result) => {}
};

View File

@ -0,0 +1,31 @@
{
"connection": "default",
"info": {
"name": "role",
"description": ""
},
"attributes": {
"name": {
"type": "string",
"minLength": 3,
"required": true
},
"description": {
"type": "string"
},
"type": {
"type": "string",
"unique": true
},
"users": {
"collection": "user",
"via": "role",
"plugin": "users-permissions"
},
"permissions": {
"collection": "permission",
"via": "role",
"plugin": "users-permissions"
}
}
}

View File

@ -1,5 +1,5 @@
{
"collectionName": "",
"connection": "default",
"info": {
"name": "user",
"description": ""
@ -34,9 +34,10 @@
"configurable": false
},
"role": {
"type": "integer",
"model": "role",
"via": "users",
"plugin": "users-permissions",
"configurable": false
}
},
"connection": "default"
}
}

View File

@ -2,7 +2,6 @@
const fs = require('fs')
const path = require('path');
const stringify = JSON.stringify;
const _ = require('lodash');
const request = require('request');
@ -59,7 +58,7 @@ module.exports = {
});
},
getActions: (plugins = []) => {
getActions: (plugins = [], withInfo = true) => {
const generateActions = (data) => (
Object.keys(data).reduce((acc, key) => {
acc[key] = { enabled: false, policy: '' };
@ -74,12 +73,20 @@ module.exports = {
}, { controllers: {} });
const pluginsPermissions = Object.keys(strapi.plugins).reduce((acc, key) => {
const initialState = {
controllers: {}
};
if (withInfo) {
initialState.information = plugins.find(plugin => plugin.id === key) || {};
}
acc[key] = Object.keys(strapi.plugins[key].controllers).reduce((obj, k) => {
obj.controllers[k] = generateActions(strapi.plugins[key].controllers[k]);
return obj;
}, { controllers: {}, information: plugins.find(plugin => plugin.id === key) || {} });
}, initialState);
return acc;
}, {});
@ -108,111 +115,107 @@ module.exports = {
},
getRoles: async () => {
const roles = strapi.plugins['users-permissions'].config.roles;
const usersCount = await strapi.query('user', 'users-permissions').countByRoles();
const formattedRoles = Object.keys(roles).reduce((acc, key) => {
const role = _.pick(roles[key], ['name', 'description']);
const roles = await strapi.query('role', 'users-permissions').find({ sort: 'name ASC' }, []);
_.set(role, 'id', key);
_.set(role, 'nb_users', _.get(_.find(usersCount, { _id: parseFloat(key) }), 'total', 0));
acc.push(role);
for (let i = 0; i < roles.length; ++i) {
role.nb_users = await strapi.query('user', 'users-permissions').count({ role: roles[i].id || roles[i]._id });
}
return acc;
}, []);
return formattedRoles;
return roles;
},
getRoutes: async () => {
const apiRoutes = strapi.api ? Object.keys(strapi.api).reduce((acc, current) => {
const routes = Object.keys(strapi.api || {}).reduce((acc, current) => {
return acc.concat(strapi.api[current].config.routes);
}, []) : [];
}, []);
const pluginsRoutes = Object.keys(strapi.plugins).reduce((acc, current) => {
const pluginsRoutes = Object.keys(strapi.plugins || {}).reduce((acc, current) => {
acc[current] = strapi.plugins[current].config.routes;
return acc;
}, []);
return _.merge({ application: apiRoutes}, pluginsRoutes);
return _.merge({ application: routes }, pluginsRoutes);
},
getRoleConfigPath: () => (
path.join(
strapi.config.appPath,
'plugins',
'users-permissions',
'config',
'roles.json',
)
),
updatePermissions: async function (cb) {
const actions = strapi.plugins['users-permissions'].config.actions || [];
updateData: (data, diff = 'unset') => {
const dataToCompare = strapi.plugins['users-permissions'].services.userspermissions.getActions();
// Aggregate first level actions.
const appActions = Object.keys(strapi.api || {}).reduce((acc, api) => {
Object.keys(strapi.api[api].controllers)
.map(controller => {
const actions = Object.keys(strapi.api[api].controllers[controller])
.map(action => `application.${controller}.${action}`);
_.forEach(data, (roleData, roleId) => {
const obj = diff === 'unset' ? roleData.permissions : dataToCompare;
_.forEach(obj, (pluginData, pluginName) => {
_.forEach(pluginData.controllers, (controllerActions, controllerName) => {
_.forEach(controllerActions, (actionData, actionName) => {
if (diff === 'unset') {
if (!_.get(dataToCompare, [pluginName, 'controllers', controllerName])) {
_.unset(data, [roleId, 'permissions', pluginName, 'controllers', controllerName]);
return;
}
if (!_.get(dataToCompare, [pluginName, 'controllers', controllerName, actionName])) {
_.unset(data, [roleId, 'permissions', pluginName, 'controllers', controllerName, actionName]);
}
} else if (!_.get(data, [roleId, 'permissions', pluginName, 'controllers', controllerName, actionName])) {
const isCallback = actionName === 'callback' && controllerName === 'auth' && pluginName === 'users-permissions' && roleId === '1';
const isRegister = actionName === 'register' && controllerName === 'auth' && pluginName === 'users-permissions' && roleId === '1';
const isPassword = actionName === 'forgotPassword' && controllerName === 'auth' && pluginName === 'users-permissions' && roleId === '1';
const isNewPassword = actionName === 'changePassword' && controllerName === 'auth' && pluginName === 'users-permissions' && roleId === '1';
const isInit = actionName === 'init' && controllerName === 'userspermissions';
const isMe = actionName === 'me' && controllerName === 'user' && pluginName === 'users-permissions';
const enabled = isCallback || isRegister || roleId === '0' || isInit || isPassword || isNewPassword || isMe;
_.set(data, [roleId, 'permissions', pluginName, 'controllers', controllerName, actionName], { enabled, policy: '' })
}
});
});
acc = acc.concat(actions);
});
});
return data;
},
return acc;
}, []);
updatePermissions: async (cb) => {
const appActions = module.exports.getActions();
const writePermissions = module.exports.writePermissions;
const currentRoles = strapi.plugins['users-permissions'].config.roles || {
'0': {
description: '',
name: 'Administrator',
permissions: {
application: {
controllers: {},
},
},
},
'1': {
description: '',
name: 'Guest',
permissions: {
application: {
controllers: {},
},
},
},
};
// Aggregate plugins' actions.
const pluginsActions = Object.keys(strapi.plugins).reduce((acc, plugin) => {
Object.keys(strapi.plugins[plugin].controllers)
.map(controller => {
const actions = Object.keys(strapi.plugins[plugin].controllers[controller])
.map(action => `${plugin}.${controller}.${action}`);
const remove = await module.exports.updateData(_.cloneDeep(currentRoles));
const added = await module.exports.updateData(_.cloneDeep(remove), 'set');
acc = acc.concat(actions);
});
if (!_.isEqual(currentRoles, added)) {
writePermissions(added);
return acc;
}, []);
// Merge array into one.
const currentActions = appActions.concat(pluginsActions);
// Count permissions available.
const permissions = await strapi.query('permission', 'users-permissions').count();
// Compare to know if actions have been added or removed from controllers.
if (!_.isEqual(actions, currentActions) || permissions < 1) {
const splitted = (str) => {
const [type, controller, action] = str.split('.');
return { type, controller, action };
};
const defaultPolicy = (obj, role) => {
const isCallback = obj.action === 'callback' && obj.controller === 'auth' && obj.type === 'users-permissions' && role.type === 'guest';
const isRegister = obj.action === 'register' && obj.controller === 'auth' && obj.type === 'users-permissions' && role.type === 'guest';
const isPassword = obj.action === 'forgotPassword' && obj.controller === 'auth' && obj.type === 'users-permissions' && role.type === 'guest';
const isNewPassword = obj.action === 'changePassword' && obj.controller === 'auth' && obj.type === 'users-permissions' && role.type === 'guest';
const isInit = obj.action === 'init' && obj.controller === 'userspermissions';
const isMe = obj.action === 'me' && obj.controller === 'user' && obj.type === 'users-permissions';
const enabled = isCallback || isRegister || role.type === 'root' || isInit || isPassword || isNewPassword || isMe;
return Object.assign(obj, { enabled, policy: '' });
};
// Retrieve roles
const roles = await strapi.query('role', 'users-permissions').find();
// We have to know the difference to add or remove
// the permissions entries in the database.
const toRemove = _.difference(actions, currentActions).map(splitted);
const toAdd = (permissions < 1 ? currentActions : _.difference(currentActions, actions))
.map(splitted);
// Execute request to update entries in database for each role.
await Promise.all(
roles.map(role =>
Promise.all(
toAdd
.map(action => defaultPolicy(action, role))
.map(action => strapi.query('permission', 'users-permissions')
.addPermission(Object.assign(action, { role: role.id || role._id }))
)
)
),
Promise.all(toRemove.map(action => strapi.query('permission', 'users-permissions').removePermission(action)))
);
this.writeActions(currentActions);
}
if (cb) {
@ -220,11 +223,40 @@ module.exports = {
}
},
initialize: async function (cb) {
const roles = await strapi.query('role', 'users-permissions').count();
// It's has been already initialized.
if (roles > 0) {
return await this.updatePermissions(cb);
}
// Create two first default roles.
await Promise.all([
strapi.query('role', 'users-permissions').createRole({
name: 'Administrator',
description: 'These users have all access in the project.',
type: 'root'
}),
strapi.query('role', 'users-permissions').createRole({
name: 'Guest',
description: 'Default role given to unauthenticated user.',
type: 'guest'
}),
]);
await this.updatePermissions(cb);
},
updateRole: async (roleId, body) => {
const appRoles = strapi.plugins['users-permissions'].config.roles
const updatedRole = _.pick(body, ['name', 'description', 'permissions']);
_.set(appRoles, [roleId], updatedRole);
// TODO:
// - Call request.
// Role.update()
module.exports.writePermissions(appRoles);
const currentUsers = await strapi.query('user', 'users-permissions').find(strapi.utils.models.convertParams('user', {
@ -248,11 +280,13 @@ module.exports = {
});
},
writePermissions: (data) => {
const roleConfigPath = module.exports.getRoleConfigPath();
writeActions: (data) => {
const actionsPath = path.join(strapi.config.appPath, 'plugins', 'users-permissions', 'config', 'actions.json');
try {
fs.writeFileSync(roleConfigPath, stringify({ roles: data }, null, 2), 'utf8');
// Rewrite actions.json file.
fs.writeFileSync(actionsPath, JSON.stringify({ actions: data }), 'utf8');
// Set value to AST to avoid restart.
_.set(strapi.plugins['users-permissions'], 'config.roles', data);
} catch(err) {
strapi.log.error(err);