mirror of
https://github.com/jambonz/jambonz-api-server.git
synced 2026-01-25 02:08:24 +00:00
Feat/371 view only user implementation using user_permissions (#381)
* https://github.com/jambonz/jambonz-api-server/issues/371 Implemented view_only permission feature * calling prepare-permissions in create-test-db.js * check if there is only 1 permission and if it is VIEW_ONLY then consider user as read-only user * setting is_view_only flag for view user by userid
This commit is contained in:
14
app.js
14
app.js
@@ -7,6 +7,7 @@ const nocache = require('nocache');
|
|||||||
const rateLimit = require('express-rate-limit');
|
const rateLimit = require('express-rate-limit');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const passport = require('passport');
|
const passport = require('passport');
|
||||||
|
const {verifyViewOnlyUser} = require('./lib/middleware');
|
||||||
const routes = require('./lib/routes');
|
const routes = require('./lib/routes');
|
||||||
const Registrar = require('@jambonz/mw-registrar');
|
const Registrar = require('@jambonz/mw-registrar');
|
||||||
|
|
||||||
@@ -172,6 +173,19 @@ app.use('/v1', unless(
|
|||||||
'/InviteCodes',
|
'/InviteCodes',
|
||||||
'/PredefinedCarriers'
|
'/PredefinedCarriers'
|
||||||
], passport.authenticate('bearer', {session: false})));
|
], passport.authenticate('bearer', {session: false})));
|
||||||
|
app.use('/v1', unless(
|
||||||
|
[
|
||||||
|
'/register',
|
||||||
|
'/forgot-password',
|
||||||
|
'/signin',
|
||||||
|
'/login',
|
||||||
|
'/messaging',
|
||||||
|
'/outboundSMS',
|
||||||
|
'/AccountTest',
|
||||||
|
'/InviteCodes',
|
||||||
|
'/PredefinedCarriers',
|
||||||
|
'/logout'
|
||||||
|
], verifyViewOnlyUser));
|
||||||
app.use('/', routes);
|
app.use('/', routes);
|
||||||
app.use((err, req, res, next) => {
|
app.use((err, req, res, next) => {
|
||||||
logger.error(err, 'burped error');
|
logger.error(err, 'burped error');
|
||||||
|
|||||||
11
db/prepare-permissions-test.sql
Normal file
11
db/prepare-permissions-test.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/* remove VIEW_ONLY permission for admin user as it will prevent write operations*/
|
||||||
|
delete from user_permissions;
|
||||||
|
|
||||||
|
delete from permissions;
|
||||||
|
|
||||||
|
insert into permissions (permission_sid, name, description)
|
||||||
|
values
|
||||||
|
('ffbc342a-546a-11ed-bdc3-0242ac120002', 'VIEW_ONLY', 'Can view data but not make changes'),
|
||||||
|
('ffbc3a10-546a-11ed-bdc3-0242ac120002', 'PROVISION_SERVICES', 'Can provision services'),
|
||||||
|
('ffbc3c5e-546a-11ed-bdc3-0242ac120002', 'PROVISION_USERS', 'Can provision users');
|
||||||
|
|
||||||
@@ -35,8 +35,8 @@ function makeStrategy(logger) {
|
|||||||
debug(err);
|
debug(err);
|
||||||
logger.info({err}, 'Error checking redis for jwt');
|
logger.info({err}, 'Error checking redis for jwt');
|
||||||
}
|
}
|
||||||
const { user_sid, service_provider_sid, account_sid, email, name, scope, permissions } = decoded;
|
const { user_sid, service_provider_sid, account_sid, email,
|
||||||
|
name, scope, permissions, is_view_only } = decoded;
|
||||||
const user = {
|
const user = {
|
||||||
service_provider_sid,
|
service_provider_sid,
|
||||||
account_sid,
|
account_sid,
|
||||||
@@ -45,6 +45,7 @@ function makeStrategy(logger) {
|
|||||||
email,
|
email,
|
||||||
name,
|
name,
|
||||||
permissions,
|
permissions,
|
||||||
|
is_view_only,
|
||||||
hasScope: (s) => s === scope,
|
hasScope: (s) => s === scope,
|
||||||
hasAdminAuth: scope === 'admin',
|
hasAdminAuth: scope === 'admin',
|
||||||
hasServiceProviderAuth: scope === 'service_provider',
|
hasServiceProviderAuth: scope === 'service_provider',
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const logger = require('./logger');
|
const logger = require('./logger');
|
||||||
|
const {UserPermissionError} = require('./utils/errors');
|
||||||
|
|
||||||
function delayLoginMiddleware(req, res, next) {
|
function delayLoginMiddleware(req, res, next) {
|
||||||
if (req.path.includes('/login') || req.path.includes('/signin')) {
|
if (req.path.includes('/login') || req.path.includes('/signin')) {
|
||||||
@@ -27,6 +28,22 @@ function delayLoginMiddleware(req, res, next) {
|
|||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function verifyViewOnlyUser(req, res, next) {
|
||||||
|
// Skip check for GET requests
|
||||||
|
if (req.method === 'GET') {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
// Check if user is read-only
|
||||||
|
if (req.user && !!req.user.is_view_only) {
|
||||||
|
const upError = new UserPermissionError('User has view-only access');
|
||||||
|
upError.status = 403;
|
||||||
|
throw upError;
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
delayLoginMiddleware
|
delayLoginMiddleware,
|
||||||
|
verifyViewOnlyUser
|
||||||
};
|
};
|
||||||
|
|||||||
45
lib/models/permissions.js
Normal file
45
lib/models/permissions.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
const Model = require('./model');
|
||||||
|
const {promisePool} = require('../db');
|
||||||
|
const sqlAll = `
|
||||||
|
SELECT * from permissions
|
||||||
|
`;
|
||||||
|
const sqlByName = `
|
||||||
|
SELECT * from permissions where name = ?
|
||||||
|
`;
|
||||||
|
|
||||||
|
class Permissions extends Model {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async retrieveAll() {
|
||||||
|
const [rows] = await promisePool.query(sqlAll);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async retrieveByName(name) {
|
||||||
|
const [rows] = await promisePool.query(sqlByName, [name]);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Permissions.table = 'permissions';
|
||||||
|
Permissions.fields = [
|
||||||
|
{
|
||||||
|
name: 'permission_sid',
|
||||||
|
type: 'string',
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'string',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
type: 'string',
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
module.exports = Permissions;
|
||||||
53
lib/models/user-permissions.js
Normal file
53
lib/models/user-permissions.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
const Model = require('./model');
|
||||||
|
const {promisePool} = require('../db');
|
||||||
|
const sqlAll = `
|
||||||
|
SELECT * from user_permissions
|
||||||
|
`;
|
||||||
|
const sqlByUserIdPermissionSid = `
|
||||||
|
SELECT * from user_permissions where user_sid = ? and permission_sid = ?
|
||||||
|
`;
|
||||||
|
const sqlByUserId = `
|
||||||
|
SELECT * from user_permissions where user_sid = ?
|
||||||
|
`;
|
||||||
|
|
||||||
|
class UserPermissions extends Model {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async retrieveAll() {
|
||||||
|
const [rows] = await promisePool.query(sqlAll);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async retrieveByUserIdPermissionSid(user_sid, permission_sid) {
|
||||||
|
const [rows] = await promisePool.query(sqlByUserIdPermissionSid, [user_sid, permission_sid]);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
static async retrieveByUserId(user_sid) {
|
||||||
|
const [rows] = await promisePool.query(sqlByUserId, [user_sid]);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
UserPermissions.table = 'user_permissions';
|
||||||
|
UserPermissions.fields = [
|
||||||
|
{
|
||||||
|
name: 'user_permissions_sid',
|
||||||
|
type: 'string',
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'user_sid',
|
||||||
|
type: 'string',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'permission_sid',
|
||||||
|
type: 'string',
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
module.exports = UserPermissions;
|
||||||
@@ -71,9 +71,13 @@ router.post('/', async(req, res) => {
|
|||||||
obj.service_provider_sid = r[0].service_provider_sid;
|
obj.service_provider_sid = r[0].service_provider_sid;
|
||||||
obj.service_provider_name = service_provider[0].name;
|
obj.service_provider_name = service_provider[0].name;
|
||||||
}
|
}
|
||||||
|
// if there is only one permission and it is VIEW_ONLY, then the user is view only
|
||||||
|
// this is to prevent write operations on the API
|
||||||
|
const is_view_only = permissions.length === 1 && permissions.includes('VIEW_ONLY');
|
||||||
const payload = {
|
const payload = {
|
||||||
scope: obj.scope,
|
scope: obj.scope,
|
||||||
permissions,
|
permissions,
|
||||||
|
is_view_only,
|
||||||
...(obj.service_provider_sid && {
|
...(obj.service_provider_sid && {
|
||||||
service_provider_sid: obj.service_provider_sid,
|
service_provider_sid: obj.service_provider_sid,
|
||||||
service_provider_name: obj.service_provider_name
|
service_provider_name: obj.service_provider_name
|
||||||
@@ -83,7 +87,8 @@ router.post('/', async(req, res) => {
|
|||||||
account_name: obj.account_name,
|
account_name: obj.account_name,
|
||||||
service_provider_name: obj.service_provider_name
|
service_provider_name: obj.service_provider_name
|
||||||
}),
|
}),
|
||||||
user_sid: obj.user_sid
|
user_sid: obj.user_sid,
|
||||||
|
name: username
|
||||||
};
|
};
|
||||||
|
|
||||||
const expiresIn = parseInt(process.env.JWT_EXPIRES_IN || 60) * 60;
|
const expiresIn = parseInt(process.env.JWT_EXPIRES_IN || 60) * 60;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
const router = require('express').Router();
|
const router = require('express').Router();
|
||||||
const User = require('../../models/user');
|
const User = require('../../models/user');
|
||||||
|
const UserPermissions = require('../../models/user-permissions');
|
||||||
|
const Permissions = require('../../models/permissions');
|
||||||
const {DbErrorBadRequest, BadRequestError, DbErrorForbidden} = require('../../utils/errors');
|
const {DbErrorBadRequest, BadRequestError, DbErrorForbidden} = require('../../utils/errors');
|
||||||
const {generateHashedPassword, verifyPassword} = require('../../utils/password-utils');
|
const {generateHashedPassword, verifyPassword} = require('../../utils/password-utils');
|
||||||
const {promisePool} = require('../../db');
|
const {promisePool} = require('../../db');
|
||||||
@@ -38,7 +40,8 @@ const validateRequest = async(user_sid, req) => {
|
|||||||
email,
|
email,
|
||||||
email_activation_code,
|
email_activation_code,
|
||||||
force_change,
|
force_change,
|
||||||
is_active
|
is_active,
|
||||||
|
is_view_only
|
||||||
} = payload;
|
} = payload;
|
||||||
|
|
||||||
const [r] = await promisePool.query(retrieveSql, user_sid);
|
const [r] = await promisePool.query(retrieveSql, user_sid);
|
||||||
@@ -93,7 +96,8 @@ const validateRequest = async(user_sid, req) => {
|
|||||||
if (email_activation_code && !email) {
|
if (email_activation_code && !email) {
|
||||||
throw new DbErrorBadRequest('email and email_activation_code both required');
|
throw new DbErrorBadRequest('email and email_activation_code both required');
|
||||||
}
|
}
|
||||||
if (!name && !new_password && !email && !initial_password && !force_change && !is_active)
|
if (!name && !new_password && !email && !initial_password && !force_change && !is_active &&
|
||||||
|
is_view_only === undefined)
|
||||||
throw new DbErrorBadRequest('no updates requested');
|
throw new DbErrorBadRequest('no updates requested');
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
@@ -140,7 +144,35 @@ const ensureUserRetrievalIsAllowed = (req, user) => {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
async function updateViewOnlyUserPermission(is_view_only, user_sid) {
|
||||||
|
try {
|
||||||
|
const [viewOnlyPermission] = await Permissions.retrieveByName('VIEW_ONLY');
|
||||||
|
if (!viewOnlyPermission) {
|
||||||
|
throw new Error('VIEW_ONLY permission not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingPermissions = await UserPermissions.retrieveByUserIdPermissionSid(
|
||||||
|
user_sid,
|
||||||
|
viewOnlyPermission.permission_sid
|
||||||
|
);
|
||||||
|
if (is_view_only && existingPermissions.length === 0) {
|
||||||
|
await UserPermissions.make({
|
||||||
|
user_sid,
|
||||||
|
permission_sid: viewOnlyPermission.permission_sid,
|
||||||
|
});
|
||||||
|
} else if (!is_view_only && existingPermissions.length > 0) {
|
||||||
|
await UserPermissions.remove(existingPermissions[0].user_permissions_sid);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Failed to update user permissions: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function removeViewOnlyUserPermission(user_id) {
|
||||||
|
const [viewOnlyPermission] = await Permissions.retrieveByName('VIEW_ONLY');
|
||||||
|
if (viewOnlyPermission) {
|
||||||
|
await UserPermissions.remove(user_id, viewOnlyPermission.permission_sid);
|
||||||
|
}
|
||||||
|
}
|
||||||
router.get('/', async(req, res) => {
|
router.get('/', async(req, res) => {
|
||||||
const logger = req.app.locals.logger;
|
const logger = req.app.locals.logger;
|
||||||
|
|
||||||
@@ -308,7 +340,14 @@ router.get('/:user_sid', async(req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ensureUserRetrievalIsAllowed(req, user);
|
ensureUserRetrievalIsAllowed(req, user);
|
||||||
|
const [viewOnlyPermission] = await Permissions.retrieveByName('VIEW_ONLY');
|
||||||
|
const existingPermissions = await UserPermissions.retrieveByUserId(
|
||||||
|
user_sid
|
||||||
|
);
|
||||||
|
logger.debug(`existingPermissions of ${user_sid}: ${JSON.stringify(existingPermissions)}`);
|
||||||
|
user.is_view_only = existingPermissions.length === 1 &&
|
||||||
|
existingPermissions[0].permission_sid === viewOnlyPermission.permission_sid;
|
||||||
|
logger.debug(`User ${user_sid} is view-only user: ${user.is_view_only}`);
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
const { hashed_password, ...rest } = user;
|
const { hashed_password, ...rest } = user;
|
||||||
return res.status(200).json(rest);
|
return res.status(200).json(rest);
|
||||||
@@ -332,9 +371,9 @@ router.put('/:user_sid', async(req, res) => {
|
|||||||
is_active,
|
is_active,
|
||||||
force_change,
|
force_change,
|
||||||
account_sid,
|
account_sid,
|
||||||
service_provider_sid
|
service_provider_sid,
|
||||||
|
is_view_only
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
//if (req.user.user_sid && req.user.user_sid !== user_sid) return res.sendStatus(403);
|
//if (req.user.user_sid && req.user.user_sid !== user_sid) return res.sendStatus(403);
|
||||||
|
|
||||||
if (!hasAdminAuth &&
|
if (!hasAdminAuth &&
|
||||||
@@ -427,6 +466,8 @@ router.put('/:user_sid', async(req, res) => {
|
|||||||
//TODO: send email with activation code
|
//TODO: send email with activation code
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// update user permissions
|
||||||
|
await updateViewOnlyUserPermission(is_view_only, user_sid);
|
||||||
res.sendStatus(204);
|
res.sendStatus(204);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
sysError(logger, res, err);
|
sysError(logger, res, err);
|
||||||
@@ -443,6 +484,8 @@ router.post('/', async(req, res) => {
|
|||||||
};
|
};
|
||||||
const allUsers = await User.retrieveAll();
|
const allUsers = await User.retrieveAll();
|
||||||
delete payload.initial_password;
|
delete payload.initial_password;
|
||||||
|
const is_view_only = payload.is_view_only;
|
||||||
|
delete payload.is_view_only;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (req.body.initial_password) {
|
if (req.body.initial_password) {
|
||||||
@@ -464,6 +507,7 @@ router.post('/', async(req, res) => {
|
|||||||
if (req.user.hasAdminAuth) {
|
if (req.user.hasAdminAuth) {
|
||||||
logger.debug({payload}, 'POST /users');
|
logger.debug({payload}, 'POST /users');
|
||||||
const uuid = await User.make(payload);
|
const uuid = await User.make(payload);
|
||||||
|
await updateViewOnlyUserPermission(is_view_only, uuid);
|
||||||
res.status(201).json({user_sid: uuid});
|
res.status(201).json({user_sid: uuid});
|
||||||
}
|
}
|
||||||
else if (req.user.hasAccountAuth) {
|
else if (req.user.hasAccountAuth) {
|
||||||
@@ -472,6 +516,7 @@ router.post('/', async(req, res) => {
|
|||||||
...payload,
|
...payload,
|
||||||
account_sid: req.user.account_sid,
|
account_sid: req.user.account_sid,
|
||||||
});
|
});
|
||||||
|
await updateViewOnlyUserPermission(is_view_only, uuid);
|
||||||
res.status(201).json({user_sid: uuid});
|
res.status(201).json({user_sid: uuid});
|
||||||
}
|
}
|
||||||
else if (req.user.hasServiceProviderAuth) {
|
else if (req.user.hasServiceProviderAuth) {
|
||||||
@@ -480,6 +525,7 @@ router.post('/', async(req, res) => {
|
|||||||
...payload,
|
...payload,
|
||||||
service_provider_sid: req.user.service_provider_sid,
|
service_provider_sid: req.user.service_provider_sid,
|
||||||
});
|
});
|
||||||
|
await updateViewOnlyUserPermission(is_view_only, uuid);
|
||||||
res.status(201).json({user_sid: uuid});
|
res.status(201).json({user_sid: uuid});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -497,6 +543,8 @@ router.delete('/:user_sid', async(req, res) => {
|
|||||||
const user = allUsers.filter((user) => user.user_sid === user_sid);
|
const user = allUsers.filter((user) => user.user_sid === user_sid);
|
||||||
|
|
||||||
ensureUserDeletionIsAllowed(req, activeAdminUsers, user);
|
ensureUserDeletionIsAllowed(req, activeAdminUsers, user);
|
||||||
|
logger.debug(`Removing view-only permission for user ${user_sid}`);
|
||||||
|
await removeViewOnlyUserPermission(user_sid);
|
||||||
await User.remove(user_sid);
|
await User.remove(user_sid);
|
||||||
|
|
||||||
/* invalidate the jwt of the deleted user */
|
/* invalidate the jwt of the deleted user */
|
||||||
|
|||||||
@@ -27,11 +27,17 @@ class DbErrorForbidden extends DbError {
|
|||||||
super(msg);
|
super(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
class UserPermissionError extends Error {
|
||||||
|
constructor(msg) {
|
||||||
|
super(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
BadRequestError,
|
BadRequestError,
|
||||||
DbError,
|
DbError,
|
||||||
DbErrorBadRequest,
|
DbErrorBadRequest,
|
||||||
DbErrorUnprocessableRequest,
|
DbErrorUnprocessableRequest,
|
||||||
DbErrorForbidden
|
DbErrorForbidden,
|
||||||
|
UserPermissionError
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,3 +32,11 @@ test('add predefined carriers', (t) => {
|
|||||||
t.end();
|
t.end();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('prepare permissions', (t) => {
|
||||||
|
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/../db/prepare-permissions-test.sql`, (err, stdout, stderr) => {
|
||||||
|
if (err) return t.end(err);
|
||||||
|
t.pass('permissions prepared');
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ require('./ms-teams');
|
|||||||
require('./speech-credentials');
|
require('./speech-credentials');
|
||||||
require('./recent-calls');
|
require('./recent-calls');
|
||||||
require('./users');
|
require('./users');
|
||||||
|
require('./users-view-only');
|
||||||
require('./login');
|
require('./login');
|
||||||
require('./webapp_tests');
|
require('./webapp_tests');
|
||||||
// require('./homer');
|
// require('./homer');
|
||||||
|
|||||||
296
test/users-view-only.js
Normal file
296
test/users-view-only.js
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
const test = require('tape') ;
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const request = require('request-promise-native').defaults({
|
||||||
|
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||||
|
});
|
||||||
|
const exec = require('child_process').exec ;
|
||||||
|
const {generateHashedPassword} = require('../lib/utils/password-utils');
|
||||||
|
process.on('unhandledRejection', (reason, p) => {
|
||||||
|
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||||
|
});
|
||||||
|
test('add an admin user', (t) => {
|
||||||
|
exec(`${__dirname}/../db/reset_admin_password.js`, (err, stdout, stderr) => {
|
||||||
|
console.log(stderr);
|
||||||
|
console.log(stdout);
|
||||||
|
if (err) return t.end(err);
|
||||||
|
t.pass('successfully added admin user');
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('prepare permissions', (t) => {
|
||||||
|
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/../db/prepare-permissions-test.sql`, (err, stdout, stderr) => {
|
||||||
|
if (err) return t.end(err);
|
||||||
|
t.pass('permissions prepared');
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('view-only user tests', async(t) => {
|
||||||
|
const app = require('../app');
|
||||||
|
const password = 'abcde12345-';
|
||||||
|
try {
|
||||||
|
let result;
|
||||||
|
/* login as admin to get a jwt */
|
||||||
|
result = await request.post('/login', {
|
||||||
|
resolveWithFullResponse: true,
|
||||||
|
json: true,
|
||||||
|
body: {
|
||||||
|
username: 'admin',
|
||||||
|
password: 'admin',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
t.ok(result.statusCode === 200 && result.body.token, 'successfully logged in as admin');
|
||||||
|
const authAdmin = {bearer: result.body.token};
|
||||||
|
const decodedJwt = jwt.verify(result.body.token, process.env.JWT_SECRET);
|
||||||
|
/* add admin user */
|
||||||
|
result = await request.post(`/Users`, {
|
||||||
|
resolveWithFullResponse: true,
|
||||||
|
json: true,
|
||||||
|
auth: authAdmin,
|
||||||
|
body: {
|
||||||
|
name: 'admin2',
|
||||||
|
email: 'admin2@jambonz.com',
|
||||||
|
is_active: true,
|
||||||
|
force_change: true,
|
||||||
|
initial_password: password,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
t.ok(result.statusCode === 201 && result.body.user_sid, 'admin user created');
|
||||||
|
const admin_user_sid = result.body.user_sid;
|
||||||
|
/* add a service provider */
|
||||||
|
result = await request.post('/ServiceProviders', {
|
||||||
|
resolveWithFullResponse: true,
|
||||||
|
auth: authAdmin,
|
||||||
|
json: true,
|
||||||
|
body: {
|
||||||
|
name: 'sp1',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
t.ok(result.statusCode === 201, 'successfully created service provider');
|
||||||
|
const sp_sid = result.body.sid;
|
||||||
|
/* add service_provider read only user */
|
||||||
|
result = await request.post(`/Users`, {
|
||||||
|
resolveWithFullResponse: true,
|
||||||
|
json: true,
|
||||||
|
auth: authAdmin,
|
||||||
|
body: {
|
||||||
|
name: 'service_provider',
|
||||||
|
email: 'sp@jambonz.com',
|
||||||
|
is_active: true,
|
||||||
|
force_change: true,
|
||||||
|
initial_password: password,
|
||||||
|
service_provider_sid: sp_sid,
|
||||||
|
is_view_only: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
t.ok(result.statusCode === 201 && result.body.user_sid, 'service_provider scope view-only user created');
|
||||||
|
|
||||||
|
// login as service_provider read only user
|
||||||
|
result = await request.post('/login', {
|
||||||
|
resolveWithFullResponse: true,
|
||||||
|
json: true,
|
||||||
|
body: {
|
||||||
|
username: 'service_provider',
|
||||||
|
password: password,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
t.ok(result.statusCode === 200 && result.body.token, 'successfully logged in as service provider view-only user');
|
||||||
|
const spToken = {bearer: result.body.token};
|
||||||
|
const spDecodedJwt = jwt.verify(result.body.token, process.env.JWT_SECRET);
|
||||||
|
try {
|
||||||
|
result = await request.post('/Accounts', {
|
||||||
|
resolveWithFullResponse: true,
|
||||||
|
auth: spToken,
|
||||||
|
json: true,
|
||||||
|
body: {
|
||||||
|
name: 'sample_account',
|
||||||
|
service_provider_sid: sp_sid,
|
||||||
|
registration_hook: {
|
||||||
|
url: 'http://example.com/reg',
|
||||||
|
method: 'get'
|
||||||
|
},
|
||||||
|
webhook_secret: 'foobar'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch(err) {
|
||||||
|
t.ok(err.statusCode === 403, 'As a view-only user, you cannot create an account');
|
||||||
|
}
|
||||||
|
result = await request.post('/Accounts', {
|
||||||
|
resolveWithFullResponse: true,
|
||||||
|
auth: authAdmin,
|
||||||
|
json: true,
|
||||||
|
body: {
|
||||||
|
name: 'sample_account',
|
||||||
|
service_provider_sid: sp_sid,
|
||||||
|
registration_hook: {
|
||||||
|
url: 'http://example.com/reg',
|
||||||
|
method: 'get'
|
||||||
|
},
|
||||||
|
webhook_secret: 'foobar'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.ok(result.statusCode === 201, 'successfully created account using admin token');
|
||||||
|
const account_sid = result.body.sid;
|
||||||
|
/* add account scope view-only user */
|
||||||
|
result = await request.post(`/Users`, {
|
||||||
|
resolveWithFullResponse: true,
|
||||||
|
json: true,
|
||||||
|
auth: authAdmin,
|
||||||
|
body: {
|
||||||
|
name: 'account',
|
||||||
|
email: 'account@jambonz.com',
|
||||||
|
is_active: true,
|
||||||
|
force_change: true,
|
||||||
|
initial_password: password,
|
||||||
|
service_provider_sid: sp_sid,
|
||||||
|
account_sid: account_sid,
|
||||||
|
is_view_only: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
t.ok(result.statusCode === 201 && result.body.user_sid, 'account scope user created');
|
||||||
|
const account_user_sid = result.body.user_sid;
|
||||||
|
// login as account read only user
|
||||||
|
result = await request.post('/login', {
|
||||||
|
resolveWithFullResponse: true,
|
||||||
|
json: true,
|
||||||
|
body: {
|
||||||
|
username: 'account',
|
||||||
|
password: password,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
t.ok(result.statusCode === 200 && result.body.token, 'successfully logged in as account view-only user');
|
||||||
|
let userToken = {bearer: result.body.token};
|
||||||
|
/* add an application which should fail as the logged in user is a view-only user */
|
||||||
|
try {
|
||||||
|
result = await request.post('/Applications', {
|
||||||
|
resolveWithFullResponse: true,
|
||||||
|
auth: userToken,
|
||||||
|
json: true,
|
||||||
|
body: {
|
||||||
|
name: 'daveh',
|
||||||
|
account_sid,
|
||||||
|
call_hook: {
|
||||||
|
url: 'http://example.com'
|
||||||
|
},
|
||||||
|
call_status_hook: {
|
||||||
|
url: 'http://example.com/status',
|
||||||
|
method: 'POST'
|
||||||
|
},
|
||||||
|
messaging_hook: {
|
||||||
|
url: 'http://example.com/sms'
|
||||||
|
},
|
||||||
|
app_json : '[\
|
||||||
|
{\
|
||||||
|
"verb": "play",\
|
||||||
|
"url": "https://example.com/example.mp3",\
|
||||||
|
"timeoutSecs": 10,\
|
||||||
|
"seekOffset": 8000,\
|
||||||
|
"actionHook": "/play/action"\
|
||||||
|
}\
|
||||||
|
]',
|
||||||
|
use_for_fallback_speech: 1,
|
||||||
|
fallback_speech_synthesis_vendor: 'google',
|
||||||
|
fallback_speech_synthesis_language: 'en-US',
|
||||||
|
fallback_speech_synthesis_voice: 'man',
|
||||||
|
fallback_speech_synthesis_label: 'label1',
|
||||||
|
fallback_speech_recognizer_vendor: 'google',
|
||||||
|
fallback_speech_recognizer_language: 'en-US',
|
||||||
|
fallback_speech_recognizer_label: 'label1'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch(err) {
|
||||||
|
t.ok(err.statusCode === 403, 'As a view-only user, you cannot create an application');
|
||||||
|
}
|
||||||
|
// change user as read/write user and create an application - it should succeed
|
||||||
|
result = await request.put(`/Users/${account_user_sid}`, {
|
||||||
|
resolveWithFullResponse: true,
|
||||||
|
json: true,
|
||||||
|
auth: authAdmin,
|
||||||
|
body: {
|
||||||
|
is_view_only: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
t.ok(result.statusCode === 204, 'successfully updated user to read/write permissions');
|
||||||
|
// login as account read only user
|
||||||
|
result = await request.post('/login', {
|
||||||
|
resolveWithFullResponse: true,
|
||||||
|
json: true,
|
||||||
|
body: {
|
||||||
|
username: 'account',
|
||||||
|
password: password,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
t.ok(result.statusCode === 200 && result.body.token, 'successfully logged in as account read-write user');
|
||||||
|
userToken = {bearer: result.body.token};
|
||||||
|
/* add an application which should succeed as the logged in user is a read-write user */
|
||||||
|
result = await request.post('/Applications', {
|
||||||
|
resolveWithFullResponse: true,
|
||||||
|
auth: userToken,
|
||||||
|
json: true,
|
||||||
|
body: {
|
||||||
|
name: 'daveh',
|
||||||
|
account_sid,
|
||||||
|
call_hook: {
|
||||||
|
url: 'http://example.com'
|
||||||
|
},
|
||||||
|
call_status_hook: {
|
||||||
|
url: 'http://example.com/status',
|
||||||
|
method: 'POST'
|
||||||
|
},
|
||||||
|
messaging_hook: {
|
||||||
|
url: 'http://example.com/sms'
|
||||||
|
},
|
||||||
|
app_json : '[\
|
||||||
|
{\
|
||||||
|
"verb": "play",\
|
||||||
|
"url": "https://example.com/example.mp3",\
|
||||||
|
"timeoutSecs": 10,\
|
||||||
|
"seekOffset": 8000,\
|
||||||
|
"actionHook": "/play/action"\
|
||||||
|
}\
|
||||||
|
]',
|
||||||
|
use_for_fallback_speech: 1,
|
||||||
|
fallback_speech_synthesis_vendor: 'google',
|
||||||
|
fallback_speech_synthesis_language: 'en-US',
|
||||||
|
fallback_speech_synthesis_voice: 'man',
|
||||||
|
fallback_speech_synthesis_label: 'label1',
|
||||||
|
fallback_speech_recognizer_vendor: 'google',
|
||||||
|
fallback_speech_recognizer_language: 'en-US',
|
||||||
|
fallback_speech_recognizer_label: 'label1'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
t.ok(result.statusCode === 201, 'successfully created an application');
|
||||||
|
// change user back to view-only and query the application - it should succeed
|
||||||
|
result = await request.put(`/Users/${account_user_sid}`, {
|
||||||
|
resolveWithFullResponse: true,
|
||||||
|
json: true,
|
||||||
|
auth: authAdmin,
|
||||||
|
body: {
|
||||||
|
is_view_only: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
t.ok(result.statusCode === 204, 'successfully updated user permission to view-only');
|
||||||
|
// login as account read only user
|
||||||
|
result = await request.post('/login', {
|
||||||
|
resolveWithFullResponse: true,
|
||||||
|
json: true,
|
||||||
|
body: {
|
||||||
|
username: 'account',
|
||||||
|
password: password,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
t.ok(result.statusCode === 200 && result.body.token, 'successfully logged in as account view-only user');
|
||||||
|
userToken = {bearer: result.body.token};
|
||||||
|
result = await request.get('/Applications', {
|
||||||
|
auth: userToken,
|
||||||
|
json: true,
|
||||||
|
});
|
||||||
|
//console.log(`result: ${JSON.stringify(result)}`);
|
||||||
|
t.ok(result.length === 1 , 'successfully queried all applications with view-only user');
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
t.end(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -20,7 +20,13 @@ test('add an admin user', (t) => {
|
|||||||
t.end();
|
t.end();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
test('prepare permissions', (t) => {
|
||||||
|
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/../db/prepare-permissions-test.sql`, (err, stdout, stderr) => {
|
||||||
|
if (err) return t.end(err);
|
||||||
|
t.pass('permissions prepared');
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
test('user tests', async(t) => {
|
test('user tests', async(t) => {
|
||||||
const app = require('../app');
|
const app = require('../app');
|
||||||
const password = 'abcde12345-';
|
const password = 'abcde12345-';
|
||||||
|
|||||||
@@ -24,7 +24,13 @@ test('seeding database for webapp tests', (t) => {
|
|||||||
t.end();
|
t.end();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
test('prepare permissions', (t) => {
|
||||||
|
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/../db/prepare-permissions-test.sql`, (err, stdout, stderr) => {
|
||||||
|
if (err) return t.end(err);
|
||||||
|
t.pass('permissions prepared');
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
test('webapp tests', async(t) => {
|
test('webapp tests', async(t) => {
|
||||||
const app = require('../app');
|
const app = require('../app');
|
||||||
let sid;
|
let sid;
|
||||||
|
|||||||
Reference in New Issue
Block a user