Permissions authorization middleware.

This commit is contained in:
Ahmed Bouhuolia
2019-09-16 01:08:19 +02:00
parent ed4d37c8fb
commit de905d7e7c
23 changed files with 318 additions and 51 deletions

View File

@@ -1,4 +1,5 @@
import express from 'express';
import helmet from 'helmet';
import boom from 'express-boom';
import '../config';
import routes from '@/http';
@@ -8,6 +9,7 @@ const app = express();
// Express configuration
app.set('port', process.env.PORT || 3000);
app.use(helmet());
app.use(boom());
app.use(express.json());

View File

@@ -1,9 +1,10 @@
import express from 'express';
import { check, validationResult, oneOf } from 'express-validator';
import { difference } from 'lodash';
import knex from 'knex';
import asyncMiddleware from '../middleware/asyncMiddleware';
import Account from '@/models/Account';
// import AccountBalance from '@/models/AccountBalance';
import '@/models/AccountBalance';
export default {
/**
@@ -35,6 +36,7 @@ export default {
],
async handler(req, res) {
const validationErrors = validationResult(req);
// const defaultCurrency = 'USD';
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
@@ -48,11 +50,12 @@ export default {
const accountsCollection = await Account.query((query) => {
query.select(['id']);
query.whereIn('id', accountsIds);
}).fetchAll();
}).fetchAll({
withRelated: ['balances'],
});
const accountsStoredIds = accountsCollection.map((account) => account.attributes.id);
const notFoundAccountsIds = difference(accountsIds, accountsStoredIds);
const errorReasons = [];
if (notFoundAccountsIds.length > 0) {
@@ -64,6 +67,29 @@ export default {
return res.boom.badData(null, { errors: errorReasons });
}
const storedAccountsBalances = accountsCollection.related('balances');
const submitBalancesMap = new Map(accounts.map((account) => [account, account.id]));
const storedBalancesMap = new Map(storedAccountsBalances.map((balance) => [
balance.attributes, balance.attributes.id,
]));
// const updatedStoredBalanced = [];
const notStoredBalances = [];
accountsIds.forEach((id) => {
if (!storedBalancesMap.get(id)) {
notStoredBalances.push(id);
}
});
await knex('accounts_balances').insert([
...notStoredBalances.map((id) => {
const account = submitBalancesMap.get(id);
return { ...account };
}),
]);
return res.status(200).send();
},
},

View File

@@ -152,7 +152,6 @@ export default {
if (!account) {
return res.boom.notFound();
}
return res.status(200).send({ item: { ...account.attributes } });
},
},

View File

@@ -4,6 +4,7 @@ import { check, validationResult } from 'express-validator';
import path from 'path';
import fs from 'fs';
import Mustache from 'mustache';
import jwt from 'jsonwebtoken';
import User from '@/models/User';
import asyncMiddleware from '../middleware/asyncMiddleware';
import PasswordReset from '@/models/PasswordReset';
@@ -49,6 +50,7 @@ export default {
});
}
const { crediential, password } = req.body;
const { JWT_SECRET_KEY } = process.env;
const user = await User.query({
where: { email: crediential },
@@ -70,8 +72,15 @@ export default {
errors: [{ type: 'USER_INACTIVE', code: 120 }],
});
}
user.save({ alst_login_at: new Date() });
return res.status(200).send({});
user.save({ last_login_at: new Date() });
const token = jwt.sign({
email: user.attributes.email,
_id: user.id,
}, JWT_SECRET_KEY, {
expiresIn: '1d',
});
return res.status(200).send({ token });
},
},

View File

@@ -2,7 +2,8 @@ import express from 'express';
import { check, param, validationResult } from 'express-validator';
import asyncMiddleware from '../middleware/asyncMiddleware';
import ItemCategory from '@/models/ItemCategory';
// import JWTAuth from '@/http/middleware/jwtAuth';
import Authorization from '@/http/middleware/authorization';
import JWTAuth from '@/http/middleware/jwtAuth';
export default {
/**
@@ -10,24 +11,32 @@ export default {
*/
router() {
const router = express.Router();
const permit = Authorization('items_categories');
router.use(JWTAuth);
router.post('/:id',
permit('create', 'edit'),
this.editCategory.validation,
asyncMiddleware(this.editCategory.handler));
router.post('/',
permit('create'),
this.newCategory.validation,
asyncMiddleware(this.newCategory.handler));
router.delete('/:id',
permit('create', 'edit', 'delete'),
this.deleteItem.validation,
asyncMiddleware(this.deleteItem.handler));
// router.get('/:id',
// this.getCategory.validation,
// asyncMiddleware(this.getCategory.handler));
router.get('/:id',
permit('view'),
this.getCategory.validation,
asyncMiddleware(this.getCategory.handler));
router.get('/',
permit('view'),
this.getList.validation,
asyncMiddleware(this.getList.validation));
@@ -152,6 +161,9 @@ export default {
},
},
/**
* Retrieve details of the given category.
*/
getCategory: {
validation: [
param('category_id').toInt(),

View File

@@ -1,21 +1,30 @@
import express from 'express';
import { check, validationResult } from 'express-validator';
import moment from 'moment';
import asyncMiddleware from '../middleware/asyncMiddleware';
import { difference } from 'lodash';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import jwtAuth from '@/http/middleware/jwtAuth';
import Item from '@/models/Item';
import Account from '@/models/Account';
import ItemCategory from '@/models/ItemCategory';
import Resource from '@/models/Resource';
import ResourceField from '@/models/ResourceField';
import Authorization from '@/http/middleware/authorization';
export default {
router() {
const router = express.Router();
const permit = Authorization('items');
router.use(jwtAuth);
// router.post('/:id',
// this.editItem.validation,
// asyncMiddleware(this.editCategory.handler));
router.post('/',
permit('create'),
this.newItem.validation,
asyncMiddleware(this.newItem.handler));
@@ -46,6 +55,11 @@ export default {
check('cost_account_id').exists().isInt(),
check('sell_account_id').exists().isInt(),
check('category_id').optional().isInt(),
check('custom_fields').isArray({ min: 1 }),
check('custom_fields.*.key').exists().isNumeric().toInt(),
check('custom_fields.*.value').exists(),
check('note').optional(),
],
async handler(req, res) {
@@ -58,19 +72,38 @@ export default {
}
const { sell_account_id: sellAccountId, cost_account_id: costAccountId } = req.body;
const { category_id: categoryId } = req.body;
const { category_id: categoryId, custom_fields: customFields } = req.body;
const errorReasons = [];
const costAccountPromise = Account.where('id', costAccountId).fetch();
const sellAccountPromise = Account.where('id', sellAccountId).fetch();
const itemCategoryPromise = (categoryId)
? ItemCategory.where('id', categoryId).fetch() : null;
// Validate the custom fields key and value type.
if (customFields.length > 0) {
const customFieldsKeys = customFields.map((field) => field.key);
// Get resource id than get all resource fields.
const resource = await Resource.where('name', 'items').fetch();
const fields = await ResourceField.query((query) => {
query.where('resource_id', resource.id);
query.whereIn('key', customFieldsKeys);
}).fetchAll();
const storedFieldsKey = fields.map((f) => f.attributes.key);
// Get all not defined resource fields.
const notFoundFields = difference(customFieldsKeys, storedFieldsKey);
if (notFoundFields.length > 0) {
errorReasons.push({ type: 'FIELD_KEY_NOT_FOUND', code: 150, fields: notFoundFields });
}
}
const [costAccount, sellAccount, itemCategory] = await Promise.all([
costAccountPromise, sellAccountPromise, itemCategoryPromise,
]);
const errorReasons = [];
if (!costAccount) {
errorReasons.push({ type: 'COST_ACCOUNT_NOT_FOUND', code: 100 });
}
@@ -92,7 +125,6 @@ export default {
currency_code: req.body.currency_code,
note: req.body.note,
});
await item.save();
return res.status(200).send();

View File

@@ -2,6 +2,8 @@ import express from 'express';
import { check, validationResult } from 'express-validator';
import User from '@/models/User';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import jwtAuth from '@/http/middleware/jwtAuth';
import Authorization from '@/http/middleware/authorization';
export default {
@@ -10,24 +12,32 @@ export default {
*/
router() {
const router = express.Router();
const permit = Authorization('users');
router.use(jwtAuth);
router.post('/',
permit('create'),
this.newUser.validation,
asyncMiddleware(this.newUser.handler));
router.post('/:id',
permit('create', 'edit'),
this.editUser.validation,
asyncMiddleware(this.editUser.handler));
// router.get('/',
// this.listUsers.validation,
// asyncMiddleware(this.listUsers.handler));
router.get('/',
permit('view'),
this.listUsers.validation,
asyncMiddleware(this.listUsers.handler));
// router.get('/:id',
// this.getUser.validation,
// asyncMiddleware(this.getUser.handler));
router.get('/:id',
permit('view'),
this.getUser.validation,
asyncMiddleware(this.getUser.handler));
router.delete('/:id',
permit('create', 'edit', 'delete'),
this.deleteUser.validation,
asyncMiddleware(this.deleteUser.handler));

View File

@@ -1,4 +1,16 @@
const authorization = (req, res, next) => {
/* eslint-disable consistent-return */
const authorization = (resourceName) => (...permissions) => (req, res, next) => {
const { user } = req;
};
const onError = () => {
res.boom.unauthorized();
};
user.hasPermissions(resourceName, permissions)
.then((authorized) => {
if (!authorized) {
return onError();
}
next();
}).catch(onError);
};
export default authorization;

View File

@@ -1,23 +1,23 @@
/* eslint-disable consistent-return */
import jwt from 'jsonwebtoken';
import User from '@/models/User';
import Auth from '@/models/Auth';
// import Auth from '@/models/Auth';
const authMiddleware = (req, res, next) => {
const { JWT_SECRET_KEY } = process.env;
const token = req.headers['x-access-token'] || req.query.token;
const onError = () => {
Auth.loggedOut();
// Auth.loggedOut();
res.status(401).send({
success: false,
message: 'unauthorized',
});
}
};
if (!token) {
return onError();
}
const { JWT_SECRET_KEY } = process.env;
const verify = new Promise((resolve, reject) => {
jwt.verify(token, JWT_SECRET_KEY, async (error, decoded) => {
@@ -26,7 +26,7 @@ const authMiddleware = (req, res, next) => {
} else {
// eslint-disable-next-line no-underscore-dangle
req.user = await User.where('id', decoded._id).fetch();
Auth.setAuthenticatedUser(req.user);
// Auth.setAuthenticatedUser(req.user);
if (!req.user) {
return onError();

View File

@@ -18,8 +18,11 @@ const Account = bookshelf.Model.extend({
return this.belongsTo('AccountType', 'account_type_id');
},
/**
* Account model may has many balances accounts.
*/
balances() {
return this.hasMany('AccountBalance', 'accounnt_id');
return this.hasMany('AccountBalance', 'account_id');
},
}, {
/**

View File

@@ -1,7 +1,6 @@
import bookshelf from './bookshelf';
const Item = bookshelf.Model.extend({
/**
* Table name
*/
@@ -25,6 +24,11 @@ const Item = bookshelf.Model.extend({
category() {
return this.belongsTo('ItemCategory', 'category_id');
},
}, {
/**
* Cascade delete dependents.
*/
dependents: ['ItemMetadata'],
});
export default bookshelf.model('Item', Item);

View File

@@ -24,6 +24,10 @@ const Resource = bookshelf.Model.extend({
fields() {
return this.hasMany('ResourceField', 'resource_id');
},
permissions() {
return this.belongsToMany('Permission', 'role_has_permissions', 'resource_id', 'permission_id');
},
});
export default bookshelf.model('Resource', Resource);

View File

@@ -1,7 +1,9 @@
import bcrypt from 'bcryptjs';
import bookshelf from './bookshelf';
import PermissionsService from '@/services/PermissionsService';
const User = bookshelf.Model.extend({
...PermissionsService,
/**
* Table name
@@ -13,6 +15,10 @@ const User = bookshelf.Model.extend({
*/
hasTimestamps: ['created_at', 'updated_at'],
initialize() {
this.initializeCache();
},
/**
* Verify the password of the user.
* @param {String} password - The given password.

View File

@@ -0,0 +1,75 @@
import cache from 'memory-cache';
import { difference } from 'lodash';
import Role from '@/models/Role';
export default {
cacheKey: 'ratteb.cache,',
cacheExpirationTime: null,
permissions: [],
cache: null,
initializeCache() {
if (!this.cache) {
this.cache = new cache.Cache();
}
},
/**
* Purge all cached permissions.
*/
forgetCachePermissions() {
this.cache.del(this.cacheKey);
this.permissions = [];
},
/**
* Get all stored permissions.
*/
async getPermissions() {
if (this.permissions.length <= 0) {
const cachedPerms = this.cache.get(this.cacheKey);
if (!cachedPerms) {
this.permissions = await this.getPermissionsFromStorage();
this.cache.put(this.cacheKey, this.permissions);
} else {
this.permissions = cachedPerms;
}
}
return this.permissions;
},
/**
* Fetches all roles and permissions from the storage.
*/
async getPermissionsFromStorage() {
const roles = await Role.fetchAll({
withRelated: ['resources.permissions'],
});
return roles.toJSON();
},
/**
* Detarmine the given resource has the permissions.
* @param {String} resource -
* @param {Array} permissions -
*/
async hasPermissions(resource, permissions) {
await this.getPermissions();
const userRoles = this.permissions.filter((role) => role.id === this.id);
const perms = [];
userRoles.forEach((role) => {
const roleResources = role.resources || [];
const foundResource = roleResources.find((r) => r.name === resource);
if (foundResource && foundResource.permissions) {
foundResource.permissions.forEach((p) => perms.push(p.name));
}
});
const notAllowedPerms = difference(permissions, perms);
return (notAllowedPerms.length <= 0);
},
};