Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion config/custom-environment-variables.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,11 @@
"EMBED_REPORTS_MAPPING": "EMBED_REPORTS_MAPPING",
"ALLOWED_USERS": "REPORTS_ALLOWED_USERS"
},
"DEFAULT_M2M_USERID": "DEFAULT_M2M_USERID"
"DEFAULT_M2M_USERID": "DEFAULT_M2M_USERID",
"salesforce": {
"CLIENT_AUDIENCE": "SALESFORCE_AUDIENCE",
"CLIENT_KEY": "SALESFORCE_CLIENT_KEY",
"SUBJECT": "SALESFORCE_SUBJECT",
"CLIENT_ID": "SALESFORCE_CLIENT_ID"
}
}
8 changes: 7 additions & 1 deletion config/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,11 @@
"ALLOWED_USERS": "[]"
},
"DEFAULT_M2M_USERID": -101,
"taasJobApiUrl": "https://api.topcoder.com/v5/jobs"
"taasJobApiUrl": "https://api.topcoder.com/v5/jobs",
"salesforce": {
"CLIENT_KEY": "",
"CLIENT_AUDIENCE": "",
"SUBJECT": "",
"CLIENT_ID": ""
}
}
1 change: 1 addition & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ export const M2M_SCOPES = {
ALL: 'all:projects',
READ: 'read:projects',
WRITE: 'write:projects',
READ_BILLING_ACCOUNTS: 'read:user-billing-accounts',
WRITE_BILLING_ACCOUNTS: 'write:projects-billing-accounts',
},
PROJECT_MEMBERS: {
Expand Down
22 changes: 22 additions & 0 deletions src/permissions/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,15 @@ const SCOPES_PROJECTS_WRITE = [
M2M_SCOPES.PROJECTS.WRITE,
];

/**
* M2M scopes to "read" available Billing Accounts for the project
*/
const SCOPES_PROJECTS_READ_AVL_BILLING_ACCOUNTS = [
M2M_SCOPES.CONNECT_PROJECT_ADMIN,
M2M_SCOPES.READ_BILLING_ACCOUNTS,
M2M_SCOPES.PROJECTS.ALL,
];

/**
* M2M scopes to "write" billingAccountId property
*/
Expand Down Expand Up @@ -252,6 +261,19 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export
scopes: SCOPES_PROJECTS_WRITE,
},

/*
* Project Invite
*/
READ_AVL_PROJECT_BILLING_ACCOUNTS: {
meta: {
title: 'Read Available Project Billing Accounts',
group: 'Project Billing Accounts',
description: 'Who can view the Billing Accounts available for the project',
},
topcoderRoles: ALL,
scopes: SCOPES_PROJECTS_READ_AVL_BILLING_ACCOUNTS,
},

