Compare commits

..

13 Commits

Author SHA1 Message Date
Markus Frindt
1dcc92a177 Fix bug in forgot-password req.user destruction (#159)
* Fix bug in forgot-password req.user destruction

* add test for forgot password

---------

Co-authored-by: Markus Frindt <m.frindt@cognigy.com>
2023-04-28 08:43:23 -04:00
EgleH
105aa16ffe SP users were not able to update Phone numbers (#158)
Co-authored-by: eglehelms <e.helms@cognigy.com>
2023-04-24 07:46:57 -04:00
Anton Voylenko
a574045f8a endpoint to retrieve active queues (#156) 2023-04-22 14:48:07 -04:00
Anton Voylenko
af3d03bef9 support filtering for retrieve info endpoint (#153)
* support filtering for retrieve info endpoint

* bump realtimedb-helpers
2023-04-19 07:33:24 -04:00
Anton Voylenko
5b1b50c3a3 remove unnecessary await (#152) 2023-04-18 13:01:38 -04:00
EgleH
ba431aeb35 Fix 403 for SP calling RecentCalls/Alerts via /Accounts route (#149)
* fix 403 for SP calling RecentCalls/Alerts via /Accounts route

* update base image

* update base image

---------

Co-authored-by: eglehelms <e.helms@cognigy.com>
2023-04-12 13:22:40 -04:00
Antony Jukes
36607b505f added retrieve jaeger trace endpoint. (#147) 2023-04-10 13:35:22 -04:00
Dave Horton
616a0b364d push to docker 2023-04-10 09:40:50 -04:00
Dave Horton
1b764b31e6 update statement for sbc_addresses.last_updated 2023-04-06 09:15:11 -04:00
Markus Frindt
009396becc Feature/delay middleware (#146)
* add delay middleware to login and signin routes

* Different delay for sendStatus and json

---------

Co-authored-by: Markus Frindt <m.frindt@cognigy.com>
2023-04-06 08:25:45 -04:00
Dave Horton
84305e30cc add sbc_addresses.last_updated 2023-04-06 07:37:27 -04:00
EgleH
9c7f8b4e7b fix small issues in the code (#145)
Co-authored-by: eglehelms <e.helms@cognigy.com>
2023-04-06 07:31:14 -04:00
EgleH
b2dce18c7a 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>
2023-04-05 14:20:51 -04:00
34 changed files with 1157 additions and 176 deletions

View File

@@ -2,16 +2,8 @@ name: Docker
on:
push:
# Publish `main` as Docker `latest` image.
branches:
- main
# Publish `v1.2.3` tags as releases.
tags:
- v*
env:
IMAGE_NAME: api-server
- '*'
jobs:
push:
@@ -20,32 +12,41 @@ jobs:
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v3
- name: Checkout code
uses: actions/checkout@v3
- name: Build image
run: docker build . --file Dockerfile --tag $IMAGE_NAME
- name: Log into registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Push image
- name: prepare tag
id: prepare_tag
run: |
IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
IMAGE_ID=$GITHUB_REPOSITORY
# Change all uppercase to lowercase
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
# Strip git ref prefix from version
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
# Strip git ref prefix from version
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
# Strip "v" prefix from tag name
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
# Strip "v" prefix from tag name
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
# Use Docker `latest` tag convention
[ "$VERSION" == "main" ] && VERSION=latest
# Use Docker `latest` tag convention
[ "$VERSION" == "main" ] && VERSION=latest
echo IMAGE_ID=$IMAGE_ID
echo VERSION=$VERSION
echo IMAGE_ID=$IMAGE_ID
echo VERSION=$VERSION
echo "image_id=$IMAGE_ID" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
docker push $IMAGE_ID:$VERSION
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.prepare_tag.outputs.image_id }}:${{ steps.prepare_tag.outputs.version }}
build-args: |
GITHUB_REPOSITORY=$GITHUB_REPOSITORY
GITHUB_REF=$GITHUB_REF

View File

@@ -1,4 +1,4 @@
FROM --platform=linux/amd64 node:18.14.1-alpine3.16 as base
FROM --platform=linux/amd64 node:18.15-alpine3.16 as base
RUN apk --update --no-cache add --virtual .builds-deps build-base python3

4
app.js
View File

@@ -33,6 +33,7 @@ const {
retrieveCall,
deleteCall,
listCalls,
listQueues,
purgeCalls,
retrieveSet,
addKey,
@@ -64,6 +65,7 @@ const {
}, logger);
const PORT = process.env.HTTP_PORT || 3000;
const authStrategy = require('./lib/auth')(logger, retrieveKey);
const {delayLoginMiddleware} = require('./lib/middleware');
passport.use(authStrategy);
@@ -74,6 +76,7 @@ app.locals = {
retrieveCall,
deleteCall,
listCalls,
listQueues,
purgeCalls,
retrieveSet,
addKey,
@@ -129,6 +132,7 @@ app.use(nocache());
app.use(passport.initialize());
app.use(cors());
app.use(express.urlencoded({extended: true}));
app.use(delayLoginMiddleware);
app.use(unless(['/stripe'], express.json()));
app.use('/v1', unless(
[

View File

@@ -249,6 +249,7 @@ sbc_address_sid CHAR(36) NOT NULL UNIQUE ,
ipv4 VARCHAR(255) NOT NULL,
port INTEGER NOT NULL DEFAULT 5060,
service_provider_sid CHAR(36),
last_updated DATETIME,
PRIMARY KEY (sbc_address_sid)
);

View File

@@ -2472,7 +2472,7 @@
</location>
<size>
<width>281.00</width>
<height>120.00</height>
<height>140.00</height>
</size>
<zorder>13</zorder>
<SQLField>
@@ -2529,6 +2529,11 @@
<indexed><![CDATA[1]]></indexed>
<uid><![CDATA[6F249D1F-111F-45B4-B76C-8B5E6B9CB43F]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[last_updated]]></name>
<type><![CDATA[DATETIME]]></type>
<uid><![CDATA[CD43B91B-F34E-4422-9C0F-A4B92E2E7B95]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[21]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[F0EE651E-DBF6-4CAC-A517-AC85BCC2D3AF]]></uid>
@@ -2722,9 +2727,9 @@
<SQLEditorFileFormatVersion><![CDATA[4]]></SQLEditorFileFormatVersion>
<uid><![CDATA[58C99A00-06C9-478C-A667-C63842E088F3]]></uid>
<windowHeight><![CDATA[868.000000]]></windowHeight>
<windowLocationX><![CDATA[-1164.000000]]></windowLocationX>
<windowLocationY><![CDATA[1169.000000]]></windowLocationY>
<windowScrollOrigin><![CDATA[{0, 170}]]></windowScrollOrigin>
<windowLocationX><![CDATA[11.000000]]></windowLocationX>
<windowLocationY><![CDATA[54.000000]]></windowLocationY>
<windowScrollOrigin><![CDATA[{461, 0}]]></windowScrollOrigin>
<windowWidth><![CDATA[1512.000000]]></windowWidth>
</SQLDocumentInfo>
<AllowsIndexRenamingOnInsert><![CDATA[1]]></AllowsIndexRenamingOnInsert>

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

View File

@@ -90,6 +90,7 @@ const sql = {
],
8003: [
'ALTER TABLE `voip_carriers` ADD COLUMN `register_status` VARCHAR(4096)',
'ALTER TABLE `sbc_addresses` ADD COLUMN `last_updated` DATETIME',
]
};

View File

@@ -4,6 +4,7 @@ const {
retrieveCall,
deleteCall,
listCalls,
listQueues,
purgeCalls,
retrieveSet,
addKey,
@@ -20,6 +21,7 @@ module.exports = {
retrieveCall,
deleteCall,
listCalls,
listQueues,
purgeCalls,
retrieveSet,
addKey,

32
lib/middleware.js Normal file
View File

@@ -0,0 +1,32 @@
const logger = require('./logger');
function delayLoginMiddleware(req, res, next) {
if (req.path.includes('/login') || req.path.includes('/signin')) {
const min = 200;
const max = 1000;
/* Random delay between 200 - 1000ms */
const sendStatusDelay = Math.floor(Math.random() * (max - min + 1)) + min;
/* the res.json take longer, we decrease the max delay slightly to 0-800ms */
const jsonDelay = Math.floor(Math.random() * 800);
logger.debug(`delayLoginMiddleware: sendStatus ${sendStatusDelay} - json ${jsonDelay}`);
const sendStatus = res.sendStatus;
const json = res.json;
res.sendStatus = function(status) {
setTimeout(() => {
sendStatus.call(res, status);
}, sendStatusDelay);
};
res.json = function(body) {
setTimeout(() => {
json.call(res, body);
}, jsonDelay);
};
}
next();
}
module.exports = {
delayLoginMiddleware
};

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 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 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 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 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 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 validateRequest(req, sid);
await validateCreateCall(logger, sid, req);
updateLastUsed(logger, sid, req).catch((err) => {});
request({
@@ -724,10 +737,17 @@ router.post('/:sid/Calls', async(req, res) => {
*/
router.get('/:sid/Calls', async(req, res) => {
const {logger, listCalls} = req.app.locals;
const {direction, from, to, callStatus} = req.query || {};
try {
const accountSid = parseAccountSid(req);
const calls = await listCalls(accountSid);
await validateRequest(req, accountSid);
const calls = await listCalls({
accountSid,
direction,
from,
to,
callStatus
});
logger.debug(`retrieved ${calls.length} calls for account sid ${accountSid}`);
res.status(200).json(coerceNumbers(snakeCase(calls)));
updateLastUsed(logger, accountSid, req).catch((err) => {});
@@ -744,6 +764,7 @@ router.get('/:sid/Calls/:callSid', async(req, res) => {
try {
const accountSid = parseAccountSid(req);
await validateRequest(req, accountSid);
const callSid = parseCallSid(req);
const callInfo = await retrieveCall(accountSid, callSid);
if (callInfo) {
@@ -768,6 +789,7 @@ router.delete('/:sid/Calls/:callSid', async(req, res) => {
try {
const accountSid = parseAccountSid(req);
await validateRequest(req, accountSid);
const callSid = parseCallSid(req);
const result = await deleteCall(accountSid, callSid);
if (result) {
@@ -792,6 +814,7 @@ const updateCall = async(req, res) => {
try {
const accountSid = parseAccountSid(req);
await validateRequest(req, accountSid);
const callSid = parseCallSid(req);
validateUpdateCall(req.body);
const call = await retrieveCall(accountSid, callSid);
@@ -832,6 +855,8 @@ router.post('/:sid/Messages', async(req, res) => {
try {
const account_sid = parseAccountSid(req);
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);
@@ -865,4 +890,22 @@ router.post('/:sid/Messages', async(req, res) => {
}
});
/**
* retrieve info for a group of queues under an account
*/
router.get('/:sid/Queues', async(req, res) => {
const {logger, listQueues} = req.app.locals;
const { search } = req.query || {};
try {
const accountSid = parseAccountSid(req);
await validateRequest(req, accountSid);
const queues = search ? await listQueues(accountSid, search) : await listQueues(accountSid);
logger.debug(`retrieved ${queues.length} queues for account sid ${accountSid}`);
res.status(200).json(queues);
updateLastUsed(logger, accountSid, req).catch((err) => {});
} catch (err) {
sysError(logger, res, err);
}
});
module.exports = router;

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) {

View File

@@ -46,7 +46,7 @@ function createResetEmailText(link) {
router.post('/', async(req, res) => {
const {logger, addKey} = req.app.locals;
const {email} = req.body;
const {user_sid} = req.user;
let obj;
try {
if (!email || !validateEmail(email)) {
@@ -58,7 +58,9 @@ router.post('/', async(req, res) => {
return res.status(400).json({error: 'email does not exist'});
}
obj = r[0];
if (!obj.acc.is_active) {
if (!obj.user.is_active) {
return res.status(400).json({error: 'you may not reset the password of an inactive user'});
} else if (obj.acc.account_sid !== null && !obj.acc.is_active) {
return res.status(400).json({error: 'you may not reset the password of an inactive account'});
}
res.sendStatus(204);
@@ -81,7 +83,7 @@ router.post('/', async(req, res) => {
emailSimpleText(logger, email, 'Reset password request', createResetEmailText(link));
}
const redisKey = cacheClient.generateRedisKey('jwt', user_sid, 'v2');
const redisKey = cacheClient.generateRedisKey('jwt', obj.user.user_sid, 'v2');
await cacheClient.delete(redisKey);
});

View File

@@ -1,8 +1,10 @@
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 Account = require('../../models/account');
const decorate = require('./decorate');
const {promisePool} = require('../../db');
const {e164} = require('../../utils/phone-number-utils');
const preconditions = {
'add': validateAdd,
@@ -21,6 +23,10 @@ async function validateAdd(req) {
req.body.account_sid = req.user.account_sid;
}
if (req.user.hasServiceProviderAuth) {
req.body.service_provider_sid = req.user.service_provider_sid;
}
if (!req.body.number) throw new DbErrorBadRequest('number is required');
const formattedNumber = e164(req.body.number);
req.body.number = formattedNumber;
@@ -42,11 +48,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,10 +64,23 @@ 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');
}
}
if (req.user.hasServiceProviderAuth) {
let service_provider_sid;
if (!phoneNumber[0].service_provider_sid) {
const [r] = await Account.retrieve(phoneNumber[0].account_sid);
service_provider_sid = r.service_provider_sid;
} else {
service_provider_sid = phoneNumber[0].service_provider_sid;
}
if (phoneNumber && phoneNumber.length && service_provider_sid !== req.user.service_provider_sid) {
throw new DbErrorForbidden('insufficient privileges');
}
}
// TODO: if we are assigning to an account, verify it exists
// TODO: if we are assigning to an application, verify it is associated to the same account
@@ -75,7 +94,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 +111,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) {

View File

@@ -2,6 +2,8 @@ const router = require('express').Router();
const sysError = require('../error');
const {DbErrorBadRequest} = require('../../utils/errors');
const {getHomerApiKey, getHomerSipTrace, getHomerPcap} = require('../../utils/homer-utils');
const {getJaegerTrace} = require('../../utils/jaeger-utils');
const parseAccountSid = (url) => {
const arr = /Accounts\/([^\/]*)/.exec(url);
if (arr) return arr[1];
@@ -93,4 +95,20 @@ router.get('/:call_id/pcap', async(req, res) => {
}
});
router.get('/trace/:trace_id', async(req, res) => {
const {logger} = req.app.locals;
const {trace_id} = req.params;
try {
const obj = await getJaegerTrace(logger, trace_id);
if (!obj) {
logger.info(`/RecentCalls: unable to get spans from jaeger for ${trace_id}`);
return res.sendStatus(404);
}
res.status(200).json(obj.result);
} catch (err) {
logger.error({err}, `/RecentCalls error retrieving jaeger trace ${trace_id}`);
res.sendStatus(500);
}
});
module.exports = router;

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);

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();

View File

@@ -6,8 +6,14 @@ 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 validateRequest = (req) => {
const {email, password} = req.body || {};
/* check required properties are there */
@@ -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,

View File

@@ -1,11 +1,46 @@
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 (!voip_carrier_sid) {
throw new DbErrorBadRequest('missing voip_carrier_sid');
}
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 +52,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 +68,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'});

View File

@@ -1,11 +1,38 @@
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 (!voip_carrier_sid) {
throw new DbErrorBadRequest('missing voip_carrier_sid');
}
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 +44,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 +61,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'});

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 = {

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);
}

View File

@@ -218,20 +218,59 @@ 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);
const account_sid = parseAccountSid(req);
if (service_provider_sid) {
if (service_provider_sid === req.user.service_provider_sid) {
return next();
}
}
if (account_sid) {
const [r] = await Account.retrieve(account_sid);
if (r && r.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 +444,8 @@ module.exports = {
parseSpeechCredentialSid,
parseVoipCarrierSid,
parseWebhookSid,
parseSipGatewaySid,
parseUserSid,
hasAccountPermissions,
hasServiceProviderPermissions,
checkLimits,

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) {

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) {

View File

@@ -3793,6 +3793,43 @@ paths:
required: true
schema:
type: string
- in: query
name: direction
required: false
schema:
type: string
enum:
- inbound
- outbound
description: call direction to retrieve
- in: query
name: from
required: false
schema:
type: string
description: calling number to retrieve
- in: query
name: to
required: false
schema:
type: string
description: called number to retrieve
- in: query
name: callStatus
required: false
schema:
type: string
enum:
- trying
- ringing
- early-media
- in-progress
- completed
- failed
- busy
- no-answer
- queued
description: call status to retrieve
responses:
200:
description: list of calls for a specified account
@@ -4001,7 +4038,46 @@ paths:
smpp_err_code:
type: string
400:
description: bad request
description: bad request
/Accounts/{AccountSid}/Queues:
parameters:
- name: AccountSid
in: path
required: true
schema:
type: string
format: uuid
- in: query
name: search
required: false
schema:
type: string
description: queue name of data to retrieve
get:
tags:
- Accounts
summary: retrieve active queues for an account
operationId: listQueues
responses:
200:
description: retrieve active queues records for a specified account
content:
application/json:
schema:
type: array
items:
type: object
properties:
name:
type: string
length:
type: string
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
components:
securitySchemes:
bearerAuth:

18
lib/utils/jaeger-utils.js Normal file
View File

@@ -0,0 +1,18 @@
const bent = require('bent');
const getJSON = bent(process.env.JAEGER_BASE_URL || 'http://127.0.0.1', 'GET', 'json', 200);
const getJaegerTrace = async(logger, traceId) => {
if (!process.env.JAEGER_BASE_URL) {
logger.debug('getJaegerTrace: jaeger integration not installed');
return null;
}
try {
return await getJSON(`/api/v3/traces/${traceId}`);
} catch (err) {
logger.error({err}, `getJaegerTrace: Error retrieving spans for traceId ${traceId}`);
}
};
module.exports = {
getJaegerTrace
};

14
package-lock.json generated
View File

@@ -13,7 +13,7 @@
"@deepgram/sdk": "^1.10.2",
"@google-cloud/speech": "^5.1.0",
"@jambonz/db-helpers": "^0.7.3",
"@jambonz/realtimedb-helpers": "^0.7.0",
"@jambonz/realtimedb-helpers": "^0.7.1",
"@jambonz/speech-utils": "^0.0.8",
"@jambonz/time-series": "^0.2.5",
"@jambonz/verb-specifications": "^0.0.3",
@@ -1710,9 +1710,9 @@
}
},
"node_modules/@jambonz/realtimedb-helpers": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@jambonz/realtimedb-helpers/-/realtimedb-helpers-0.7.0.tgz",
"integrity": "sha512-hu+CcV0jzQU6J4vW8bBEz52NqEroLHOB0wjVb/VmSL4N0+MAJ1wbPUNk8go5aMk809Vr/Od2z9nHInGnTiBHzA==",
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/@jambonz/realtimedb-helpers/-/realtimedb-helpers-0.7.1.tgz",
"integrity": "sha512-eL4bJG/b/CN7tuxWcmsHjbSEtcDzBIBw3Wzg8+mL6sXHl0bmCLhPu4xdRBkf+UFu3dPCzaRxBQfyGCB3z6aCXw==",
"dependencies": {
"@jambonz/promisify-redis": "^0.0.6",
"debug": "^4.3.4",
@@ -10224,9 +10224,9 @@
}
},
"@jambonz/realtimedb-helpers": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@jambonz/realtimedb-helpers/-/realtimedb-helpers-0.7.0.tgz",
"integrity": "sha512-hu+CcV0jzQU6J4vW8bBEz52NqEroLHOB0wjVb/VmSL4N0+MAJ1wbPUNk8go5aMk809Vr/Od2z9nHInGnTiBHzA==",
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/@jambonz/realtimedb-helpers/-/realtimedb-helpers-0.7.1.tgz",
"integrity": "sha512-eL4bJG/b/CN7tuxWcmsHjbSEtcDzBIBw3Wzg8+mL6sXHl0bmCLhPu4xdRBkf+UFu3dPCzaRxBQfyGCB3z6aCXw==",
"requires": {
"@jambonz/promisify-redis": "^0.0.6",
"debug": "^4.3.4",

View File

@@ -23,7 +23,7 @@
"@deepgram/sdk": "^1.10.2",
"@google-cloud/speech": "^5.1.0",
"@jambonz/db-helpers": "^0.7.3",
"@jambonz/realtimedb-helpers": "^0.7.0",
"@jambonz/realtimedb-helpers": "^0.7.1",
"@jambonz/speech-utils": "^0.0.8",
"@jambonz/time-series": "^0.2.5",
"@jambonz/verb-specifications": "^0.0.3",

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'
});
@@ -9,6 +13,12 @@ const {
createServiceProvider,
createPhoneNumber,
deleteObjectBySid} = require('./utils');
const logger = require('../lib/logger');
const { pushBack } = require('@jambonz/realtimedb-helpers')({
host: process.env.JAMBONES_REDIS_HOST,
port: process.env.JAMBONES_REDIS_PORT || 6379
}, logger);
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
@@ -257,6 +267,31 @@ test('account tests', async(t) => {
});
t.ok(result.statusCode === 204, 'successfully deleted a call session limit for an account');
/* query account queues */
await pushBack(`queue:${sid}:test`, 'url1');
await pushBack(`queue:${sid}:dummy`, 'url2');
result = await request.get(`/Accounts/${sid}/Queues`, {
auth: authAdmin,
resolveWithFullResponse: true,
json: true,
});
t.ok(result.statusCode === 200 && result.body.length === 2, 'successfully queried account queues info for an account');
result = await request.get(`/Accounts/${sid}/Queues?search=test`, {
auth: authAdmin,
resolveWithFullResponse: true,
json: true,
});
t.ok(result.statusCode === 200 && result.body.length === 1, 'successfully queried account queue info with search for an account');
result = await request.get(`/Accounts/29d41725-9d3a-4f89-9f0b-f32b3e4d3159/Queues`, {
auth: authAdmin,
resolveWithFullResponse: true,
json: true,
});
t.ok(result.statusCode === 200 && result.body.length === 0, 'successfully queried account queue info with for an invalid account');
/* delete account */
result = await request.delete(`/Accounts/${sid}`, {
auth: authAdmin,

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', {

325
test/forgot-password.js Normal file
View File

@@ -0,0 +1,325 @@
const test = require('tape');
const request = require("request-promise-native").defaults({
baseUrl: "http://127.0.0.1:3000/v1",
});
let authAdmin;
let admin_user_sid;
let sp_sid;
let sp_user_sid;
let account_sid;
let account_sid2;
let account_user_sid;
let account_user_sid2;
const password = "12345foobar";
const adminEmail = "joe@foo.bar";
const emailInactiveAccount = 'inactive-account@example.com';
const emailInactiveUser = 'inactive-user@example.com';
test('forgot password - prepare', async (t) => {
/* login as admin to get a jwt */
let 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"
);
authAdmin = { bearer: result.body.token };
admin_user_sid = result.body.user_sid;
/* add a service provider */
result = await request.post("/ServiceProviders", {
resolveWithFullResponse: true,
auth: authAdmin,
json: true,
body: {
name: "sp" + Date.now(),
},
});
t.ok(result.statusCode === 201, "successfully created service provider");
sp_sid = result.body.sid;
/* add service_provider user */
const randomNumber = Math.floor(Math.random() * 101);
result = await request.post(`/Users`, {
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
body: {
name: "service_provider" + Date.now(),
email: `sp${randomNumber}@example.com`,
is_active: true,
force_change: true,
initial_password: password,
service_provider_sid: sp_sid,
},
});
t.ok(
result.statusCode === 201 && result.body.user_sid,
"service_provider scope user created"
);
sp_user_sid = result.body.user_sid;
/* add an account - inactive */
result = await request.post("/Accounts", {
resolveWithFullResponse: true,
auth: authAdmin,
json: true,
body: {
name: "sample_account inactive" + Date.now(),
service_provider_sid: sp_sid,
registration_hook: {
url: "http://example.com/reg",
method: "get",
},
is_active: false,
webhook_secret: "foobar",
},
});
t.ok(result.statusCode === 201, "successfully created account");
account_sid = result.body.sid;
/* add an account - inactive */
result = await request.post("/Accounts", {
resolveWithFullResponse: true,
auth: authAdmin,
json: true,
body: {
name: "sample_account active" + Date.now(),
service_provider_sid: sp_sid,
registration_hook: {
url: "http://example.com/reg",
method: "get",
},
is_active: true,
webhook_secret: "foobar",
},
});
t.ok(result.statusCode === 201, "successfully created account");
account_sid2 = result.body.sid;
/* add account user connected to an inactive account */
result = await request.post(`/Users`, {
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
body: {
name: "account user active - inactive account" + randomNumber,
email: emailInactiveAccount,
is_active: true,
force_change: true,
initial_password: password,
service_provider_sid: sp_sid,
account_sid: account_sid,
},
});
t.ok(
result.statusCode === 201 && result.body.user_sid,
"account scope user created"
);
account_user_sid = result.body.user_sid;
/* add account user that is not active */
result = await request.post(`/Users`, {
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
body: {
name: "account user inactive - active account" + randomNumber,
email: emailInactiveUser,
is_active: false,
force_change: true,
initial_password: password,
service_provider_sid: sp_sid,
account_sid: account_sid2,
},
});
t.ok(
result.statusCode === 201 && result.body.user_sid,
"account scope user created"
);
account_user_sid2 = result.body.user_sid;
});
test('forgot password with valid email', async (t) => {
const res = await request
.post('/forgot-password',
{
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
body: { email: adminEmail }
});
t.equal(res.statusCode, 204, 'returns 204 status code');
t.end();
});
test('forgot password with invalid email', async (t) => {
const statusCode = 400;
const errorMessage = 'invalid or missing email';
const email = 'invalid-email';
try {
await request
.post('/forgot-password', {
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
body: { email }
});
} catch (error) {
t.throws(
() => {
throw error;
},
{
name: "StatusCodeError",
statusCode,
message: `${statusCode} - {"error":"${errorMessage}"}`,
}
);
}
t.end();
});
test('forgot password with non-existent email', async (t) => {
const statusCode = 400;
const errorMessage = 'email does not exist';
const email = 'non-existent-email@example.com';
try {
await request
.post('/forgot-password', {
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
body: { email }
});
} catch (error) {
t.throws(
() => {
throw error;
},
{
name: "StatusCodeError",
statusCode,
message: `${statusCode} - {"error":"${errorMessage}"}`,
}
);
}
t.end();
});
test('forgot password with inactive user', async (t) => {
const statusCode = 400;
const errorMessage = 'you may not reset the password of an inactive user';
try {
await request
.post('/forgot-password', {
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
body: { email: emailInactiveUser }
});
} catch (error) {
t.throws(
() => {
throw error;
},
{
name: "StatusCodeError",
statusCode,
message: `${statusCode} - {"error":"${errorMessage}"}`,
}
);
}
t.end();
});
test('forgot password with inactive account', async (t) => {
const statusCode = 400;
const errorMessage = 'you may not reset the password of an inactive account';
try {
await request
.post('/forgot-password', {
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
body: { email: emailInactiveAccount }
});
} catch (error) {
t.throws(
() => {
throw error;
},
{
name: "StatusCodeError",
statusCode,
message: `${statusCode} - {"error":"${errorMessage}"}`,
}
);
}
t.end();
});
test('cleanup', async (t) => {
/* login as admin to get a jwt */
let 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"
);
authAdmin = { bearer: result.body.token };
/* list users */
result = await request.get(`/Users`, {
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
});
const users = result.body;
/* delete all users except admin */
for (const user of users) {
if (user.user_sid === admin_user_sid) continue;
result = await request.delete(`/Users/${user.user_sid}`, {
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
});
t.ok(result.statusCode === 204, "user deleted");
}
/* list accounts */
result = await request.get(`/Accounts`, {
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
});
const accounts = result.body;
for (const acc of accounts) {
result = await request.delete(`/Accounts/${acc.account_sid}`, {
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
});
t.ok(result.statusCode === 204, "acc deleted");
}
});

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,

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`, {

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}`, {