Limit access to resources according to user scoped Account or SP (#140)

* limit access to resources according to user scope

* fix error change

* speech credentials validation

* fix speech credentials validation

* fix the issues that didnt allow tests to pass

* speech credential validation

* retrieve speech cred list

* fixt speech credential test valodation

* check scope of smpp-gateways

* check scope of smpp-gateways

* testing time

* /signin for hosted system needs to return scope in jwt

* fix user delete route and adjust tests

* get refactor

---------

Co-authored-by: eglehelms <e.helms@cognigy.com>
Co-authored-by: Dave Horton <daveh@beachdognet.com>
Co-authored-by: Guilherme Rauen <g.rauen@cognigy.com>
This commit is contained in:
EgleH
2023-04-05 20:20:51 +02:00
committed by GitHub
parent 8f93b69af0
commit b2dce18c7a
19 changed files with 530 additions and 126 deletions
+8
View File
@@ -27,6 +27,14 @@ values ('2708b1b3-2736-40ea-b502-c53d8396247f', 'default service provider', 'sip
insert into accounts (account_sid, service_provider_sid, name, webhook_secret)
values ('9351f46a-678c-43f5-b8a6-d4eb58d131af','2708b1b3-2736-40ea-b502-c53d8396247f', 'default account', 'wh_secret_cJqgtMDPzDhhnjmaJH6Mtk');
-- create account level api key
insert into api_keys (api_key_sid, token, service_provider_sid)
values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36fa', '38700987-c7a4-4685-a5bb-af378f9734da', '9351f46a-678c-43f5-b8a6-d4eb58d131af');
-- create SP level api key
insert into api_keys (api_key_sid, token, account_sid)
values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36fs', '38700987-c7a4-4685-a5bb-af378f9734ds', '2708b1b3-2736-40ea-b502-c53d8396247f');
-- create two applications
insert into webhooks(webhook_sid, url, method)
values
+27 -8
View File
@@ -52,7 +52,7 @@ const stripPort = (hostport) => {
return hostport;
};
const validateUpdateForCarrier = async(req, account_sid) => {
const validateRequest = async(req, account_sid) => {
try {
if (req.user.hasScope('admin')) {
return;
@@ -63,7 +63,7 @@ const validateUpdateForCarrier = async(req, account_sid) => {
return;
}
throw new DbErrorForbidden('insufficient permissions to update account');
throw new DbErrorForbidden('insufficient permissions');
}
if (req.user.hasScope('service_provider')) {
@@ -75,7 +75,7 @@ const validateUpdateForCarrier = async(req, account_sid) => {
return;
}
throw new DbErrorForbidden('insufficient permissions to update account');
throw new DbErrorForbidden('insufficient permissions');
}
} catch (error) {
throw error;
@@ -93,6 +93,7 @@ router.get('/:sid/Applications', async(req, res) => {
const logger = req.app.locals.logger;
try {
const account_sid = parseAccountSid(req);
await validateRequest(req, account_sid);
const results = await Application.retrieveAll(null, account_sid);
res.status(200).json(results);
} catch (err) {
@@ -103,6 +104,7 @@ router.get('/:sid/VoipCarriers', async(req, res) => {
const logger = req.app.locals.logger;
try {
const account_sid = parseAccountSid(req);
await validateRequest(req, account_sid);
const results = await VoipCarrier.retrieveAll(account_sid);
res.status(200).json(results);
} catch (err) {
@@ -115,7 +117,7 @@ router.put('/:sid/VoipCarriers/:voip_carrier_sid', async(req, res) => {
try {
const sid = parseVoipCarrierSid(req);
const account_sid = parseAccountSid(req);
await validateUpdateForCarrier(req, account_sid);
await await validateRequest(req, account_sid);
const rowsAffected = await VoipCarrier.update(sid, req.body);
if (rowsAffected === 0) {
@@ -133,6 +135,8 @@ router.post('/:sid/VoipCarriers', async(req, res) => {
const payload = req.body;
try {
const account_sid = parseAccountSid(req);
await await validateRequest(req, account_sid);
logger.debug({payload}, 'POST /:sid/VoipCarriers');
const uuid = await VoipCarrier.make({
account_sid,
@@ -343,7 +347,7 @@ async function validateCreateMessage(logger, sid, req) {
async function validateAdd(req) {
/* account-level token can not be used to add accounts */
if (req.user.hasAccountAuth) {
throw new DbErrorUnprocessableRequest('insufficient permissions to create accounts');
throw new DbErrorForbidden('insufficient permissions');
}
if (req.user.hasServiceProviderAuth && req.user.service_provider_sid) {
/* service providers can only create accounts under themselves */
@@ -365,7 +369,7 @@ async function validateAdd(req) {
async function validateUpdate(req, sid) {
if (req.user.hasAccountAuth && req.user.account_sid !== sid) {
throw new DbErrorUnprocessableRequest('insufficient privileges to update this account');
throw new DbErrorForbidden('insufficient privileges');
}
if (req.user.hasAccountAuth && req.body.sip_realm) {
throw new DbErrorBadRequest('use POST /Accounts/:sid/sip_realm/:realm to set or change the sip realm');
@@ -377,7 +381,7 @@ async function validateUpdate(req, sid) {
throw new DbErrorBadRequest(`account not found for sid ${sid}`);
}
if (result[0].service_provider_sid !== req.user.service_provider_sid) {
throw new DbErrorUnprocessableRequest('cannot update account from different service provider');
throw new DbErrorForbidden('insufficient privileges');
}
}
if (req.user.hasScope('admin')) {
@@ -397,7 +401,7 @@ async function validateDelete(req, sid) {
if (req.user.service_provider_sid && !req.user.hasScope('admin')) {
const result = await Account.retrieve(sid);
if (result[0].service_provider_sid !== req.user.service_provider_sid) {
throw new DbErrorUnprocessableRequest('cannot delete account from different service provider');
throw new DbErrorForbidden('insufficient privileges');
}
}
}
@@ -444,6 +448,8 @@ router.get('/:sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
const account_sid = parseAccountSid(req);
await validateRequest(req, account_sid);
const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null;
const results = await Account.retrieve(account_sid, service_provider_sid);
if (results.length === 0) return res.status(404).end();
@@ -458,6 +464,7 @@ router.get('/:sid/WebhookSecret', async(req, res) => {
const logger = req.app.locals.logger;
try {
const account_sid = parseAccountSid(req);
await validateRequest(req, account_sid);
const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null;
const results = await Account.retrieve(account_sid, service_provider_sid);
if (results.length === 0) return res.status(404).end();
@@ -535,6 +542,7 @@ router.put('/:sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
const sid = parseAccountSid(req);
await await validateRequest(req, sid);
// create webhooks if provided
const obj = Object.assign({}, req.body);
@@ -601,6 +609,7 @@ router.delete('/:sid', async(req, res) => {
try {
const sid = parseAccountSid(req);
await await validateRequest(req, sid);
await validateDelete(req, sid);
const [account] = await promisePool.query('SELECT * FROM accounts WHERE account_sid = ?', sid);
@@ -672,6 +681,8 @@ router.get('/:sid/ApiKeys', async(req, res) => {
try {
const sid = parseAccountSid(req);
await await validateRequest(req, sid);
const results = await ApiKey.retrieveAll(sid);
res.status(200).json(results);
updateLastUsed(logger, sid, req).catch((err) => {});
@@ -694,6 +705,8 @@ router.post('/:sid/Calls', async(req, res) => {
try {
const sid = parseAccountSid(req);
await await validateRequest(req, sid);
await validateCreateCall(logger, sid, req);
updateLastUsed(logger, sid, req).catch((err) => {});
request({
@@ -727,6 +740,7 @@ router.get('/:sid/Calls', async(req, res) => {
try {
const accountSid = parseAccountSid(req);
await await validateRequest(req, accountSid);
const calls = await listCalls(accountSid);
logger.debug(`retrieved ${calls.length} calls for account sid ${accountSid}`);
res.status(200).json(coerceNumbers(snakeCase(calls)));
@@ -744,6 +758,7 @@ router.get('/:sid/Calls/:callSid', async(req, res) => {
try {
const accountSid = parseAccountSid(req);
await await validateRequest(req, accountSid);
const callSid = parseCallSid(req);
const callInfo = await retrieveCall(accountSid, callSid);
if (callInfo) {
@@ -768,6 +783,7 @@ router.delete('/:sid/Calls/:callSid', async(req, res) => {
try {
const accountSid = parseAccountSid(req);
await await validateRequest(req, accountSid);
const callSid = parseCallSid(req);
const result = await deleteCall(accountSid, callSid);
if (result) {
@@ -792,6 +808,7 @@ const updateCall = async(req, res) => {
try {
const accountSid = parseAccountSid(req);
await await validateRequest(req, accountSid);
const callSid = parseCallSid(req);
validateUpdateCall(req.body);
const call = await retrieveCall(accountSid, callSid);
@@ -832,6 +849,8 @@ router.post('/:sid/Messages', async(req, res) => {
try {
const account_sid = parseAccountSid(req);
await await validateRequest(req, account_sid);
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
const serviceUrl = await getFsUrl(logger, retrieveSet, setName);
if (!serviceUrl) res.json({msg: 'no available feature servers at this time'}).status(480);
+64 -8
View File
@@ -1,5 +1,5 @@
const router = require('express').Router();
const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors');
const {DbErrorBadRequest, DbErrorUnprocessableRequest, DbErrorForbidden} = require('../../utils/errors');
const Application = require('../../models/application');
const Account = require('../../models/account');
const Webhook = require('../../models/webhook');
@@ -13,6 +13,36 @@ const preconditions = {
'update': validateUpdate
};
const validateRequest = async(req, account_sid) => {
try {
if (req.user.hasScope('admin')) {
return;
}
if (req.user.hasScope('account')) {
if (account_sid === req.user.account_sid) {
return;
}
throw new DbErrorForbidden('insufficient permissions');
}
if (req.user.hasScope('service_provider')) {
const [r] = await promisePool.execute(
'SELECT service_provider_sid from accounts WHERE account_sid = ?', [account_sid]
);
if (r.length === 1 && r[0].service_provider_sid === req.user.service_provider_sid) {
return;
}
throw new DbErrorForbidden('insufficient permissions');
}
} catch (error) {
throw error;
}
};
/* only user-level tokens can add applications */
async function validateAdd(req) {
if (req.user.account_sid) {
@@ -23,7 +53,7 @@ async function validateAdd(req) {
if (!req.body.account_sid) throw new DbErrorBadRequest('missing required field: \'account_sid\'');
const result = await Account.retrieve(req.body.account_sid, req.user.service_provider_sid);
if (result.length === 0) {
throw new DbErrorBadRequest('insufficient privileges to create an application under the specified account');
throw new DbErrorForbidden('insufficient privileges');
}
}
if (req.body.call_hook && typeof req.body.call_hook !== 'object') {
@@ -35,12 +65,26 @@ async function validateAdd(req) {
}
async function validateUpdate(req, sid) {
if (req.user.account_sid) {
const app = await Application.retrieve(sid);
if (!app || !app.length || app[0].account_sid !== req.user.account_sid) {
throw new DbErrorBadRequest('you may not update or delete an application associated with a different account');
const app = await Application.retrieve(sid);
if (req.user.hasAccountAuth) {
if (!app || 0 === app.length || app[0].account_sid !== req.user.account_sid) {
throw new DbErrorForbidden('insufficient privileges');
}
}
if (req.user.hasServiceProviderAuth) {
const [r] = await promisePool.execute(
'SELECT service_provider_sid from accounts WHERE account_sid = ?', [app[0].account_sid]
);
if (r.length === 1 && r[0].service_provider_sid === req.user.service_provider_sid) {
return;
}
throw new DbErrorForbidden('insufficient permissions');
}
if (req.body.call_hook && typeof req.body.call_hook !== 'object') {
throw new DbErrorBadRequest('\'call_hook\' must be an object when updating an application');
}
@@ -50,13 +94,24 @@ async function validateUpdate(req, sid) {
}
async function validateDelete(req, sid) {
const result = await Application.retrieve(sid);
if (req.user.hasAccountAuth) {
const result = await Application.retrieve(sid);
if (!result || 0 === result.length) throw new DbErrorBadRequest('application does not exist');
if (result[0].account_sid !== req.user.account_sid) {
throw new DbErrorUnprocessableRequest('cannot delete application owned by a different account');
throw new DbErrorUnprocessableRequest('insufficient permissions');
}
}
if (req.user.hasServiceProviderAuth) {
const [r] = await promisePool.execute(
'SELECT service_provider_sid from accounts WHERE account_sid = ?', [result[0].account_sid]
);
if (r.length === 1 && r[0].service_provider_sid === req.user.service_provider_sid) {
return;
}
throw new DbErrorForbidden('insufficient permissions');
}
const assignedPhoneNumbers = await Application.getForeignKeyReferences('phone_numbers.application_sid', sid);
if (assignedPhoneNumbers > 0) throw new DbErrorUnprocessableRequest('cannot delete application with phone numbers');
}
@@ -117,6 +172,7 @@ router.get('/:sid', async(req, res) => {
const account_sid = req.user.hasAccountAuth ? req.user.account_sid : null;
const results = await Application.retrieve(application_sid, service_provider_sid, account_sid);
if (results.length === 0) return res.status(404).end();
await validateRequest(req, results[0].account_sid);
return res.status(200).json(results[0]);
}
catch (err) {
+17 -5
View File
@@ -1,8 +1,9 @@
const router = require('express').Router();
const {DbErrorUnprocessableRequest, DbErrorBadRequest} = require('../../utils/errors');
const {DbErrorBadRequest, DbErrorForbidden} = require('../../utils/errors');
const PhoneNumber = require('../../models/phone-number');
const VoipCarrier = require('../../models/voip-carrier');
const decorate = require('./decorate');
const {promisePool} = require('../../db');
const {e164} = require('../../utils/phone-number-utils');
const preconditions = {
'add': validateAdd,
@@ -42,11 +43,11 @@ async function checkInUse(req, sid) {
const phoneNumber = await PhoneNumber.retrieve(sid);
if (req.user.hasAccountAuth) {
if (phoneNumber && phoneNumber.length && phoneNumber[0].account_sid !== req.user.account_sid) {
throw new DbErrorUnprocessableRequest('cannot delete a phone number that belongs to another account');
throw new DbErrorForbidden('insufficient privileges');
}
}
if (!req.user.hasAccountAuth && phoneNumber.account_sid) {
throw new DbErrorUnprocessableRequest('cannot delete phone number that is assigned to an account');
throw new DbErrorForbidden('insufficient privileges');
}
}
@@ -58,7 +59,7 @@ async function validateUpdate(req, sid) {
const phoneNumber = await PhoneNumber.retrieve(sid);
if (req.user.hasAccountAuth) {
if (phoneNumber && phoneNumber.length && phoneNumber[0].account_sid !== req.user.account_sid) {
throw new DbErrorUnprocessableRequest('cannot operate on a phone number that belongs to another account');
throw new DbErrorForbidden('insufficient privileges');
}
}
@@ -75,7 +76,9 @@ decorate(router, PhoneNumber, ['add', 'update', 'delete'], preconditions);
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
try {
const results = await PhoneNumber.retrieveAll(req.user.hasAccountAuth ? req.user.account_sid : null);
const results = req.user.hasAdminAuth ?
await PhoneNumber.retrieveAll(req.user.hasAccountAuth ? req.user.account_sid : null) :
await PhoneNumber.retrieveAllForSP(req.user.service_provider_sid);
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
@@ -90,6 +93,15 @@ router.get('/:sid', async(req, res) => {
const account_sid = req.user.hasAccountAuth ? req.user.account_sid : null;
const results = await PhoneNumber.retrieve(sid, account_sid);
if (results.length === 0) return res.status(404).end();
if (req.user.hasServiceProviderAuth && results.length === 1) {
const account_sid = results[0].account_sid;
const [r] = await promisePool.execute(
'SELECT service_provider_sid from accounts WHERE account_sid = ?', [account_sid]);
if (r.length === 1 && r[0].service_provider_sid === !req.user.service_provider_sid) {
throw new DbErrorBadRequest('insufficient privileges');
}
}
return res.status(200).json(results[0]);
}
catch (err) {
+36 -8
View File
@@ -2,25 +2,53 @@ const router = require('express').Router();
const Sbc = require('../../models/sbc');
const decorate = require('./decorate');
const sysError = require('../error');
//const {DbErrorBadRequest} = require('../../utils/errors');
//const {promisePool} = require('../../db');
const {DbErrorBadRequest} = require('../../utils/errors');
const {promisePool} = require('../../db');
decorate(router, Sbc, ['add', 'delete']);
const validate = (req, res) => {
if (req.user.hasScope('admin')) return;
res.status(403).json({
status: 'fail',
message: 'insufficient privileges'
});
};
const preconditions = {
'add': validate,
'delete': validate
};
decorate(router, Sbc, ['add', 'delete'], preconditions);
/* list */
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
try {
const service_provider_sid = req.query.service_provider_sid;
/*
let service_provider_sid = req.query.service_provider_sid;
if (req.user.hasAccountAuth) {
const [r] = await promisePool.query('SELECT * from accounts WHERE account_sid = ?', req.user.account_sid);
if (0 === r.length) throw new Error('invalid account_sid');
service_provider_sid = r[0].service_provider_sid;
}
if (!service_provider_sid) throw new DbErrorBadRequest('missing service_provider_sid in query');
*/
const results = await Sbc.retrieveAll(service_provider_sid);
if (req.user.hasServiceProviderAuth) {
const [r] = await promisePool.query(
'SELECT * from service_providers where service_provider_sid = ?',
service_provider_sid);
if (0 === r.length) throw new Error('invalid account_sid');
service_provider_sid = r[0].service_provider_sid;
if (!service_provider_sid) throw new DbErrorBadRequest('missing service_provider_sid in query');
}
/** generally, we have a global set of SBCs that all accounts use.
* However, we can have a set of SBCs that are specific for use by a service provider.
*/
let results = await Sbc.retrieveAll(service_provider_sid);
if (results.length === 0) results = await Sbc.retrieveAll();
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
+14 -19
View File
@@ -1,6 +1,6 @@
const router = require('express').Router();
const {promisePool} = require('../../db');
const {DbErrorUnprocessableRequest, DbErrorForbidden} = require('../../utils/errors');
const {DbErrorForbidden} = require('../../utils/errors');
const Webhook = require('../../models/webhook');
const ServiceProvider = require('../../models/service-provider');
const Account = require('../../models/account');
@@ -8,7 +8,11 @@ const VoipCarrier = require('../../models/voip-carrier');
const Application = require('../../models/application');
const PhoneNumber = require('../../models/phone-number');
const ApiKey = require('../../models/api-key');
const {hasServiceProviderPermissions, parseServiceProviderSid, parseVoipCarrierSid} = require('./utils');
const {
hasServiceProviderPermissions,
parseServiceProviderSid,
parseVoipCarrierSid,
} = require('./utils');
const sysError = require('../error');
const decorate = require('./decorate');
const preconditions = {
@@ -42,18 +46,11 @@ async function validateRetrieve(req) {
return;
}
if (req.user.hasScope('service_provider')) {
if (service_provider_sid === req.user.service_provider_sid) return ;
if (req.user.hasScope('service_provider') || req.user.hasScope('account')) {
if (service_provider_sid === req.user.service_provider_sid) return;
}
if (req.user.hasScope('account')) {
/* allow account users to retrieve service provider data from parent SP */
const sid = req.user.account_sid;
const [r] = await promisePool.execute('SELECT service_provider_sid from accounts WHERE account_sid = ?', [sid]);
if (r.length === 1 && r[0].service_provider_sid === req.user.service_provider_sid) return;
}
throw new DbErrorForbidden('insufficient permissions to update service provider');
throw new DbErrorForbidden('insufficient permissions');
} catch (error) {
throw error;
}
@@ -84,14 +81,10 @@ async function noActiveAccountsOrUsers(req, sid) {
}
const activeAccounts = await ServiceProvider.getForeignKeyReferences('accounts.service_provider_sid', sid);
const activeUsers = await ServiceProvider.getForeignKeyReferences('users.service_provider_sid', sid);
if (activeAccounts > 0 && activeUsers > 0) throw new DbErrorUnprocessableRequest(
'cannot delete service provider with active accounts or users'
);
if (activeAccounts > 0 && activeUsers > 0) throw new DbErrorForbidden('insufficient privileges');
if (activeAccounts > 0) throw new DbErrorUnprocessableRequest('cannot delete service provider with active accounts');
if (activeUsers > 0) throw new DbErrorUnprocessableRequest(
'cannot delete service provider with active service provider level users'
);
if (activeAccounts > 0) throw new DbErrorForbidden('insufficient privileges');
if (activeUsers > 0) throw new DbErrorForbidden('insufficient privileges');
/* ok we can delete -- no active accounts. remove carriers and speech credentials */
await promisePool.execute('DELETE from speech_credentials WHERE service_provider_sid = ?', [sid]);
@@ -122,6 +115,7 @@ router.get('/:sid/Accounts', async(req, res) => {
sysError(logger, res, err);
}
});
router.get('/:sid/Applications', async(req, res) => {
const logger = req.app.locals.logger;
try {
@@ -250,6 +244,7 @@ router.get('/', async(req, res) => {
router.get('/:sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
await validateRetrieve(req);
const sid = parseServiceProviderSid(req);
const results = await ServiceProvider.retrieve(sid);
if (results.length === 0) return res.status(404).end();
+20 -3
View File
@@ -6,6 +6,12 @@ const {verifyPassword} = require('../../utils/password-utils');
const {cacheClient} = require('../../helpers');
const jwt = require('jsonwebtoken');
const sysError = require('../error');
const retrievePermissionsSql = `
SELECT p.name
FROM permissions p, user_permissions up
WHERE up.permission_sid = p.permission_sid
AND up.user_sid = ?
`;
const validateRequest = async(req) => {
const {email, password} = req.body || {};
@@ -53,6 +59,7 @@ router.post('/', async(req, res) => {
email: user.email,
phone: user.phone,
account_sid: user.account_sid,
service_provider_sid: a[0].service_provider_sid,
force_change: !!user.force_change,
provider: user.provider,
provider_userid: user.provider_userid,
@@ -65,12 +72,22 @@ router.post('/', async(req, res) => {
pristine: false
});
const [p] = await promisePool.query(retrievePermissionsSql, user.user_sid);
const permissions = p.map((x) => x.name);
const expiresIn = parseInt(process.env.JWT_EXPIRES_IN || 60) * 60;
// generate a json web token for this session
const token = jwt.sign({
const payload = {
scope: 'account',
permissions,
user_sid: userProfile.user_sid,
account_sid: userProfile.account_sid
}, process.env.JWT_SECRET, { expiresIn });
account_sid: userProfile.account_sid,
service_provider_sid: userProfile.service_provider_sid
};
const token = jwt.sign(payload,
process.env.JWT_SECRET,
{ expiresIn }
);
logger.debug({
user_sid: userProfile.user_sid,
+35 -9
View File
@@ -1,11 +1,42 @@
const router = require('express').Router();
const SipGateway = require('../../models/sip-gateway');
const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors');
const {DbErrorBadRequest, DbErrorForbidden} = require('../../utils/errors');
//const {parseSipGatewaySid} = require('./utils');
const decorate = require('./decorate');
const sysError = require('../error');
const checkUserScope = async(req, voip_carrier_sid) => {
const {lookupCarrierBySid} = req.app.locals;
if (req.user.hasAdminAuth) return;
if (req.user.hasAccountAuth) {
const carrier = await lookupCarrierBySid(voip_carrier_sid);
if (!carrier) throw new DbErrorBadRequest('invalid voip_carrier_sid');
if ((!carrier.service_provider_sid || carrier.service_provider_sid === req.user.service_provider_sid) &&
(!carrier.account_sid || carrier.account_sid === req.user.account_sid)) {
if (req.method !== 'GET' && !carrier.account_sid) {
throw new DbErrorForbidden('insufficient privileges');
}
return;
}
}
if (req.user.hasServiceProviderAuth) {
const carrier = await lookupCarrierBySid(voip_carrier_sid);
if (!carrier) {
throw new DbErrorBadRequest('invalid voip_carrier_sid');
}
if (carrier.service_provider_sid === req.user.service_provider_sid) {
return;
}
}
throw new DbErrorForbidden('insufficient privileges');
};
const validate = async(req, sid) => {
const {lookupCarrierBySid, lookupSipGatewayBySid} = req.app.locals;
const {lookupSipGatewayBySid} = req.app.locals;
let voip_carrier_sid;
if (sid) {
@@ -17,13 +48,7 @@ const validate = async(req, sid) => {
voip_carrier_sid = req.body.voip_carrier_sid;
if (!voip_carrier_sid) throw new DbErrorBadRequest('missing voip_carrier_sid');
}
if (req.hasAccountAuth) {
const carrier = await lookupCarrierBySid(voip_carrier_sid);
if (!carrier) throw new DbErrorBadRequest('invalid voip_carrier_sid');
if (carrier.account_sid !== req.user.account_sid) {
throw new DbErrorUnprocessableRequest('user can not add gateway for voip_carrier belonging to other account');
}
}
await checkUserScope(req, voip_carrier_sid);
};
const preconditions = {
@@ -39,6 +64,7 @@ router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
const voip_carrier_sid = req.query.voip_carrier_sid;
try {
await checkUserScope(req, voip_carrier_sid);
if (!voip_carrier_sid) {
logger.info('GET /SipGateways missing voip_carrier_sid param');
return res.status(400).json({message: 'missing voip_carrier_sid query param'});
+28 -9
View File
@@ -1,11 +1,34 @@
const router = require('express').Router();
const SmppGateway = require('../../models/smpp-gateway');
const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors');
const {DbErrorBadRequest, DbErrorForbidden} = require('../../utils/errors');
const decorate = require('./decorate');
const sysError = require('../error');
const checkUserScope = async(req, voip_carrier_sid) => {
const {lookupCarrierBySid} = req.app.locals;
if (req.user.hasAdminAuth) return;
if (req.user.hasAccountAuth) {
const carrier = await lookupCarrierBySid(voip_carrier_sid);
if (!carrier) throw new DbErrorBadRequest('invalid voip_carrier_sid');
if ((!carrier.service_provider_sid || carrier.service_provider_sid === req.user.service_provider_sid) &&
(!carrier.account_sid || carrier.account_sid === req.user.account_sid)) {
return;
}
}
if (req.user.hasServiceProviderAuth) {
const carrier = await lookupCarrierBySid(voip_carrier_sid);
if (!carrier) throw new DbErrorBadRequest('invalid voip_carrier_sid');
if (carrier.service_provider_sid === req.user.service_provider_sid) {
return;
}
}
throw new DbErrorForbidden('insufficient privileges');
};
const validate = async(req, sid) => {
const {lookupCarrierBySid, lookupSmppGatewayBySid} = req.app.locals;
const {lookupSmppGatewayBySid} = req.app.locals;
let voip_carrier_sid;
if (sid) {
@@ -17,13 +40,8 @@ const validate = async(req, sid) => {
voip_carrier_sid = req.body.voip_carrier_sid;
if (!voip_carrier_sid) throw new DbErrorBadRequest('missing voip_carrier_sid');
}
if (req.hasAccountAuth) {
const carrier = await lookupCarrierBySid(voip_carrier_sid);
if (!carrier) throw new DbErrorBadRequest('invalid voip_carrier_sid');
if (carrier.account_sid !== req.user.account_sid) {
throw new DbErrorUnprocessableRequest('user can not add gateway for voip_carrier belonging to other account');
}
}
await checkUserScope(req, voip_carrier_sid);
};
const preconditions = {
@@ -39,6 +57,7 @@ router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
const voip_carrier_sid = req.query.voip_carrier_sid;
try {
await checkUserScope(req, voip_carrier_sid);
if (!voip_carrier_sid) {
logger.info('GET /SmppGateways missing voip_carrier_sid param');
return res.status(400).json({message: 'missing voip_carrier_sid query param'});
+116 -3
View File
@@ -5,7 +5,7 @@ const SpeechCredential = require('../../models/speech-credential');
const sysError = require('../error');
const {decrypt, encrypt} = require('../../utils/encrypt-decrypt');
const {parseAccountSid, parseServiceProviderSid, parseSpeechCredentialSid} = require('./utils');
const {DbErrorUnprocessableRequest} = require('../../utils/errors');
const {DbErrorUnprocessableRequest, DbErrorForbidden} = require('../../utils/errors');
const {
testGoogleTts,
testGoogleStt,
@@ -21,6 +21,83 @@ const {
testIbmTts,
testIbmStt
} = require('../../utils/speech-utils');
const {promisePool} = require('../../db');
const validateAdd = async(req) => {
const account_sid = parseAccountSid(req);
const service_provider_sid = parseServiceProviderSid(req);
if (service_provider_sid) {
if (req.user.hasServiceProviderAuth && service_provider_sid !== req.user.service_provider_sid) {
throw new DbErrorForbidden('Insufficient privileges');
}
if (req.user.hasAccountAuth && service_provider_sid !== req.user.service_provider_sid &&
req.body.account_sid !== req.user.account_sid) {
throw new DbErrorForbidden('Insufficient privileges');
}
}
if (account_sid) {
if (req.user.hasAccountAuth && account_sid !== req.user.account_sid) {
throw new DbErrorForbidden('Insufficient privileges');
}
const [r] = await promisePool.execute(
'SELECT service_provider_sid from accounts WHERE account_sid = ?', [account_sid]
);
if (req.user.hasServiceProviderAuth && r[0].service_provider_sid !== req.user.service_provider_sid) {
throw new DbErrorForbidden('Insufficient privileges');
}
}
return;
};
const validateRetrieveUpdateDelete = async(req, speech_credentials) => {
if (req.user.hasServiceProviderAuth && speech_credentials[0].service_provider_sid !== req.user.service_provider_sid) {
throw new DbErrorForbidden('Insufficient privileges');
}
if (req.user.hasAccountAuth && speech_credentials[0].account_sid !== req.user.account_sid) {
throw new DbErrorForbidden('Insufficient privileges');
}
return;
};
const validateRetrieveList = async(req) => {
const service_provider_sid = parseServiceProviderSid(req);
if (service_provider_sid) {
if ((req.user.hasServiceProviderAuth || req.user.hasAccountAuth) &&
service_provider_sid !== req.user.service_provider_sid) {
throw new DbErrorForbidden('Insufficient privileges');
}
}
return;
};
const validateTest = async(req, speech_credentials) => {
if (req.user.hasAdminAuth) {
return;
}
if (!req.user.hasAdminAuth && speech_credentials.service_provider_sid !== req.user.service_provider_sid) {
throw new DbErrorForbidden('Insufficient privileges');
}
if (speech_credentials.service_provider_sid === req.user.service_provider_sid) {
if (req.user.hasServiceProviderAuth) {
return;
}
if (req.user.hasAccountAuth && (!speech_credentials.account_sid ||
speech_credentials.account_sid === req.user.account_sid)) {
return;
}
throw new DbErrorForbidden('Insufficient privileges');
}
};
const obscureKey = (key) => {
const key_spoiler_length = 6;
@@ -146,6 +223,8 @@ router.post('/', async(req, res) => {
const service_provider_sid = req.user.service_provider_sid ||
req.body.service_provider_sid || parseServiceProviderSid(req);
await validateAdd(req);
if (!account_sid) {
if (!req.user.hasServiceProviderAuth && !req.user.hasAdminAuth) {
logger.error('POST /SpeechCredentials invalid credentials');
@@ -175,8 +254,11 @@ router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
try {
const account_sid = parseAccountSid(req) || req.user.account_sid;
const service_provider_sid = parseServiceProviderSid(req) || req.user.service_provider_sid;
const account_sid = parseAccountSid(req) ? parseAccountSid(req) : req.user.account_sid;
const service_provider_sid = parseServiceProviderSid(req);
await validateRetrieveList(req);
const credsAccount = account_sid ? await SpeechCredential.retrieveAll(account_sid) : [];
const credsSP = service_provider_sid ?
await SpeechCredential.retrieveAllForSP(service_provider_sid) :
@@ -190,6 +272,7 @@ router.get('/', async(req, res) => {
res.status(200).json(creds.map((c) => {
const {credential, ...obj} = c;
if ('google' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
const key_header = '-----BEGIN PRIVATE KEY-----\n';
@@ -250,6 +333,15 @@ router.get('/', async(req, res) => {
obj.custom_stt_url = o.custom_stt_url;
obj.custom_tts_url = o.custom_tts_url;
}
if (req.user.hasAccountAuth && obj.account_sid === null) {
delete obj.api_key;
delete obj.secret_access_key;
delete obj.secret;
delete obj.auth_token;
delete obj.stt_api_key;
delete obj.tts_api_key;
}
return obj;
}));
} catch (err) {
@@ -266,6 +358,9 @@ router.get('/:sid', async(req, res) => {
const sid = parseSpeechCredentialSid(req);
const cred = await SpeechCredential.retrieve(sid);
if (0 === cred.length) return res.sendStatus(404);
await validateRetrieveUpdateDelete(req, cred);
const {credential, ...obj} = cred[0];
if ('google' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
@@ -327,6 +422,16 @@ router.get('/:sid', async(req, res) => {
obj.custom_stt_url = o.custom_stt_url;
obj.custom_tts_url = o.custom_tts_url;
}
if (req.user.hasAccountAuth && obj.account_sid === null) {
delete obj.api_key;
delete obj.secret_access_key;
delete obj.secret;
delete obj.auth_token;
delete obj.stt_api_key;
delete obj.tts_api_key;
}
res.status(200).json(obj);
} catch (err) {
sysError(logger, res, err);
@@ -340,6 +445,8 @@ router.delete('/:sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
const sid = parseSpeechCredentialSid(req);
const cred = await SpeechCredential.retrieve(sid);
await validateRetrieveUpdateDelete(req, cred);
const count = await SpeechCredential.remove(sid);
if (0 === count) return res.sendStatus(404);
res.sendStatus(204);
@@ -372,6 +479,9 @@ router.put('/:sid', async(req, res) => {
/* update the credential if provided */
try {
const cred = await SpeechCredential.retrieve(sid);
await validateRetrieveUpdateDelete(req, cred);
if (1 === cred.length) {
const {credential, vendor} = cred[0];
const o = JSON.parse(decrypt(credential));
@@ -428,8 +538,11 @@ router.get('/:sid/test', async(req, res) => {
try {
const sid = parseSpeechCredentialSid(req);
const creds = await SpeechCredential.retrieve(sid);
if (!creds || 0 === creds.length) return res.sendStatus(404);
await validateTest(req, creds[0]);
const cred = creds[0];
const credential = JSON.parse(decrypt(cred.credential));
const results = {
+63 -30
View File
@@ -1,9 +1,9 @@
const router = require('express').Router();
const User = require('../../models/user');
const {DbErrorBadRequest} = require('../../utils/errors');
const {DbErrorBadRequest, BadRequestError, DbErrorForbidden} = require('../../utils/errors');
const {generateHashedPassword, verifyPassword} = require('../../utils/password-utils');
const {promisePool} = require('../../db');
const {validatePasswordSettings} = require('./utils');
const {validatePasswordSettings, parseUserSid} = require('./utils');
const {decrypt} = require('../../utils/encrypt-decrypt');
const {cacheClient} = require('../../helpers');
const sysError = require('../error');
@@ -99,6 +99,48 @@ const validateRequest = async(user_sid, req) => {
return user;
};
const getActiveAdminUsers = (users) => {
return users.filter((e) => !e.account_sid && !e.service_provider_sid && e.is_active);
};
const ensureUserActionIsAllowed = (req, user) => {
if (req.user.hasAdminAuth) {
return;
}
if (req.user.hasServiceProviderAuth && req.user.service_provider_sid === user.service_provider_sid) {
return;
}
if (req.user.hasAccountAuth && req.user.account_sid === user.account_sid) {
return;
}
throw new DbErrorForbidden('insufficient permissions');
};
const ensureUserDeletionIsAllowed = (req, activeAdminUsers, user) => {
try {
if (req.user.hasAdminAuth && activeAdminUsers.length === 1 && activeAdminUsers[0].user_sid === user[0].user_sid) {
throw new BadRequestError('cannot delete this admin user - there are no other active admin users');
}
ensureUserActionIsAllowed(req, user[0]);
return;
} catch (error) {
throw error;
}
};
const ensureUserRetrievalIsAllowed = (req, user) => {
try {
ensureUserActionIsAllowed(req, user);
return;
} catch (error) {
throw error;
}
};
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
@@ -256,22 +298,20 @@ router.get('/me', async(req, res) => {
router.get('/:user_sid', async(req, res) => {
const logger = req.app.locals.logger;
const {user_sid} = req.params;
try {
const user_sid = parseUserSid(req);
const [user] = await User.retrieve(user_sid);
// eslint-disable-next-line no-unused-vars
const {hashed_password, ...rest} = user;
if (!user) throw new Error('failure retrieving user');
if (req.user.hasAdminAuth ||
req.user.hasAccountAuth && req.user.account_sid === user.account_sid ||
req.user.hasServiceProviderAuth && req.user.service_provider_sid === user.service_provider_sid) {
res.status(200).json(rest);
} else {
res.sendStatus(403);
if (!user) {
throw new Error('failure retrieving user');
}
ensureUserRetrievalIsAllowed(req, user);
// eslint-disable-next-line no-unused-vars
const { hashed_password, ...rest } = user;
return res.status(200).json(rest);
} catch (err) {
sysError(logger, res, err);
}
@@ -449,28 +489,21 @@ router.post('/', async(req, res) => {
router.delete('/:user_sid', async(req, res) => {
const logger = req.app.locals.logger;
const {user_sid} = req.params;
const allUsers = await User.retrieveAll();
const activeAdminUsers = allUsers.filter((e) => !e.account_sid && !e.service_provider_sid && e.is_active);
const user = await User.retrieve(user_sid);
try {
if (req.user.hasAdminAuth && activeAdminUsers.length === 1) {
throw new Error('cannot delete this admin user - there are no other active admin users');
}
const user_sid = parseUserSid(req);
const allUsers = await User.retrieveAll();
const activeAdminUsers = getActiveAdminUsers(allUsers);
const user = allUsers.filter((user) => user.user_sid === user_sid);
if (req.user.hasAdminAuth ||
(req.user.hasAccountAuth && req.user.account_sid === user[0].account_sid) ||
(req.user.hasServiceProviderAuth && req.user.service_provider_sid === user[0].service_provider_sid)) {
await User.remove(user_sid);
/* invalidate the jwt of the deleted user */
const redisKey = cacheClient.generateRedisKey('jwt', user_sid, 'v2');
await cacheClient.delete(redisKey);
ensureUserDeletionIsAllowed(req, activeAdminUsers, user);
await User.remove(user_sid);
return res.sendStatus(204);
} else {
throw new DbErrorBadRequest('invalid request');
}
/* invalidate the jwt of the deleted user */
const redisKey = cacheClient.generateRedisKey('jwt', user_sid, 'v2');
await cacheClient.delete(redisKey);
return res.sendStatus(204);
} catch (err) {
sysError(logger, res, err);
}
+34 -4
View File
@@ -218,20 +218,48 @@ const parseWebhookSid = (req) => {
}
};
const hasAccountPermissions = (req, res, next) => {
const parseSipGatewaySid = (req) => {
try {
return validateSid('SipGateways', req);
} catch (error) {
throw error;
}
};
const parseUserSid = (req) => {
try {
return validateSid('Users', req);
} catch (error) {
throw error;
}
};
const hasAccountPermissions = async(req, res, next) => {
try {
if (req.user.hasScope('admin')) {
return next();
}
if (req.user.hasScope('service_provider')) {
return next();
const service_provider_sid = parseServiceProviderSid(req);
if (service_provider_sid === req.user.service_provider_sid) return next();
}
if (req.user.hasScope('account')) {
const account_sid = parseAccountSid(req);
if (account_sid === req.user.account_sid) {
return next();
const service_provider_sid = parseServiceProviderSid(req);
const [r] = await Account.retrieve(account_sid);
if (account_sid) {
if (r && r.account_sid === req.user.account_sid) {
return next();
}
}
if (service_provider_sid) {
if (r && r.service_provider_sid === req.user.service_provider_sid) {
return next();
}
}
}
@@ -405,6 +433,8 @@ module.exports = {
parseSpeechCredentialSid,
parseVoipCarrierSid,
parseWebhookSid,
parseSipGatewaySid,
parseUserSid,
hasAccountPermissions,
hasServiceProviderPermissions,
checkLimits,
+20 -1
View File
@@ -74,7 +74,14 @@ decorate(router, VoipCarrier, ['add', 'update', 'delete'], preconditions);
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
try {
const results = await VoipCarrier.retrieveAll(req.user.hasAccountAuth ? req.user.account_sid : null);
const results = req.user.hasAdminAuth ?
await VoipCarrier.retrieveAll(req.user.hasAccountAuth ? req.user.account_sid : null) :
await VoipCarrier.retrieveAllForSP(req.user.service_provider_sid);
if (req.user.hasScope('account')) {
return res.status(200).json(results.filter((c) => c.account_sid === req.user.account_sid || !c.account_sid));
}
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
@@ -91,6 +98,18 @@ router.get('/:sid', async(req, res) => {
if (results.length === 0) return res.status(404).end();
const ret = results[0];
ret.register_status = JSON.parse(ret.register_status || '{}');
if (req.user.hasServiceProviderAuth && results.length === 1) {
if (results.length === 1 && results[0].service_provider_sid !== req.user.service_provider_sid) {
throw new DbErrorBadRequest('insufficient privileges');
}
}
if (req.user.hasAccountAuth && results.length === 1) {
if (results.length === 1 && results[0].account_sid !== req.user.account_sid) {
throw new DbErrorBadRequest('insufficient privileges');
}
}
return res.status(200).json(results[0]);
}
catch (err) {
+20
View File
@@ -2,17 +2,37 @@ const router = require('express').Router();
const Webhook = require('../../models/webhook');
const decorate = require('./decorate');
const sysError = require('../error');
const {DbErrorForbidden} = require('../../utils/errors');
const { parseWebhookSid } = require('./utils');
const {promisePool} = require('../../db');
decorate(router, Webhook, ['add']);
/* retrieve */
router.get('/:sid', async(req, res) => {
const logger = req.app.locals.logger;
try {
const sid = parseWebhookSid(req);
const results = await Webhook.retrieve(sid);
if (results.length === 0) return res.status(404).end();
if (req.user.hasAccountAuth) {
/* can only update carriers for the user's account */
if (results[0].account_sid != req.user.account_sid) {
throw new DbErrorForbidden('insufficient privileges');
}
}
if (req.user.hasServiceProviderAuth) {
const [r] = await promisePool.execute(
'SELECT service_provider_sid from accounts WHERE account_sid = ?', [results[0].account_sid]
);
if (r.length === 1 && r[0].service_provider_sid === req.user.service_provider_sid) {
return;
}
throw new DbErrorForbidden('insufficient permissions');
}
return res.status(200).json(results[0]);
}
catch (err) {
+4
View File
@@ -1,6 +1,10 @@
const test = require('tape') ;
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
const authAdmin = {bearer: ADMIN_TOKEN};
const SP_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734ds';
const authSP = {bearer: ADMIN_TOKEN};
const ACC_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734da';
const authAcc = {bearer: ADMIN_TOKEN};
const request = require('request-promise-native').defaults({
baseUrl: 'http://127.0.0.1:3000/v1'
});
+6 -6
View File
@@ -127,7 +127,7 @@ test('authentication tests', async(t) => {
sip_realm: 'sip.foo.bar'
}
});
t.ok(result.statusCode === 422 && result.body.msg === 'cannot update account from different service provider',
t.ok(result.statusCode === 403 && result.body.msg === 'insufficient permissions',
'service provider token B cannot be used to update account from service provider A');
/* cannot delete account from different service provider */
@@ -137,7 +137,7 @@ test('authentication tests', async(t) => {
simple: false,
json: true,
});
t.ok(result.statusCode === 422 && result.body.msg === 'cannot delete account from different service provider',
t.ok(result.statusCode === 403 && result.body.msg === 'insufficient permissions',
'service provider token B cannot be used to delete account from service provider A');
/* service provider token A can update account A1 */
@@ -179,7 +179,7 @@ test('authentication tests', async(t) => {
}
});
//console.log(`result: ${JSON.stringify(result)}`);
t.ok(result.statusCode === 422 && result.body.msg === 'insufficient permissions to create accounts',
t.ok(result.statusCode === 403 && result.body.msg === 'insufficient permissions',
'cannot create an account using an account-level token');
/* using account token we see one account */
@@ -200,8 +200,7 @@ test('authentication tests', async(t) => {
sip_realm: 'sip.foo.bar'
}
});
//console.log(`result: ${JSON.stringify(result)}`);
t.ok(result.statusCode === 422 && result.body.msg === 'insufficient privileges to update this account',
t.ok(result.statusCode === 403 && result.body.msg === 'insufficient permissions',
'cannot update account A2 using auth token for account A1');
/* can update an account using an appropriate account-level token */
@@ -251,7 +250,8 @@ test('authentication tests', async(t) => {
}
}
});
t.ok(result.statusCode === 400 && result.body.msg === 'insufficient privileges to create an application under the specified account',
//console.log(`result: ${JSON.stringify(result)}`);
t.ok(result.statusCode === 403 && result.body.msg === 'insufficient privileges',
'cannot create application for account A2 using service provider token B');
result = await request.post('/Applications', {
+2
View File
@@ -71,6 +71,7 @@ test('speech credentials tests', async(t) => {
const token = jwt.sign({
account_sid,
service_provider_sid,
scope: 'account',
permissions: ["PROVISION_USERS", "PROVISION_SERVICES", "VIEW_ONLY"]
}, process.env.JWT_SECRET, { expiresIn: '1h' });
@@ -393,6 +394,7 @@ test('speech credentials tests', async(t) => {
auth: authUser,
json: true,
body: {
service_provider_sid: service_provider_sid,
vendor: 'nvidia',
use_for_stt: true,
use_for_tts: true,
+2 -2
View File
@@ -83,7 +83,7 @@ test('user tests', async(t) => {
}
});
t.ok(result.statusCode === 201 && result.body.user_sid, 'service_provider scope user created');
const sp_user_sid = result.body.sid;
const sp_user_sid = result.body.user_sid;
/* add an account */
result = await request.post('/Accounts', {
@@ -119,7 +119,7 @@ test('user tests', async(t) => {
}
});
t.ok(result.statusCode === 201 && result.body.user_sid, 'account scope user created');
const account_user_sid = result.body.sid;
const account_user_sid = result.body.user_sid;
/* retrieve list of users */
result = await request.get(`/Users`, {
+14 -11
View File
@@ -243,17 +243,20 @@ test('webapp tests', async(t) => {
t.ok(result.statusCode === 200 &&
result.body.phonenumbers.length === 1 && result.body.applications.length === 1, 'retrieves test number and application');
/* update user name */
result = await request.put(`/Users/foobar`, {
resolveWithFullResponse: true,
json: true,
simple: false,
auth: authUser,
body: {
name: 'Jane Doe'
}
});
t.ok(result.statusCode === 403, 'rejects attempt to update different user');
/* try to update user name passing an invalid uuid */
try {
await request.put(`/Users/foobar`, {
resolveWithFullResponse: true,
json: true,
simple: false,
auth: authUser,
body: {
name: 'Jane Doe'
}
});
} catch (error) {
t.ok(error.statusCode === 400, 'returns 400 bad request if user sid param is not a valid uuid');
}
/* update user name */
result = await request.put(`/Users/${user_sid}`, {