Skip to content

Commit 9c9d95f

Browse files
authored
add dialog for creating branches
Feat: Add dialog for creating branches and indicators for protected branches
2 parents 08f4bd5 + a80e91c commit 9c9d95f

33 files changed

+986
-337
lines changed

td.server/src/config/routes.config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ const routes = (router) => {
4242
router.get('/api/threatmodel/:organisation/:repo/:branch/models', threatmodelController.models);
4343
router.get('/api/threatmodel/:organisation/:repo/:branch/:model/data', threatmodelController.model);
4444

45+
router.post('/api/threatmodel/:organisation/:repo/:branch/createBranch', threatmodelController.createBranch);
46+
4547
// removed because of security denial of service concerns (denial of models)
4648
//router.delete('/api/threatmodel/:organisation/:repo/:branch/:model', threatmodelController.deleteModel);
4749

td.server/src/controllers/threatmodelcontroller.js

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,11 @@ const branches = (req, res) => responseWrapper.sendResponseAsync(async () => {
5454
const headers = branchesResp[1];
5555
const pageLinks = branchesResp[2];
5656

57-
const branchNames = branches.map((x) => x.name);
57+
const branchNames = branches.map((x) => ({
58+
name: x.name,
59+
// Protected branches are not so easy to determine from the API on Bitbucket
60+
protected: x.protected||false
61+
}));
5862

5963
const pagination = getPagination(headers, pageLinks, repoInfo.page);
6064

@@ -212,6 +216,26 @@ const organisation = (req, res) => {
212216
return res.status(200).send(organisation);
213217
};
214218

219+
const createBranch = async (req, res) => {
220+
const repository = repositories.get();
221+
222+
const branchInfo = {
223+
organisation: req.params.organisation,
224+
repo: req.params.repo,
225+
branch: req.params.branch,
226+
ref: req.body.refBranch
227+
};
228+
logger.debug(`API createBranch request: ${logger.transformToString(req)}`);
229+
230+
try {
231+
const createBranchResp = await repository.createBranchAsync(branchInfo, req.provider.access_token);
232+
return res.status(201).send(createBranchResp);
233+
} catch (err) {
234+
logger.error(err);
235+
return serverError('Error creating branch', res, logger);
236+
}
237+
};
238+
215239
export default {
216240
branches,
217241
create,
@@ -220,5 +244,6 @@ export default {
220244
models,
221245
organisation,
222246
repos,
223-
update
247+
update,
248+
createBranch
224249
};

td.server/src/repositories/bitbucketrepo.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,10 +154,28 @@ export const deleteAsync = async (modelInfo, accessToken) => {
154154
throw new Error(`Bitbucket deleteAsync is not implemented yet`);
155155
};
156156

157+
export const createBranchAsync = (repoInfo, accessToken) => {
158+
const workspace = env.get().config.BITBUCKET_WORKSPACE;
159+
160+
const client = getClient(accessToken);
161+
const repo = getRepoFullName(repoInfo);
162+
return client.refs.createBranch({
163+
_body: {
164+
name: repoInfo.branch,
165+
target: {
166+
hash: repoInfo.ref
167+
}
168+
},
169+
repo_slug: repo,
170+
workspace: workspace
171+
});
172+
};
173+
157174
const getRepoFullName = (info) => `${info.repo}`;
158175
const getModelPath = (modelInfo) => `${repoRootDirectory()}/${modelInfo.model}/${modelInfo.model}.json`;
159176
const getModelContent = (modelInfo) => JSON.stringify(modelInfo.body, null, ' ');
160177

178+
161179
export default {
162180
branchesAsync,
163181
createAsync,
@@ -168,5 +186,6 @@ export default {
168186
searchAsync,
169187
updateAsync,
170188
userAsync,
171-
getClient
189+
getClient,
190+
createBranchAsync
172191
};

td.server/src/repositories/githubrepo.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,14 @@ const deleteAsync = async (modelInfo, accessToken) => {
8181
);
8282
};
8383

84+
const createBranchAsync = async (repoInfo, accessToken) => {
85+
const client = getClient(accessToken);
86+
const repo = getRepoFullName(repoInfo);
87+
const resp = await client.repo(repo).refAsync(`heads/${repoInfo.ref}`);
88+
const sha = resp[0].object.sha;
89+
return client.repo(repo).createRefAsync(`refs/heads/${repoInfo.branch}`, sha);
90+
};
91+
8492
const getRepoFullName = (info) => `${info.organisation}/${info.repo}`;
8593
const getModelPath = (modelInfo) => `${repoRootDirectory()}/${modelInfo.model}/${modelInfo.model}.json`;
8694
const getModelContent = (modelInfo) => JSON.stringify(modelInfo.body, null, ' ');
@@ -94,5 +102,6 @@ export default {
94102
reposAsync,
95103
searchAsync,
96104
updateAsync,
97-
userAsync
105+
userAsync,
106+
createBranchAsync
98107
};

td.server/src/repositories/gitlabrepo.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,18 @@ export const deleteAsync = (modelInfo, accessToken) => getClient(accessToken).Re
100100
'Deleted by OWASP Threat Dragon',
101101
);
102102

103+
export const createBranchAsync = (repoInfo, accessToken) => {
104+
const client = getClient(accessToken);
105+
const repo = getRepoFullName(repoInfo);
106+
return client.Branches.create(repo, repoInfo.branch, repoInfo.ref);
107+
};
108+
103109
const getRepoFullName = (info) => `${info.organisation}/${info.repo}`;
104110
const getModelPath = (modelInfo) => `${repoRootDirectory()}/${modelInfo.model}/${modelInfo.model}.json`;
105111
const getModelContent = (modelInfo) => JSON.stringify(modelInfo.body, null, ' ');
106112

107113
export default {
114+
createBranchAsync,
108115
branchesAsync,
109116
createAsync,
110117
deleteAsync,
@@ -113,5 +120,5 @@ export default {
113120
reposAsync,
114121
searchAsync,
115122
updateAsync,
116-
userAsync
123+
userAsync,
117124
};

td.server/test/repositories/bitbucketrepo.spec.js

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {getClient, userAsync} from '../../src/repositories/bitbucketrepo.js';
99

1010
describe('repositories/bitbucketrepo.js', () => {
1111
const workspace = 'threat-workspace';
12-
const repoPath = 'ThreatDragonModels'
12+
const repoPath = 'ThreatDragonModels';
1313

1414
const info = {
1515
body: {
@@ -19,6 +19,8 @@ describe('repositories/bitbucketrepo.js', () => {
1919
branch: 'testBranch',
2020
organisation: 'test org',
2121
page: 'testPage',
22+
repo: 'test repo',
23+
ref: 'testRef',
2224
listBranches: {
2325
workspace: workspace,
2426
repo_slug: 'repoInfo.repo',
@@ -68,7 +70,8 @@ describe('repositories/bitbucketrepo.js', () => {
6870
values: [
6971
{full_name: 'Threat-Workspace/Repo1'},
7072
{full_name: 'Threat-Workspace/Repo2'},
71-
{full_name: 'Threat-Workspace/Repo3'},]
73+
{full_name: 'Threat-Workspace/Repo3'},
74+
]
7275
}
7376
})),
7477
getBranch: sinon.stub().returns(Promise.resolve({data: {target: {hash: info.readInfo.commit}}})),
@@ -82,6 +85,9 @@ describe('repositories/bitbucketrepo.js', () => {
8285
users: {
8386
getAuthedUser: sinon.stub().returns(Promise.resolve({data: {id: 1, username: 'Test User'}})),
8487
},
88+
refs:{
89+
createBranch: sinon.stub().returns(Promise.resolve({data: {values: []}})),
90+
}
8591
};
8692

8793
const clientOptions = {
@@ -124,7 +130,7 @@ describe('repositories/bitbucketrepo.js', () => {
124130
});
125131

126132
it('creates the bitbucket client', () => {
127-
threatModelRepository.userAsync(accessToken)
133+
threatModelRepository.userAsync(accessToken);
128134
sinon.stub(env, 'get').returns({config: {BITBUCKET_WORKSPACE: workspace}});
129135
expect(BitbucketClientWrapper.getClient).to.have.been.calledWith(clientOptions);
130136

@@ -146,7 +152,7 @@ describe('repositories/bitbucketrepo.js', () => {
146152
});
147153

148154
// Setup the transformed data
149-
let transformedReposData = [
155+
const transformedReposData = [
150156
{full_name: 'repo1'},
151157
{full_name: 'repo2'},
152158
{full_name: 'repo3'},
@@ -163,7 +169,7 @@ describe('repositories/bitbucketrepo.js', () => {
163169
});
164170

165171
describe('branchesAsync', () => {
166-
let repoInfo = {page: info.listBranches.page, repo: info.listBranches.repo_slug}
172+
const repoInfo = {page: info.listBranches.page, repo: info.listBranches.repo_slug};
167173

168174
beforeEach(async () => {
169175
sinon.stub(env, 'get').returns({config: {BITBUCKET_WORKSPACE: workspace}});
@@ -181,13 +187,15 @@ describe('repositories/bitbucketrepo.js', () => {
181187
});
182188

183189
describe('modelsAsync', () => {
184-
let branchInfo = {branch: info.branchInfo.name, repo: info.branchInfo.repo_slug};
190+
const branchInfo = {branch: info.branchInfo.name, repo: info.branchInfo.repo_slug};
185191

186192
beforeEach(async () => {
187193
sinon.stub(env, 'get').returns({config: {BITBUCKET_WORKSPACE: workspace, BITBUCKET_REPO_ROOT_DIRECTORY: repoPath}});
188-
sinon.stub(mockClient.source, 'read').returns(Promise.resolve({data: {values: [{path: 'ThreatDragonModels/model1'},
194+
sinon.stub(mockClient.source, 'read').returns(Promise.resolve({data: {values: [
195+
{path: 'ThreatDragonModels/model1'},
189196
{path: 'ThreatDragonModels/model2'},
190-
{path: 'ThreatDragonModels/model3'}]}}));
197+
{path: 'ThreatDragonModels/model3'}
198+
]}}));
191199
await threatModelRepository.modelsAsync(branchInfo, accessToken);
192200
});
193201

@@ -222,7 +230,7 @@ describe('repositories/bitbucketrepo.js', () => {
222230
});
223231

224232
describe('modelAsync', () => {
225-
let modelInfo = {branch: info.modelInfo.name, repo: info.modelInfo.repo_slug, model: info.modelInfo.model};
233+
const modelInfo = {branch: info.modelInfo.name, repo: info.modelInfo.repo_slug, model: info.modelInfo.model};
226234

227235
beforeEach(async () => {
228236
sinon.stub(env, 'get').returns({config: {BITBUCKET_WORKSPACE: workspace, BITBUCKET_REPO_ROOT_DIRECTORY: repoPath}});
@@ -245,14 +253,14 @@ describe('repositories/bitbucketrepo.js', () => {
245253
});
246254

247255
describe('createAsync', () => {
248-
let createAysncInfo = {
256+
const createAysncInfo = {
249257
branch: info.createAsync.name,
250258
repo: info.createAsync.repo_slug,
251259
model: info.createAsync.model,
252260
body: info.createAsync.body
253261
};
254262

255-
let createFileCommitInfo = {
263+
const createFileCommitInfo = {
256264
'ThreatDragonModels/my model/my model.json': JSON.stringify(createAysncInfo.body, null, ' '),
257265
repo_slug: createAysncInfo.repo,
258266
files: 'ThreatDragonModels/my model/my model.json',
@@ -280,14 +288,14 @@ describe('repositories/bitbucketrepo.js', () => {
280288

281289
describe('updateAsync', () => {
282290

283-
let updateAysncInfo = {
291+
const updateAysncInfo = {
284292
branch: info.createAsync.name,
285293
repo: info.createAsync.repo_slug,
286294
model: info.createAsync.model,
287295
body: info.createAsync.body
288296
};
289297

290-
let updateFileCommitInfo = {
298+
const updateFileCommitInfo = {
291299
'ThreatDragonModels/my model/my model.json': JSON.stringify(updateAysncInfo.body, null, ' '),
292300
repo_slug: updateAysncInfo.repo,
293301
files: 'ThreatDragonModels/my model/my model.json',
@@ -324,6 +332,21 @@ describe('repositories/bitbucketrepo.js', () => {
324332

325333
});
326334

335+
describe('create branch', () => {
336+
beforeEach(async () => {
337+
sinon.stub(env, 'get').returns({config: {BITBUCKET_WORKSPACE: workspace, BITBUCKET_REPO_ROOT_DIRECTORY: repoPath}});
338+
await threatModelRepository.createBranchAsync(info, accessToken);
339+
});
340+
it('creates a new branch', () => {
341+
expect(mockClient.refs.createBranch).to.have.been.calledWith(
342+
{
343+
_body: { name: info.branch, target: { hash: info.ref } },
344+
repo_slug: info.repo,
345+
workspace: workspace
346+
}
347+
);
348+
});
349+
});
327350

328351
});
329352

0 commit comments

Comments
 (0)