/*
* Project Member
*/
Expand Down
4 changes: 4 additions & 0 deletions src/permissions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ module.exports = () => {
Authorizer.setPolicy('project.edit', generalPermission(PERMISSION.UPDATE_PROJECT));
Authorizer.setPolicy('project.delete', generalPermission(PERMISSION.DELETE_PROJECT));

Authorizer.setPolicy('projectBillingAccounts.view', generalPermission([
PERMISSION.READ_AVL_PROJECT_BILLING_ACCOUNTS,
]));

Authorizer.setPolicy('projectMember.create', generalPermission([
PERMISSION.CREATE_PROJECT_MEMBER_OWN,
PERMISSION.CREATE_PROJECT_MEMBER_NOT_OWN,
Expand Down
39 changes: 39 additions & 0 deletions src/routes/billingAccounts/list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// import _ from 'lodash';
import validate from 'express-validation';
import Joi from 'joi';
import { middleware as tcMiddleware } from 'tc-core-library-js';
import SalesforceService from '../../services/salesforceService';

/**
* API to get project attachments.
*
*/

const permissions = tcMiddleware.permissions;

const schema = {
params: {
projectId: Joi.number().integer().positive().required(),
},
};

module.exports = [
validate(schema),
permissions('projectBillingAccounts.view'),
async (req, res, next) => {
// const projectId = _.parseInt(req.params.projectId);
const userId = req.authUser.userId;
try {
const { accessToken, instanceUrl } = await SalesforceService.authenticate();
// eslint-disable-next-line
const sql = `SELECT Topcoder_Billing_Account__r.id, Topcoder_Billing_Account__r.TopCoder_Billing_Account_Id__c, Topcoder_Billing_Account__r.Billing_Account_Name__c, Topcoder_Billing_Account__r.Start_Date__c, Topcoder_Billing_Account__r.End_Date__c from Topcoder_Billing_Account_Resource__c tbar where UserID__c='${userId}'`;
// and Topcoder_Billing_Account__r.TC_Connect_Project_ID__c='${projectId}'
req.log.debug(sql);
const billingAccounts = await SalesforceService.query(sql, accessToken, instanceUrl, req.log);
res.json(billingAccounts);
} catch (error) {
req.log.error(error);
next(error);
}
},
];
133 changes: 133 additions & 0 deletions src/routes/billingAccounts/list.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/* eslint-disable no-unused-expressions */
// import chai from 'chai';
import request from 'supertest';
import sinon from 'sinon';

import models from '../../models';
import server from '../../app';
import testUtil from '../../tests/util';
import SalesforceService from '../../services/salesforceService';

// const should = chai.should();

describe('Project Billing Accounts list', () => {
let project1;
let salesforceAuthenticate;
let salesforceQuery;

beforeEach((done) => {
testUtil.clearDb()
.then(() => testUtil.clearES())
.then(() => {
models.Project.create({
type: 'generic',
directProjectId: 1,
billingAccountId: 1,
name: 'test1',
description: 'test project1',
status: 'draft',
details: {},
createdBy: 1,
updatedBy: 1,
lastActivityAt: 1,
lastActivityUserId: '1',
}).then((p) => {
project1 = p;
// create members
return models.ProjectMember.create({
userId: testUtil.userIds.copilot,
projectId: project1.id,
role: 'copilot',
isPrimary: true,
createdBy: 1,
updatedBy: 1,
}).then(() => models.ProjectMember.create({
userId: testUtil.userIds.member,
projectId: project1.id,
role: 'customer',
isPrimary: false,
createdBy: 1,
updatedBy: 1,
}));
}).then(() => {
salesforceAuthenticate = sinon.stub(SalesforceService, 'authenticate', () => Promise.resolve({
accessToken: 'mock',
instanceUrl: 'mock_url',
}));
salesforceQuery = sinon.stub(SalesforceService, 'query', () => Promise.resolve([{
accessToken: 'mock',
instanceUrl: 'mock_url',
}]));
done();
});
});
});

afterEach((done) => {
salesforceAuthenticate.restore();
salesforceQuery.restore();
done();
});

after((done) => {
testUtil.clearDb(done);
});

describe('List /projects/{id}/billingAccounts', () => {
it('should return 403 for anonymous user', (done) => {
request(server)
.get(`/v5/projects/${project1.id}/billingAccounts`)
.expect(403, done);
});

it('should return 403 for a regular user who is not a member of the project', (done) => {
request(server)
.get(`/v5/projects/${project1.id}/billingAccounts`)
.set({
Authorization: `Bearer ${testUtil.jwts.member2}`,
})
.send()
.expect(403, done);
});

it('should return all attachments to admin', (done) => {
request(server)
.get(`/v5/projects/${project1.id}/billingAccounts`)
.set({
Authorization: `Bearer ${testUtil.jwts.admin}`,
})
.send()
.expect(200)
.end((err, res) => {
if (err) {
done(err);
} else {
const resJson = res.body;
resJson.should.have.length(2);
// TODO verify BA fields
done();
}
});
});

xit('should return all attachments using M2M token with "read:user-billing-accounts" scope', (done) => {
request(server)
.get(`/v5/projects/${project1.id}/billingAccounts`)
.set({
Authorization: `Bearer ${testUtil.m2m['read:user-billing-accounts']}`,
})
.send()
.expect(200)
.end((err, res) => {
if (err) {
done(err);
} else {
const resJson = res.body;
resJson.should.have.length(2);
// TODO verify BA fields
done();
}
});
});
});
});
3 changes: 3 additions & 0 deletions src/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ router.route('/v5/projects/:projectId(\\d+)/scopeChangeRequests/:requestId(\\d+)
.patch(require('./scopeChangeRequests/update'));
// .delete(require('./scopeChangeRequests/delete'));

router.route('/v5/projects/:projectId(\\d+)/billingAccounts')
.get(require('./billingAccounts/list'));

router.route('/v5/projects/:projectId(\\d+)/members')
.get(require('./projectMembers/list'))
.post(require('./projectMembers/create'));
Expand Down
77 changes: 77 additions & 0 deletions src/services/salesforceService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* Represents the Salesforce service
*/
import _ from 'lodash';
import config from 'config';
import jwt from 'jsonwebtoken';

const axios = require('axios');

const loginBaseUrl = config.salesforce.CLIENT_AUDIENCE || 'https://login.salesforce.com';
// we are using dummy private key to fail safe when key is not provided in env
let privateKey = config.salesforce.CLIENT_KEY || 'privateKey';
privateKey = privateKey.replace(/\\n/g, '\n');

const urlEncodeForm = k =>
Object.keys(k).reduce((a, b) => `${a}&${b}=${encodeURIComponent(k[b])}`, '');

/**
* Helper class to abstract salesforce API calls
*/
class SalesforceService {
/**
* Authenticate to Salesforce with pre-configured credentials
* @returns {{accessToken: String, instanceUrl: String}} the result
*/
static authenticate() {
const jwtToken = jwt.sign({}, privateKey, {
expiresIn: '1h', // any expiration
issuer: config.salesforce.CLIENT_ID,
audience: config.salesforce.CLIENT_AUDIENCE,
subject: config.salesforce.SUBJECT,
algorithm: 'RS256',
});
return axios({
method: 'post',
url: `${loginBaseUrl}/services/oauth2/token`,
data: urlEncodeForm({
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
assertion: jwtToken,
}),
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
}).then(res => ({
accessToken: res.data.access_token,
instanceUrl: res.data.instance_url,
}));
}

/**
* Run the query statement
* @param {String} sql the Saleforce sql statement
* @param {String} accessToken the access token
* @param {String} instanceUrl the salesforce instance url
* @param {Object} logger logger to be used for logging
* @returns {{totalSize: Number, done: Boolean, records: Array}} the result
*/
static query(sql, accessToken, instanceUrl, logger) {
return axios({
url: `${instanceUrl}/services/data/v37.0/query?q=${sql}`,
method: 'get',
headers: { authorization: `Bearer ${accessToken}` },
}).then((res) => {
if (logger) {
logger.debug(_.get(res, 'data.records', []));
}
const billingAccounts = _.get(res, 'data.records', []).map(o => ({
sfBillingAccountId: _.get(o, 'Topcoder_Billing_Account__r.Id'),
tcBillingAccountId: _.get(o, 'Topcoder_Billing_Account__r.TopCoder_Billing_Account_Id__c'),
name: _.get(o, 'Topcoder_Billing_Account__r.Billing_Account_Name__c'),
startDate: _.get(o, 'Topcoder_Billing_Account__r.Start_Date__c'),
endDate: _.get(o, 'Topcoder_Billing_Account__r.End_Date__c'),
}));
return billingAccounts;
});
}
}

export default new SalesforceService();
2 changes: 2 additions & 0 deletions src/tests/util.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.