From 2025508b914e6f414037a59c51a50dc605e984d3 Mon Sep 17 00:00:00 2001 From: "Bieber, Austin J" Date: Mon, 3 Feb 2020 18:12:19 +0000 Subject: [PATCH 1/5] References #6, #7, and #8 - Added swagger for API documentation, JSDoc for developer documentation, and cleaned up controllers and API definition. --- CHANGELOG.md | 16 + CONTRIBUTING.md | 4 +- SECURITY.md | 15 + app/api-routes.js | 127 ++++- app/app.js | 3 +- app/artifact/README.md | 45 ++ app/artifact/local-strategy.js | 137 +++--- app/artifact/s3-strategy.js | 453 ++++++++++++++++++ app/auth/ldap-strategy.js | 3 +- app/auth/local-strategy.js | 2 +- app/controllers/api-controller.js | 219 +++++---- app/controllers/artifact-controller.js | 53 +- app/controllers/element-controller.js | 14 +- app/controllers/organization-controller.js | 9 +- app/controllers/project-controller.js | 7 +- app/controllers/user-controller.js | 44 +- app/controllers/webhook-controller.js | 4 + app/lib/events.js | 1 + app/lib/get-public-data.js | 142 +++--- app/lib/logger.js | 4 +- app/lib/middleware.js | 29 ++ app/lib/migrate.js | 2 +- app/lib/permissions.js | 31 +- app/lib/test-utils.js | 8 +- app/lib/utils.js | 2 +- app/lib/validators.js | 6 +- app/models/user.js | 6 + app/models/webhook.js | 4 +- app/routes.js | 16 + .../admin-console-views/create-user.jsx | 63 +-- .../admin-console-views/user-list.jsx | 17 + .../profile-views/password-edit.jsx | 51 +- .../components/profile-views/profile-edit.jsx | 76 ++- app/ui/components/profile-views/profile.jsx | 24 +- .../project-views/artifacts/artifact-form.jsx | 35 +- .../project-views/branches/branch-new.jsx | 25 +- .../elements/element-edit-form.jsx | 64 ++- .../project-views/elements/element-new.jsx | 8 +- .../elements/element-selector.jsx | 2 +- .../elements/element-textarea.jsx | 4 +- .../components/project-views/project-home.jsx | 7 +- .../search/advanced-search/advanced-row.jsx | 4 +- .../project-views/search/search-result.jsx | 14 +- .../project-views/search/search.jsx | 15 +- app/ui/components/shared-views/create.jsx | 40 +- app/ui/js/mbee.js | 3 +- config/example.cfg | 2 - package.json | 9 +- plugins/routes.js | 19 +- scripts/build.js | 23 +- scripts/migrations/1.0.1.js | 85 ++++ scripts/start.js | 27 +- scripts/test.js | 1 - test/0xx_init/000-init.js | 22 +- test/2xx_ut_lib/206-lib-validators.js | 12 +- test/2xx_ut_lib/209-lib-permissions.js | 40 ++ .../core_tests/301a-user-model-core-tests.js | 2 +- .../301b-user-model-error-tests.js | 4 +- .../303b-project-model-error-tests.js | 2 +- .../304b-branch-model-error-tests.js | 2 +- .../401a-user-controller-core-tests.js | 3 +- .../406a-artifact-controller-core-tests.js | 28 +- .../407a-webhook-controller-core-tests.js | 3 +- .../core_tests/501a-user-mock-core-tests.js | 9 +- .../506a-artifact-mock-core-test.js | 40 +- .../507a-webhook-mock-core-tests.js | 16 +- .../error_tests/501b-user-mock-error-tests.js | 5 +- .../507b-webhook-mock-error-tests.js | 6 +- .../501c-user-mock-specific-tests.js | 52 ++ .../507c-webhook-mock-specific-tests.js | 3 +- .../606a-artifact-api-core-tests.js | 29 ++ .../core_tests/607a-webhook-api-core-tests.js | 28 +- .../artifact_tests/821-local-strategy.js | 63 ++- .../artifact_tests/822-s3-strategy.js | 209 ++++++++ .../auth_tests/801-local-strategy.js | 71 +-- test/test_data.json | 10 +- 76 files changed, 2048 insertions(+), 635 deletions(-) create mode 100644 app/artifact/README.md create mode 100644 app/artifact/s3-strategy.js create mode 100644 scripts/migrations/1.0.1.js create mode 100644 test/8xx_system_tests/artifact_tests/822-s3-strategy.js diff --git a/CHANGELOG.md b/CHANGELOG.md index d39e25f2..809dc4ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,22 @@ # Changelog All notable changes to this project will be documented in this file. +## [1.0.1] - 2020-01-31 +### Major Features and Improvements +* Implemented HTTP/2 in place of HTTPS/1.1. This requires no change for the + current user +* Added a new artifact strategy for Amazon's S3 +* Added the ability for system wide admins to reset a users password +* Added support for temporary passwords. Whenever a local user is created or + has their password reset, they must change their password upon first login + +### Bug Fixes and Other Changes +* Fixed a bug causing the cursor to flicker while hovering over buttons in the + UI +* Fixed a bug causing local plugins to not load properly on Windows +* Added an API endpoint which lists the filename and location of all artifact + blobs on a project + ## [1.0.0] - 2020-01-20 ### Bug Fixes and Other Changes * Added CONTRIBUTING.md file for detailing expectations for code contribution diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9ab6d93f..8c5b1f02 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -113,8 +113,7 @@ server configuration, training and even physical access to machines. ### Contributors -Thanks to all of the following people below who have directly contributed code -to MBEE +Thanks to all of the following people who have directly contributed code to MBEE - Austin Bieber - Danny Chiu @@ -123,4 +122,5 @@ to MBEE - Jimmy Eckstein - Josh Kaplan - Phillip Lee +- Donte McDaniel - Jake Ursetta diff --git a/SECURITY.md b/SECURITY.md index 10514066..4559ade2 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -118,6 +118,21 @@ Due to the nature of how plugins are loaded, the API and UI are not accessible until a plugin has been succesfully loaded. This can cause issues if plugins hang while loading, and can prevent the UI and API from being accessible. +#### Cursor Flickering in UI Tables +Hovering over edit/delete/add buttons within tables causes a flicker in +reactstrap 8.0.1. This issue has been reported as of 12/9/2019. +Per discussion on the [issue](https://github.com/reactstrap/reactstrap/issues/1728), +there is a suggestion to downgrade to reactstrap 7.1.0. That version +introduces 'Component Lifecycle Deprecation Warnings' for the Modal Component. + +Specifying "reactstrap": "7.1.0" within the package.json devDependencies or +running `npm i reactstrap@7.1.0 -D` within the project directory will +downgrade this package. + +Upon testing reactstrap 8.4.0, no cursor flickering was observed regarding the +aforementioned buttons. Additionally, this upgrade resolved the +'Component Lifecycle Deprecation Warnings' for the Modal Component. + ## Security Related Configuration ### Plugins and Integrations diff --git a/app/api-routes.js b/app/api-routes.js index 9dc199e3..50bbd251 100644 --- a/app/api-routes.js +++ b/app/api-routes.js @@ -50,6 +50,7 @@ api.get('/test', Middleware.logRoute, APIController.test); api.get( '/coffee', AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, (req, res) => { const str = 'I\'m a teapot.'; @@ -103,6 +104,7 @@ api.get('/doc/swagger.json', api.route('/login') .post( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logSecurityRoute, Middleware.logRoute, AuthController.doLogin, @@ -135,6 +137,7 @@ api.route('/login') api.route('/version') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, APIController.version, Middleware.logResponse, @@ -191,6 +194,7 @@ api.route('/version') api.route('/logs') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logSecurityRoute, Middleware.logRoute, Middleware.pluginPre('getLogs'), @@ -567,6 +571,7 @@ api.route('/logs') api.route('/orgs') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('getOrgs'), APIController.getOrgs, @@ -576,6 +581,7 @@ api.route('/orgs') ) .post( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logSecurityRoute, Middleware.logRoute, Middleware.pluginPre('postOrgs'), @@ -587,6 +593,7 @@ api.route('/orgs') ) .put( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logSecurityRoute, Middleware.logRoute, Middleware.pluginPre('putOrgs'), @@ -598,6 +605,7 @@ api.route('/orgs') ) .patch( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('patchOrgs'), APIController.patchOrgs, @@ -607,6 +615,7 @@ api.route('/orgs') ) .delete( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logSecurityRoute, Middleware.logRoute, Middleware.pluginPre('deleteOrgs'), @@ -935,6 +944,7 @@ api.route('/orgs') api.route('/orgs/:orgid') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('getOrg'), APIController.getOrg, @@ -944,6 +954,7 @@ api.route('/orgs/:orgid') ) .post( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logSecurityRoute, Middleware.logRoute, Middleware.pluginPre('postOrg'), @@ -955,6 +966,7 @@ api.route('/orgs/:orgid') ) .put( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logSecurityRoute, Middleware.logRoute, Middleware.pluginPre('putOrg'), @@ -966,6 +978,7 @@ api.route('/orgs/:orgid') ) .patch( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('patchOrg'), APIController.patchOrg, @@ -975,6 +988,7 @@ api.route('/orgs/:orgid') ) .delete( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logSecurityRoute, Middleware.logRoute, Middleware.pluginPre('deleteOrg'), @@ -1091,6 +1105,7 @@ api.route('/orgs/:orgid') api.route('/projects') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('getAllProjects'), APIController.getAllProjects, @@ -1524,6 +1539,7 @@ api.route('/projects') api.route('/orgs/:orgid/projects') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('getProjects'), APIController.getProjects, @@ -1533,6 +1549,7 @@ api.route('/orgs/:orgid/projects') ) .post( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('postProjects'), APIController.postProjects, @@ -1542,6 +1559,7 @@ api.route('/orgs/:orgid/projects') ) .put( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('putProjects'), APIController.putProjects, @@ -1551,6 +1569,7 @@ api.route('/orgs/:orgid/projects') ) .patch( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('patchProjects'), APIController.patchProjects, @@ -1560,6 +1579,7 @@ api.route('/orgs/:orgid/projects') ) .delete( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logSecurityRoute, Middleware.logRoute, Middleware.pluginPre('deleteProjects'), @@ -1941,6 +1961,7 @@ api.route('/orgs/:orgid/projects') api.route('/orgs/:orgid/projects/:projectid') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('getProject'), APIController.getProject, @@ -1950,6 +1971,7 @@ api.route('/orgs/:orgid/projects/:projectid') ) .post( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('postProject'), APIController.postProject, @@ -1959,6 +1981,7 @@ api.route('/orgs/:orgid/projects/:projectid') ) .put( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('putProject'), APIController.putProject, @@ -1968,6 +1991,7 @@ api.route('/orgs/:orgid/projects/:projectid') ) .patch( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('patchProject'), APIController.patchProject, @@ -1977,6 +2001,7 @@ api.route('/orgs/:orgid/projects/:projectid') ) .delete( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logSecurityRoute, Middleware.logRoute, Middleware.pluginPre('deleteProject'), @@ -2359,6 +2384,7 @@ api.route('/orgs/:orgid/projects/:projectid') api.route('/orgs/:orgid/projects/:projectid/branches') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('getBranches'), APIController.getBranches, @@ -2368,6 +2394,7 @@ api.route('/orgs/:orgid/projects/:projectid/branches') ) .post( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('postBranches'), APIController.postBranches, @@ -2377,6 +2404,7 @@ api.route('/orgs/:orgid/projects/:projectid/branches') ) .patch( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('patchBranches'), APIController.patchBranches, @@ -2386,6 +2414,7 @@ api.route('/orgs/:orgid/projects/:projectid/branches') ) .delete( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('deleteBranches'), APIController.deleteBranches, @@ -2701,6 +2730,7 @@ api.route('/orgs/:orgid/projects/:projectid/branches') api.route('/orgs/:orgid/projects/:projectid/branches/:branchid') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('getBranch'), APIController.getBranch, @@ -2710,6 +2740,7 @@ api.route('/orgs/:orgid/projects/:projectid/branches/:branchid') ) .post( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('postBranch'), APIController.postBranch, @@ -2719,6 +2750,7 @@ api.route('/orgs/:orgid/projects/:projectid/branches/:branchid') ) .patch( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('patchBranch'), APIController.patchBranch, @@ -2728,6 +2760,7 @@ api.route('/orgs/:orgid/projects/:projectid/branches/:branchid') ) .delete( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('deleteBranch'), APIController.deleteBranch, @@ -2887,6 +2920,7 @@ api.route('/orgs/:orgid/projects/:projectid/branches/:branchid') api.route('/orgs/:orgid/projects/:projectid/branches/:branchid/elements/search') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('searchElements'), APIController.searchElements, @@ -3518,6 +3552,7 @@ api.route('/orgs/:orgid/projects/:projectid/branches/:branchid/elements/search') api.route('/orgs/:orgid/projects/:projectid/branches/:branchid/elements') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('getElements'), APIController.getElements, @@ -3527,6 +3562,7 @@ api.route('/orgs/:orgid/projects/:projectid/branches/:branchid/elements') ) .post( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('postElements'), APIController.postElements, @@ -3536,6 +3572,7 @@ api.route('/orgs/:orgid/projects/:projectid/branches/:branchid/elements') ) .put( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('putElements'), APIController.putElements, @@ -3545,6 +3582,7 @@ api.route('/orgs/:orgid/projects/:projectid/branches/:branchid/elements') ) .patch( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('patchElements'), APIController.patchElements, @@ -3554,6 +3592,7 @@ api.route('/orgs/:orgid/projects/:projectid/branches/:branchid/elements') ) .delete( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('deleteElements'), APIController.deleteElements, @@ -4114,6 +4153,7 @@ api.route('/orgs/:orgid/projects/:projectid/branches/:branchid/elements') api.route('/orgs/:orgid/projects/:projectid/branches/:branchid/elements/:elementid') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('getElement'), APIController.getElement, @@ -4123,6 +4163,7 @@ api.route('/orgs/:orgid/projects/:projectid/branches/:branchid/elements/:element ) .post( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('postElement'), APIController.postElement, @@ -4132,6 +4173,7 @@ api.route('/orgs/:orgid/projects/:projectid/branches/:branchid/elements/:element ) .put( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPost('putElement'), APIController.putElement, @@ -4141,6 +4183,7 @@ api.route('/orgs/:orgid/projects/:projectid/branches/:branchid/elements/:element ) .patch( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('patchElement'), APIController.patchElement, @@ -4150,6 +4193,7 @@ api.route('/orgs/:orgid/projects/:projectid/branches/:branchid/elements/:element ) .delete( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('deleteElement'), APIController.deleteElement, @@ -4159,6 +4203,58 @@ api.route('/orgs/:orgid/projects/:projectid/branches/:branchid/elements/:element ); +/** + * @swagger + * /api/orgs/{orgid}/projects/{projectid}/artifacts/list: + * get: + * tags: + * - artifacts + * description: Returns a list of artifact blob locations and filenames within a project. + * Requesting user must have read access on the project to list artifacts. + * produces: + * - application/json + * parameters: + * - name: orgid + * description: The ID of the organization containing the specified + * project. + * in: path + * required: true + * type: string + * - name: projectid + * description: The ID of the project containing the artifact blob. + * in: path + * required: true + * type: string + * responses: + * 200: + * description: OK, Succeeded to GET artifact, returns artifact list. + * 400: + * description: Bad Request, Failed to GET artifact list due to invalid data. + * 401: + * description: Unauthorized, Failed to GET artifact list due to not being + * logged in. + * 403: + * description: Forbidden, Failed to GET artifact list due to not having + * permissions. + * 404: + * description: Not Found, Failed to GET blob list due to artifact not + * existing. + * 500: + * description: Internal Server Error, Failed to GET blob list due to + * server side issue. + */ +api.route('/orgs/:orgid/projects/:projectid/artifacts/list') +.get( + AuthController.authenticate, + Middleware.expiredPassword, + Middleware.logRoute, + Middleware.pluginPre('listBlobs'), + APIController.listBlobs, + Middleware.pluginPost('listBlobs'), + Middleware.logResponse, + Middleware.respond +); + /** * @swagger * /api/orgs/{orgid}/projects/{projectid}/artifacts/blob: @@ -4329,6 +4425,7 @@ api.route('/orgs/:orgid/projects/:projectid/branches/:branchid/elements/:element api.route('/orgs/:orgid/projects/:projectid/artifacts/blob') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('getBlob'), APIController.getBlob, @@ -4338,6 +4435,7 @@ api.route('/orgs/:orgid/projects/:projectid/artifacts/blob') ) .post( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('postBlob'), APIController.postBlob, @@ -4347,6 +4445,7 @@ api.route('/orgs/:orgid/projects/:projectid/artifacts/blob') ) .delete( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('deleteBlob'), APIController.deleteBlob, @@ -4768,6 +4867,7 @@ api.route('/orgs/:orgid/projects/:projectid/artifacts/blob') api.route('/orgs/:orgid/projects/:projectid/branches/:branchid/artifacts') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('getArtifacts'), APIController.getArtifacts, @@ -4777,6 +4877,7 @@ api.route('/orgs/:orgid/projects/:projectid/branches/:branchid/artifacts') ) .post( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('postArtifacts'), APIController.postArtifacts, @@ -4786,6 +4887,7 @@ api.route('/orgs/:orgid/projects/:projectid/branches/:branchid/artifacts') ) .patch( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('patchArtifacts'), APIController.patchArtifacts, @@ -4795,6 +4897,7 @@ api.route('/orgs/:orgid/projects/:projectid/branches/:branchid/artifacts') ) .delete( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('deleteArtifacts'), APIController.deleteArtifacts, @@ -5144,6 +5247,7 @@ api.route('/orgs/:orgid/projects/:projectid/branches/:branchid/artifacts') api.route('/orgs/:orgid/projects/:projectid/branches/:branchid/artifacts/:artifactid') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('getArtifact'), APIController.getArtifact, @@ -5153,6 +5257,7 @@ api.route('/orgs/:orgid/projects/:projectid/branches/:branchid/artifacts/:artifa ) .post( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('postArtifact'), APIController.postArtifact, @@ -5162,6 +5267,7 @@ api.route('/orgs/:orgid/projects/:projectid/branches/:branchid/artifacts/:artifa ) .patch( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('patchArtifact'), APIController.patchArtifact, @@ -5171,6 +5277,7 @@ api.route('/orgs/:orgid/projects/:projectid/branches/:branchid/artifacts/:artifa ) .delete( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('deleteArtifact'), APIController.deleteArtifact, @@ -5238,6 +5345,7 @@ api.route('/orgs/:orgid/projects/:projectid/branches/:branchid/artifacts/:artifa api.route('/orgs/:orgid/projects/:projectid/branches/:branchid/artifacts/:artifactid/blob') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.pluginPre('getBlobById'), APIController.getBlobById, @@ -5667,6 +5775,7 @@ api.route('/orgs/:orgid/projects/:projectid/branches/:branchid/artifacts/:artifa api.route('/users') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.disableUserAPI, Middleware.pluginPre('getUsers'), @@ -5677,6 +5786,7 @@ api.route('/users') ) .post( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logSecurityRoute, Middleware.logRoute, Middleware.disableUserAPI, @@ -5689,6 +5799,7 @@ api.route('/users') ) .put( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logSecurityRoute, Middleware.logRoute, Middleware.disableUserAPI, @@ -5701,6 +5812,7 @@ api.route('/users') ) .patch( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.disableUserAPI, Middleware.pluginPre('patchUsers'), @@ -5711,6 +5823,7 @@ api.route('/users') ) .delete( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logSecurityRoute, Middleware.logRoute, Middleware.disableUserAPI, @@ -5842,6 +5955,7 @@ api.route('/users/whoami') api.route('/users/search') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.disableUserAPI, Middleware.pluginPre('searchUsers'), @@ -6199,6 +6313,7 @@ api.route('/users/search') api.route('/users/:username') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.disableUserAPI, Middleware.pluginPre('getUser'), @@ -6209,6 +6324,7 @@ api.route('/users/:username') ) .post( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logSecurityRoute, Middleware.logRoute, Middleware.disableUserAPI, @@ -6221,6 +6337,7 @@ api.route('/users/:username') ) .put( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logSecurityRoute, Middleware.logRoute, Middleware.disableUserAPI, @@ -6233,6 +6350,7 @@ api.route('/users/:username') ) .patch( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, Middleware.disableUserAPI, Middleware.pluginPre('patchUser'), @@ -6243,6 +6361,7 @@ api.route('/users/:username') ) .delete( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logSecurityRoute, Middleware.logRoute, Middleware.disableUserAPI, @@ -6724,6 +6843,7 @@ api.route('/users/:username/password') api.route('/webhooks') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logSecurityRoute, Middleware.logRoute, Middleware.pluginPre('getWebhooks'), @@ -6735,6 +6855,7 @@ api.route('/webhooks') ) .post( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logSecurityRoute, Middleware.logRoute, Middleware.pluginPre('postWebhooks'), @@ -6746,6 +6867,7 @@ api.route('/webhooks') ) .patch( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logSecurityRoute, Middleware.logRoute, Middleware.pluginPre('patchWebhooks'), @@ -6757,6 +6879,7 @@ api.route('/webhooks') ) .delete( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logSecurityRoute, Middleware.logRoute, Middleware.pluginPre('deleteWebhooks'), @@ -6799,7 +6922,6 @@ api.route('/webhooks') */ api.route('/webhooks/trigger/:encodedid') .post( - AuthController.authenticate, Middleware.logRoute, Middleware.pluginPre('triggerWebhook'), APIController.triggerWebhook, @@ -7014,6 +7136,7 @@ api.route('/webhooks/trigger/:encodedid') api.route('/webhooks/:webhookid') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logSecurityRoute, Middleware.logRoute, Middleware.pluginPre('getWebhook'), @@ -7025,6 +7148,7 @@ api.route('/webhooks/:webhookid') ) .patch( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logSecurityRoute, Middleware.logRoute, Middleware.pluginPre('patchWebhook'), @@ -7036,6 +7160,7 @@ api.route('/webhooks/:webhookid') ) .delete( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logSecurityRoute, Middleware.logRoute, Middleware.pluginPre('deleteWebhook'), diff --git a/app/app.js b/app/app.js index e14142ac..5d638be6 100644 --- a/app/app.js +++ b/app/app.js @@ -208,7 +208,8 @@ async function createDefaultAdmin() { _id: M.config.server.defaultAdminUsername, password: M.config.server.defaultAdminPassword, provider: 'local', - admin: true + admin: true, + changePassword: false }; User.hashPassword(adminUserData); diff --git a/app/artifact/README.md b/app/artifact/README.md new file mode 100644 index 00000000..a1d50ed1 --- /dev/null +++ b/app/artifact/README.md @@ -0,0 +1,45 @@ +# Supported Artifact Configurations + +MBEE supports two types of Artifact storage strategies for blob (arbitrary +binary files) storage. All artifact strategies require specific functions to be +implemented to work with MBEE. Each strategy may require different components +for configuration. + +Below are a list of currently supported artifact strategies and information on +how to configure each to work with MBEE. + +### Local Strategy Configuration +The local artifact strategy stores blobs locally on the same server that MBEE is +running on. + +To configure MBEE to use the local strategy, the `artifact` section of the +running MBEE config should appear as follows: + +```json +"artifact": { + "strategy": "local-strategy" +} +``` + +### Amazon S3 Strategy Configuration +The S3 artifact strategy requires an existing Amazon S3 account, bucket, and +user with permissions to access the bucket via an Access Key ID and Secret +Access Key. + +To configure MBEE to use the remote S3 strategy, the `artifact` section of the +running MBEE config should appear as follows: + +```json +"artifact": { + "strategy": "s3-strategy", + "s3": { + "accessKeyId": "your-access-key-id", + "secretAccessKey": "your-secret-access-key", + "region": "your-region", + "Bucket": "your-bucket-name", + "ca": "your/ssl/cert.pem", + "proxy": "http://your-proxy.com:80" + } + } +``` + diff --git a/app/artifact/local-strategy.js b/app/artifact/local-strategy.js index f5ec13af..59fa8eb4 100644 --- a/app/artifact/local-strategy.js +++ b/app/artifact/local-strategy.js @@ -11,18 +11,10 @@ * * @author Phillip Lee * - * @description This implements an artifact strategy for local - * artifact storage. This should be the default artifact strategy for MBEE. + * @description Implements an artifact strategy for local artifact storage. This + * should be the default artifact strategy for MBEE. */ -// Validator regex for this strategy -const validator = { - location: '^[^.]+$', - filename: '^[^!\\<>:"\'|?*]+$', - // Matches filename + extensions - extension: '^[^!\\<>:"\'|?*]+[.][\\w]+$' -}; - // Define the root storage path for blobs const rootStoragePath = '/storage'; @@ -36,11 +28,57 @@ const { execSync } = require('child_process'); const utils = M.require('lib.utils'); const errors = M.require('lib.errors'); +// Validator regex for this strategy +const validator = { + location: '^[^.]+$', + filename: '^[^!\\<>:"\'|?*]+$', + // Matches filename + extensions + extension: '^[^!\\<>:"\'|?*]+[.][\\w]+$' +}; + /** - * @description This function gets the artifact blob file - * from the local file system. + * @description List all blobs under a project. + * + * @param {object} artMetadata - Artifact metadata. + * @param {string} artMetadata.org - The org of the artifact blob. + * @param {string} artMetadata.project - The project of the artifact blob. * - * @param {string} artMetadata - Artifact metadata. + * @returns {object[]} Array of objects that contain blob location and filename. + */ +function listBlobs(artMetadata) { + try { + // Define blob name array + const blobList = []; + + // Get project id + const projID = utils.parseID(artMetadata.project).pop(); + + // Create full path + const fullPath = path.join(M.root, rootStoragePath, artMetadata.org, projID); + const files = fs.readdirSync(fullPath); + + files.forEach(file => { + // Split filename by delimiter + const filePath = file.split('.'); + // Extract location and filename + // Push obj into array + blobList.push({ + location: filePath.slice(0, filePath.length - 2).join('/'), + filename: filePath.slice(-2).join('.') + }); + }); + + return blobList; + } + catch (err) { + throw errors.captureError(err); + } +} + +/** + * @description Gets the artifact blob file from the local file system. + * + * @param {object} artMetadata - Artifact metadata. * @param {string} artMetadata.filename - The filename of the artifact. * @param {string} artMetadata.location - The location of the artifact. * @param {string} artMetadata.org - The org of the artifact blob. @@ -66,10 +104,10 @@ function getBlob(artMetadata) { } /** - * @description This function writes an artifact blob - * to the local file system. This function does NOT overwrite existing blob. + * @description Saves an artifact blob to the local file system. + * This function does NOT overwrite existing blob. * - * @param {string} artMetadata - Artifact metadata. + * @param {object} artMetadata - Artifact metadata. * @param {string} artMetadata.filename - The filename of the artifact. * @param {string} artMetadata.location - The location of the artifact. * @param {string} artMetadata.org - The org of the artifact blob. @@ -86,6 +124,7 @@ function postBlob(artMetadata, artifactBlob) { // Check if artifact file exist if (fs.existsSync(fullPath)) { + // Object Exist, throw error throw new M.DataFormatError('Artifact blob already exists.', 'warn'); } @@ -101,7 +140,7 @@ function postBlob(artMetadata, artifactBlob) { * @description This function writes an artifact blob to the local file system. * Existing files will be overwritten. * - * @param {string} artMetadata - Artifact metadata. + * @param {object} artMetadata - Artifact metadata. * @param {string} artMetadata.filename - The filename of the artifact. * @param {string} artMetadata.location - The location of the artifact. * @param {string} artMetadata.org - The org of the artifact blob. @@ -137,7 +176,7 @@ function putBlob(artMetadata, artifactBlob) { * @description This function deletes an artifact blob from the local file * system. * - * @param {string} artMetadata - Artifact metadata. + * @param {object} artMetadata - Artifact metadata. * @param {string} artMetadata.filename - The filename of the artifact. * @param {string} artMetadata.location - The location of the artifact. * @param {string} artMetadata.org - The org of the artifact blob. @@ -159,7 +198,7 @@ function deleteBlob(artMetadata) { // Note: Use sync to ensure file is removed before advancing fs.unlinkSync(blobPath); - // Check if project directory is empty + // Read the directory path const files = fs.readdirSync(projDirPath); // Check if no file exist @@ -177,7 +216,7 @@ function deleteBlob(artMetadata) { } /** - * @description This function recursively creates directories based on + * @description This helper function recursively creates directories based on * the input path. * * @param {string} pathString - The full directory path. @@ -231,28 +270,14 @@ function createDirectory(pathString) { return artifactPath; } -/** - * @description This function recursively deletes directories based on - * the input path. - * - * @param {string} pathString - The full directory path. - */ -function deleteDirectory(pathString) { - // Create the root artifact path - const dirToDelete = path.join(M.root, rootStoragePath, pathString); - - // Remove artifacts - const rmd = (process.platform === 'win32') ? 'RMDIR /S /Q' : 'rm -rf'; - execSync(`${rmd} ${dirToDelete}`); -} - /** * @description This function creates the blob path using the local * storage path, location field, and filename. + * * Handles specific cases to format path and filename consistently * across the artifact strategy. * - * @param {string} artMetadata - Artifact metadata. + * @param {object} artMetadata - Artifact metadata. * @param {string} artMetadata.filename - The filename of the artifact. * @param {string} artMetadata.location - The location of the artifact. * @param {string} artMetadata.org - The org of the artifact blob. @@ -275,7 +300,7 @@ function createBlobPath(artMetadata) { // Ensure location ends with separator if not present if (location[location.length - 1] !== path.sep) { - // Add separator for location and filename + // Add separator for location location += path.sep; } @@ -296,7 +321,7 @@ function createBlobPath(artMetadata) { * @description This function validates the artifact object metadata. * Ensures fields such as 'location' and 'filename' are defined. * - * @param {string} artMetadata - Artifact metadata. + * @param {object} artMetadata - Artifact metadata. * @param {string} artMetadata.filename - The filename of the artifact. * @param {string} artMetadata.location - The location of the artifact. */ @@ -327,37 +352,29 @@ function validateBlobMeta(artMetadata) { } /** - * @description This function deletes multiple blobs. + * @description This function removes a directory and all objects/folders within it + * recursively. * - * @param {object} clearObj - Contains meta data to clear blobs. - * @param {string} [clearObj.orgID] - The organization ID. If provided and no - * projectID is provided, deletes all blobs in the organization. - * @param {string} [clearObj.projectID] - The project ID. If provided, deletes - * all blobs in the project. + * @param {string} clearPath - Path to clear. */ -function clear(clearObj) { - let dirPath; - // Check if project id is defined - if (clearObj.hasOwnProperty('projectID')) { - // Create the Project path - dirPath = path.join(clearObj.orgID, clearObj.projectID); - } - else if (clearObj.hasOwnProperty('orgID')) { - // Create the Org path - dirPath = path.join(clearObj.orgID); +async function clear(clearPath) { + try { + // Create the root artifact path + const dirToDelete = path.join(M.root, rootStoragePath, clearPath); + + // Remove artifacts + const rmd = (process.platform === 'win32') ? 'RMDIR /S /Q' : 'rm -rf'; + execSync(`${rmd} ${dirToDelete}`); } - else { - // Skip deletion - return; + catch (err) { + throw errors.captureError(err); } - - // Delete the org directory - deleteDirectory(dirPath); } // Expose artifact strategy functions module.exports = { getBlob, + listBlobs, postBlob, putBlob, deleteBlob, diff --git a/app/artifact/s3-strategy.js b/app/artifact/s3-strategy.js new file mode 100644 index 00000000..c3fe971e --- /dev/null +++ b/app/artifact/s3-strategy.js @@ -0,0 +1,453 @@ +/** + * @classification UNCLASSIFIED + * + * @module artifact.s3-strategy + * + * @copyright Copyright (C) 2018, Lockheed Martin Corporation + * + * @license MIT + * + * @owner Phillip Lee + * + * @author Phillip Lee + * + * @description Implements an artifact storage strategy using Amazon S3, + * a cloud storage service. This strategy uses the aws-sdk library. + */ +// Define the root storage path for blobs +const rootStoragePath = 'storage'; + +// NPM modules +const AWS = require('aws-sdk'); +const proxy = require('proxy-agent'); + +// Node modules +const path = require('path'); +const fs = require('fs'); +const assert = require('assert'); + +// MBEE modules +const utils = M.require('lib.utils'); +const errors = M.require('lib.errors'); + +// Validator regex for this strategy +const validator = { + location: '^[^.]+$', + filename: '^[^!\\<>:"\'|?*]+$', + // Matches filename + extensions + extension: '^[^!\\<>:"\'|?*]+[.][\\w]+$' +}; + +// Global instance of s3 handler +let s3 = null; + +// Check if s3 strategy is used. +if (M.config.artifact.strategy === 's3-strategy') { + // Extract and update the configuration + AWS.config.update({ + httpOptions: { + agent: proxy(M.config.artifact.s3.proxy), + ca: fs.readFileSync(M.config.artifact.s3.ca) + }, + region: M.config.artifact.s3.region, + accessKeyId: M.config.artifact.s3.accessKeyId, + secretAccessKey: M.config.artifact.s3.secretAccessKey, + Bucket: M.config.artifact.s3.Bucket + }); + + s3 = new AWS.S3(); +} + +/** + * @description List all blobs under a project. + * + * @param {object} artMetadata - Artifact metadata. + * @param {string} artMetadata.org - The org of the artifact blob. + * @param {string} artMetadata.project - The project of the artifact blob. + * + * @returns {object[]} Array of objects that content blob location and filename. + */ +async function listBlobs(artMetadata) { + try { + // Define blob name array + const blobList = []; + + // Get project id + const projID = utils.parseID(artMetadata.project).pop(); + + // Create full path + const fullPath = path.join(rootStoragePath, artMetadata.org, projID); + + // Initialize truncated to true + let isTruncated = true; + + // Define search obj + const searchObj = { + Bucket: M.config.artifact.s3.Bucket, + Prefix: fullPath + }; + + // Keep looping while there are objects + while (isTruncated) { + // Get all objects + const foundObj = await s3.listObjectsV2(searchObj).promise(); // eslint-disable-line + + // Check if objects found + if (foundObj.Contents.length === 0) { + // No objects found, break out of loop + break; + } + else { + // Objects found, set isTruncated + isTruncated = foundObj.IsTruncated; + + // Loop through each found object + foundObj.Contents.forEach((obj) => { + const paths = obj.Key.split('/'); + // Remove the first three directory path [storage, org, project] + paths.splice(0, 3); + + // Extract location and filename + // Push obj into array + blobList.push({ + location: paths.slice(0, paths.length - 1).join('/'), + filename: paths.slice(-1)[0] + + }); + }); + // Update next search token + searchObj.ContinuationToken = foundObj.NextContinuationToken; + } + } + return blobList; + } + catch (err) { + throw errors.captureError(err); + } +} + +/** + * @description Gets the artifact blob. + * + * @param {object} artMetadata - Artifact metadata. + * @param {string} artMetadata.filename - The filename of the artifact. + * @param {string} artMetadata.location - The location of the artifact. + * @param {string} artMetadata.org - The org of the artifact blob. + * @param {string} artMetadata.project - The project of the artifact blob. + * + * @returns {Buffer} Artifact binary. + */ +async function getBlob(artMetadata) { + try { + // Validate metadata + validateBlobMeta(artMetadata); + + // Create artifact path + const blobPath = createBlobPath(artMetadata); + + // Set params + const params = { + Bucket: M.config.artifact.s3.Bucket, + Key: blobPath + }; + + // Get the s3 object + const data = await s3.getObject(params).promise(); + // Return the object + return data.Body; + } + catch (err) { + // Check status code + if (err.statusCode === 404) { + throw new M.NotFoundError('Artifact blob not found.', 'warn'); + } + throw errors.captureError(err); + } +} + +/** + * @description Post an artifact blob. This function does NOT overwrite existing blob. + * + * @param {object} artMetadata - Artifact metadata. + * @param {string} artMetadata.filename - The filename of the artifact. + * @param {string} artMetadata.location - The location of the artifact. + * @param {string} artMetadata.org - The org of the artifact blob. + * @param {string} artMetadata.project - The project of artifact blob. + * @param {Buffer} artifactBlob - A binary large object artifact. + */ +async function postBlob(artMetadata, artifactBlob) { + try { + // Validate metadata + validateBlobMeta(artMetadata); + + // Create artifact path + const blobPath = createBlobPath(artMetadata); + + // Set params + const params = { + Bucket: M.config.artifact.s3.Bucket, + Key: blobPath, + Body: artifactBlob + }; + + // Check object exists + if (await doesObjectExist(params.Bucket, params.Key)) { + // Object exists, throw error + throw new M.DataFormatError('Artifact blob already exists.', 'warn'); + } + + // Upload the blob + await s3.upload(params).promise(); + } + catch (error) { + throw errors.captureError(error); + } +} + +/** + * @description Helper function that checks if an object exists. + * + * @param {string} bucket - The s3 bucket where the object is stored. + * @param {string} key - The filename of the object. + * + * @returns {boolean} Returns true if a blob already exists, else false. + */ +async function doesObjectExist(bucket, key) { + try { + // Check if object exists + // Note: If object not found, this will throw an error + await s3.headObject({ Bucket: bucket, Key: key }).promise(); + + // An object was found, return true + return true; + } + catch (err) { + // Error occured, check if object not found + if (err.code !== 'NotFound') { + throw errors.captureError(err); + } + return false; + } +} + +/** + * @description Uploads an artifact blob. Existing files will be overwritten. + * + * @param {object} artMetadata - Artifact metadata. + * @param {string} artMetadata.filename - The filename of the artifact. + * @param {string} artMetadata.location - The location of the artifact. + * @param {string} artMetadata.org - The org of the artifact blob. + * @param {string} artMetadata.project - The project of artifact blob. + * @param {Buffer} artifactBlob - A binary large object artifact. + */ +async function putBlob(artMetadata, artifactBlob) { + try { + // Validate metadata + validateBlobMeta(artMetadata); + + // Create artifact path + const blobPath = createBlobPath(artMetadata); + + // Set params + const params = { + Bucket: M.config.artifact.s3.Bucket, + Key: blobPath, + Body: artifactBlob + }; + + // Upload the blob + await s3.upload(params).promise(); + } + catch (err) { + throw errors.captureError(err); + } +} + +/** + * @description Deletes an artifact blob. + * + * @param {object} artMetadata - Artifact metadata. + * @param {string} artMetadata.filename - The filename of the artifact. + * @param {string} artMetadata.location - The location of the artifact. + * @param {string} artMetadata.org - The org of the artifact blob. + * @param {string} artMetadata.project - The project of artifact blob. + */ +async function deleteBlob(artMetadata) { + try { + // Validate metadata + validateBlobMeta(artMetadata); + + // Create artifact path + const blobPath = createBlobPath(artMetadata); + + // Set params + const params = { + Bucket: M.config.artifact.s3.Bucket, + Key: blobPath + }; + + // Check object does NOT exist + if (!(await doesObjectExist(params.Bucket, params.Key))) { + // Object does NOT exist, throw error + throw new M.DataFormatError('Artifact blob not found.', 'warn'); + } + + // Delete the blob + await s3.deleteObject(params).promise(); + } + catch (err) { + throw errors.captureError(err); + } +} + +/** + * @description This function creates the blob path based on storage path, + * location field, and filename. Calling this function ensures path and + * filename are formatted consistently across the artifact strategy. + * + * @param {object} artMetadata - Artifact metadata. + * @param {string} artMetadata.filename - The filename of the artifact. + * @param {string} artMetadata.location - The location of the artifact. + * @param {string} artMetadata.org - The org of the artifact blob. + * @param {string} artMetadata.project - The project of artifact blob. + * + * @returns {string} The blob file path. + */ +function createBlobPath(artMetadata) { + // defined blob location + let location = artMetadata.location; + + // Get org id + const orgID = utils.parseID(artMetadata.org).pop(); + + // Get project id + const projID = utils.parseID(artMetadata.project).pop(); + + // Ensure location ends with separator if not present + if (location[location.length - 1] !== path.sep) { + // Add separator for location + location += path.sep; + } + + // Remove os separator with periods + const convertedLocation = location.replace( + // eslint-disable-next-line security/detect-non-literal-regexp + new RegExp(`\\${path.sep}`, 'g'), '/' + ); + + // Form the blob name, location concat with filename + const concatenName = convertedLocation + artMetadata.filename; + + // Create and return the complete path + return path.join(rootStoragePath, orgID, projID, concatenName); +} + +/** + * @description Validates the artifact object metadata. + * Ensures fields such as 'location' and 'filename' are defined. + * + * @param {object} artMetadata - Artifact metadata. + * @param {string} artMetadata.filename - The filename of the artifact. + * @param {string} artMetadata.location - The location of the artifact. + */ +function validateBlobMeta(artMetadata) { + try { + // Define the required blob fields + const requiredBlobFields = ['location', 'filename']; + + if (typeof artMetadata !== 'object' || artMetadata === null) { + throw new M.DataFormatError('Artifact metadata must be an object.', 'warn'); + } + + requiredBlobFields.forEach((field) => { + assert.ok(artMetadata.hasOwnProperty(field), 'Artifact metadata requires' + + `the ${field} field.`); + }); + + assert.ok((RegExp(validator.filename).test(artMetadata.filename) + && RegExp(validator.extension).test(artMetadata.filename)), + `Artifact filename [${artMetadata.filename}] is improperly formatted.`); + + assert.ok(RegExp(validator.location).test(artMetadata.location), + `Artifact location [${artMetadata.location}] is improperly formatted.`); + } + catch (error) { + throw new M.DataFormatError(error.message, 'warn'); + } +} + +/** + * @description Removes a directory and all objects/folders within it + * recursively. + * + * @param {string} clearPath - Path to clear. + */ +async function clear(clearPath) { + try { + // Create the root artifact path + const dirToDelete = path.join(rootStoragePath, clearPath); + + // Initialize truncated to true + let isTruncated = true; + + // Define search obj + const searchObj = { + Bucket: M.config.artifact.s3.Bucket, + Prefix: dirToDelete + }; + + // Keep looping while there are objects + while (isTruncated) { + const objToDelete = []; + + // Find all objects with given prefix + const foundObj = await s3.listObjects({ // eslint-disable-line no-await-in-loop + Bucket: M.config.artifact.s3.Bucket, + Prefix: dirToDelete + }).promise(); + + // Check if objects found + if (foundObj.Contents.length > 0) { + // Set if truncated + isTruncated = foundObj.IsTruncated; + + // Loop through each found object + foundObj.Contents.forEach((obj) => { + objToDelete.push({ + Key: obj.Key + + }); + }); + + // Update next search token + searchObj.ContinuationToken = foundObj.NextContinuationToken; + + // Delete current batch of objects + await s3.deleteObjects({ // eslint-disable-line no-await-in-loop + Bucket: M.config.artifact.s3.Bucket, + Delete: { + Objects: objToDelete + } + }).promise(); + } + else { + // No objects found, set truncated to false + isTruncated = false; + } + } + } + catch (error) { + throw errors.captureError(error); + } +} + +// Expose artifact strategy functions +module.exports = { + getBlob, + listBlobs, + postBlob, + putBlob, + deleteBlob, + clear, + validator +}; diff --git a/app/auth/ldap-strategy.js b/app/auth/ldap-strategy.js index bdda29d1..14f71e5b 100644 --- a/app/auth/ldap-strategy.js +++ b/app/auth/ldap-strategy.js @@ -402,7 +402,8 @@ async function ldapSync(ldapUserObj) { preferredName: ldapUserObj[ldapConfig.attributes.preferredName], lname: ldapUserObj[ldapConfig.attributes.lastName], email: ldapUserObj[ldapConfig.attributes.email], - provider: 'ldap' + provider: 'ldap', + changePassword: false }; // Save ldap user diff --git a/app/auth/local-strategy.js b/app/auth/local-strategy.js index 53b83473..2f4605e3 100644 --- a/app/auth/local-strategy.js +++ b/app/auth/local-strategy.js @@ -127,7 +127,7 @@ async function handleBasicAuth(req, res, username, password) { } // User is within allowed number of failed attempts; throw an error else { - throw new M.AuthorizationError('Invalid password.', 'warn'); + throw new M.AuthorizationError('Invalid username or password.', 'warn'); } } // Authenticated, return user diff --git a/app/controllers/api-controller.js b/app/controllers/api-controller.js index 5682a72f..b9421329 100644 --- a/app/controllers/api-controller.js +++ b/app/controllers/api-controller.js @@ -39,6 +39,7 @@ const BranchController = M.require('controllers.branch-controller'); const OrgController = M.require('controllers.organization-controller'); const ProjectController = M.require('controllers.project-controller'); const UserController = M.require('controllers.user-controller'); +const User = M.require('models.user'); const WebhookController = M.require('controllers.webhook-controller'); const Webhook = M.require('models.webhook'); const EventEmitter = M.require('lib.events'); @@ -123,6 +124,7 @@ module.exports = { postBlob, deleteBlob, getBlobById, + listBlobs, getWebhooks, postWebhooks, patchWebhooks, @@ -451,7 +453,7 @@ async function getOrgs(req, res, next) { // Get the public data of each org const orgsPublicData = sani.html( - orgs.map(o => publicData.getPublicData(o, 'org', options)) + orgs.map(o => publicData.getPublicData(req.user, o, 'org', options)) ); // Format JSON @@ -535,7 +537,7 @@ async function postOrgs(req, res, next) { const orgs = await OrgController.create(req.user, orgData, options); // Get the public data of each org const orgsPublicData = sani.html( - orgs.map(o => publicData.getPublicData(o, 'org', options)) + orgs.map(o => publicData.getPublicData(req.user, o, 'org', options)) ); // Format JSON @@ -620,7 +622,7 @@ async function putOrgs(req, res, next) { const orgs = await OrgController.createOrReplace(req.user, orgData, options); // Get the public data of each org const orgsPublicData = sani.html( - orgs.map(o => publicData.getPublicData(o, 'org', options)) + orgs.map(o => publicData.getPublicData(req.user, o, 'org', options)) ); // Format JSON @@ -704,7 +706,7 @@ async function patchOrgs(req, res, next) { const orgs = await OrgController.update(req.user, orgData, options); // Get the public data of each org const orgsPublicData = sani.html( - orgs.map(o => publicData.getPublicData(o, 'org', options)) + orgs.map(o => publicData.getPublicData(req.user, o, 'org', options)) ); // Format JSON @@ -847,7 +849,7 @@ async function getOrg(req, res, next) { // Get the public data of each org const orgsPublicData = sani.html( - orgs.map(o => publicData.getPublicData(o, 'org', options)) + orgs.map(o => publicData.getPublicData(req.user, o, 'org', options)) ); // Format JSON @@ -933,7 +935,7 @@ async function postOrg(req, res, next) { const orgs = await OrgController.create(req.user, req.body, options); // Get the public data of each org const orgsPublicData = sani.html( - orgs.map(o => publicData.getPublicData(o, 'org', options)) + orgs.map(o => publicData.getPublicData(req.user, o, 'org', options)) ); // Format JSON @@ -1019,7 +1021,7 @@ async function putOrg(req, res, next) { const orgs = await OrgController.createOrReplace(req.user, req.body, options); // Get the public data of each org const orgsPublicData = sani.html( - orgs.map(o => publicData.getPublicData(o, 'org', options)) + orgs.map(o => publicData.getPublicData(req.user, o, 'org', options)) ); // Format JSON @@ -1104,7 +1106,7 @@ async function patchOrg(req, res, next) { const orgs = await OrgController.update(req.user, req.body, options); // Get the public data of each org const orgsPublicData = sani.html( - orgs.map(o => publicData.getPublicData(o, 'org', options)) + orgs.map(o => publicData.getPublicData(req.user, o, 'org', options)) ); // Format JSON @@ -1260,7 +1262,7 @@ async function getAllProjects(req, res, next) { } const publicProjectData = sani.html( - projects.map(p => publicData.getPublicData(p, 'project', options)) + projects.map(p => publicData.getPublicData(req.user, p, 'project', options)) ); // Format JSON @@ -1371,7 +1373,7 @@ async function getProjects(req, res, next) { } const publicProjectData = sani.html( - projects.map(p => publicData.getPublicData(p, 'project', options)) + projects.map(p => publicData.getPublicData(req.user, p, 'project', options)) ); // Format JSON @@ -1455,7 +1457,7 @@ async function postProjects(req, res, next) { const projects = await ProjectController.create(req.user, req.params.orgid, projectData, options); const publicProjectData = sani.html( - projects.map(p => publicData.getPublicData(p, 'project', options)) + projects.map(p => publicData.getPublicData(req.user, p, 'project', options)) ); // Format JSON @@ -1540,7 +1542,7 @@ async function putProjects(req, res, next) { const projects = await ProjectController.createOrReplace(req.user, req.params.orgid, projectData, options); const publicProjectData = sani.html( - projects.map(p => publicData.getPublicData(p, 'project', options)) + projects.map(p => publicData.getPublicData(req.user, p, 'project', options)) ); // Format JSON @@ -1624,7 +1626,7 @@ async function patchProjects(req, res, next) { const projects = await ProjectController.update(req.user, req.params.orgid, projectData, options); const publicProjectData = sani.html( - projects.map(p => publicData.getPublicData(p, 'project', options)) + projects.map(p => publicData.getPublicData(req.user, p, 'project', options)) ); // Format JSON @@ -1770,7 +1772,7 @@ async function getProject(req, res, next) { } const publicProjectData = sani.html( - projects.map(p => publicData.getPublicData(p, 'project', options)) + projects.map(p => publicData.getPublicData(req.user, p, 'project', options)) ); // Format JSON @@ -1855,7 +1857,7 @@ async function postProject(req, res, next) { // NOTE: create() sanitizes req.params.orgid and req.body const projects = await ProjectController.create(req.user, req.params.orgid, req.body, options); const publicProjectData = sani.html( - projects.map(p => publicData.getPublicData(p, 'project', options)) + projects.map(p => publicData.getPublicData(req.user, p, 'project', options)) ); // Format JSON @@ -1941,7 +1943,7 @@ async function putProject(req, res, next) { const projects = await ProjectController.createOrReplace(req.user, req.params.orgid, req.body, options); const publicProjectData = sani.html( - projects.map(p => publicData.getPublicData(p, 'project', options)) + projects.map(p => publicData.getPublicData(req.user, p, 'project', options)) ); // Format JSON @@ -2026,7 +2028,7 @@ async function patchProject(req, res, next) { const projects = await ProjectController.update(req.user, req.params.orgid, req.body, options); const publicProjectData = sani.html( - projects.map(p => publicData.getPublicData(p, 'project', options)) + projects.map(p => publicData.getPublicData(req.user, p, 'project', options)) ); // Format JSON @@ -2190,11 +2192,8 @@ async function getUsers(req, res, next) { // NOTE: find() sanitizes req.usernames const users = await UserController.find(req.user, usernames, options); - // Set the failedlogins parameter to true if the requesting user is an admin - if (req.user.admin) options.failedlogins = true; - const publicUserData = sani.html( - users.map(u => publicData.getPublicData(u, 'user', options)) + users.map(u => publicData.getPublicData(req.user, u, 'user', options)) ); // Verify users public data array is not empty @@ -2283,7 +2282,7 @@ async function postUsers(req, res, next) { // NOTE: create() sanitizes userData const users = await UserController.create(req.user, userData, options); const publicUserData = sani.html( - users.map(u => publicData.getPublicData(u, 'user', options)) + users.map(u => publicData.getPublicData(req.user, u, 'user', options)) ); // Format JSON @@ -2367,7 +2366,7 @@ async function putUsers(req, res, next) { // NOTE: createOrReplace() sanitizes userData const users = await UserController.createOrReplace(req.user, userData, options); const publicUserData = sani.html( - users.map(u => publicData.getPublicData(u, 'user', options)) + users.map(u => publicData.getPublicData(req.user, u, 'user', options)) ); // Format JSON @@ -2451,7 +2450,7 @@ async function patchUsers(req, res, next) { // NOTE: update() sanitizes userData const users = await UserController.update(req.user, userData, options); const publicUserData = sani.html( - users.map(u => publicData.getPublicData(u, 'user', options)) + users.map(u => publicData.getPublicData(req.user, u, 'user', options)) ); // Format JSON @@ -2588,11 +2587,8 @@ async function getUser(req, res, next) { ); } - // Set the failedlogins parameter to true if the requesting user is an admin - if (req.user.admin) options.failedlogins = true; - const publicUserData = sani.html( - users.map(u => publicData.getPublicData(u, 'user', options)) + users.map(u => publicData.getPublicData(req.user, u, 'user', options)) ); // Format JSON @@ -2677,7 +2673,7 @@ async function postUser(req, res, next) { // NOTE: create() sanitizes req.body const users = await UserController.create(req.user, req.body, options); const publicUserData = sani.html( - users.map(u => publicData.getPublicData(u, 'user', options)) + users.map(u => publicData.getPublicData(req.user, u, 'user', options)) ); // Format JSON @@ -2762,7 +2758,7 @@ async function putUser(req, res, next) { // NOTE: createOrReplace() sanitizes req.body const users = await UserController.createOrReplace(req.user, req.body, options); const publicUserData = sani.html( - users.map(u => publicData.getPublicData(u, 'user', options)) + users.map(u => publicData.getPublicData(req.user, u, 'user', options)) ); // Format JSON @@ -2847,7 +2843,7 @@ async function patchUser(req, res, next) { // NOTE: update() sanitizes req.body const users = await UserController.update(req.user, req.body, options); const publicUserData = sani.html( - users.map(u => publicData.getPublicData(u, 'user', options)) + users.map(u => publicData.getPublicData(req.user, u, 'user', options)) ); // Format JSON @@ -2978,7 +2974,7 @@ async function whoami(req, res, next) { } const publicUserData = sani.html( - publicData.getPublicData(req.user, 'user', options) + publicData.getPublicData(req.user, req.user, 'user', options) ); // Format JSON @@ -3056,11 +3052,8 @@ async function searchUsers(req, res, next) { throw new M.NotFoundError('No users found.', 'warn'); } - // Set the failedlogins parameter to true if the requesting user is an admin - if (req.user.admin) options.failedlogins = true; - const usersPublicData = sani.html( - users.map(u => publicData.getPublicData(u, 'user', options)) + users.map(u => publicData.getPublicData(req.user, u, 'user', options)) ); // Format JSON @@ -3104,10 +3097,15 @@ async function patchPassword(req, res, next) { // Sanity Check: there should always be a user in the request if (!req.user) return noUserError(req, res, next); - // Ensure old password was provided + // Ensure old password was provided if user is changing their own password if (!req.body.oldPassword) { - const error = new M.DataFormatError('Old password not in request body.', 'warn'); - return utils.formatResponse(req, res, error.message, errors.getStatusCode(error), next); + if (req.user._id !== req.params.username) { + req.body.oldPassword = null; + } + else { + const error = new M.DataFormatError('Old password not in request body.', 'warn'); + return utils.formatResponse(req, res, error.message, errors.getStatusCode(error), next); + } } // Ensure new password was provided @@ -3122,12 +3120,6 @@ async function patchPassword(req, res, next) { return utils.formatResponse(req, res, error.message, errors.getStatusCode(error), next); } - // Ensure user is not trying to change another user's password - if (req.user._id !== req.params.username) { - const error = new M.OperationError('Cannot change another user\'s password.', 'warn'); - return utils.formatResponse(req, res, error.message, errors.getStatusCode(error), next); - } - // Attempt to parse query options try { // Extract options from request query @@ -3146,10 +3138,10 @@ async function patchPassword(req, res, next) { try { // Update the password - const user = await UserController.updatePassword(req.user, req.body.oldPassword, - req.body.password, req.body.confirmPassword); + const user = await UserController.updatePassword(req.user, req.params.username, + req.body.oldPassword, req.body.password, req.body.confirmPassword); const publicUserData = sani.html( - publicData.getPublicData(user, 'user', options) + publicData.getPublicData(req.user, user, 'user', options) ); // Format JSON @@ -3274,7 +3266,7 @@ async function getElements(req, res, next) { const elements = await ElementController.find(req.user, req.params.orgid, req.params.projectid, req.params.branchid, elemIDs, options); const elementsPublicData = sani.html( - elements.map(e => publicData.getPublicData(e, 'element', options)) + elements.map(e => publicData.getPublicData(req.user, e, 'element', options)) ); // Verify elements public data array is not empty @@ -3398,7 +3390,7 @@ async function postElements(req, res, next) { const elements = await ElementController.create(req.user, req.params.orgid, req.params.projectid, req.params.branchid, elementData, options); const elementsPublicData = sani.html( - elements.map(e => publicData.getPublicData(e, 'element', options)) + elements.map(e => publicData.getPublicData(req.user, e, 'element', options)) ); // Format JSON @@ -3483,7 +3475,7 @@ async function putElements(req, res, next) { const elements = await ElementController.createOrReplace(req.user, req.params.orgid, req.params.projectid, req.params.branchid, elementData, options); const elementsPublicData = sani.html( - elements.map(e => publicData.getPublicData(e, 'element', options)) + elements.map(e => publicData.getPublicData(req.user, e, 'element', options)) ); // Format JSON @@ -3567,7 +3559,7 @@ async function patchElements(req, res, next) { const elements = await ElementController.update(req.user, req.params.orgid, req.params.projectid, req.params.branchid, elementData, options); const elementsPublicData = sani.html( - elements.map(e => publicData.getPublicData(e, 'element', options)) + elements.map(e => publicData.getPublicData(req.user, e, 'element', options)) ); // Format JSON @@ -3696,7 +3688,12 @@ async function searchElements(req, res, next) { Object.keys(req.query).forEach((k) => { // If the key starts with custom., add it to the validOptions object if (k.startsWith('custom.')) { - validOptions[k] = 'string'; + if (req.query[k] === 'true' || req.query[k] === 'false') { + validOptions[k] = 'boolean'; + } + else { + validOptions[k] = 'string'; + } } }); } @@ -3737,7 +3734,7 @@ async function searchElements(req, res, next) { } const elementsPublicData = sani.html( - elements.map(e => publicData.getPublicData(e, 'element', options)) + elements.map(e => publicData.getPublicData(req.user, e, 'element', options)) ); // Format JSON @@ -3815,7 +3812,7 @@ async function getElement(req, res, next) { } let elementsPublicData = sani.html( - elements.map(e => publicData.getPublicData(e, 'element', options)) + elements.map(e => publicData.getPublicData(req.user, e, 'element', options)) ); // If the subtree option was not provided, return only the first element @@ -3905,7 +3902,7 @@ async function postElement(req, res, next) { const elements = await ElementController.create(req.user, req.params.orgid, req.params.projectid, req.params.branchid, req.body, options); const elementsPublicData = sani.html( - elements.map(e => publicData.getPublicData(e, 'element', options)) + elements.map(e => publicData.getPublicData(req.user, e, 'element', options)) ); // Format JSON @@ -3991,7 +3988,7 @@ async function putElement(req, res, next) { const elements = await ElementController.createOrReplace(req.user, req.params.orgid, req.params.projectid, req.params.branchid, req.body, options); const elementsPublicData = sani.html( - elements.map(e => publicData.getPublicData(e, 'element', options)) + elements.map(e => publicData.getPublicData(req.user, e, 'element', options)) ); // Format JSON @@ -4076,7 +4073,7 @@ async function patchElement(req, res, next) { const elements = await ElementController.update(req.user, req.params.orgid, req.params.projectid, req.params.branchid, req.body, options); const elementsPublicData = sani.html( - elements.map(e => publicData.getPublicData(e, 'element', options)) + elements.map(e => publicData.getPublicData(req.user, e, 'element', options)) ); // Format JSON @@ -4246,7 +4243,7 @@ async function getBranches(req, res, next) { const branches = await BranchController.find(req.user, req.params.orgid, req.params.projectid, branchIDs, options); const branchesPublicData = sani.html( - branches.map(b => publicData.getPublicData(b, 'branch', options)) + branches.map(b => publicData.getPublicData(req.user, b, 'branch', options)) ); // Verify branches public data array is not empty @@ -4335,7 +4332,7 @@ async function postBranches(req, res, next) { const branches = await BranchController.create(req.user, req.params.orgid, req.params.projectid, branchData, options); const publicBranchData = sani.html( - branches.map(b => publicData.getPublicData(b, 'branch', options)) + branches.map(b => publicData.getPublicData(req.user, b, 'branch', options)) ); // Format JSON @@ -4419,7 +4416,7 @@ async function patchBranches(req, res, next) { const branches = await BranchController.update(req.user, req.params.orgid, req.params.projectid, branchData, options); const branchesPublicData = sani.html( - branches.map(b => publicData.getPublicData(b, 'branch', options)) + branches.map(b => publicData.getPublicData(req.user, b, 'branch', options)) ); // Format JSON @@ -4564,7 +4561,7 @@ async function getBranch(req, res, next) { } const publicBranchData = sani.html( - branch.map(b => publicData.getPublicData(b, 'branch', options)) + branch.map(b => publicData.getPublicData(req.user, b, 'branch', options)) ); // Format JSON @@ -4649,7 +4646,7 @@ async function postBranch(req, res, next) { const branch = await BranchController.create(req.user, req.params.orgid, req.params.projectid, req.body, options); const branchesPublicData = sani.html( - branch.map(b => publicData.getPublicData(b, 'branch', options)) + branch.map(b => publicData.getPublicData(req.user, b, 'branch', options)) ); // Format JSON @@ -4734,7 +4731,7 @@ async function patchBranch(req, res, next) { const branch = await BranchController.update(req.user, req.params.orgid, req.params.projectid, req.body, options); const branchPublicData = sani.html( - branch.map(b => publicData.getPublicData(b, 'branch', options)) + branch.map(b => publicData.getPublicData(req.user, b, 'branch', options)) ); // Format JSON @@ -4908,7 +4905,7 @@ async function getArtifacts(req, res, next) { const artifacts = await ArtifactController.find(req.user, req.params.orgid, req.params.projectid, req.params.branchid, artIDs, options); const artifactsPublicData = sani.html( - artifacts.map(a => publicData.getPublicData(a, 'artifact', options)) + artifacts.map(a => publicData.getPublicData(req.user, a, 'artifact', options)) ); // Verify artifacts public data array is not empty @@ -5030,7 +5027,7 @@ async function postArtifacts(req, res, next) { req.params.projectid, req.params.branchid, artifactData, options); const artifactsPublicData = sani.html( - artifacts.map(a => publicData.getPublicData(a, 'artifact', options)) + artifacts.map(a => publicData.getPublicData(req.user, a, 'artifact', options)) ); // Format JSON @@ -5115,7 +5112,7 @@ async function patchArtifacts(req, res, next) { req.params.projectid, req.params.branchid, artifactData, options); const artifactsPublicData = sani.html( - artifacts.map(a => publicData.getPublicData(a, 'artifact', options)) + artifacts.map(a => publicData.getPublicData(req.user, a, 'artifact', options)) ); // Format JSON @@ -5273,7 +5270,7 @@ async function getArtifact(req, res, next) { } const publicArtifactData = sani.html( - artifact.map(a => publicData.getPublicData(a, 'artifact', options)) + artifact.map(a => publicData.getPublicData(req.user, a, 'artifact', options)) ); // Format JSON @@ -5359,7 +5356,7 @@ async function postArtifact(req, res, next) { req.params.projectid, req.params.branchid, req.body, options); const artifactsPublicData = sani.html( - artifact.map(a => publicData.getPublicData(a, 'artifact', options)) + artifact.map(a => publicData.getPublicData(req.user, a, 'artifact', options)) ); // Format JSON const json = formatJSON(artifactsPublicData[0], minified); @@ -5446,7 +5443,7 @@ async function patchArtifact(req, res, next) { req.params.projectid, req.params.branchid, req.body, options); const artifactsPublicData = sani.html( - artifact.map(a => publicData.getPublicData(a, 'artifact', options)) + artifact.map(a => publicData.getPublicData(req.user, a, 'artifact', options)) ); // Format JSON @@ -5529,6 +5526,38 @@ async function deleteArtifact(req, res, next) { } } +/** + * GET /api/orgs/:orgid/projects/:projectid/artifacts/list + * + * @description Gets a list of artifact blobs' location and filename by org.id, project.id. + * + * @param {object} req - Request express object + * @param {object} res - Response express object + * @param {Function} next - Middleware callback to trigger the next function + * + * @returns {object[]} An array of objects that contain artifact location, filename. + */ +async function listBlobs(req, res, next) { + // Sanity Check: there should always be a user in the request + if (!req.user) return noUserError(req, res); + + try { + const artifactList = await ArtifactController.listBlobs(req.user, req.params.orgid, + req.params.projectid); + + // Sets the message to the public artifact data and the status code to 200 + res.locals = { + message: artifactList, + statusCode: 200 + }; + next(); + } + catch (error) { + // If an error was thrown, return it and its status + return utils.returnResponse(req, res, error.message, errors.getStatusCode(error)); + } +} + /** * GET /api/orgs/:orgid/projects/:projectid/artifacts/blob * @@ -5824,7 +5853,7 @@ async function getWebhooks(req, res, next) { // Get public data of webhooks const webhooksPublicData = sani.html( - webhooks.map((w) => publicData.getPublicData(w, 'webhook', options)) + webhooks.map((w) => publicData.getPublicData(req.user, w, 'webhook', options)) ); // Verify the webhooks public data array is not empty @@ -5900,7 +5929,7 @@ async function postWebhooks(req, res, next) { // Get the webhooks' public data const webhookPublicData = sani.html( - webhooks.map((w) => publicData.getPublicData(w, 'webhook', options)) + webhooks.map((w) => publicData.getPublicData(req.user, w, 'webhook', options)) ); // Format JSON @@ -5971,7 +6000,7 @@ async function patchWebhooks(req, res, next) { // Get the webhooks' public data const webhookPublicData = sani.html( - webhooks.map((w) => publicData.getPublicData(w, 'webhook', options)) + webhooks.map((w) => publicData.getPublicData(req.user, w, 'webhook', options)) ); // Format JSON @@ -6115,7 +6144,7 @@ async function getWebhook(req, res, next) { // Get the public data for the webhook const webhookPublicData = sani.html( - publicData.getPublicData(webhook, 'webhook', options) + publicData.getPublicData(req.user, webhook, 'webhook', options) ); // Format JSON @@ -6203,7 +6232,7 @@ async function patchWebhook(req, res, next) { // Get the webhook public data const webhookPublicData = sani.html( - publicData.getPublicData(webhook, 'webhook', options) + publicData.getPublicData(req.user, webhook, 'webhook', options) ); // Format JSON @@ -6299,17 +6328,15 @@ async function deleteWebhook(req, res, next) { */ async function triggerWebhook(req, res, next) { // Parse the webhook id from the base64 encoded url - const webhookID = Buffer.from(req.params.encodedid, 'base64').toString('ascii'); + const webhookID = sani.db(Buffer.from(req.params.encodedid, 'base64').toString('ascii')); try { - const webhooks = await WebhookController.find(req.user, webhookID); + const webhook = await Webhook.findOne({ _id: webhookID }); - if (webhooks.length < 1) { - throw new M.NotFoundError('No webhooks found', 'warn'); + if (webhook === null) { + throw new M.NotFoundError('Webhook not found', 'warn'); } - const webhook = webhooks[0]; - // Sanity check: ensure the webhook is incoming and has a token and tokenLocation field if (webhook.type !== 'Incoming') { throw new M.ServerError(`Webhook [${webhook._id}] is not listening for external calls`, 'warn'); @@ -6321,13 +6348,13 @@ async function triggerWebhook(req, res, next) { throw new M.ServerError(`Webhook [${webhook._id}] does not have a token`, 'warn'); } - // Get the token from an arbitrary depth of key nesting - let token = req; + // Get the raw token from an arbitrary depth of key nesting + let rawToken = req; try { const tokenPath = webhook.tokenLocation.split('.'); for (let i = 0; i < tokenPath.length; i++) { const key = tokenPath[i]; - token = token[key]; + rawToken = rawToken[key]; } } catch (error) { @@ -6335,18 +6362,29 @@ async function triggerWebhook(req, res, next) { throw new M.DataFormatError('Token could not be found in the request.', 'warn'); } - if (typeof token !== 'string') { + if (typeof rawToken !== 'string') { throw new M.DataFormatError('Token is not a string', 'warn'); } + // Get the user and the original token value from the raw token + const decodedToken = Buffer.from(rawToken, 'base64').toString('ascii'); + const decomposedToken = decodedToken.split(':'); + const username = sani.db(decomposedToken[0]); + + // Get the user + const user = await User.findOne({ _id: username }); + if (user === null) { + throw new M.DataFormatError('Invalid token', 'warn'); + } + // Parse data from request - const data = req.body.data ? req.body.data : null; + const data = req.body.data ? req.body.data : req.body; - Webhook.verifyAuthority(webhook, token); + Webhook.verifyAuthority(webhook, decodedToken); webhook.triggers.forEach((trigger) => { - if (Array.isArray(data)) EventEmitter.emit(trigger, ...data); - else EventEmitter.emit(trigger, data); + if (Array.isArray(data)) EventEmitter.emit(trigger, user, ...data); + else EventEmitter.emit(trigger, user, data); }); // Sets the message to "success" and the status code to 200 @@ -6354,6 +6392,9 @@ async function triggerWebhook(req, res, next) { message: 'success', statusCode: 200 }; + + // Set a mock user object to be used in the response logging middleware + req.user = { _id: `Trigger of webhook ${webhook._id}` }; next(); } catch (error) { diff --git a/app/controllers/artifact-controller.js b/app/controllers/artifact-controller.js index 6fdcf1c0..a65cab66 100644 --- a/app/controllers/artifact-controller.js +++ b/app/controllers/artifact-controller.js @@ -26,7 +26,8 @@ module.exports = { remove, getBlob, postBlob, - deleteBlob + deleteBlob, + listBlobs }; // Node modules @@ -795,7 +796,7 @@ async function getBlob(requestingUser, organizationID, saniArt.org = orgID; // Include artifact blob in return obj - return ArtifactStrategy.getBlob(saniArt); + return await ArtifactStrategy.getBlob(saniArt); } catch (error) { throw errors.captureError(error); @@ -853,7 +854,7 @@ async function postBlob(requestingUser, organizationID, saniArt.org = orgID; // Return artifact object - ArtifactStrategy.postBlob(saniArt, artifactBlob); + await ArtifactStrategy.postBlob(saniArt, artifactBlob); // Return artifact object return saniArt; @@ -906,7 +907,7 @@ async function deleteBlob(requestingUser, organizationID, projectID, saniArt.org = orgID; // Delete the artifact blob - ArtifactStrategy.deleteBlob(saniArt); + await ArtifactStrategy.deleteBlob(saniArt); // Return Artifact obj return saniArt; @@ -915,3 +916,47 @@ async function deleteBlob(requestingUser, organizationID, projectID, throw errors.captureError(error); } } + +/** + * @description This function returns a list of blobs location and filenames within a project. + * + * @param {User} requestingUser - The requesting user. + * @param {string} organizationID - The organization ID for the org the + * project belongs to. + * @param {string} projectID - The project ID of the project which contains + * the artifact blobs. + * @param {object} [options] - A parameter that provides supported options. + * + * @returns {Promise} An array of objects that contain artifact location, filename. + */ +async function listBlobs(requestingUser, organizationID, projectID, options) { + try { + // Ensure input parameters are correct type + helper.checkParams(requestingUser, options, organizationID, projectID); + + // Sanitize input parameters + const reqUser = JSON.parse(JSON.stringify(requestingUser)); + const orgID = sani.db(organizationID); + const projID = sani.db(projectID); + + // Find the organization + const organization = await helper.findAndValidate(Org, orgID); + + // Find the project + const project = await helper.findAndValidate(Project, utils.createID(orgID, projID)); + + // Permissions check + permissions.listBlobs(reqUser, organization, project); + + const artData = { + project: projID, + org: orgID + }; + + // Return the list of blob data + return ArtifactStrategy.listBlobs(artData); + } + catch (error) { + throw errors.captureError(error); + } +} diff --git a/app/controllers/element-controller.js b/app/controllers/element-controller.js index 917e73f4..612fe38e 100644 --- a/app/controllers/element-controller.js +++ b/app/controllers/element-controller.js @@ -836,13 +836,13 @@ async function update(requestingUser, organizationID, projectID, branchID, eleme } // If updating source, add ID to sourceTargetIDs - if (elem.source) { + if (elem.source && !elem.hasOwnProperty('sourceNamespace')) { elem.source = utils.createID(orgID, projID, branID, elem.source); sourceTargetIDs.push(elem.source); } // If updating target, add ID to sourceTargetIDs - if (elem.target) { + if (elem.target && !elem.hasOwnProperty('targetNamespace')) { elem.target = utils.createID(orgID, projID, branID, elem.target); sourceTargetIDs.push(elem.target); } @@ -1790,7 +1790,7 @@ async function search(requestingUser, organizationID, projectID, branchID, query throw new M.DataFormatError(`The option '${o}' is not a boolean.`, 'warn'); } // Ensure the search option is a string - else if (typeof options[o] !== 'string' && o !== 'archived') { + else if (typeof options[o] !== 'string' && o !== 'archived' && !o.startsWith('custom.')) { throw new M.DataFormatError(`The option '${o}' is not a string.`, 'warn'); } @@ -1821,7 +1821,7 @@ async function search(requestingUser, organizationID, projectID, branchID, query // Permissions check permissions.readElement(reqUser, organization, project, branch); - searchQuery.$text = query; + if (query) searchQuery.$text = query; // If the includeArchived field is true, remove archived from the query; return everything if (validatedOptions.includeArchived) { delete searchQuery.archived; @@ -1967,9 +1967,8 @@ function sourceTargetNamespaceValidator(elem, index, orgID, projID, projectRefs, // Delete sourceNamespace, it does not get stored in the database delete elem.sourceNamespace; - // Remove the last source which has the wrong project + // Add source to array, used to ensure element exists if (sourceTargetIDs) { - sourceTargetIDs.pop(); sourceTargetIDs.push(elem.source); } } @@ -2008,9 +2007,8 @@ function sourceTargetNamespaceValidator(elem, index, orgID, projID, projectRefs, // Delete targetNamespace, it does not get stored in the database delete elem.targetNamespace; - // Remove the last target which has the wrong project + // Add target to array, used to ensure element exists if (sourceTargetIDs) { - sourceTargetIDs.pop(); sourceTargetIDs.push(elem.target); } } diff --git a/app/controllers/organization-controller.js b/app/controllers/organization-controller.js index 0c023ece..b020d611 100644 --- a/app/controllers/organization-controller.js +++ b/app/controllers/organization-controller.js @@ -924,12 +924,11 @@ async function remove(requestingUser, orgs, options) { // Delete any artifacts in the found projects await Artifact.deleteMany({ project: { $in: projectIDs } }); + const promises = []; // Remove all blobs under org - foundOrgIDs.forEach((orgID) => { - ArtifactStrategy.clear({ - orgID: orgID - }); - }); + foundOrgIDs.forEach((orgID) => promises.push(ArtifactStrategy.clear(orgID))); + await Promise.all(promises); + // Delete any branches in the found projects await Branch.deleteMany({ project: { $in: projectIDs } }); diff --git a/app/controllers/project-controller.js b/app/controllers/project-controller.js index 7001af82..2ced4098 100644 --- a/app/controllers/project-controller.js +++ b/app/controllers/project-controller.js @@ -1251,13 +1251,12 @@ async function remove(requestingUser, organizationID, projects, options) { // Delete any artifacts in the projects await Artifact.deleteMany(ownedQuery); + const promises = []; // Remove all blobs under the projects foundProjectIDs.forEach((p) => { - ArtifactStrategy.clear({ - orgID: orgID, - projectID: utils.parseID(p).pop() - }); + promises.push(ArtifactStrategy.clear(path.join(orgID, utils.parseID(p).pop()))); }); + await Promise.all(promises); // Delete any branches in the projects await Branch.deleteMany(ownedQuery); diff --git a/app/controllers/user-controller.js b/app/controllers/user-controller.js index c281b263..3bc2150c 100644 --- a/app/controllers/user-controller.js +++ b/app/controllers/user-controller.js @@ -948,8 +948,9 @@ async function search(requestingUser, query, options) { * currently stored password. * * @param {object} requestingUser - The object containing the requesting user. - * This is the users whose password is being changed. - * @param {string} oldPassword - The old password to confirm. + * @param {string} targetUser - The object containing the user whose password + * is to be changed. + * @param {string|null} oldPassword - The old password to confirm. * @param {string} newPassword - THe new password the user would like to set. * @param {string} confirmPassword - The new password entered a second time * to confirm they match. @@ -965,7 +966,8 @@ async function search(requestingUser, query, options) { * M.log.error(error); * }); */ -async function updatePassword(requestingUser, oldPassword, newPassword, confirmPassword) { +async function updatePassword(requestingUser, targetUser, oldPassword, newPassword, + confirmPassword) { try { // Ensure input parameters are correct type try { @@ -973,9 +975,13 @@ async function updatePassword(requestingUser, oldPassword, newPassword, confirmP assert.ok(requestingUser !== null, 'Requesting user cannot be null.'); // Ensure that requesting user has an _id field assert.ok(requestingUser._id, 'Requesting user is not populated.'); + assert.ok(typeof targetUser === 'string', 'Target username is not a string.'); - // Ensure all provided passwords are strings - assert.ok(typeof oldPassword === 'string', 'Old Password is not a string.'); + // Ensure all provided passwords are strings (oldPassword is only validated if a user is + // trying to set their own password) + if (requestingUser._id === targetUser) { + assert.ok(typeof oldPassword === 'string', 'Old Password is not a string.'); + } assert.ok(typeof newPassword === 'string', 'New Password is not a string.'); assert.ok(typeof confirmPassword === 'string', 'Confirm password is not a string'); assert.ok(confirmPassword === newPassword, 'Passwords do not match.'); @@ -986,9 +992,10 @@ async function updatePassword(requestingUser, oldPassword, newPassword, confirmP // Sanitize input parameters and create function-wide variables const reqUser = JSON.parse(JSON.stringify(requestingUser)); + const tarUser = JSON.parse(JSON.stringify(targetUser)); - // Find the requesting user - const userQuery = { _id: reqUser._id }; + // Find the target user + const userQuery = { _id: tarUser }; const foundUser = await User.findOne(userQuery); // Ensure the user was found @@ -996,12 +1003,18 @@ async function updatePassword(requestingUser, oldPassword, newPassword, confirmP throw new M.NotFoundError('User not found.', 'warn'); } - // Verify the old password matches - const verified = await User.verifyPassword(foundUser, oldPassword); + // Check if requesting and target user are the same, and requesting user is not an admin + if (reqUser._id !== tarUser && !reqUser.admin) { + throw new M.PermissionError('Cannot set another user\'s password.', 'warn'); + } + else if (reqUser._id === tarUser) { + // Verify the old password matches + const verified = await User.verifyPassword(foundUser, oldPassword); - // Ensure old password was verified - if (!verified) { - throw new M.AuthorizationError('Old password is incorrect.', 'warn'); + // Ensure old password was verified + if (!verified) { + throw new M.AuthorizationError('Old password is incorrect.', 'warn'); + } } // Verify that the new password has not been used in the previous stored passwords @@ -1013,8 +1026,11 @@ async function updatePassword(requestingUser, oldPassword, newPassword, confirmP User.hashPassword(foundUser); // Save the user with the updated password - await User.updateOne(userQuery, { password: foundUser.password, - oldPasswords: oldPasswords }); + await User.updateOne(userQuery, { + password: foundUser.password, + oldPasswords: oldPasswords, + changePassword: reqUser._id !== tarUser + }); // Find and return the updated user return await User.findOne(userQuery); diff --git a/app/controllers/webhook-controller.js b/app/controllers/webhook-controller.js index 989688e8..250571d3 100644 --- a/app/controllers/webhook-controller.js +++ b/app/controllers/webhook-controller.js @@ -324,6 +324,10 @@ async function create(requestingUser, webhooks, options) { webhookObj.reference = ''; } + if (webhookObj.hasOwnProperty('token')) { + webhookObj.token = `${reqUser._id}:${webhookObj.token}`; + } + webhookObj.lastModifiedBy = reqUser._id; webhookObj.createdBy = reqUser._id; webhookObj.updatedOn = Date.now(); diff --git a/app/lib/events.js b/app/lib/events.js index 5f2b76cf..2fb9e4c4 100644 --- a/app/lib/events.js +++ b/app/lib/events.js @@ -38,6 +38,7 @@ class CustomEmitter extends EventEmitter { // Find all webhooks that include the triggered event const webhooks = await Webhook.find({ type: 'Outgoing', triggers: event }); webhooks.forEach((webhook) => { + M.log.info(`Webhook ${webhook._id} triggered by event ${event}`); // Send the request with the provided arguments. Webhook.sendRequest(webhook, args); }); diff --git a/app/lib/get-public-data.js b/app/lib/get-public-data.js index 587f136d..2eaa977b 100644 --- a/app/lib/get-public-data.js +++ b/app/lib/get-public-data.js @@ -25,6 +25,7 @@ const utils = M.require('lib.utils'); * to be passed in, along with a string that says what type that object is. * Valid types are currently org, project, element and user. * + * @param {User} requestingUser - The user who made the request. * @param {object} object - The raw JSON of the object whose public data is * being returned. * @param {string} type - The type of item that the object is. Can be an org, @@ -33,7 +34,7 @@ const utils = M.require('lib.utils'); * * @returns {object} The modified object. */ -module.exports.getPublicData = function(object, type, options) { +module.exports.getPublicData = function(requestingUser, object, type, options) { // If options is undefined, set it equal to an empty object if (options === undefined) { options = {}; // eslint-disable-line @@ -42,19 +43,19 @@ module.exports.getPublicData = function(object, type, options) { // Call correct getPublicData() function switch (type.toLowerCase()) { case 'artifact': - return getArtifactPublicData(object, options); + return getArtifactPublicData(requestingUser, object, options); case 'element': - return getElementPublicData(object, options); + return getElementPublicData(requestingUser, object, options); case 'branch': - return getBranchPublicData(object, options); + return getBranchPublicData(requestingUser, object, options); case 'project': - return getProjectPublicData(object, options); + return getProjectPublicData(requestingUser, object, options); case 'org': - return getOrgPublicData(object, options); + return getOrgPublicData(requestingUser, object, options); case 'user': - return getUserPublicData(object, options); + return getUserPublicData(requestingUser, object, options); case 'webhook': - return getWebhookPublicData(object, options); + return getWebhookPublicData(requestingUser, object, options); default: throw new M.DataFormatError(`Invalid model type [${type}]`, 'warn'); } @@ -63,13 +64,14 @@ module.exports.getPublicData = function(object, type, options) { /** * @description Returns an artifacts public data. * + * @param {User} requestingUser - The user who made the request. * @param {object} artifact - The raw JSON of the artifact. * @param {object} options - A list of options passed in by the user to * the API Controller. * * @returns {object} The public data of the artifact. */ -function getArtifactPublicData(artifact, options) { +function getArtifactPublicData(requestingUser, artifact, options) { // Parse the artifact ID const idParts = utils.parseID(artifact._id); @@ -84,7 +86,7 @@ function getArtifactPublicData(artifact, options) { // If artifact.createdBy is populated if (typeof artifact.createdBy === 'object') { // Get the public data of createdBy - createdBy = getUserPublicData(artifact.createdBy, {}); + createdBy = getUserPublicData(requestingUser, artifact.createdBy, {}); } else { createdBy = artifact.createdBy; @@ -96,7 +98,7 @@ function getArtifactPublicData(artifact, options) { // If artifact.lastModifiedBy is populated if (typeof artifact.lastModifiedBy === 'object') { // Get the public data of lastModifiedBy - lastModifiedBy = getUserPublicData(artifact.lastModifiedBy, {}); + lastModifiedBy = getUserPublicData(requestingUser, artifact.lastModifiedBy, {}); } else { lastModifiedBy = artifact.lastModifiedBy; @@ -108,7 +110,7 @@ function getArtifactPublicData(artifact, options) { // If artifact.archivedBy is populated if (typeof artifact.archivedBy === 'object') { // Get the public data of archivedBy - archivedBy = getUserPublicData(artifact.archivedBy, {}); + archivedBy = getUserPublicData(requestingUser, artifact.archivedBy, {}); } else { archivedBy = artifact.archivedBy; @@ -120,7 +122,7 @@ function getArtifactPublicData(artifact, options) { // If artifact.branch is populated if (typeof artifact.branch === 'object') { // Get the public data of branch - branch = getBranchPublicData(artifact.branch, {}); + branch = getBranchPublicData(requestingUser, artifact.branch, {}); } else { branch = utils.parseID(artifact.branch).pop(); @@ -132,7 +134,7 @@ function getArtifactPublicData(artifact, options) { // If artifact.project is populated if (typeof artifact.project === 'object') { // Get the public data of project - project = getProjectPublicData(artifact.project, {}); + project = getProjectPublicData(requestingUser, artifact.project, {}); } else { project = idParts[1]; @@ -166,12 +168,14 @@ function getArtifactPublicData(artifact, options) { if (artifact.referencedBy.every(a => typeof a === 'object')) { // If the includeArchived option is supplied if (options.hasOwnProperty('includeArchived') && options.includeArchived === true) { - data.referencedBy = artifact.referencedBy.map(a => getElementPublicData(a, {})); + data.referencedBy = artifact.referencedBy + .map(a => getElementPublicData(requestingUser, a, {})); } else { // Remove all archived elements const nonArchivedElements = artifact.referencedBy.filter(a => a.archived !== true); - data.referencedBy = nonArchivedElements.map(a => getElementPublicData(a, {})); + data.referencedBy = nonArchivedElements + .map(a => getElementPublicData(requestingUser, a, {})); } } } @@ -206,12 +210,13 @@ function getArtifactPublicData(artifact, options) { /** * @description Returns an elements public data. * + * @param {User} requestingUser - The user who made the request. * @param {object} element - The raw JSON of the element. * @param {object} options - A list of options passed in by the user to the API Controller. * * @returns {object} The public data of the element. */ -function getElementPublicData(element, options) { +function getElementPublicData(requestingUser, element, options) { // Parse the element ID const idParts = utils.parseID(element._id); @@ -232,7 +237,7 @@ function getElementPublicData(element, options) { // If element.createdBy is populated if (typeof element.createdBy === 'object') { // Get the public data of createdBy - createdBy = getUserPublicData(element.createdBy, {}); + createdBy = getUserPublicData(requestingUser, element.createdBy, {}); } else { createdBy = element.createdBy; @@ -244,7 +249,7 @@ function getElementPublicData(element, options) { // If element.lastModifiedBy is populated if (typeof element.lastModifiedBy === 'object') { // Get the public data of lastModifiedBy - lastModifiedBy = getUserPublicData(element.lastModifiedBy, {}); + lastModifiedBy = getUserPublicData(requestingUser, element.lastModifiedBy, {}); } else { lastModifiedBy = element.lastModifiedBy; @@ -256,7 +261,7 @@ function getElementPublicData(element, options) { // If element.archivedBy is populated if (typeof element.archivedBy === 'object') { // Get the public data of archivedBy - archivedBy = getUserPublicData(element.archivedBy, {}); + archivedBy = getUserPublicData(requestingUser, element.archivedBy, {}); } else { archivedBy = element.archivedBy; @@ -268,7 +273,7 @@ function getElementPublicData(element, options) { // If element.parent is populated if (typeof element.parent === 'object') { // Get the public data of parent - parent = getElementPublicData(element.parent, {}); + parent = getElementPublicData(requestingUser, element.parent, {}); } else { parent = utils.parseID(element.parent).pop(); @@ -280,7 +285,7 @@ function getElementPublicData(element, options) { // If element.source is populated if (typeof element.source === 'object') { // Get the public data of source - source = getElementPublicData(element.source, {}); + source = getElementPublicData(requestingUser, element.source, {}); } else { const sourceIdParts = utils.parseID(element.source); @@ -306,7 +311,7 @@ function getElementPublicData(element, options) { // If element.target is populated if (typeof element.target === 'object') { // Get the public data of target - target = getElementPublicData(element.target, {}); + target = getElementPublicData(requestingUser, element.target, {}); } else { const targetIdParts = utils.parseID(element.target); @@ -332,7 +337,7 @@ function getElementPublicData(element, options) { // If element.branch is populated if (typeof element.branch === 'object') { // Get the public data of branch - branch = getBranchPublicData(element.branch, {}); + branch = getBranchPublicData(requestingUser, element.branch, {}); } else { branch = utils.parseID(element.branch).pop(); @@ -344,7 +349,7 @@ function getElementPublicData(element, options) { // If element.project is populated if (typeof element.project === 'object') { // Get the public data of project - project = getProjectPublicData(element.project, {}); + project = getProjectPublicData(requestingUser, element.project, {}); } else { project = utils.parseID(element.project)[1]; @@ -356,7 +361,7 @@ function getElementPublicData(element, options) { // If element.artifact is populated if (typeof element.artifact === 'object') { // Get the public data of parent - artifact = getArtifactPublicData(element.artifact, {}); + artifact = getArtifactPublicData(requestingUser, element.artifact, {}); } else { artifact = utils.parseID(element.artifact).pop(); @@ -396,7 +401,7 @@ function getElementPublicData(element, options) { if (options.hasOwnProperty('includeArchived') && options.includeArchived === true) { // If the user specified 'contains' in the populate field of options if (options.populate && options.populate.includes('contains')) { - data.contains = element.contains.map(e => getElementPublicData(e, {})); + data.contains = element.contains.map(e => getElementPublicData(requestingUser, e, {})); } else { data.contains = element.contains.map(e => utils.parseID(e._id).pop()); @@ -406,7 +411,7 @@ function getElementPublicData(element, options) { // Remove all archived elements const tmpContains = element.contains.filter(e => e.archived !== true); if (options.populate && options.populate.includes('contains')) { - data.contains = tmpContains.map(e => getElementPublicData(e, {})); + data.contains = tmpContains.map(e => getElementPublicData(requestingUser, e, {})); } else { data.contains = tmpContains.map(e => utils.parseID(e._id).pop()); @@ -423,7 +428,7 @@ function getElementPublicData(element, options) { if (options.hasOwnProperty('includeArchived') && options.includeArchived === true) { // If user is populating sourceOf, return objects else just ids if (options.populate && options.populate.includes('sourceOf')) { - data.sourceOf = element.sourceOf.map(e => getElementPublicData(e, {})); + data.sourceOf = element.sourceOf.map(e => getElementPublicData(requestingUser, e, {})); } else { data.sourceOf = element.sourceOf.map(e => utils.parseID(e._id).pop()); @@ -434,7 +439,7 @@ function getElementPublicData(element, options) { const tmpSourceOf = element.sourceOf.filter(e => e.archived !== true); // If user is populating sourceOf, return objects else just ids if (options.populate && options.populate.includes('sourceOf')) { - data.sourceOf = tmpSourceOf.map(e => getElementPublicData(e, {})); + data.sourceOf = tmpSourceOf.map(e => getElementPublicData(requestingUser, e, {})); } else { data.sourceOf = tmpSourceOf.map(e => utils.parseID(e._id).pop()); @@ -451,7 +456,7 @@ function getElementPublicData(element, options) { if (options.hasOwnProperty('includeArchived') && options.includeArchived === true) { // If user is populating targetOf, return objects else just ids if (options.populate && options.populate.includes('targetOf')) { - data.targetOf = element.targetOf.map(e => getElementPublicData(e, {})); + data.targetOf = element.targetOf.map(e => getElementPublicData(requestingUser, e, {})); } else { data.targetOf = element.targetOf.map(e => utils.parseID(e._id).pop()); @@ -462,7 +467,7 @@ function getElementPublicData(element, options) { const tmpTargetOf = element.targetOf.filter(e => e.archived !== true); // If user is populating targetOf, return objects else just ids if (options.populate && options.populate.includes('targetOf')) { - data.targetOf = tmpTargetOf.map(e => getElementPublicData(e, {})); + data.targetOf = tmpTargetOf.map(e => getElementPublicData(requestingUser, e, {})); } else { data.targetOf = tmpTargetOf.map(e => utils.parseID(e._id).pop()); @@ -501,12 +506,13 @@ function getElementPublicData(element, options) { /** * @description Returns a branch public data. * + * @param {User} requestingUser - The user who made the request. * @param {object} branch - The raw JSON of the branch. * @param {object} options - A list of options passed in by the user to the API Controller. * * @returns {object} The public data of the branch. */ -function getBranchPublicData(branch, options) { +function getBranchPublicData(requestingUser, branch, options) { // Parse the branch ID const idParts = utils.parseID(branch._id); let createdBy = null; @@ -520,7 +526,7 @@ function getBranchPublicData(branch, options) { // If branch.createdBy is populated if (typeof branch.createdBy === 'object') { // Get the public data of createdBy - createdBy = getUserPublicData(branch.createdBy, {}); + createdBy = getUserPublicData(requestingUser, branch.createdBy, {}); } else { createdBy = branch.createdBy; @@ -532,7 +538,7 @@ function getBranchPublicData(branch, options) { // If branch.lastModifiedBy is populated if (typeof branch.lastModifiedBy === 'object') { // Get the public data of lastModifiedBy - lastModifiedBy = getUserPublicData(branch.lastModifiedBy, {}); + lastModifiedBy = getUserPublicData(requestingUser, branch.lastModifiedBy, {}); } else { lastModifiedBy = branch.lastModifiedBy; @@ -544,7 +550,7 @@ function getBranchPublicData(branch, options) { // If branch.archivedBy is populated if (typeof branch.archivedBy === 'object') { // Get the public data of archivedBy - archivedBy = getUserPublicData(branch.archivedBy, {}); + archivedBy = getUserPublicData(requestingUser, branch.archivedBy, {}); } else { archivedBy = branch.archivedBy; @@ -556,7 +562,7 @@ function getBranchPublicData(branch, options) { // If branch.project is populated if (typeof branch.project === 'object') { // Get the public data of project - project = getProjectPublicData(branch.project, {}); + project = getProjectPublicData(requestingUser, branch.project, {}); } else { project = utils.parseID(branch.project)[1]; @@ -568,7 +574,7 @@ function getBranchPublicData(branch, options) { // If branch.source is populated if (typeof branch.source === 'object') { // Get the public data of branch - source = getBranchPublicData(branch.source, {}); + source = getBranchPublicData(requestingUser, branch.source, {}); } else { source = utils.parseID(branch.source).pop(); @@ -624,12 +630,13 @@ function getBranchPublicData(branch, options) { /** * @description Returns a projects public data. * + * @param {User} requestingUser - The user who made the request. * @param {object} project - The raw JSON of the project. * @param {object} options - A list of options passed in by the user to the API Controller. * * @returns {object} The public data of the project. */ -function getProjectPublicData(project, options) { +function getProjectPublicData(requestingUser, project, options) { const permissions = (project.permissions) ? {} : undefined; let createdBy = null; let lastModifiedBy = null; @@ -654,7 +661,7 @@ function getProjectPublicData(project, options) { // If project.createdBy is populated if (typeof project.createdBy === 'object') { // Get the public data of createdBy - createdBy = getUserPublicData(project.createdBy, {}); + createdBy = getUserPublicData(requestingUser, project.createdBy, {}); } else { createdBy = project.createdBy; @@ -666,7 +673,7 @@ function getProjectPublicData(project, options) { // If project.lastModifiedBy is populated if (typeof project.lastModifiedBy === 'object') { // Get the public data of lastModifiedBy - lastModifiedBy = getUserPublicData(project.lastModifiedBy, {}); + lastModifiedBy = getUserPublicData(requestingUser, project.lastModifiedBy, {}); } else { lastModifiedBy = project.lastModifiedBy; @@ -678,7 +685,7 @@ function getProjectPublicData(project, options) { // If project.archivedBy is populated if (typeof project.archivedBy === 'object') { // Get the public data of archivedBy - archivedBy = getUserPublicData(project.archivedBy, {}); + archivedBy = getUserPublicData(requestingUser, project.archivedBy, {}); } else { archivedBy = project.archivedBy; @@ -689,7 +696,7 @@ function getProjectPublicData(project, options) { const data = { id: utils.parseID(project._id).pop(), org: (project.org && project.org._id) - ? getOrgPublicData(project.org, {}) + ? getOrgPublicData(requestingUser, project.org, {}) : utils.parseID(project._id)[0], name: project.name, permissions: permissions, @@ -735,12 +742,13 @@ function getProjectPublicData(project, options) { /** * @description Returns an orgs public data. * + * @param {User} requestingUser - The user who made the request. * @param {object} org - The raw JSON of the org. * @param {object} options - A list of options passed in by the user to the API Controller. * * @returns {object} The public data of the org. */ -function getOrgPublicData(org, options) { +function getOrgPublicData(requestingUser, org, options) { const permissions = (org.permissions) ? {} : undefined; let createdBy = null; let lastModifiedBy = null; @@ -766,7 +774,7 @@ function getOrgPublicData(org, options) { // If org.createdBy is populated if (typeof org.createdBy === 'object') { // Get the public data of createdBy - createdBy = getUserPublicData(org.createdBy, {}); + createdBy = getUserPublicData(requestingUser, org.createdBy, {}); } else { createdBy = org.createdBy; @@ -778,7 +786,7 @@ function getOrgPublicData(org, options) { // If org.lastModifiedBy is populated if (typeof org.lastModifiedBy === 'object') { // Get the public data of lastModifiedBy - lastModifiedBy = getUserPublicData(org.lastModifiedBy, {}); + lastModifiedBy = getUserPublicData(requestingUser, org.lastModifiedBy, {}); } else { lastModifiedBy = org.lastModifiedBy; @@ -790,7 +798,7 @@ function getOrgPublicData(org, options) { // If org.archivedBy is populated if (typeof org.archivedBy === 'object') { // Get the public data of archivedBy - archivedBy = getUserPublicData(org.archivedBy, {}); + archivedBy = getUserPublicData(requestingUser, org.archivedBy, {}); } else { archivedBy = org.archivedBy; @@ -803,12 +811,13 @@ function getOrgPublicData(org, options) { if (org.projects.every(p => typeof p === 'object')) { // If the archived option is supplied if (options.hasOwnProperty('includeArchived') && options.includeArchived === true) { - projects = org.projects.map(p => getProjectPublicData(p, { archived: true })); + projects = org.projects + .map(p => getProjectPublicData(requestingUser, p, { archived: true })); } else { // Remove all archived projects const tmpContains = org.projects.filter(p => p.archived !== true); - projects = tmpContains.map(p => getProjectPublicData(p, {})); + projects = tmpContains.map(p => getProjectPublicData(requestingUser, p, {})); } } } @@ -860,12 +869,13 @@ function getOrgPublicData(org, options) { /** * @description Returns a users public data. * + * @param {User} requestingUser - The user who made the request. * @param {object} user - The raw JSON of the user. * @param {object} options - A list of options passed in by the user to the API Controller. * * @returns {object} The public data of the user. */ -function getUserPublicData(user, options) { +function getUserPublicData(requestingUser, user, options) { let createdBy = null; let lastModifiedBy = null; let archivedBy; @@ -875,7 +885,7 @@ function getUserPublicData(user, options) { // If user.createdBy is populated if (typeof user.createdBy === 'object') { // Get the public data of createdBy - createdBy = getUserPublicData(user.createdBy, {}); + createdBy = getUserPublicData(requestingUser, user.createdBy, {}); } else { createdBy = user.createdBy; @@ -887,7 +897,7 @@ function getUserPublicData(user, options) { // If user.lastModifiedBy is populated if (typeof user.lastModifiedBy === 'object') { // Get the public data of lastModifiedBy - lastModifiedBy = getUserPublicData(user.lastModifiedBy, {}); + lastModifiedBy = getUserPublicData(requestingUser, user.lastModifiedBy, {}); } else { lastModifiedBy = user.lastModifiedBy; @@ -899,7 +909,7 @@ function getUserPublicData(user, options) { // If user.archivedBy is populated if (typeof user.archivedBy === 'object') { // Get the public data of archivedBy - archivedBy = getUserPublicData(user.archivedBy, {}); + archivedBy = getUserPublicData(requestingUser, user.archivedBy, {}); } else { archivedBy = user.archivedBy; @@ -921,10 +931,15 @@ function getUserPublicData(user, options) { archivedOn: (user.archivedOn) ? user.archivedOn.toString() : undefined, archivedBy: archivedBy, admin: user.admin, - provider: user.provider, - failedlogins: (options.failedlogins) ? user.failedlogins : undefined + provider: user.provider }; + // Add in admin/self specific fields + if (requestingUser.admin || requestingUser._id === data.username) { + data.changePassword = user.changePassword; + data.failedlogins = user.failedlogins; + } + // If the fields options is defined if (options.hasOwnProperty('fields')) { // If fields should be excluded @@ -956,12 +971,13 @@ function getUserPublicData(user, options) { /** * @description Returns a webhook's public data. * + * @param {User} requestingUser - The user who made the request. * @param {object} webhook - The raw JSON of the webhook. * @param {object} options - A list of options passed in by the user to the API Controller. * * @returns {object} The public data of the webhook. */ -function getWebhookPublicData(webhook, options) { +function getWebhookPublicData(requestingUser, webhook, options) { let reference = {}; let createdBy = null; let lastModifiedBy = null; @@ -983,7 +999,7 @@ function getWebhookPublicData(webhook, options) { // If webhook.createdBy is populated if (typeof webhook.createdBy === 'object') { // Get the public data of createdBy - createdBy = getUserPublicData(webhook.createdBy, {}); + createdBy = getUserPublicData(requestingUser, webhook.createdBy, {}); } else { createdBy = webhook.createdBy; @@ -995,7 +1011,7 @@ function getWebhookPublicData(webhook, options) { // If webhook.lastModifiedBy is populated if (typeof webhook.lastModifiedBy === 'object') { // Get the public data of lastModifiedBy - lastModifiedBy = getUserPublicData(webhook.lastModifiedBy, {}); + lastModifiedBy = getUserPublicData(requestingUser, webhook.lastModifiedBy, {}); } else { lastModifiedBy = webhook.lastModifiedBy; @@ -1007,13 +1023,19 @@ function getWebhookPublicData(webhook, options) { // If webhook.archivedBy is populated if (typeof webhook.archivedBy === 'object') { // Get the public data of archivedBy - archivedBy = getUserPublicData(webhook.archivedBy, {}); + archivedBy = getUserPublicData(requestingUser, webhook.archivedBy, {}); } else { archivedBy = webhook.archivedBy; } } + // Process token for incoming webhooks + let token; + if (webhook.type === 'Incoming') { + token = Buffer.from(webhook.token, 'ascii').toString('base64'); + } + // Return the webhook public fields const data = { id: webhook._id, @@ -1022,7 +1044,7 @@ function getWebhookPublicData(webhook, options) { description: webhook.description, triggers: webhook.triggers, response: webhook.response ? webhook.response : undefined, - token: webhook.token ? webhook.token : undefined, + token: token, tokenLocation: webhook.tokenLocation ? webhook.tokenLocation : undefined, reference: reference, custom: webhook.custom || {}, diff --git a/app/lib/logger.js b/app/lib/logger.js index 403ab213..c1f6a742 100644 --- a/app/lib/logger.js +++ b/app/lib/logger.js @@ -248,7 +248,9 @@ function logSecurityResponse(req, res) { */ function formatResponseLog(req, res) { const responseMessage = (res.locals && res.locals.message) ? res.locals.message : ''; - const responseLength = responseMessage.length; + const responseLength = typeof responseMessage === 'string' + ? responseMessage.length + : responseMessage.toString().length; const statusCode = res.locals.statusCode ? res.locals.statusCode : res.statusCode; // Set username to anonymous if req.user is not defined diff --git a/app/lib/middleware.js b/app/lib/middleware.js index d32e1045..3cd93134 100644 --- a/app/lib/middleware.js +++ b/app/lib/middleware.js @@ -22,6 +22,7 @@ const path = require('path'); const fs = require('fs'); // MBEE modules +const errors = M.require('lib.errors'); const utils = M.require('lib.utils'); const logger = M.require('lib.logger'); @@ -239,3 +240,31 @@ module.exports.respond = function respond(req, res) { return res; }; + +/** + * @description Checks a requesting user to see if their password has expired. + * If so, a 401 Unauthorized error is returned. + * @param {object} req - Request express object. + * @param {object} res - Response express object. + * @param {Function} next - Callback to express authentication flow. + */ +// eslint-disable-next-line consistent-return +module.exports.expiredPassword = function(req, res, next) { + // If the user needs to change their password + if (req.user.changePassword) { + // If it is NOT an API request + if (!req.originalUrl.startsWith('/api')) { + // Redirect user to their profile page + return res.redirect('/profile'); + } + // API request, return a 401 error + else { + const error = new M.AuthorizationError('User\'s password has expired.'); + return res.status(errors.getStatusCode(error)).send(error.message); + } + } + // User does not need to change password, proceed with request + else { + next(); + } +}; diff --git a/app/lib/migrate.js b/app/lib/migrate.js index e7060106..4e9438eb 100644 --- a/app/lib/migrate.js +++ b/app/lib/migrate.js @@ -72,7 +72,7 @@ module.exports.migrate = async function(args) { const knownVersions = ['0.6.0', '0.6.0.1', '0.7.0', '0.7.1', '0.7.2', '0.7.3', '0.7.3.1', '0.8.0', '0.8.1', '0.8.2', '0.8.3', '0.9.0', '0.9.1', '0.9.2', '0.9.3', '0.9.4', '0.9.5', '0.10.0', '0.10.1', '0.10.2', '0.10.3', - '0.10.4', '0.10.5', '1.0.0']; + '0.10.4', '0.10.5', '1.0.0', '1.0.1']; // Run the migrations await runMigrations(knownVersions.slice(knownVersions.indexOf(fromVersion) + 1)); diff --git a/app/lib/permissions.js b/app/lib/permissions.js index 21d9040d..512084ce 100644 --- a/app/lib/permissions.js +++ b/app/lib/permissions.js @@ -54,7 +54,8 @@ module.exports = { updateBranch, updateArtifact, updateWebhook, - getLogs + getLogs, + listBlobs }; /** @@ -689,6 +690,34 @@ function deleteBlob(user, org, project) { } } +/** + * @description Verify if user has permission to list blobs in the project. + * + * @param {User} user - The user object to check permissions for. + * @param {Organization} org - The org object containing the project. + * @param {Project} project - The project containing the artifact blob. + * + * @throws {PermissionError} + */ +function listBlobs(user, org, project) { + try { + if (!user.admin) { + // User needs read permission of the org, regardless of the project visibility + assert.ok(org.permissions.hasOwnProperty(user._id), + `User does not have permission to find items in the org [${org._id}].`); + + if (project.visibility === 'private') { + assert.ok(project.permissions.hasOwnProperty(user._id), + 'User does not have permission to get artifacts in the project ' + + `[${utils.parseID(project._id).pop()}].`); + } + } + } + catch (error) { + throw new M.PermissionError(error.message, 'warn'); + } +} + /** * @description Verifies that the user has permission to create webhooks. * diff --git a/app/lib/test-utils.js b/app/lib/test-utils.js index 968d6f25..a9c6a603 100644 --- a/app/lib/test-utils.js +++ b/app/lib/test-utils.js @@ -120,7 +120,8 @@ module.exports.createTestAdmin = async function() { _id: testData.adminUser.username, password: testData.adminUser.password, provider: 'local', - admin: true + admin: true, + changePassword: false }; User.hashPassword(user); @@ -277,9 +278,8 @@ module.exports.removeTestOrg = async function() { // Delete any artifacts in the org await Artifact.deleteMany({ project: { $in: projectIDs } }); - ArtifactStrategy.clear({ - orgID: testData.orgs[0].id - }); + // Clear blobs + await ArtifactStrategy.clear(testData.orgs[0].id); // Delete any elements in the found projects await Element.deleteMany({ project: { $in: projectIDs } }); diff --git a/app/lib/utils.js b/app/lib/utils.js index 34903abe..b4ad14b5 100644 --- a/app/lib/utils.js +++ b/app/lib/utils.js @@ -87,7 +87,7 @@ module.exports.render = function(req, res, name, params) { opts.pluginNames = (M.config.server.plugins.enabled) ? require(path.join(M.root, 'plugins', 'routes.js')).loadedPlugins : []; // eslint-disable-line global-require opts.ui = opts.ui || M.config.server.ui; - opts.user = opts.user || ((req.user) ? publicData.getPublicData(req.user, 'user') : ''); + opts.user = opts.user || ((req.user) ? publicData.getPublicData(req.user, req.user, 'user', {}) : ''); opts.title = opts.title || 'Model-Based Engineering Environment'; return res.render(name, opts); }; diff --git a/app/lib/validators.js b/app/lib/validators.js index 2bbc2fe7..6f7d0e0c 100644 --- a/app/lib/validators.js +++ b/app/lib/validators.js @@ -28,15 +28,15 @@ const artifactVal = M.require(`artifact.${M.config.artifact.strategy}`).validato const customValidators = M.config.validators || {}; // This ID is used as the common regex for other ID fields in this module -const id = customValidators.id || '([_a-z0-9])([-_a-z0-9.]){0,}'; -const idLength = customValidators.id_length || 36; +const id = customValidators.id || '([_a-zA-Z0-9])([-_a-zA-Z0-9.]){0,}'; +const idLength = Number(customValidators.id_length) || 40; // A list of reserved keywords which cannot be used in ids const reservedKeywords = ['css', 'js', 'img', 'doc', 'docs', 'webfonts', 'login', 'about', 'assets', 'static', 'public', 'api', 'organizations', 'orgs', 'projects', 'users', 'plugins', 'ext', 'extension', 'search', 'whoami', 'profile', 'edit', 'proj', 'elements', 'branch', 'anonymous', - 'blob', 'artifact', 'artifacts']; + 'blob', 'artifact', 'artifacts', 'list']; // Create a validator function to test ids against the reserved keywords const reserved = function(data) { diff --git a/app/models/user.js b/app/models/user.js index fdeb1ca0..a13932e8 100644 --- a/app/models/user.js +++ b/app/models/user.js @@ -72,6 +72,8 @@ const extensions = M.require('models.plugin.extensions'); * @property {object} failedlogins - Stores the history of failed login attempts. * @property {object} oldPasswords - Stores previous passwords; used to prevent users * from re-using recent passwords. + * @property {boolean} [changePassword=true] - A boolean which if true, blocks + * users from making requests until they change their password. * */ const UserSchema = new db.Schema({ @@ -158,6 +160,10 @@ const UserSchema = new db.Schema({ }, oldPasswords: { type: 'Object' + }, + changePassword: { + type: 'Boolean', + default: true } }); diff --git a/app/models/webhook.js b/app/models/webhook.js index 11ab5b0d..f44ceda6 100644 --- a/app/models/webhook.js +++ b/app/models/webhook.js @@ -217,7 +217,9 @@ WebhookSchema.static('sendRequest', function(webhook, data) { if (webhook.response.ca) options.ca = webhook.response.ca; if (webhook.response.token) options.token = webhook.response.token; // Send an HTTP request to given URL - request(options); + request(options, (err) => { + if (err) M.log.warn(`Webhook ${webhook._id} request error: ${err.message}`); + }); }); /** diff --git a/app/routes.js b/app/routes.js index 22cb3bd1..7e9c712a 100644 --- a/app/routes.js +++ b/app/routes.js @@ -78,6 +78,7 @@ router.route('/login') router.route('/') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, UIController.home ); @@ -89,6 +90,7 @@ router.route('/') router.route('/admin') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, UIController.adminConsole ); @@ -100,6 +102,7 @@ router.route('/admin') router.route('/admin/orgs') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, UIController.adminConsole ); @@ -111,6 +114,7 @@ router.route('/admin/orgs') router.route('/admin/projects') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, UIController.adminConsole ); @@ -137,6 +141,7 @@ router.param('username', (req, res, next, username) => { router.route('/profile/:username') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, UIController.profile ); @@ -186,6 +191,7 @@ router.param('elementid', (req, res, next, branch) => { router.route('/orgs/:orgid') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, UIController.organization ); @@ -196,6 +202,7 @@ router.route('/orgs/:orgid') router.route('/orgs/:orgid/users') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, UIController.organization ); @@ -206,6 +213,7 @@ router.route('/orgs/:orgid/users') router.route('/orgs/:orgid/projects') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, UIController.organization ); @@ -216,6 +224,7 @@ router.route('/orgs/:orgid/projects') router.route('/orgs/:orgid/projects/:projectid/info') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, UIController.project ); @@ -226,6 +235,7 @@ router.route('/orgs/:orgid/projects/:projectid/info') router.route('/orgs/:orgid/projects/:projectid/users') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, UIController.project ); @@ -236,6 +246,7 @@ router.route('/orgs/:orgid/projects/:projectid/users') router.route('/orgs/:orgid/projects/:projectid/branches') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, UIController.project ); @@ -246,6 +257,7 @@ router.route('/orgs/:orgid/projects/:projectid/branches') router.route('/orgs/:orgid/projects/:projectid/branches/:branchid') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, UIController.project ); @@ -256,6 +268,7 @@ router.route('/orgs/:orgid/projects/:projectid/branches/:branchid') router.route('/orgs/:orgid/projects/:projectid/branches/:branchid/elements') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, UIController.project ); @@ -266,6 +279,7 @@ router.route('/orgs/:orgid/projects/:projectid/branches/:branchid/elements') router.route('/orgs/:orgid/projects/:projectid/branches/:branchid/elements#:elementid') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, UIController.project ); @@ -276,6 +290,7 @@ router.route('/orgs/:orgid/projects/:projectid/branches/:branchid/elements#:elem router.route('/orgs/:orgid/projects/:projectid/branches/:branchid/artifacts') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, UIController.project ); @@ -286,6 +301,7 @@ router.route('/orgs/:orgid/projects/:projectid/branches/:branchid/artifacts') router.route('/orgs/:orgid/projects/:projectid/branches/:branchid/search') .get( AuthController.authenticate, + Middleware.expiredPassword, Middleware.logRoute, UIController.project ); diff --git a/app/ui/components/admin-console-views/create-user.jsx b/app/ui/components/admin-console-views/create-user.jsx index 89dc9023..9084cb53 100644 --- a/app/ui/components/admin-console-views/create-user.jsx +++ b/app/ui/components/admin-console-views/create-user.jsx @@ -151,50 +151,21 @@ class CreateUser extends Component { render() { // Initialize validators - let usernameInvalid; - let fnameInvalid; - let preferredInvalid; - let lnameInvalid; - let emailInvalid; - let disableSubmit; - let customInvalid; + let usernameInvalid = false; + const usernameLengthInvalid = (this.state.username.length > validators.user.usernameLength); + const fnameInvalid = (!RegExp(validators.user.firstName).test(this.state.fname)); + const lnameInvalid = (!RegExp(validators.user.lastName).test(this.state.lname)); + const preferredInvalid = (!RegExp(validators.user.firstName).test(this.state.preferredname)); + let emailInvalid = false; + let customInvalid = false; - if (this.state.username.length !== 0) { - // Verify if project name is valid - if (!RegExp(validators.user.username).test(this.state.username)) { - // Set invalid fields - usernameInvalid = true; - disableSubmit = true; - } - } - - if (!RegExp(validators.user.fname).test(this.state.fname)) { - // Set invalid fields - fnameInvalid = true; - disableSubmit = true; - } - - if (!RegExp(validators.user.fname).test(this.state.preferredname)) { - // Set invalid fields - preferredInvalid = true; - disableSubmit = true; + if (this.state.email.length !== 0) { + emailInvalid = (!RegExp(validators.user.email).test(this.state.email)); } - if (!RegExp(validators.user.lname).test(this.state.lname)) { - // Set invalid fields - lnameInvalid = true; - disableSubmit = true; - } - - // Verify if project name is valid - if (!RegExp(validators.user.email).test(this.state.email)) { - // Set invalid fields - emailInvalid = true; - disableSubmit = true; - } - - if (this.state.passwordInvalid) { - disableSubmit = true; + if (this.state.username.length !== 0) { + // eslint-disable-next-line max-len + usernameInvalid = ((!RegExp(validators.user.username).test(this.state.username)) || usernameLengthInvalid); } // Verify if custom data is correct JSON format @@ -202,11 +173,17 @@ class CreateUser extends Component { JSON.parse(this.state.custom); } catch (err) { - // Set invalid fields customInvalid = true; - disableSubmit = true; } + const disableSubmit = (fnameInvalid + || lnameInvalid + || preferredInvalid + || emailInvalid + || usernameInvalid + || customInvalid + || this.state.passwordInvalid); + // Return the form to create a project return (
diff --git a/app/ui/components/admin-console-views/user-list.jsx b/app/ui/components/admin-console-views/user-list.jsx index 526aafb0..ed364daa 100644 --- a/app/ui/components/admin-console-views/user-list.jsx +++ b/app/ui/components/admin-console-views/user-list.jsx @@ -31,6 +31,7 @@ import UserListItem from '../shared-views/list-items/user-list-item.jsx'; import CreateUser from './create-user.jsx'; import DeleteUser from './delete-user.jsx'; import EditUser from '../profile-views/profile-edit.jsx'; +import PasswordEdit from '../profile-views/password-edit.jsx'; // Define component class UserList extends Component { @@ -49,6 +50,7 @@ class UserList extends Component { modalDelete: false, modalEdit: false, selectedUser: null, + editPasswordModal: false, error: null }; @@ -59,6 +61,7 @@ class UserList extends Component { this.handleEditToggle = this.handleEditToggle.bind(this); this.handleCreateToggle = this.handleCreateToggle.bind(this); this.handleDeleteToggle = this.handleDeleteToggle.bind(this); + this.togglePasswordModal = this.togglePasswordModal.bind(this); } handleDeleteToggle(username) { @@ -153,6 +156,13 @@ class UserList extends Component { this.setState({ width: this.ref.current.clientWidth }); } + // Define toggle function + togglePasswordModal() { + // Open or close modal + this.setState((prevState) => ({ editPasswordModal: !prevState.editPasswordModal })); + } + + render() { let users; @@ -204,9 +214,16 @@ class UserList extends Component { + + + + + {/* Display the list of users */}
diff --git a/app/ui/components/profile-views/password-edit.jsx b/app/ui/components/profile-views/password-edit.jsx index ca68bf33..42929bc2 100644 --- a/app/ui/components/profile-views/password-edit.jsx +++ b/app/ui/components/profile-views/password-edit.jsx @@ -39,13 +39,18 @@ class PasswordEdit extends Component { // Initialize parent props super(props); + // Get the session user, used to see if an admin is changing another user's password + const sessionUser = JSON.parse(window.sessionStorage.getItem('mbee-user')); + // Initialize state props this.state = { oldPassword: '', newPassword: '', confirmNewPassword: '', newPasswordInvalid: false, - error: null + noOldPassword: sessionUser.admin && sessionUser.username !== this.props.user.username, + error: null, + sessionUser: sessionUser }; // Bind component functions @@ -106,7 +111,17 @@ class PasswordEdit extends Component { contentType: 'application/json', data: JSON.stringify(data), statusCode: { - 200: () => { window.location.replace('/profile'); }, + 200: () => { + this.props.toggle(); + + // Destroy the session if user changed their own password + if (this.state.sessionUser.username === this.props.user.username) { + window.sessionStorage.removeItem('mbee-user'); + } + }, + 400: (err) => { + this.setState({ error: err.responseText }); + }, 401: (err) => { this.setState({ error: err.responseText }); @@ -142,7 +157,7 @@ class PasswordEdit extends Component { return (
-

User Edit

+

Change Password

@@ -155,15 +170,17 @@ class PasswordEdit extends Component { {/* Create form to update user password */}
{/* Input old password */} - - - - + {(this.state.noOldPassword) + ? '' + : + + + } {/* Input new password */} @@ -190,13 +207,17 @@ class PasswordEdit extends Component { invalid={confirmPasswordInvalid} onChange={this.handleChange}/> - Invalid: Password are not the same. + Invalid: Passwords are not the same. {/* Button to submit or cancel */} - + {' '} - +
diff --git a/app/ui/components/profile-views/profile-edit.jsx b/app/ui/components/profile-views/profile-edit.jsx index df344260..c158022a 100644 --- a/app/ui/components/profile-views/profile-edit.jsx +++ b/app/ui/components/profile-views/profile-edit.jsx @@ -128,14 +128,20 @@ class ProfileEdit extends Component { render() { // Initialize variables - let fnameInvalid; - let lnameInvalid; - let preferredInvalid; - let customInvalid; - let disableSubmit; + const fnameInvalid = (!RegExp(validators.user.firstName).test(this.state.fname)); + const lnameInvalid = (!RegExp(validators.user.lastName).test(this.state.lname)); + const preferredInvalid = (!RegExp(validators.user.firstName).test(this.state.preferredname)); + let emailInvalid = false; + let customInvalid = false; let titleClass = 'workspace-title workspace-title-padding'; let localUser = false; let adminUser = false; + + // Ensure the characters have been entered first + if (this.state.email.length !== 0) { + emailInvalid = (!RegExp(validators.user.email).test(this.state.email)); + } + // Check admin/write permissions if (this.props.user.provider === 'local') { localUser = true; @@ -147,30 +153,9 @@ class ProfileEdit extends Component { } if (this.props.viewingUser) { - localUser = false; adminUser = this.props.viewingUser.admin; } - // Verify if user's first name is valid - if (!RegExp(validators.user.fname).test(this.state.fname)) { - // Set invalid fields - fnameInvalid = true; - disableSubmit = true; - } - - if (!RegExp(validators.user.fname).test(this.state.preferredname)) { - // Set invalid fields - preferredInvalid = true; - disableSubmit = true; - } - - // Verify if user's last name is valid - if (!RegExp(validators.user.lname).test(this.state.lname)) { - // Set invalid fields - lnameInvalid = true; - disableSubmit = true; - } - // Verify if custom data is correct JSON format try { JSON.parse(this.state.custom); @@ -178,33 +163,39 @@ class ProfileEdit extends Component { catch (err) { // Set invalid fields customInvalid = true; - disableSubmit = true; } + // eslint-disable-next-line max-len + const disableSubmit = (fnameInvalid + || lnameInvalid + || preferredInvalid + || emailInvalid + || customInvalid); + // Render user edit page return (

User Edit

- {(!localUser) - ? '' - : (
- -
) + {(localUser) + ? (
+ +
) + : '' }
- {(!this.state.error) - ? '' - : ( - {this.state.error} - ) + {(this.state.error) + ? ( + {this.state.error} + ) + : '' } {/* Create form to update user data */}
@@ -261,6 +252,7 @@ class ProfileEdit extends Component { id="email" placeholder="email@example.com" value={this.state.email || ''} + invalid={emailInvalid} onChange={this.handleChange}/> {/* Form section for custom data */} diff --git a/app/ui/components/profile-views/profile.jsx b/app/ui/components/profile-views/profile.jsx index c808f3f5..9a869ab4 100644 --- a/app/ui/components/profile-views/profile.jsx +++ b/app/ui/components/profile-views/profile.jsx @@ -34,10 +34,14 @@ class Profile extends Component { // Initialize parent props super(props); + const { changePassword } = this.props.user; + // Initialize state props this.state = { modal: false, - editPasswordModal: false + editPasswordModal: changePassword, + passwordExpired: changePassword, + staticBackdrop: (changePassword) ? 'static' : true }; // Bind component functions @@ -58,6 +62,7 @@ class Profile extends Component { // Define toggle function togglePasswordModal() { // Open or close modal + this.setState({ modal: false }); this.setState({ editPasswordModal: !this.state.editPasswordModal }); } @@ -72,14 +77,19 @@ class Profile extends Component { {/* Modal for editing the information */} - {(!this.state.editPasswordModal) - ? () - : () - } + toggle={this.handleToggle}/> + + + + +
diff --git a/app/ui/components/project-views/artifacts/artifact-form.jsx b/app/ui/components/project-views/artifacts/artifact-form.jsx index 88f0568a..0a130ccd 100644 --- a/app/ui/components/project-views/artifacts/artifact-form.jsx +++ b/app/ui/components/project-views/artifacts/artifact-form.jsx @@ -194,8 +194,12 @@ class ArtifactForm extends Component { let title = 'Create Artifact'; let artifactId = this.state.id; let disableUpdate = false; + let customInvalid = false; + let idInvalid = false; + let locationInvalid = false; + let filenameInvalid = false; - // If user is editing an Artifact Document + // If user is editing an Artifact Document use ID from props if (this.props.artifactId) { title = 'Edit Artifact'; artifactId = this.props.artifactId; @@ -204,9 +208,23 @@ class ArtifactForm extends Component { disableUpdate = (!this.state.file); } - // Validate input - const idInvalid = (artifactId.length > 0) ? (!RegExp(validators.id).test(artifactId)) : false; - let customInvalid; + // Only validate if ID has been entered + if (artifactId !== 0) { + const validatorsArtifactId = validators.artifact.id.split(validators.ID_DELIMITER).pop(); + const maxLength = validators.artifact.idLength - validators.branch.idLength - 1; + const validLength = (artifactId.length <= maxLength); + idInvalid = (!validLength) && (!RegExp(validatorsArtifactId).test(artifactId)); + } + + const { location, file, filename } = this.state; + + // Validate if location is entered or file has been selected + if (location.length !== 0 || file || filename.length !== 0) { + const validatorLocation = validators.artifact.locationRegEx; + const validatorFilename = validators.artifact.filenameRegEx; + locationInvalid = (!RegExp(validatorLocation).test(location)); + filenameInvalid = (!RegExp(validatorFilename).test(filename)); + } // Verify custom data is valid try { @@ -216,6 +234,8 @@ class ArtifactForm extends Component { customInvalid = true; } + const disableSubmit = (idInvalid || locationInvalid || filenameInvalid || customInvalid); + // Error alert const error = (this.state.error) ? ( @@ -267,13 +287,14 @@ class ArtifactForm extends Component { {/* Form section for artifact location */} - + {/* Form section for artifact filename */} @@ -285,6 +306,7 @@ class ArtifactForm extends Component { placeholder="Filename" disabled={disableUpdate} value={this.state.filename || ''} + invalid={filenameInvalid} onChange={this.handleChange}/> {/* Radio Buttons for file browser */} @@ -339,9 +361,10 @@ class ArtifactForm extends Component { Archive +
* required fields.
{/* Button to submit changes */} diff --git a/app/ui/components/project-views/branches/branch-new.jsx b/app/ui/components/project-views/branches/branch-new.jsx index abb0afe3..519b847e 100644 --- a/app/ui/components/project-views/branches/branch-new.jsx +++ b/app/ui/components/project-views/branches/branch-new.jsx @@ -148,9 +148,8 @@ class CreateBranch extends Component { render() { // Initialize validators - let idInvalid; - let customInvalid; - let disableSubmit; + let idInvalid = false; + let customInvalid = false; const branchOptions = []; const tagOptions = []; @@ -177,15 +176,12 @@ class CreateBranch extends Component { } // Verify if id is valid - if (this.state.id.length !== 0) { - if (!RegExp(validators.id).test(this.state.id)) { - // Set invalid fields - idInvalid = true; - disableSubmit = true; - } - } - else { - disableSubmit = true; + const { id } = this.state; + const validatorsBranchId = validators.branch.id.split(validators.ID_DELIMITER).pop(); + const maxLength = validators.branch.idLength - validators.project.idLength - 1; + + if (id.length !== 0) { + idInvalid = (id.length > maxLength || (!RegExp(validatorsBranchId).test(id))); } // Verify custom data is valid @@ -195,9 +191,10 @@ class CreateBranch extends Component { catch (err) { // Set invalid fields customInvalid = true; - disableSubmit = true; } + const disableSubmit = (customInvalid || idInvalid || id.length === 0); + // Return the form to create a project return (
@@ -240,7 +237,7 @@ class CreateBranch extends Component { onChange={this.handleChange}/> {/* If invalid id, notify user */} - Invalid: A id may only contain lower case letters, numbers, or dashes. + Invalid: An id may only contain letters, numbers, or dashes. {/* Create an input for project name */} diff --git a/app/ui/components/project-views/elements/element-edit-form.jsx b/app/ui/components/project-views/elements/element-edit-form.jsx index fa89c10c..696ee0f2 100644 --- a/app/ui/components/project-views/elements/element-edit-form.jsx +++ b/app/ui/components/project-views/elements/element-edit-form.jsx @@ -59,7 +59,8 @@ class ElementEditForm extends Component { custom: {}, org: null, project: null, - error: null + error: null, + customInvalid: '' }; this.textboxProps = [ @@ -80,7 +81,6 @@ class ElementEditForm extends Component { { label: 'Custom Data' } ]; - this.handleSubmit = this.handleSubmit.bind(this); this.handleChange = this.handleChange.bind(this); this.getElement = this.getElement.bind(this); this.onSubmit = this.onSubmit.bind(this); @@ -91,17 +91,27 @@ class ElementEditForm extends Component { handleChange(event) { const { name, value } = event.target; - const state = (name === 'archived') ? { [name]: !this.state.archived } : { [name]: value }; - this.setState(state); - if (name === 'custom') { + if (name === 'archived') { + // Change the archived state to opposite value + this.setState(prevState => ({ archived: !prevState.archived })); + } + else if (name === 'customData') { + this.setState({ custom: value }); // Verify if custom data is correct JSON format try { - JSON.parse(this.state.custom); + if (value.length > 0) { + JSON.parse(value); + } + this.setState({ customInvalid: '' }); } catch (err) { - this.setState({ error: 'Custom data must be valid JSON.' }); + this.setState({ customInvalid: 'Custom data must be valid JSON.' }); } } + else { + // Change the state with new value + this.setState({ [name]: value }); + } } /** @@ -167,11 +177,6 @@ class ElementEditForm extends Component { this.setState({ target: _id }); } - // eslint-disable-next-line class-methods-use-this - handleSubmit(event) { - event.preventDefault(); - } - // eslint-disable-next-line class-methods-use-this renderColumnComponents(componentList, numColumn) { const style = { padding: 4, margin: 0, border: 6 }; @@ -238,15 +243,21 @@ class ElementEditForm extends Component { renderTextareas(numColumn) { // eslint-disable-next-line no-undef const stateNames = this.textareaProps.map(propObj => toCamel(propObj.label)); - const textareas = this.textareaProps.map( - (propObj, index) => () - ); + const textareas = []; + this.textareaProps.forEach((propObj, index) => { + const value = (stateNames[index] === 'customData') ? 'custom' : 'documentation'; + textareas.push( + + ); + }); + return this.renderColumnComponents(textareas, numColumn); } @@ -326,6 +337,7 @@ class ElementEditForm extends Component { documentation: documentation, custom: JSON.parse(custom) }; + // Verify that there is a source and target if (source !== null && target !== null) { data.source = source; @@ -381,7 +393,7 @@ class ElementEditForm extends Component { return ( - + Element: {id} {(error) ? {error} : ''} @@ -404,8 +416,12 @@ class ElementEditForm extends Component { - {' '} - + {' '} + diff --git a/app/ui/components/project-views/elements/element-new.jsx b/app/ui/components/project-views/elements/element-new.jsx index 5849709a..b1beae03 100644 --- a/app/ui/components/project-views/elements/element-new.jsx +++ b/app/ui/components/project-views/elements/element-new.jsx @@ -194,8 +194,10 @@ class ElementNew extends Component { let idInvalid; let disableSubmit; - // Verify if user's first name is valid - if (!RegExp(validators.id).test(this.state.id)) { + // Verify element id is valid + const validatorsElementId = validators.element.id.split(validators.ID_DELIMITER).pop(); + const validLen = validators.element.idLength - validators.branch.idLength - 1; + if (!RegExp(validatorsElementId).test(this.state.id) || validLen < this.state.id.length) { // Set invalid fields idInvalid = true; disableSubmit = true; @@ -236,7 +238,7 @@ class ElementNew extends Component { onChange={this.handleChange}/> {/* If invalid id, notify user */} - Invalid: A id may only contain lower case letters, numbers, or dashes. + Invalid: An id may only contain letters, numbers, or dashes. diff --git a/app/ui/components/project-views/elements/element-selector.jsx b/app/ui/components/project-views/elements/element-selector.jsx index 797b1993..d6f6842e 100644 --- a/app/ui/components/project-views/elements/element-selector.jsx +++ b/app/ui/components/project-views/elements/element-selector.jsx @@ -94,7 +94,7 @@ class ElementSelector extends React.Component { */ selectElementHandler(id) { // Verify id is not self - if (id === this.props.self) { + if (id === this.props.self && this.state.project === this.props.project.id) { // Display error this.setState({ selectedElementPreview: null, diff --git a/app/ui/components/project-views/elements/element-textarea.jsx b/app/ui/components/project-views/elements/element-textarea.jsx index 84385811..c428e478 100644 --- a/app/ui/components/project-views/elements/element-textarea.jsx +++ b/app/ui/components/project-views/elements/element-textarea.jsx @@ -30,7 +30,8 @@ import { /* eslint-enable no-unused-vars */ function ElementTextarea(props) { - const { name, label, value, id, placeholder, onChange } = props; + const { name, label, value, id, placeholder, onChange, invalid } = props; + const _invalid = (invalid.length > 0 && name === 'customData'); return ( @@ -46,6 +47,7 @@ function ElementTextarea(props) { id={id} placeholder={placeholder} onChange={onChange} + invalid={_invalid} style={{ fontSize: 14, height: 150 }}/> diff --git a/app/ui/components/project-views/project-home.jsx b/app/ui/components/project-views/project-home.jsx index 7c8e7633..4d147327 100644 --- a/app/ui/components/project-views/project-home.jsx +++ b/app/ui/components/project-views/project-home.jsx @@ -142,7 +142,12 @@ class ProjectHome extends Component { const urlBranch = branchSubString.split('/')[2]; // Validate id - if (RegExp(validators.id).test(urlBranch)) { + const validatorsBranchId = validators.branch.id.split(validators.ID_DELIMITER).pop(); + const maxLength = validators.branch.idLength - validators.project.idLength - 1; + const branchLength = urlBranch.length; + const validLen = (branchLength > 0 && branchLength <= maxLength); + + if (RegExp(validatorsBranchId).test(urlBranch) && validLen) { // Validated, set id branch = urlBranch; } diff --git a/app/ui/components/project-views/search/advanced-search/advanced-row.jsx b/app/ui/components/project-views/search/advanced-search/advanced-row.jsx index 876a8125..b1e515d7 100644 --- a/app/ui/components/project-views/search/advanced-search/advanced-row.jsx +++ b/app/ui/components/project-views/search/advanced-search/advanced-row.jsx @@ -45,7 +45,9 @@ function AdvancedRow(props) { className='adv-input-field' name='value' value={props.val} - onChange={(event) => props.handleChange(props.idx, event)}> + onChange={(event) => props.handleChange(props.idx, event)} + onKeyDown={props.onKeyDown} + > { btnDeleteRow } diff --git a/app/ui/components/project-views/search/search-result.jsx b/app/ui/components/project-views/search/search-result.jsx index 16f6eb74..3ebf20cf 100644 --- a/app/ui/components/project-views/search/search-result.jsx +++ b/app/ui/components/project-views/search/search-result.jsx @@ -25,16 +25,22 @@ import React from 'react'; function SearchResult(props) { const cols = []; + const { org, project, branch, id } = props.data; + const href = `/orgs/${org}/projects/${project}/branches/${branch}/elements#${id}`; props.keys.forEach((key, index) => { // Check if element has value defined for respective key const currentValue = (typeof props.data[key] === 'undefined' || !props.data[key]) ? '' : props.data[key].toString(); // Convert Custom data to string const displayValue = (key === 'custom') ? JSON.stringify(props.data[key]) : currentValue; - - cols.push( - {displayValue} - ); + const col = (key === 'id') + ? + + {displayValue} + + + : {displayValue}; + cols.push(col); }); return cols; diff --git a/app/ui/components/project-views/search/search.jsx b/app/ui/components/project-views/search/search.jsx index 4a39b145..a1158b21 100644 --- a/app/ui/components/project-views/search/search.jsx +++ b/app/ui/components/project-views/search/search.jsx @@ -117,6 +117,7 @@ class Search extends Component { this.onChange = this.onChange.bind(this); this.doSearch = this.doSearch.bind(this); this.handleChange = this.handleChange.bind(this); + this.handleOnEnterKey = this.handleOnEnterKey.bind(this); this.handlePageChange = this.handlePageChange.bind(this); this.filterSelected = this.filterSelected.bind(this); } @@ -148,6 +149,13 @@ class Search extends Component { this.setState({ rows: rows }); } + // Handle when enter key pressed to begin search. + handleOnEnterKey(event) { + if (event.key === 'Enter') { + this.doSearch(); + } + } + // Handle filter checkbox changes filterSelected(i, event) { const filter = event.target.name; @@ -261,8 +269,9 @@ class Search extends Component { )); } @@ -349,7 +358,9 @@ class Search extends Component { id="search-query-input" placeholder="Search" value={this.state.basicQuery || ''} - onChange={this.onChange}/> + onChange={this.onChange} + onKeyDown={this.handleOnEnterKey} + /> diff --git a/app/ui/components/project-views/elements/element.jsx b/app/ui/components/project-views/elements/element.jsx index be2fb52c..a73b5cb8 100644 --- a/app/ui/components/project-views/elements/element.jsx +++ b/app/ui/components/project-views/elements/element.jsx @@ -26,8 +26,7 @@ import { Modal, ModalBody, UncontrolledTooltip, - Badge, - Tooltip + Badge } from 'reactstrap'; import Delete from '../../shared-views/delete.jsx'; import CustomData from '../../general/custom-data/custom-data.jsx'; @@ -48,14 +47,12 @@ class Element extends Component { this.state = { element: null, modalDelete: false, - isTooltipOpen: false, error: null }; // Bind component functions this.getElement = this.getElement.bind(this); this.handleDeleteToggle = this.handleDeleteToggle.bind(this); - this.handleTooltipToggle = this.handleTooltipToggle.bind(this); this.handleCrossRefs = this.handleCrossRefs.bind(this); } @@ -101,14 +98,6 @@ class Element extends Component { this.setState({ modalDelete: !this.state.modalDelete }); } - componentDidMount() { - // Set the mounted variable - this.mounted = true; - - // Get element information - this.getElement(); - } - handleCrossRefs(_element) { return new Promise((resolve, reject) => { // Match/find all cross references @@ -161,14 +150,14 @@ class Element extends Component { // Capture the element ID and link const id = uniqCrossRefs[refs[i]].id; if (!elements.hasOwnProperty(id)) { - doc = doc.replace(re, ` ${refs[i]} `); + doc = doc.replace(re, `${refs[i]}`); continue; } const oid = elements[id].org; const pid = elements[id].project; const bid = elements[id].branch; - const link = `/api/orgs/${oid}/projects/${pid}/branches/${bid}/elements/${id}`; - doc = doc.replace(re, ` ${elements[id].name} `); + const link = `/orgs/${oid}/projects/${pid}/branches/${bid}/elements#${id}`; + doc = doc.replace(re, `${elements[id].name}`); } // Resolve the element @@ -189,18 +178,6 @@ class Element extends Component { }); } - // Toggles the tooltip - handleTooltipToggle() { - const isTooltipOpen = this.state.isTooltipOpen; - - // Verify component is not unmounted - if (!this.mounted) { - return; - } - - return this.setState({ isTooltipOpen: !isTooltipOpen }); - } - componentDidUpdate(prevProps) { // Typical usage (don't forget to compare props): if (this.props.id !== prevProps.id) { @@ -302,13 +279,9 @@ class Element extends Component { - + Edit - + ) : '' } diff --git a/app/ui/components/project-views/elements/project-elements.jsx b/app/ui/components/project-views/elements/project-elements.jsx index ca7152a5..4e01e238 100644 --- a/app/ui/components/project-views/elements/project-elements.jsx +++ b/app/ui/components/project-views/elements/project-elements.jsx @@ -82,19 +82,11 @@ class ProjectElements extends Component { $('.element-tree').removeClass('tree-selected'); $(`#tree-${id}`).addClass('tree-selected'); - if (this.state.sidePanel === 'addElement') { - // Only set the refresh function - // The ID is not set here to avoid updating the 'parent' field on the - // add element panel. That parent field should only be passed in when - // the addElement panel is first opened. - } - else { - // Toggle the element side panel - this.setState({ - id: id, - sidePanel: 'elementInfo' - }); - } + // Toggle the element side panel + this.setState({ + id: id, + sidePanel: 'elementInfo' + }); // Get the sidebar html element and toggle it document.getElementById('side-panel').classList.add('side-panel-expanded'); diff --git a/app/ui/js/mbee.js b/app/ui/js/mbee.js index f39f25ec..af4329ab 100644 --- a/app/ui/js/mbee.js +++ b/app/ui/js/mbee.js @@ -18,9 +18,6 @@ /* eslint-disable jsdoc/require-description-complete-sentence */ /* eslint-disable jsdoc/require-jsdoc */ -// ESLint disabled for client-side JS for now. -/* eslint-disabled */ - $.fn.extend({ autoResize: function() { const nlines = $(this).html().split('\n').length; diff --git a/app/views/about.ejs b/app/views/about.ejs index 67610db7..9b6cc5dc 100644 --- a/app/views/about.ejs +++ b/app/views/about.ejs @@ -55,12 +55,6 @@

Development Team

- - - -
-

Austin Bieber

-
Software Architect / Engineer (Back-End Lead)

Danny Chiu

@@ -101,6 +95,12 @@

Former Team Members

+ + + +
+

Austin Bieber

+
Software Architect / Engineer (Back-End)

Leah De Laurell

diff --git a/config/default.cfg b/config/default.cfg index a76c034a..d156ea45 100644 --- a/config/default.cfg +++ b/config/default.cfg @@ -1,6 +1,6 @@ { "artifact": { - "strategy": "local-strategy" + "strategy": "local-strategy" }, "auth": { "strategy": "local-strategy", @@ -71,7 +71,7 @@ "mode": "production", "loginModal": { "on": false, - "message": "This is where you login modal message gets placed." + "message": "This is where your login modal message gets placed." }, "banner": { "on": false, diff --git a/mbee.js b/mbee.js index c41876da..4530a426 100755 --- a/mbee.js +++ b/mbee.js @@ -30,13 +30,24 @@ const pkg = require(path.join(__dirname, 'package.json')); // The global MBEE helper object global.M = {}; +// Get the environment. By default, the environment is 'default' +let env = 'default'; +// If a environment is set, use that +if (process.env.MBEE_ENV) { + env = process.env.MBEE_ENV; +} +// If a dev config exists, use it over the default +else if (fs.existsSync(path.join(__dirname, 'config', 'dev.cfg'))) { + env = 'dev'; +} + /** * Defines the environment based on the MBEE_ENV environment variable. - * If the MBEE_ENV environment variable is not set, the default environment - * is set to 'default'. + * If the MBEE_ENV environment variable is not set, and a dev config does not + * exist, the default environment is set to 'default'. */ Object.defineProperty(M, 'env', { - value: process.env.MBEE_ENV || 'default', + value: env, writable: false, enumerable: true }); diff --git a/package.json b/package.json index aff31d00..d7cbfc5f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mbee", - "version": "1.0.2", + "version": "1.0.3", "build": "NO_BUILD_NUMBER", "description": "Model-Based Engineering Environment", "main": "mbee.js", diff --git a/plugins/routes.js b/plugins/routes.js index 604bcc4e..42acd11b 100644 --- a/plugins/routes.js +++ b/plugins/routes.js @@ -272,19 +272,19 @@ function downloadPluginFromWebsite(data) { // .zip files if (data.source.endsWith('.zip')) { // Set name and unzip command - fileName = `${path.join(M.root, 'plugins', data.name, `${data.name}.zip`)}`; + fileName = path.join(M.root, 'plugins', data.name, `${data.name}.zip`); unzipCmd = `unzip ${fileName} -d ${dirName}`; } // .tar.gz files else if (data.source.endsWith('.tar.gz')) { // Set name and unzip command - fileName = `${path.join(M.root, 'plugins', data.name, `${data.name}.tar.gz`)}`; + fileName = path.join(M.root, 'plugins', data.name, `${data.name}.tar.gz`); unzipCmd = `tar xvzf ${fileName} -C ${dirName}`; } // .gz files else if (data.source.endsWith('.gz')) { // Set name and unzip command - fileName = `${path.join(M.root, 'plugins', data.name, `${data.name}.gz`)}`; + fileName = path.join(M.root, 'plugins', data.name, `${data.name}.gz`); unzipCmd = `gunzip -c ${fileName} > ${dirName}`; } // Other files diff --git a/scripts/migrations/1.0.3.js b/scripts/migrations/1.0.3.js new file mode 100644 index 00000000..5cac3e6f --- /dev/null +++ b/scripts/migrations/1.0.3.js @@ -0,0 +1,86 @@ +/** + * @classification UNCLASSIFIED + * + * @module scripts.migrations.1.0.3 + * + * @copyright Copyright (C) 2018, Lockheed Martin Corporation + * + * @license MIT + * + * @owner Connor Doyle + * + * @author Connor Doyle + * + * @description Migration script for version 1.0.3. + */ + +// Node modules +const fs = require('fs'); +const path = require('path'); + +// MBEE modules +const Webhook = M.require('models.webhook'); + +/** + * @description Handles the database migration from 1.0.2 to 1.0.3. + */ +module.exports.up = async function() { + await webhookHelper(); +}; + +/** + * @description Helper function for 1.0.2 to 1.0.3 migration. Handles all + * updates to the webhook collection. + * @async + * + * @returns {Promise} Returns an empty promise upon completion. + */ +async function webhookHelper() { + const numWebhooks = await Webhook.countDocuments({}); + + if (numWebhooks > 0) { + // Create data directory if it does not exist + if (!fs.existsSync(path.join(M.root, 'data'))) { + fs.mkdirSync(path.join(M.root, 'data')); + } + + const batchLimit = 5000; + let batchSkip = 0; + + // Process batch of 5000 webhooks + for (let i = 0; i < numWebhooks / batchLimit; i++) { + batchSkip = i * 5000; + + // eslint-disable-next-line no-await-in-loop + const webhooks = await Webhook.find({}, null, { skip: batchSkip, limit: batchLimit }); + + // Save all webhooks to a JSON file in the data directory + fs.writeFileSync(path.join(M.root, 'data', `webhooks-103-${i}.json`), JSON.stringify(webhooks)); + + const bulkWrite = []; + // Add url field to all outgoing webhooks and remove the response field. + // If the response field had a token, add it to the token field. + webhooks.forEach((webhook) => { + if (webhook.type === 'Outgoing' && webhook.hasOwnProperty('response')) { + webhook.url = webhook.response.url; + if (webhook.response.hasOwnProperty('token')) webhook.token = webhook.response.token; + delete webhook.response; + bulkWrite.push({ + replaceOne: { + filter: { _id: webhook._id }, + replacement: webhook + } + }); + } + }); + + // Update all webhooks + await Webhook.bulkWrite(bulkWrite); // eslint-disable-line no-await-in-loop + + // If the backup file exists, remove it + if (fs.existsSync(path.join(M.root, 'data', `webhooks-103-${i}.json`))) { + fs.unlinkSync(path.join(M.root, 'data', `webhooks-103-${i}.json`)); + } + } + } +} diff --git a/test/1xx_test/101-config-tests.js b/test/1xx_test/101-config-tests.js index ad0effed..2b6c2a59 100644 --- a/test/1xx_test/101-config-tests.js +++ b/test/1xx_test/101-config-tests.js @@ -15,6 +15,10 @@ * object. For now, it only tests the version number. */ +// Node modules +const fs = require('fs'); +const path = require('path'); + // NPM modules const chai = require('chai'); @@ -37,8 +41,12 @@ describe(M.getModuleName(module.filename), () => { async function environmentCheck() { // Verify inputted environment is configuration environment const processEnv = process.env.MBEE_ENV; + if (typeof processEnv !== 'undefined') { - chai.expect(processEnv).to.equal(M.env); + chai.expect(M.env).to.equal(processEnv); + } + else if (fs.existsSync(path.join(M.root, 'config', 'dev.cfg'))) { + chai.expect(M.env).to.equal('dev'); } else { chai.expect(M.env).to.equal('default'); diff --git a/test/3xx_ut_models/core_tests/307a-webhook-model-core-tests.js b/test/3xx_ut_models/core_tests/307a-webhook-model-core-tests.js index 91b23f77..0796696f 100644 --- a/test/3xx_ut_models/core_tests/307a-webhook-model-core-tests.js +++ b/test/3xx_ut_models/core_tests/307a-webhook-model-core-tests.js @@ -126,8 +126,7 @@ async function findWebhook() { webhook.type.should.equal(testData.webhooks[0].type); webhook.description.should.equal(testData.webhooks[0].description); webhook.triggers.should.deep.equal(testData.webhooks[0].triggers); - webhook.response.hasOwnProperty('url').should.equal(true); - webhook.response.hasOwnProperty('method').should.equal(true); + webhook.url.should.equal(testData.webhooks[0].url); } catch (error) { M.log.error(error); @@ -213,7 +212,7 @@ async function verifyToken() { async function validUpdateFields() { try { // Set the array of correct update fields; - const updateFields = ['name', 'description', 'triggers', 'response', 'token', + const updateFields = ['name', 'description', 'triggers', 'url', 'token', 'tokenLocation', 'archived']; // Get the update fields from the webhook model diff --git a/test/3xx_ut_models/error_tests/307b-webhook-model-error-tests.js b/test/3xx_ut_models/error_tests/307b-webhook-model-error-tests.js index 270163fe..0cb38463 100644 --- a/test/3xx_ut_models/error_tests/307b-webhook-model-error-tests.js +++ b/test/3xx_ut_models/error_tests/307b-webhook-model-error-tests.js @@ -46,16 +46,11 @@ describe(M.getModuleName(module.filename), () => { it('should reject changing the type of a webhook', typeImmutable); it('should reject creating a webhook with no triggers field', noTriggers); it('should reject creating a webhook with an invalid triggers field', invalidTriggers); - it('should reject creating an outgoing webhook with no response field', noResponseOutgoing); - it('should reject creating an incoming webhook with a response field', responseIncoming); - it('should reject creating a webhook with no url in the response', noUrlInResponse); - it('should reject creating a webhook with an invalid method in a response', invalidMethodInResponse); - it('should reject creating a webhook with an invalid token in a response', invalidTokenInResponse); - it('should reject creating a webhook with an invalid field in a response', invalidFieldInResponse); + it('should reject creating an outgoing webhook without a url', noUrlOutgoing); + it('should reject creating an incoming webhook with a url', urlIncoming); it('should reject creating an incoming webhook with no token', noTokenIncoming); it('should reject creating an incoming webhook with no tokenLocation', noTokenLocationIncoming); - it('should reject creating an outgoing webhook with an incoming field', tokenOutgoing); - it('should reject creating an outgoing webhook with an incoming field', tokenLocationOutgoing); + it('should reject creating an outgoing webhook with a tokenLocation field', tokenLocationOutgoing); it('should throw an error when the input token does not match the stored token', verifyToken); }); @@ -211,16 +206,16 @@ async function invalidTriggers() { /** * @description Validates that an outgoing webhook cannot be created without a response field. */ -async function noResponseOutgoing() { +async function noUrlOutgoing() { try { const webhookData = Object.assign({}, testData.webhooks[0]); webhookData._id = webhookID; - delete webhookData.response; + delete webhookData.url; // Save webhook; expect specific error message await Webhook.insertMany(webhookData).should.eventually.be.rejectedWith('Webhook validation ' - + 'failed: type: An outgoing webhook must have a response field and cannot have a token or' + + 'failed: type: An outgoing webhook must have a url field and cannot have a' + ' tokenLocation.'); } catch (error) { @@ -236,127 +231,17 @@ async function noResponseOutgoing() { /** * @description Validates that an incoming webhook cannot be created with a response field. */ -async function responseIncoming() { +async function urlIncoming() { try { const webhookData = Object.assign({}, testData.webhooks[1]); webhookData._id = webhookID; - webhookData.response = { - url: 'test' - }; + webhookData.url = 'test'; // Save webhook; expect specific error message await Webhook.insertMany(webhookData).should.eventually.be.rejectedWith('Webhook validation ' + 'failed: type: An incoming webhook must have a token and a tokenLocation and cannot have' - + ' a response field.'); - } - catch (error) { - // Remove the webhook in case the test failed - await Webhook.deleteMany({ _id: webhookID }); - - M.log.error(error); - // There should be no error - should.not.exist(error); - } -} - -/** - * @description Validates that a webhook cannot be created with a response that's missing a url. - */ -async function noUrlInResponse() { - try { - const webhookData = Object.assign({}, testData.webhooks[0]); - webhookData._id = webhookID; - - webhookData.response = {}; - - // Save webhook; expect specific error message - await Webhook.insertMany(webhookData).should.eventually.be.rejectedWith('Webhook validation ' - + 'failed: response: The response field must have a url.'); - } - catch (error) { - // Remove the webhook in case the test failed - await Webhook.deleteMany({ _id: webhookID }); - - M.log.error(error); - // There should be no error - should.not.exist(error); - } -} - -/** - * @description Validates that a webhook cannot be created with a response that has an invalid - * method. - */ -async function invalidMethodInResponse() { - try { - const webhookData = Object.assign({}, testData.webhooks[0]); - webhookData._id = webhookID; - - webhookData.response = { - url: 'test', - method: 'invalid' - }; - - // Save webhook; expect specific error message - await Webhook.insertMany(webhookData).should.eventually.be.rejectedWith('Webhook validation ' - + 'failed: response: Invalid method in response field.'); - } - catch (error) { - // Remove the webhook in case the test failed - await Webhook.deleteMany({ _id: webhookID }); - - M.log.error(error); - // There should be no error - should.not.exist(error); - } -} - -/** - * @description Validates that a webhook cannot be created with a response that has an invalid - * token field. - */ -async function invalidTokenInResponse() { - try { - const webhookData = Object.assign({}, testData.webhooks[0]); - webhookData._id = webhookID; - - webhookData.response = { - url: 'test', - token: {} - }; - - // Save webhook; expect specific error message - await Webhook.insertMany(webhookData).should.eventually.be.rejectedWith('Webhook validation ' - + 'failed: response: Invalid token in response field.'); - } - catch (error) { - // Remove the webhook in case the test failed - await Webhook.deleteMany({ _id: webhookID }); - - M.log.error(error); - // There should be no error - should.not.exist(error); - } -} - -/** - * @description Validates that a webhook cannot be created with a response that has an invalid - * field. - */ -async function invalidFieldInResponse() { - try { - const webhookData = Object.assign({}, testData.webhooks[0]); - webhookData._id = webhookID; - - webhookData.response = { - url: 'test', - wrong: {} - }; - - // Save webhook; expect specific error message - await Webhook.insertMany(webhookData).should.eventually.be.rejectedWith('Webhook validation ' - + 'failed: response: Invalid field [wrong] in response field.'); + + ' a url field.'); } catch (error) { // Remove the webhook in case the test failed @@ -382,7 +267,7 @@ async function noTokenIncoming() { // Save webhook; expect specific error message await Webhook.insertMany(webhookData).should.eventually.be.rejectedWith('Webhook validation ' + 'failed: type: An incoming webhook must have a token and a tokenLocation and cannot have' - + ' a response field.'); + + ' a url field.'); } catch (error) { // Remove the webhook in case the test failed @@ -408,33 +293,7 @@ async function noTokenLocationIncoming() { // Save webhook; expect specific error message await Webhook.insertMany(webhookData).should.eventually.be.rejectedWith('Webhook validation ' + 'failed: type: An incoming webhook must have a token and a tokenLocation and cannot have' - + ' a response field.'); - } - catch (error) { - // Remove the webhook in case the test failed - await Webhook.deleteMany({ _id: webhookID }); - - M.log.error(error); - // There should be no error - should.not.exist(error); - } -} - -/** - * @description Validates that an outgoing webhook cannot be created with a token. - */ -async function tokenOutgoing() { - try { - // Get test data for an outgoing webhook - const webhookData = Object.assign({}, testData.webhooks[0]); - webhookData._id = webhookID; - - webhookData.token = 'test'; - - // Save webhook; expect specific error message - await Webhook.insertMany(webhookData).should.eventually.be.rejectedWith('Webhook validation ' - + 'failed: type: An outgoing webhook must have a response field and cannot have a token or' - + ' tokenLocation.'); + + ' a url field.'); } catch (error) { // Remove the webhook in case the test failed @@ -459,7 +318,7 @@ async function tokenLocationOutgoing() { // Save webhook; expect specific error message await Webhook.insertMany(webhookData).should.eventually.be.rejectedWith('Webhook validation ' - + 'failed: type: An outgoing webhook must have a response field and cannot have a token or' + + 'failed: type: An outgoing webhook must have a url field and cannot have a' + ' tokenLocation.'); } catch (error) { diff --git a/test/4xx_ut_controllers/core_tests/407a-webhook-controller-core-tests.js b/test/4xx_ut_controllers/core_tests/407a-webhook-controller-core-tests.js index c6f94ce9..93a66c26 100644 --- a/test/4xx_ut_controllers/core_tests/407a-webhook-controller-core-tests.js +++ b/test/4xx_ut_controllers/core_tests/407a-webhook-controller-core-tests.js @@ -99,8 +99,7 @@ async function createWebhook() { chai.expect(createdWebhook.type).to.equal(webhookData.type); chai.expect(createdWebhook.description).to.equal(webhookData.description); chai.expect(createdWebhook.triggers).to.deep.equal(webhookData.triggers); - chai.expect(createdWebhook.response.url).to.equal(webhookData.response.url); - chai.expect(createdWebhook.response.method).to.equal(webhookData.response.method || 'POST'); + chai.expect(createdWebhook.url).to.equal(webhookData.url); chai.expect(createdWebhook.custom).to.deep.equal(webhookData.custom || {}); // Verify additional properties @@ -147,9 +146,8 @@ async function createWebhooks() { chai.expect(createdWebhook.type).to.equal(webhookDataObj.type); chai.expect(createdWebhook.description).to.equal(webhookDataObj.description); chai.expect(createdWebhook.triggers).to.deep.equal(webhookDataObj.triggers); - if (createdWebhook.response) { - chai.expect(createdWebhook.response.url).to.equal(webhookDataObj.response.url); - chai.expect(createdWebhook.response.method).to.equal(webhookDataObj.response.method || 'POST'); + if (createdWebhook.type === 'Outgoing') { + chai.expect(createdWebhook.url).to.equal(webhookDataObj.url); } else { chai.expect(createdWebhook.token).to.equal(token); @@ -197,8 +195,7 @@ async function findWebhook() { chai.expect(foundWebhook.type).to.equal(webhookData.type); chai.expect(foundWebhook.description).to.equal(webhookData.description); chai.expect(foundWebhook.triggers).to.deep.equal(webhookData.triggers); - chai.expect(foundWebhook.response.url).to.equal(webhookData.response.url); - chai.expect(foundWebhook.response.method).to.equal(webhookData.response.method || 'POST'); + chai.expect(foundWebhook.url).to.equal(webhookData.url); chai.expect(foundWebhook.reference).to.equal(''); chai.expect(foundWebhook.custom).to.deep.equal(webhookData.custom || {}); @@ -236,6 +233,7 @@ async function findWebhooks() { webhookData.forEach((webhookDataObj) => { const foundWebhook = jmi2[webhookDataObj._id]; + const token = `${adminUser._id}:${webhookDataObj.token}`; // Verify webhook chai.expect(foundWebhook._id).to.equal(webhookDataObj._id); @@ -243,6 +241,13 @@ async function findWebhooks() { chai.expect(foundWebhook.type).to.equal(webhookDataObj.type); chai.expect(foundWebhook.description).to.equal(webhookDataObj.description); chai.expect(foundWebhook.triggers).to.deep.equal(webhookDataObj.triggers); + if (foundWebhook.type === 'Outgoing') { + chai.expect(foundWebhook.url).to.equal(webhookDataObj.url); + } + else { + chai.expect(foundWebhook.token).to.equal(token); + chai.expect(foundWebhook.tokenLocation).to.equal(webhookDataObj.tokenLocation); + } chai.expect(foundWebhook.custom).to.deep.equal(webhookDataObj.custom || {}); // Verify additional properties @@ -279,6 +284,7 @@ async function findAllWebhooks() { webhookData.forEach((webhookDataObj) => { const foundWebhook = jmi2[webhookDataObj._id]; + const token = `${adminUser._id}:${webhookDataObj.token}`; // Verify webhook chai.expect(foundWebhook._id).to.equal(webhookDataObj._id); @@ -286,6 +292,13 @@ async function findAllWebhooks() { chai.expect(foundWebhook.type).to.equal(webhookDataObj.type); chai.expect(foundWebhook.description).to.equal(webhookDataObj.description); chai.expect(foundWebhook.triggers).to.deep.equal(webhookDataObj.triggers); + if (foundWebhook.type === 'Outgoing') { + chai.expect(foundWebhook.url).to.equal(webhookDataObj.url); + } + else { + chai.expect(foundWebhook.token).to.equal(token); + chai.expect(foundWebhook.tokenLocation).to.equal(webhookDataObj.tokenLocation); + } chai.expect(foundWebhook.custom).to.deep.equal(webhookDataObj.custom || {}); // Verify additional properties @@ -327,8 +340,7 @@ async function updateWebhook() { chai.expect(updatedWebhook.type).to.equal(webhookData.type); chai.expect(updatedWebhook.description).to.equal(webhookData.description); chai.expect(updatedWebhook.triggers).to.deep.equal(webhookData.triggers); - chai.expect(updatedWebhook.response.url).to.equal(webhookData.response.url); - chai.expect(updatedWebhook.response.method).to.equal(webhookData.response.method || 'POST'); + chai.expect(updatedWebhook.url).to.equal(webhookData.url); chai.expect(updatedWebhook.custom).to.deep.equal(webhookData.custom || {}); // Verify additional properties @@ -372,6 +384,7 @@ async function updateWebhooks() { webhookData.forEach((webhookDataObj) => { const webhook = jmi2[webhookDataObj._id]; + const token = `${adminUser._id}:${webhookDataObj.token}`; // Verify webhook chai.expect(webhook._id).to.equal(webhookDataObj._id); @@ -379,6 +392,13 @@ async function updateWebhooks() { chai.expect(webhook.type).to.equal(webhookDataObj.type); chai.expect(webhook.description).to.equal(webhookDataObj.description); chai.expect(webhook.triggers).to.deep.equal(webhookDataObj.triggers); + if (webhook.type === 'Outgoing') { + chai.expect(webhook.url).to.equal(webhookDataObj.url); + } + else { + chai.expect(webhook.token).to.equal(token); + chai.expect(webhook.tokenLocation).to.equal(webhookDataObj.tokenLocation); + } chai.expect(webhook.custom).to.deep.equal(webhookDataObj.custom || {}); // Verify additional properties diff --git a/test/4xx_ut_controllers/error_tests/407b-webhook-controller-error-tests.js b/test/4xx_ut_controllers/error_tests/407b-webhook-controller-error-tests.js index 210abb9c..d5c4b82c 100644 --- a/test/4xx_ut_controllers/error_tests/407b-webhook-controller-error-tests.js +++ b/test/4xx_ut_controllers/error_tests/407b-webhook-controller-error-tests.js @@ -146,7 +146,7 @@ describe(M.getModuleName(module.filename), () => { it('should reject an attempt to create a webhook by an unauthorized user at the branch level', unauthorizedTest('branch', 'create')); it('should reject an attempt to create a webhook with an invalid key', createInvalidKey); it('should reject an attempt to create a webhook with a null value', createNull); - it('should reject an attempt to create a webhook with a boolena value', createBoolean); + it('should reject an attempt to create a webhook with a boolean value', createBoolean); it('should reject an attempt to create a webhook with a string value', createString); it('should reject an attempt to create a webhook with an array that does not contain objects', createInvalidArray); it('should reject an attempt to create a webhook with an undefined value', createUndefined); @@ -164,13 +164,9 @@ describe(M.getModuleName(module.filename), () => { it('should reject an attempt to change a webhook\'s reference id', updateReference); it('should reject an update array with duplicate _ids', updateDuplicate); it('should reject an attempt to update a webhook that doesn\'t exist', updateNotFound); - it('should reject an attempt to add a response field to an incoming webhook', updateAddResponse); it('should reject an attempt to remove the token from the incoming field', updateInvalidToken); it('should reject an attempt to remove the tokenLocation from the incoming field', updateInvalidTokenLocation); - it('should reject an attempt to add a token to an outgoing webhook', updateAddToken); it('should reject an attempt to add a tokenLocation to an outgoing webhook', updateAddTokenLocation); - it('should reject an attempt to add a non-object response field', updateInvalidResponse); - it('should reject an attempt to add a response field that does not contain a url', updateNoUrlInResponse); // ------------- Remove ------------- it('should reject an attempt to delete a webhook that doesn\'t exist', deleteNotFound); it('should reject an attempt to delete a webhook on an archived org', archivedTest(Organization, 'remove')); @@ -601,28 +597,6 @@ async function updateNotFound() { } } -/** - * @description Validates that the webhook controller will reject an update to an incoming - * webhook that attempts to add a response field. - */ -async function updateAddResponse() { - try { - // Create update for an incoming webhook - const webhookData = { - id: incomingWebhookID, - response: { url: 'test' } - }; - - await WebhookController.update(adminUser, webhookData) - .should.eventually.be.rejectedWith(`Webhook ${incomingWebhookID} validation failed: ` - + 'An incoming webhook cannot have a response field.'); - } - catch (error) { - M.log.error(error); - should.not.exist(error); - } -} - /** * @description Validates that the webhook controller will reject an update to an incoming * webhook attempting to set the token to null. @@ -668,28 +642,6 @@ async function updateInvalidTokenLocation() { } } -/** - * @description Validates that the webhook controller will reject an update to an outgoing - * webhook attempting to add a token. - */ -async function updateAddToken() { - try { - // Create update for an outgoing webhook - const webhookData = { - id: webhookID, - token: 'test' - }; - - await WebhookController.update(adminUser, webhookData) - .should.eventually.be.rejectedWith(`Webhook ${webhookID} validation failed: ` - + 'An outgoing webhook cannot have a token.'); - } - catch (error) { - M.log.error(error); - should.not.exist(error); - } -} - /** * @description Validates that the webhook controller will reject an update to an outgoing * webhook attempting to add a token. @@ -712,50 +664,6 @@ async function updateAddTokenLocation() { } } -/** - * @description Validates that the webhook controller will reject an update to an outgoing - * webhook changing the response to an invalid format. - */ -async function updateInvalidResponse() { - try { - // Create invalid update for an outgoing webhook - const webhookData = { - id: webhookID, - response: [] - }; - - await WebhookController.update(adminUser, webhookData) - .should.eventually.be.rejectedWith(`Webhook ${webhookID} validation failed: ` - + 'Invalid response: []'); - } - catch (error) { - M.log.error(error); - should.not.exist(error); - } -} - -/** - * @description Validates that the webhook controller will reject an update to an outgoing - * webhook removing the url from the response field. - */ -async function updateNoUrlInResponse() { - try { - // Create invalid update for an outgoing webhook - const webhookData = { - id: webhookID, - response: { method: 'test' } // This is wrong because a response must have a url - }; - - await WebhookController.update(adminUser, webhookData) - .should.eventually.be.rejectedWith(`Webhook ${webhookID} validation failed: ` - + 'Invalid response: [[object Object]]'); - } - catch (error) { - M.log.error(error); - should.not.exist(error); - } -} - /** * @description Validates that the webhook controller will reject a request to delete a webhook * that doesn't exist. diff --git a/test/4xx_ut_controllers/specific_tests/405c-element-controller-specific-tests.js b/test/4xx_ut_controllers/specific_tests/405c-element-controller-specific-tests.js index eaac9f0c..78181702 100644 --- a/test/4xx_ut_controllers/specific_tests/405c-element-controller-specific-tests.js +++ b/test/4xx_ut_controllers/specific_tests/405c-element-controller-specific-tests.js @@ -124,6 +124,8 @@ describe(M.getModuleName(module.filename), () => { it('should find an archived element when the option archived is provided', optionArchivedFind); it('should find an element and its subtree when the option subtree ' + 'is provided', optionSubtreeFind); + it('should find an element at its subtree up to a certain depth when the option depth is' + + 'provided', optionDepthFind); it('should return an element with only the specific fields specified from' + ' find()', optionFieldsFind); it('should return a limited number of elements from find()', optionLimitFind); @@ -284,6 +286,41 @@ async function optionSubtreeFind() { } } +/** + * @description Verifies that an element and its subtree are returned when + * using the option 'subtree' in find(). + */ +async function optionDepthFind() { + try { + // Set up + const elemID = utils.parseID(elements[2]._id).pop(); + + // Create the options object. Search for includeArchived:true since one child element + // was archived in a previous test + const options = { depth: 1, includeArchived: true }; + + // Find the element and its subtree + const foundElements = await ElementController.find(adminUser, org._id, projIDs[0], branchID, + elemID, options); + + // Expect there to be 6 elements found, the searched element and 5 in subtree + chai.expect(foundElements.length).to.equal(6); + + // Attempt to convert elements to JMI3, if successful then it's a valid tree + const jmi3Elements = jmi.convertJMI(1, 3, foundElements, '_id', '_id'); + + // Verify that there is only one top level key in jmi3, which should be the + // searched element + chai.expect(Object.keys(jmi3Elements).length).to.equal(1); + chai.expect(Object.keys(jmi3Elements)[0]).to.equal(elements[2]._id); + } + catch (error) { + M.log.error(error); + // Expect no error + chai.expect(error.message).to.equal(null); + } +} + /** * @description Verifies that option 'fields' returns an element with only * specific fields in find(). diff --git a/test/4xx_ut_controllers/specific_tests/407c-webhook-controller-specific-tests.js b/test/4xx_ut_controllers/specific_tests/407c-webhook-controller-specific-tests.js index e62324b8..df5ab9e8 100644 --- a/test/4xx_ut_controllers/specific_tests/407c-webhook-controller-specific-tests.js +++ b/test/4xx_ut_controllers/specific_tests/407c-webhook-controller-specific-tests.js @@ -128,6 +128,7 @@ async function createOnOrg() { chai.expect(createdWebhook.type).to.equal(testData.webhooks[0].type); chai.expect(createdWebhook.description).to.equal(testData.webhooks[0].description); chai.expect(createdWebhook.triggers).to.deep.equal(testData.webhooks[0].triggers); + chai.expect(createdWebhook.url).to.equal(testData.webhooks[0].url); chai.expect(createdWebhook.reference).to.equal(org._id); chai.expect(createdWebhook.custom).to.deep.equal(testData.webhooks[0].custom || {}); @@ -172,6 +173,7 @@ async function createOnProject() { chai.expect(createdWebhook.type).to.equal(testData.webhooks[0].type); chai.expect(createdWebhook.description).to.equal(testData.webhooks[0].description); chai.expect(createdWebhook.triggers).to.deep.equal(testData.webhooks[0].triggers); + chai.expect(createdWebhook.url).to.equal(testData.webhooks[0].url); chai.expect(createdWebhook.reference).to.equal(utils.createID(org._id, projID)); chai.expect(createdWebhook.custom).to.deep.equal(testData.webhooks[0].custom || {}); @@ -217,6 +219,7 @@ async function createOnBranch() { chai.expect(createdWebhook.type).to.equal(testData.webhooks[0].type); chai.expect(createdWebhook.description).to.equal(testData.webhooks[0].description); chai.expect(createdWebhook.triggers).to.deep.equal(testData.webhooks[0].triggers); + chai.expect(createdWebhook.url).to.equal(testData.webhooks[0].url); chai.expect(createdWebhook.reference).to.equal(utils.createID(org._id, projID, branchID)); chai.expect(createdWebhook.custom).to.deep.equal(testData.webhooks[0].custom || {}); @@ -678,19 +681,19 @@ async function optionSortFind() { name: 'a', type: testData.webhooks[0].type, triggers: testData.webhooks[0].triggers, - response: testData.webhooks[0].response + url: testData.webhooks[0].url }, { name: 'b', type: testData.webhooks[0].type, triggers: testData.webhooks[0].triggers, - response: testData.webhooks[0].response + url: testData.webhooks[0].url }, { name: 'c', type: testData.webhooks[0].type, triggers: testData.webhooks[0].triggers, - response: testData.webhooks[0].response + url: testData.webhooks[0].url }]; // Create sort options diff --git a/test/5xx_mock_api_tests/core_tests/507a-webhook-mock-core-tests.js b/test/5xx_mock_api_tests/core_tests/507a-webhook-mock-core-tests.js index a33f919e..b77daa1a 100644 --- a/test/5xx_mock_api_tests/core_tests/507a-webhook-mock-core-tests.js +++ b/test/5xx_mock_api_tests/core_tests/507a-webhook-mock-core-tests.js @@ -116,8 +116,7 @@ function postWebhook(done) { const postedWebhook = postedWebhooks[0]; chai.expect(postedWebhook.name).to.equal(webhookData.name); chai.expect(postedWebhook.triggers).to.deep.equal(webhookData.triggers); - chai.expect(postedWebhook.response.url).to.equal(webhookData.response.url); - chai.expect(postedWebhook.response.method).to.equal(webhookData.response.method || 'POST'); + chai.expect(postedWebhook.url).to.equal(webhookData.url); chai.expect(postedWebhook.reference).to.equal(''); chai.expect(postedWebhook.custom).to.deep.equal(webhookData.custom || {}); @@ -182,9 +181,8 @@ function postWebhooks(done) { chai.expect(createdWebhook.type).to.equal(webhookDataObj.type); chai.expect(createdWebhook.description).to.equal(webhookDataObj.description); chai.expect(createdWebhook.triggers).to.deep.equal(webhookDataObj.triggers); - if (createdWebhook.response) { - chai.expect(createdWebhook.response.url).to.equal(webhookDataObj.response.url); - chai.expect(createdWebhook.response.method).to.equal(webhookDataObj.response.method || 'POST'); + if (createdWebhook.type === 'Outgoing') { + chai.expect(createdWebhook.url).to.equal(webhookDataObj.url); } else { chai.expect(createdWebhook.token).to.equal(token); @@ -244,8 +242,7 @@ function getWebhook(done) { const foundWebhook = JSON.parse(_data); chai.expect(foundWebhook.name).to.equal(webhookData.name); chai.expect(foundWebhook.triggers).to.deep.equal(webhookData.triggers); - chai.expect(foundWebhook.response.url).to.equal(webhookData.response.url); - chai.expect(foundWebhook.response.method).to.equal(webhookData.response.method || 'POST'); + chai.expect(foundWebhook.url).to.equal(webhookData.url); chai.expect(foundWebhook.reference).to.equal(''); chai.expect(foundWebhook.custom).to.deep.equal(webhookData.custom || {}); @@ -307,9 +304,8 @@ function getWebhooks(done) { chai.expect(foundWebhook.type).to.equal(webhookDataObj.type); chai.expect(foundWebhook.description).to.equal(webhookDataObj.description); chai.expect(foundWebhook.triggers).to.deep.equal(webhookDataObj.triggers); - if (foundWebhook.response) { - chai.expect(foundWebhook.response.url).to.equal(webhookDataObj.response.url); - chai.expect(foundWebhook.response.method).to.equal(webhookDataObj.response.method || 'POST'); + if (foundWebhook.type === 'Outgoing') { + chai.expect(foundWebhook.url).to.equal(webhookDataObj.url); } else { chai.expect(foundWebhook.token).to.equal(token); @@ -377,9 +373,8 @@ function getAllWebhooks(done) { chai.expect(foundWebhook.type).to.equal(webhookDataObj.type); chai.expect(foundWebhook.description).to.equal(webhookDataObj.description); chai.expect(foundWebhook.triggers).to.deep.equal(webhookDataObj.triggers); - if (foundWebhook.response) { - chai.expect(foundWebhook.response.url).to.equal(webhookDataObj.response.url); - chai.expect(foundWebhook.response.method).to.equal(webhookDataObj.response.method || 'POST'); + if (foundWebhook.type === 'Outgoing') { + chai.expect(foundWebhook.url).to.equal(webhookDataObj.url); } else { chai.expect(foundWebhook.token).to.equal(token); @@ -438,8 +433,7 @@ function patchWebhook(done) { const patchedWebhook = JSON.parse(_data); chai.expect(patchedWebhook.name).to.equal('Patch test'); chai.expect(patchedWebhook.triggers).to.deep.equal(webhookData.triggers); - chai.expect(patchedWebhook.response.url).to.equal(webhookData.response.url); - chai.expect(patchedWebhook.response.method).to.equal(webhookData.response.method || 'POST'); + chai.expect(patchedWebhook.url).to.equal(webhookData.url); chai.expect(patchedWebhook.reference).to.equal(''); chai.expect(patchedWebhook.custom).to.deep.equal(webhookData.custom || {}); @@ -506,9 +500,8 @@ function patchWebhooks(done) { chai.expect(updatedWebhook.type).to.equal(webhookDataObj.type); chai.expect(updatedWebhook.description).to.equal(webhookDataObj.description); chai.expect(updatedWebhook.triggers).to.deep.equal(webhookDataObj.triggers); - if (updatedWebhook.response) { - chai.expect(updatedWebhook.response.url).to.equal(webhookDataObj.response.url); - chai.expect(updatedWebhook.response.method).to.equal(webhookDataObj.response.method || 'POST'); + if (updatedWebhook.type === 'Outgoing') { + chai.expect(updatedWebhook.url).to.equal(webhookDataObj.url); } else { chai.expect(updatedWebhook.token).to.equal(token); diff --git a/test/5xx_mock_api_tests/specific_tests/507c-webhook-mock-specific-tests.js b/test/5xx_mock_api_tests/specific_tests/507c-webhook-mock-specific-tests.js index 4749f8bf..160a8589 100644 --- a/test/5xx_mock_api_tests/specific_tests/507c-webhook-mock-specific-tests.js +++ b/test/5xx_mock_api_tests/specific_tests/507c-webhook-mock-specific-tests.js @@ -170,10 +170,17 @@ function post(reference) { // Verify response body const createdWebhooks = JSON.parse(_data); const createdWebhook = createdWebhooks[0]; + const token = `${adminUser._id}:${webhookData.token}`; + chai.expect(createdWebhook.name).to.equal(webhookData.name); chai.expect(createdWebhook.triggers).to.deep.equal(webhookData.triggers); - chai.expect(createdWebhook.response.url).to.equal(webhookData.response.url); - chai.expect(createdWebhook.response.method).to.equal(webhookData.response.method || 'POST'); + if (createdWebhook.type === 'Outgoing') { + chai.expect(createdWebhook.url).to.equal(webhookData.url); + } + else { + chai.expect(createdWebhook.token).to.equal(token); + chai.expect(createdWebhook.tokenLocation).to.equal(webhookData.tokenLocation); + } chai.expect(createdWebhook.reference).to.deep.equal(ref); chai.expect(createdWebhook.custom).to.deep.equal(webhookData.custom || {}); @@ -280,9 +287,8 @@ function postMany(reference) { chai.expect(createdWebhook.type).to.equal(webhookDataObj.type); chai.expect(createdWebhook.description).to.equal(webhookDataObj.description); chai.expect(createdWebhook.triggers).to.deep.equal(webhookDataObj.triggers); - if (createdWebhook.response) { - chai.expect(createdWebhook.response.url).to.equal(webhookDataObj.response.url); - chai.expect(createdWebhook.response.method).to.equal(webhookDataObj.response.method || 'POST'); + if (createdWebhook.type === 'Outgoing') { + chai.expect(createdWebhook.url).to.equal(webhookDataObj.url); } else { chai.expect(createdWebhook.token).to.equal(token); @@ -390,9 +396,8 @@ function getAll(reference) { chai.expect(foundWebhook.type).to.equal(webhookDataObj.type); chai.expect(foundWebhook.description).to.equal(webhookDataObj.description); chai.expect(foundWebhook.triggers).to.deep.equal(webhookDataObj.triggers); - if (foundWebhook.response) { - chai.expect(foundWebhook.response.url).to.equal(webhookDataObj.response.url); - chai.expect(foundWebhook.response.method).to.equal(webhookDataObj.response.method || 'POST'); + if (foundWebhook.type === 'Outgoing') { + chai.expect(foundWebhook.url).to.equal(webhookDataObj.url); } else { chai.expect(foundWebhook.token).to.equal(webhookDataObj.token); @@ -482,8 +487,7 @@ function patch(reference) { const updatedWebhook = JSON.parse(_data); chai.expect(updatedWebhook.name).to.equal('Patch test'); chai.expect(updatedWebhook.triggers).to.deep.equal(webhookData.triggers); - chai.expect(updatedWebhook.response.url).to.equal(webhookData.response.url); - chai.expect(updatedWebhook.response.method).to.equal(webhookData.response.method || 'POST'); + chai.expect(updatedWebhook.url).to.equal(webhookData.url); chai.expect(updatedWebhook.reference).to.deep.equal(ref); chai.expect(updatedWebhook.custom).to.deep.equal(webhookData.custom || {}); diff --git a/test/6xx_api_tests/core_tests/607a-webhook-api-core-tests.js b/test/6xx_api_tests/core_tests/607a-webhook-api-core-tests.js index c4a2dabd..56bf4da4 100644 --- a/test/6xx_api_tests/core_tests/607a-webhook-api-core-tests.js +++ b/test/6xx_api_tests/core_tests/607a-webhook-api-core-tests.js @@ -124,9 +124,8 @@ function postWebhooks(done) { chai.expect(createdWebhook.type).to.equal(webhookDataObj.type); chai.expect(createdWebhook.description).to.equal(webhookDataObj.description); chai.expect(createdWebhook.triggers).to.deep.equal(webhookDataObj.triggers); - if (createdWebhook.response) { - chai.expect(createdWebhook.response.url).to.equal(webhookDataObj.response.url); - chai.expect(createdWebhook.response.method).to.equal(webhookDataObj.response.method || 'POST'); + if (createdWebhook.type === 'Outgoing') { + chai.expect(createdWebhook.url).to.equal(webhookDataObj.url); } else { chai.expect(createdWebhook.token).to.equal(token); @@ -178,8 +177,7 @@ function getWebhook(done) { const foundWebhook = JSON.parse(body); chai.expect(foundWebhook.name).to.equal(webhookData.name); chai.expect(foundWebhook.triggers).to.deep.equal(webhookData.triggers); - chai.expect(foundWebhook.response.url).to.equal(webhookData.response.url); - chai.expect(foundWebhook.response.method).to.equal(webhookData.response.method || 'POST'); + chai.expect(foundWebhook.url).to.equal(webhookData.url); chai.expect(foundWebhook.reference).to.equal(''); chai.expect(foundWebhook.custom).to.deep.equal(webhookData.custom || {}); @@ -234,9 +232,8 @@ function getWebhooks(done) { chai.expect(foundWebhook.type).to.equal(webhookDataObj.type); chai.expect(foundWebhook.description).to.equal(webhookDataObj.description); chai.expect(foundWebhook.triggers).to.deep.equal(webhookDataObj.triggers); - if (foundWebhook.response) { - chai.expect(foundWebhook.response.url).to.equal(webhookDataObj.response.url); - chai.expect(foundWebhook.response.method).to.equal(webhookDataObj.response.method || 'POST'); + if (foundWebhook.type === 'Outgoing') { + chai.expect(foundWebhook.url).to.equal(webhookDataObj.url); } else { chai.expect(foundWebhook.token).to.equal(token); @@ -296,9 +293,8 @@ function getAllWebhooks(done) { chai.expect(foundWebhook.type).to.equal(webhookDataObj.type); chai.expect(foundWebhook.description).to.equal(webhookDataObj.description); chai.expect(foundWebhook.triggers).to.deep.equal(webhookDataObj.triggers); - if (foundWebhook.response) { - chai.expect(foundWebhook.response.url).to.equal(webhookDataObj.response.url); - chai.expect(foundWebhook.response.method).to.equal(webhookDataObj.response.method || 'POST'); + if (foundWebhook.type === 'Outgoing') { + chai.expect(foundWebhook.url).to.equal(webhookDataObj.url); } else { chai.expect(foundWebhook.token).to.equal(token); @@ -350,8 +346,7 @@ function patchWebhook(done) { const updatedWebhook = JSON.parse(body); chai.expect(updatedWebhook.name).to.equal('test update'); chai.expect(updatedWebhook.triggers).to.deep.equal(webhookData.triggers); - chai.expect(updatedWebhook.response.url).to.equal(webhookData.response.url); - chai.expect(updatedWebhook.response.method).to.equal(webhookData.response.method || 'POST'); + chai.expect(updatedWebhook.url).to.equal(webhookData.url); chai.expect(updatedWebhook.reference).to.equal(''); chai.expect(updatedWebhook.custom).to.deep.equal(webhookData.custom || {}); @@ -411,9 +406,8 @@ function patchWebhooks(done) { chai.expect(updatedWebhook.type).to.equal(webhookDataObj.type); chai.expect(updatedWebhook.description).to.equal(webhookDataObj.description); chai.expect(updatedWebhook.triggers).to.deep.equal(webhookDataObj.triggers); - if (updatedWebhook.response) { - chai.expect(updatedWebhook.response.url).to.equal(webhookDataObj.response.url); - chai.expect(updatedWebhook.response.method).to.equal(webhookDataObj.response.method || 'POST'); + if (updatedWebhook.type === 'Outgoing') { + chai.expect(updatedWebhook.url).to.equal(webhookDataObj.url); } else { chai.expect(updatedWebhook.token).to.equal(token); diff --git a/test/test_data.json b/test/test_data.json index 7cdeefbc..08c8abf5 100644 --- a/test/test_data.json +++ b/test/test_data.json @@ -373,7 +373,7 @@ "type": "Outgoing", "description": "test webhook description 0", "triggers": ["test-event"], - "response": { "url": "testurl0" } + "url": "testurl0" }, { "name": "test_webhook01", @@ -388,7 +388,7 @@ "type": "Outgoing", "description": "test webhook description 2", "triggers": ["test-event"], - "response": { "url": "testurl2" } + "url": "testurl2" } ] } From 0da5e5b53318517446b6c8d781eb3a47b64e03f4 Mon Sep 17 00:00:00 2001 From: "Eckstein, James" Date: Wed, 18 Mar 2020 13:47:43 +0000 Subject: [PATCH 4/5] Fixed extraneous re-renders in Edit tooltip. Fixed extraneous AJAX request when closing Element Info side panel. --- CHANGELOG.md | 7 ++++ app/controllers/webhook-controller.js | 2 +- app/lib/migrate.js | 2 +- package.json | 8 ++-- scripts/webpack-dev.config.js | 58 +++++++++++++++++++++++++++ 5 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 scripts/webpack-dev.config.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 8612a692..9d471380 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ # Changelog All notable changes to this project will be documented in this file. +## [1.0.4] - 2020-03-13 +### Bug Fixes and Other Changes +* Fixed a bug preventing Webhook creation with custom data. +* Added `scripts/webpack-dev.config.js` to support hot reload for React + development. Running `yarn watch` in a separate terminal will transpile + updates made to React component JSX files. + ## [1.0.3] - 2020-02-28 ### Major Features and Improvements * Refactored outgoing webhooks to simplify response input diff --git a/app/controllers/webhook-controller.js b/app/controllers/webhook-controller.js index 6c8b0cf1..da4f9ecc 100644 --- a/app/controllers/webhook-controller.js +++ b/app/controllers/webhook-controller.js @@ -270,7 +270,7 @@ async function create(requestingUser, webhooks, options) { // Create a list of valid keys const validWebhookKeys = ['name', 'type', 'description', 'triggers', 'url', 'token', - 'tokenLocation', 'reference']; + 'tokenLocation', 'reference', 'custom']; // Check that user has permission to create webhooks await checkPermissions(reqUser, webhooksToCreate, 'createWebhook'); diff --git a/app/lib/migrate.js b/app/lib/migrate.js index 27d2be93..dbd63e50 100644 --- a/app/lib/migrate.js +++ b/app/lib/migrate.js @@ -72,7 +72,7 @@ module.exports.migrate = async function(args) { const knownVersions = ['0.6.0', '0.6.0.1', '0.7.0', '0.7.1', '0.7.2', '0.7.3', '0.7.3.1', '0.8.0', '0.8.1', '0.8.2', '0.8.3', '0.9.0', '0.9.1', '0.9.2', '0.9.3', '0.9.4', '0.9.5', '0.10.0', '0.10.1', '0.10.2', '0.10.3', - '0.10.4', '0.10.5', '1.0.0', '1.0.1', '1.0.2', '1.0.3']; + '0.10.4', '0.10.5', '1.0.0', '1.0.1', '1.0.2', '1.0.3', '1.0.4']; // Run the migrations await runMigrations(knownVersions.slice(knownVersions.indexOf(fromVersion) + 1)); diff --git a/package.json b/package.json index d7cbfc5f..5e9c41ea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mbee", - "version": "1.0.3", + "version": "1.0.4", "build": "NO_BUILD_NUMBER", "description": "Model-Based Engineering Environment", "main": "mbee.js", @@ -66,7 +66,8 @@ "react-router-dom": "^5.0.1", "reactstrap": "^8.4.0", "sinon": "^7.5.0", - "webpack": "^4.41.3" + "webpack": "^4.41.3", + "webpack-cli": "^3.3.11" }, "scripts": { "build": "node mbee build", @@ -77,7 +78,8 @@ "postinstall": "test $NOPOSTINSTALL || node mbee build", "preinstall": "test $NOPREINSTALL || node ./scripts/clean.js --all", "start": "node mbee start", - "test": "node mbee test" + "test": "node mbee test", + "watch": "./node_modules/.bin/webpack --watch --config scripts/webpack-dev.config.js" }, "engines": { "node": ">=10.15.0" diff --git a/scripts/webpack-dev.config.js b/scripts/webpack-dev.config.js new file mode 100644 index 00000000..f7ac4926 --- /dev/null +++ b/scripts/webpack-dev.config.js @@ -0,0 +1,58 @@ +/** + * @classification UNCLASSIFIED + * + * @module scripts.webpack-dev + * + * @copyright Copyright (C) 2020, Lockheed Martin Corporation + * + * @license MIT + * + * @owner Donte McDaniel + * + * @author Donte McDaniel + * + * @description The webpack configuration for development automatic recompile. + */ + +const webpack = require('webpack'); +const path = require('path'); +const rootPath = path.join(path.dirname(__dirname).split(path.sep).join('/')); + +module.exports = { + mode: 'development', + entry: { + navbar: path.join(rootPath, 'app', 'ui', 'components', 'apps', 'nav-app.jsx'), + 'home-app': path.join(rootPath, 'app', 'ui', 'components', 'apps', 'home-app.jsx'), + 'org-app': path.join(rootPath, 'app', 'ui', 'components', 'apps', 'org-app.jsx'), + 'project-app': path.join(rootPath, 'app', 'ui', 'components', 'apps', 'project-app.jsx'), + 'profile-app': path.join(rootPath, 'app', 'ui', 'components', 'apps', 'profile-app.jsx'), + 'admin-console-app': path.join(rootPath, 'app', 'ui', 'components', 'apps', 'admin-console-app.jsx') + }, + output: { + path: path.join(rootPath, 'build', 'public', 'js'), + filename: '[name].js' + }, + devServer: { + historyApiFallback: true + }, + module: { + rules: [ + { + test: /\.jsx?$/, + loader: ['babel-loader'], + exclude: /node_modules/ + }, + { + test: /\.css$/i, + use: ['style-loader', 'css-loader'] + } + ] + }, + plugins: [ + new webpack.HotModuleReplacementPlugin(), + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1 + }) + ] + // devtool: 'source-map' +}; From f307d2267c0b798f1f6365dcf0b06cb7adf9caa8 Mon Sep 17 00:00:00 2001 From: "Eckstein, James" Date: Tue, 5 May 2020 14:44:32 +0000 Subject: [PATCH 5/5] Fixed extraneous re-renders in Edit tooltip. Fixed extraneous AJAX request when closing Element Info side panel. --- CHANGELOG.md | 12 ++- app/api-routes.js | 42 ++++++++ app/controllers/api-controller.js | 97 +++++++++++-------- app/controllers/webhook-controller.js | 2 +- app/lib/migrate.js | 2 +- app/lib/utils.js | 29 ++++++ app/models/webhook.js | 2 +- package.json | 2 +- .../307a-webhook-model-core-tests.js | 2 +- .../core_tests/501a-user-mock-core-tests.js | 11 ++- .../core_tests/502a-org-mock-core-tests.js | 10 +- .../503a-project-core-mock-tests.js | 15 ++- .../core_tests/504a-branch-mock-core-tests.js | 6 +- .../505a-element-mock-core-tests.js | 8 +- .../506a-artifact-mock-core-test.js | 7 +- .../507a-webhook-mock-core-tests.js | 8 +- .../505b-element-mock-error-tests.js | 56 +++++++++-- .../core_tests/601a-user-api-core-tests.js | 9 +- .../core_tests/602a-org-api-core-tests.js | 11 ++- .../core_tests/603a-project-api-core-tests.js | 11 ++- .../core_tests/604a-branch-api-core-tests.js | 11 ++- .../core_tests/605a-element-api-core-tests.js | 11 ++- .../606a-artifact-api-core-tests.js | 7 +- .../core_tests/607a-webhook-api-core-tests.js | 7 +- 24 files changed, 284 insertions(+), 94 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d471380..f02c1653 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,20 @@ # Changelog All notable changes to this project will be documented in this file. +## [1.1.0] - 2020-04-28 +### Bug Fixes and Other Changes +* Fixed a bug where webhook custom data could not be updated +* Fixed a bug where webhooks could not be unarchived +* Added support for ids query parameter for all batch delete endpoints +* Updated `5xx` and `6xx` core tests to leverage the `ids` query parameter + for all batch delete tests + ## [1.0.4] - 2020-03-13 ### Bug Fixes and Other Changes -* Fixed a bug preventing Webhook creation with custom data. +* Fixed a bug preventing Webhook creation with custom data * Added `scripts/webpack-dev.config.js` to support hot reload for React development. Running `yarn watch` in a separate terminal will transpile - updates made to React component JSX files. + updates made to React component JSX files ## [1.0.3] - 2020-02-28 ### Major Features and Improvements diff --git a/app/api-routes.js b/app/api-routes.js index dc701a4d..7288fd59 100644 --- a/app/api-routes.js +++ b/app/api-routes.js @@ -546,6 +546,12 @@ api.route('/logs') * type: array * items: * type: string + * - name: ids + * description: Comma separated list of organization IDs to delete. + * If both query parameter and body are provided, the + * query parameter will be prioritized over the body. + * in: query + * type: string * - name: minified * description: If true, the returned JSON is minified. If false, the * returned JSON is formatted based on the format specified @@ -1513,6 +1519,12 @@ api.route('/projects') * type: array * items: * type: string + * - name: ids + * description: Comma separated list of project IDs to delete. + * If both query parameter and body are provided, the + * query parameter will be prioritized over the body. + * in: query + * type: string * - name: minified * description: If true, the returned JSON is minified. If false, the * returned JSON is formatted based on the format specified @@ -2358,6 +2370,12 @@ api.route('/orgs/:orgid/projects/:projectid') * type: array * items: * type: string + * - name: ids + * description: Comma separated list of branch IDs to delete. + * If both query parameter and body are provided, the + * query parameter will be prioritized over the body. + * in: query + * type: string * - name: minified * description: If true, the returned JSON is minified. If false, the * returned JSON is formatted based on the format specified @@ -3526,6 +3544,12 @@ api.route('/orgs/:orgid/projects/:projectid/branches/:branchid/elements/search') * type: array * items: * type: string + * - name: ids + * description: Comma separated list of IDs to delete. If both query + * parameter and body are provided, the query parameter + * will be prioritized over the body. + * in: query + * type: string * - name: minified * description: If true, the returned JSON is minified. If false, the * returned JSON is formatted based on the format specified @@ -4831,6 +4855,12 @@ api.route('/orgs/:orgid/projects/:projectid/artifacts/blob') * type: array * items: * type: string + * - name: ids + * description: Comma separated list of artifact IDs to delete. + * If both query parameter and body are provided, the + * query parameter will be prioritized over the body. + * in: query + * type: string * - name: minified * description: If true, the returned JSON is minified. If false, the * returned JSON is formatted based on the format specified @@ -5750,6 +5780,12 @@ api.route('/orgs/:orgid/projects/:projectid/branches/:branchid/artifacts/:artifa * type: array * items: * type: string + * - name: ids + * description: Comma separated list of usernames to delete. + * If both query parameter and body are provided, the + * query parameter will be prioritized over the body. + * in: query + * type: string * - name: minified * description: If true, the returned JSON is minified. If false, the * returned JSON is formatted based on the format specified @@ -6789,6 +6825,12 @@ api.route('/users/:username/password') * type: array * items: * type: string + * - name: ids + * description: Comma separated list of webhook IDs to delete. + * If both query parameter and body are provided, the + * query parameter will be prioritized over the body. + * in: query + * type: string * - name: minified * description: If true, the returned JSON is minified. If false, the * returned JSON is formatted based on the format specified diff --git a/app/controllers/api-controller.js b/app/controllers/api-controller.js index 5234b978..233e13ce 100644 --- a/app/controllers/api-controller.js +++ b/app/controllers/api-controller.js @@ -443,7 +443,7 @@ async function getOrgs(req, res, next) { ids = options.ids; delete options.ids; } - // No IDs include in options, check body for IDs + // No IDs included in options, check body for IDs else if (Array.isArray(req.body) && req.body.every(s => typeof s === 'string')) { ids = req.body; } @@ -753,8 +753,8 @@ async function patchOrgs(req, res, next) { /** * DELETE /api/orgs * - * @description Deletes multiple orgs from an array of org IDs or array of org - * objects. + * @description Deletes multiple orgs from an array of org IDs, an array of org + * objects, or from a comma separated list of org IDs. * NOTE: This function is system-admin ONLY. * * @param {object} req - Request express object. @@ -774,7 +774,8 @@ async function deleteOrgs(req, res, next) { // Define valid option and its parsed type const validOptions = { - minified: 'boolean' + minified: 'boolean', + ids: 'array' }; // Sanity Check: there should always be a user in the request @@ -790,10 +791,11 @@ async function deleteOrgs(req, res, next) { return utils.formatResponse(req, res, error.message, errors.getStatusCode(error), next); } - // If req.body contains objects, grab the org IDs from the objects - if (Array.isArray(req.body) && req.body.every(s => typeof s === 'object')) { - req.body = req.body.map(o => o.id); - } + // Extract IDs from request + const ids = utils.parseRequestIDs(req, options); + + // Remove option IDs + delete options.ids; // Check options for minified if (options.hasOwnProperty('minified')) { @@ -803,7 +805,7 @@ async function deleteOrgs(req, res, next) { try { // Remove the specified orgs - const orgIDs = await OrgController.remove(req.user, req.body, options); + const orgIDs = await OrgController.remove(req.user, ids, options); // Format JSON const json = formatJSON(orgIDs, minified); @@ -1727,6 +1729,7 @@ async function deleteProjects(req, res, next) { // Define valid option and its parsed type const validOptions = { + ids: 'array', minified: 'boolean' }; @@ -1743,10 +1746,11 @@ async function deleteProjects(req, res, next) { return utils.formatResponse(req, res, error.message, errors.getStatusCode(error), next); } - // If req.body contains objects, grab the project IDs from the objects - if (Array.isArray(req.body) && req.body.every(s => typeof s === 'object')) { - req.body = req.body.map(p => p.id); - } + // Extract IDs from request + const ids = utils.parseRequestIDs(req, options); + + // Remove option IDs + delete options.ids; // Check options for minified if (options.hasOwnProperty('minified')) { @@ -1756,8 +1760,7 @@ async function deleteProjects(req, res, next) { try { // Remove the specified projects - const projectIDs = await ProjectController.remove(req.user, req.params.orgid, - req.body, options); + const projectIDs = await ProjectController.remove(req.user, req.params.orgid, ids, options); const parsedIDs = projectIDs.map(p => utils.parseID(p).pop()); // Format JSON @@ -2581,6 +2584,7 @@ async function deleteUsers(req, res, next) { // Define valid option and its parsed type const validOptions = { + ids: 'array', minified: 'boolean' }; @@ -2597,6 +2601,12 @@ async function deleteUsers(req, res, next) { return utils.formatResponse(req, res, error.message, errors.getStatusCode(error), next); } + // Extract IDs from request + const ids = utils.parseRequestIDs(req, options, true); + + // Remove option IDs + delete options.ids; + // Check options for minified if (options.hasOwnProperty('minified')) { minified = options.minified; @@ -2606,7 +2616,7 @@ async function deleteUsers(req, res, next) { try { // Remove the specified users // NOTE: remove() sanitizes req.body - const usernames = await UserController.remove(req.user, req.body, options); + const usernames = await UserController.remove(req.user, ids, options); // Format JSON const json = formatJSON(usernames, minified); @@ -3686,8 +3696,8 @@ async function patchElements(req, res, next) { /** * DELETE /api/orgs/:orgid/projects/:projectid/branches/:branchid/elements * - * @description Deletes multiple elements from an array of element IDs or array - * of element objects. + * @description Deletes multiple elements from an array of element IDs, an array + * of element objects, or from a comma separated list of element IDs. * * @param {object} req - Request express object * @param {object} res - Response express object @@ -3706,6 +3716,7 @@ async function deleteElements(req, res, next) { // Define valid option and its parsed type const validOptions = { + ids: 'array', minified: 'boolean' }; @@ -3722,6 +3733,12 @@ async function deleteElements(req, res, next) { return utils.formatResponse(req, res, error.message, errors.getStatusCode(error), next); } + // Extract IDs from request + const ids = utils.parseRequestIDs(req, options); + + // Remove option IDs + delete options.ids; + // Check options for minified if (options.hasOwnProperty('minified')) { minified = options.minified; @@ -3732,7 +3749,7 @@ async function deleteElements(req, res, next) { // Remove the specified elements // NOTE: remove() sanitizes input params const elements = await ElementController.remove(req.user, req.params.orgid, - req.params.projectid, req.params.branchid, req.body, options); + req.params.projectid, req.params.branchid, ids, options); const parsedIDs = elements.map(e => utils.parseID(e).pop()); // Format JSON @@ -4594,6 +4611,7 @@ async function deleteBranches(req, res, next) { // Define valid option and its parsed type const validOptions = { + ids: 'array', minified: 'boolean' }; @@ -4610,10 +4628,11 @@ async function deleteBranches(req, res, next) { return utils.formatResponse(req, res, error.message, errors.getStatusCode(error), next); } - // If req.body contains objects, grab the branch IDs from the objects - if (Array.isArray(req.body) && req.body.every(s => typeof s === 'object')) { - req.body = req.body.map(b => b.id); - } + // Extract IDs from request + const ids = utils.parseRequestIDs(req, options); + + // Remove option IDs + delete options.ids; // Check options for minified if (options.hasOwnProperty('minified')) { @@ -4624,7 +4643,7 @@ async function deleteBranches(req, res, next) { try { // Remove the specified branches const branchIDs = await BranchController.remove(req.user, req.params.orgid, - req.params.projectid, req.body, options); + req.params.projectid, ids, options); const parsedIDs = branchIDs.map(p => utils.parseID(p).pop()); // Format JSON @@ -5309,7 +5328,6 @@ async function deleteArtifacts(req, res, next) { // Define options // Note: Undefined if not set - let artIDs; let options; let minified = false; @@ -5333,19 +5351,11 @@ async function deleteArtifacts(req, res, next) { return utils.formatResponse(req, res, error.message, errors.getStatusCode(error), next); } - // Check query for artifact IDs - if (options.ids) { - artIDs = options.ids; - delete options.ids; - } - else if (Array.isArray(req.body) && req.body.every(s => typeof s === 'string')) { - // No IDs included in options, check body - artIDs = req.body; - } - // Check artifact object in body - else if (Array.isArray(req.body) && req.body.every(s => typeof s === 'object')) { - artIDs = req.body.map(a => a.id); - } + // Extract IDs from request + const ids = utils.parseRequestIDs(req, options); + + // Remove option IDs + delete options.ids; // Check options for minified if (options.hasOwnProperty('minified')) { @@ -5356,7 +5366,7 @@ async function deleteArtifacts(req, res, next) { // Remove the specified artifacts // NOTE: remove() sanitizes input params const removedArtIDs = await ArtifactController.remove(req.user, req.params.orgid, - req.params.projectid, req.params.branchid, artIDs, options); + req.params.projectid, req.params.branchid, ids, options); const parsedIDs = removedArtIDs.map(a => utils.parseID(a).pop()); // Format JSON @@ -6239,6 +6249,7 @@ async function deleteWebhooks(req, res, next) { // Define valid option and its parsed type const validOptions = { + ids: 'array', minified: 'boolean' }; @@ -6259,6 +6270,12 @@ async function deleteWebhooks(req, res, next) { return utils.formatResponse(req, res, error.message, errors.getStatusCode(error), next); } + // Extract IDs from request + const ids = utils.parseRequestIDs(req, options); + + // Remove option IDs + delete options.ids; + // Check options for minified if (options.hasOwnProperty('minified')) { minified = options.minified; @@ -6267,7 +6284,7 @@ async function deleteWebhooks(req, res, next) { try { // Remove the specified webhooks - const webhooks = await WebhookController.remove(req.user, req.body, options); + const webhooks = await WebhookController.remove(req.user, ids, options); // Format JSON const json = formatJSON(webhooks, minified); diff --git a/app/controllers/webhook-controller.js b/app/controllers/webhook-controller.js index da4f9ecc..80d7d773 100644 --- a/app/controllers/webhook-controller.js +++ b/app/controllers/webhook-controller.js @@ -451,7 +451,7 @@ async function update(requestingUser, webhooks, options) { // An archived webhook cannot be updated if (webhook.archived && (webhookUpdate.archived === undefined - || webhookUpdate.archived !== false || webhookUpdate.archived !== 'false')) { + || !(webhookUpdate.archived === false || webhookUpdate.archived === 'false'))) { throw new M.OperationError(`The Webhook [${webhook._id}] is archived. ` + 'It must first be unarchived before performing this operation.', 'warn'); } diff --git a/app/lib/migrate.js b/app/lib/migrate.js index dbd63e50..6ed8985c 100644 --- a/app/lib/migrate.js +++ b/app/lib/migrate.js @@ -72,7 +72,7 @@ module.exports.migrate = async function(args) { const knownVersions = ['0.6.0', '0.6.0.1', '0.7.0', '0.7.1', '0.7.2', '0.7.3', '0.7.3.1', '0.8.0', '0.8.1', '0.8.2', '0.8.3', '0.9.0', '0.9.1', '0.9.2', '0.9.3', '0.9.4', '0.9.5', '0.10.0', '0.10.1', '0.10.2', '0.10.3', - '0.10.4', '0.10.5', '1.0.0', '1.0.1', '1.0.2', '1.0.3', '1.0.4']; + '0.10.4', '0.10.5', '1.0.0', '1.0.1', '1.0.2', '1.0.3', '1.0.4', '1.1.0']; // Run the migrations await runMigrations(knownVersions.slice(knownVersions.indexOf(fromVersion) + 1)); diff --git a/app/lib/utils.js b/app/lib/utils.js index d751c416..42e89560 100644 --- a/app/lib/utils.js +++ b/app/lib/utils.js @@ -628,3 +628,32 @@ module.exports.formatResponse = function formatResponse(req, res, message, statu // be passed in to this function when this function is called due to an error. if (next !== null) next(); }; + +/** + * @description This is a utility function that parses IDs from a request body or query parameters. + * + * @param {object} req - The request object. + * @param {object} options - The query parameter options from the request. + * @param {boolean} users - Indicates whether usernames (T) or IDs (F) are parsed. + * + * @returns {string[]} An array of ids. + */ +module.exports.parseRequestIDs = function parseRequestIDs(req, options, users = false) { + let ids = []; + const field = (users) ? 'usernames' : 'id'; + + // Check parsed query options for IDs + if (options.ids) { + ids = options.ids; + } + // If req.body contains array of IDs + else if (Array.isArray(req.body) && req.body.every(s => typeof s === 'string')) { + ids = req.body; + } + // If req.body contains objects, grab the IDs from the objects + else if (Array.isArray(req.body) && req.body.every(s => typeof s === 'object')) { + ids = req.body.map(id => id[field]); + } + + return ids; +}; diff --git a/app/models/webhook.js b/app/models/webhook.js index 6107fe35..345544fb 100644 --- a/app/models/webhook.js +++ b/app/models/webhook.js @@ -210,7 +210,7 @@ WebhookSchema.static('verifyAuthority', function(webhook, value) { */ WebhookSchema.static('getValidUpdateFields', function() { return ['name', 'description', 'triggers', 'url', 'token', 'tokenLocation', - 'archived']; + 'archived', 'custom']; }); /** diff --git a/package.json b/package.json index 5e9c41ea..874f14e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mbee", - "version": "1.0.4", + "version": "1.1.0", "build": "NO_BUILD_NUMBER", "description": "Model-Based Engineering Environment", "main": "mbee.js", diff --git a/test/3xx_ut_models/core_tests/307a-webhook-model-core-tests.js b/test/3xx_ut_models/core_tests/307a-webhook-model-core-tests.js index 0796696f..a81895d1 100644 --- a/test/3xx_ut_models/core_tests/307a-webhook-model-core-tests.js +++ b/test/3xx_ut_models/core_tests/307a-webhook-model-core-tests.js @@ -213,7 +213,7 @@ async function validUpdateFields() { try { // Set the array of correct update fields; const updateFields = ['name', 'description', 'triggers', 'url', 'token', - 'tokenLocation', 'archived']; + 'tokenLocation', 'archived', 'custom']; // Get the update fields from the webhook model const modelUpdateFields = Webhook.getValidUpdateFields(); diff --git a/test/5xx_mock_api_tests/core_tests/501a-user-mock-core-tests.js b/test/5xx_mock_api_tests/core_tests/501a-user-mock-core-tests.js index 49b97164..25d7af15 100644 --- a/test/5xx_mock_api_tests/core_tests/501a-user-mock-core-tests.js +++ b/test/5xx_mock_api_tests/core_tests/501a-user-mock-core-tests.js @@ -816,9 +816,16 @@ function deleteUsers(done) { testData.users[2], testData.users[3] ]; + + const userIDs = userData.map(u => u.username); + const ids = userIDs.join(','); + + const body = {}; + const query = { ids: ids }; + const params = {}; const method = 'DELETE'; - const req = testUtils.createRequest(adminUser, params, userData.map(u => u.username), method); + const req = testUtils.createRequest(adminUser, params, body, method, query); // Create response object const res = {}; @@ -831,7 +838,7 @@ function deleteUsers(done) { chai.expect(deletedUsernames.length).to.equal(userData.length); // Verify expected response - chai.expect(deletedUsernames).to.have.members(userData.map(u => u.username)); + chai.expect(deletedUsernames).to.have.members(userIDs); // Expect the statusCode to be 200 chai.expect(res.statusCode).to.equal(200); diff --git a/test/5xx_mock_api_tests/core_tests/502a-org-mock-core-tests.js b/test/5xx_mock_api_tests/core_tests/502a-org-mock-core-tests.js index 400a3fd0..f69d2115 100644 --- a/test/5xx_mock_api_tests/core_tests/502a-org-mock-core-tests.js +++ b/test/5xx_mock_api_tests/core_tests/502a-org-mock-core-tests.js @@ -640,9 +640,15 @@ function deleteOrgs(done) { testData.orgs[2], testData.orgs[3] ]; + + const orgIDs = orgData.map(o => o.id); + const ids = orgIDs.join(','); + const query = { ids: ids }; + const body = {}; + const params = {}; const method = 'DELETE'; - const req = testUtils.createRequest(adminUser, params, orgData, method); + const req = testUtils.createRequest(adminUser, params, body, method, query); // Set response as empty object const res = {}; @@ -654,7 +660,7 @@ function deleteOrgs(done) { res.send = function send(_data) { const deletedIDs = JSON.parse(_data); // Verify correct orgs deleted - chai.expect(deletedIDs).to.have.members(orgData.map(p => p.id)); + chai.expect(deletedIDs).to.have.members(orgIDs); // Expect the statusCode to be 200 chai.expect(res.statusCode).to.equal(200); diff --git a/test/5xx_mock_api_tests/core_tests/503a-project-core-mock-tests.js b/test/5xx_mock_api_tests/core_tests/503a-project-core-mock-tests.js index 739227e4..a22a6065 100644 --- a/test/5xx_mock_api_tests/core_tests/503a-project-core-mock-tests.js +++ b/test/5xx_mock_api_tests/core_tests/503a-project-core-mock-tests.js @@ -783,11 +783,16 @@ function deleteProjects(done) { testData.projects[3], testData.projects[4] ]; - const params = { - orgid: org._id - }; + + const projIDs = projData.map(p => p.id); + const ids = projIDs.join(','); + + const body = {}; + const query = { ids: ids }; + const params = { orgid: org._id }; const method = 'PATCH'; - const req = testUtils.createRequest(adminUser, params, projData.map(p => p.id), method); + + const req = testUtils.createRequest(adminUser, params, body, method, query); // Set response as empty object const res = {}; @@ -801,7 +806,7 @@ function deleteProjects(done) { const deletedIDs = JSON.parse(_data); // Verify correct project found - chai.expect(deletedIDs).to.have.members(projData.map(p => p.id)); + chai.expect(deletedIDs).to.have.members(projIDs); // Expect the statusCode to be 200 chai.expect(res.statusCode).to.equal(200); diff --git a/test/5xx_mock_api_tests/core_tests/504a-branch-mock-core-tests.js b/test/5xx_mock_api_tests/core_tests/504a-branch-mock-core-tests.js index a7d7e085..4750acc6 100644 --- a/test/5xx_mock_api_tests/core_tests/504a-branch-mock-core-tests.js +++ b/test/5xx_mock_api_tests/core_tests/504a-branch-mock-core-tests.js @@ -595,10 +595,14 @@ function deleteBranches(done) { testData.branches[6] ]; const branchIDs = branchData.map(b => b.id); + const ids = branchIDs.join(','); + + const body = {}; + const query = { ids: ids }; const params = { orgid: org._id, projectid: projID }; const method = 'DELETE'; - const req = testUtils.createRequest(adminUser, params, branchIDs, method); + const req = testUtils.createRequest(adminUser, params, body, method, query); // Set response as empty object const res = {}; diff --git a/test/5xx_mock_api_tests/core_tests/505a-element-mock-core-tests.js b/test/5xx_mock_api_tests/core_tests/505a-element-mock-core-tests.js index c93a5cca..d41e0f7d 100644 --- a/test/5xx_mock_api_tests/core_tests/505a-element-mock-core-tests.js +++ b/test/5xx_mock_api_tests/core_tests/505a-element-mock-core-tests.js @@ -947,10 +947,14 @@ function deleteElements(done) { testData.elements[6] ]; const elemIDs = elemData.map(e => e.id); + const ids = elemIDs.join(','); + + const body = {}; + const query = { ids: ids }; const params = { orgid: org._id, projectid: projID, branchid: branchID }; const method = 'DELETE'; - const req = testUtils.createRequest(adminUser, params, elemIDs, method); + const req = testUtils.createRequest(adminUser, params, body, method, query); // Set response as empty object const res = {}; @@ -961,7 +965,7 @@ function deleteElements(done) { // Verifies the response data res.send = function send(_data) { const arrDeletedElemIDs = JSON.parse(_data); - chai.expect(arrDeletedElemIDs).to.have.members(elemData.map(p => p.id)); + chai.expect(arrDeletedElemIDs).to.have.members(elemIDs); // Expect the statusCode to be 200 chai.expect(res.statusCode).to.equal(200); diff --git a/test/5xx_mock_api_tests/core_tests/506a-artifact-mock-core-test.js b/test/5xx_mock_api_tests/core_tests/506a-artifact-mock-core-test.js index a47e3a4a..8b0d5814 100644 --- a/test/5xx_mock_api_tests/core_tests/506a-artifact-mock-core-test.js +++ b/test/5xx_mock_api_tests/core_tests/506a-artifact-mock-core-test.js @@ -848,6 +848,11 @@ function deleteArtifacts(done) { testData.artifacts[2].id ]; + const ids = artIDs.join(','); + + const body = {}; + const query = { ids: ids }; + // Create request params const params = { orgid: orgID, @@ -855,7 +860,7 @@ function deleteArtifacts(done) { branchid: branchID }; const method = 'DELETE'; - const req = testUtils.createRequest(adminUser, params, artIDs, method); + const req = testUtils.createRequest(adminUser, params, body, method, query); // Set response as empty object const res = {}; diff --git a/test/5xx_mock_api_tests/core_tests/507a-webhook-mock-core-tests.js b/test/5xx_mock_api_tests/core_tests/507a-webhook-mock-core-tests.js index b77daa1a..cabc6110 100644 --- a/test/5xx_mock_api_tests/core_tests/507a-webhook-mock-core-tests.js +++ b/test/5xx_mock_api_tests/core_tests/507a-webhook-mock-core-tests.js @@ -576,10 +576,14 @@ function deleteWebhook(done) { function deleteWebhooks(done) { // Create request object const deleteIDs = webhookIDs.slice(1, 3); - const body = deleteIDs; + const ids = deleteIDs.join(','); + + const body = {}; + const query = { ids: ids }; + const params = {}; const method = 'DELETE'; - const req = testUtils.createRequest(adminUser, params, body, method); + const req = testUtils.createRequest(adminUser, params, body, method, query); // Set response as empty object const res = {}; diff --git a/test/5xx_mock_api_tests/error_tests/505b-element-mock-error-tests.js b/test/5xx_mock_api_tests/error_tests/505b-element-mock-error-tests.js index c555834a..1cd5d960 100644 --- a/test/5xx_mock_api_tests/error_tests/505b-element-mock-error-tests.js +++ b/test/5xx_mock_api_tests/error_tests/505b-element-mock-error-tests.js @@ -136,11 +136,23 @@ function noReqUser(endpoint) { const method = testUtils.parseMethod(endpoint); const params = {}; const body = {}; + const query = {}; + + // Build "query" for batch DELETE + if (endpoint === 'deleteElements') { + const elemIDs = [ + testData.elements[3].id, + testData.elements[4].id, + testData.elements[5].id + ]; + + query.ids = elemIDs.join(','); + } // Create the customized mocha function return function(done) { // Create request object - const req = testUtils.createRequest(null, params, body, method); + const req = testUtils.createRequest(null, params, body, method, query); // Create response object const res = {}; @@ -174,11 +186,23 @@ function invalidOptions(endpoint) { const method = testUtils.parseMethod(endpoint); const params = {}; const body = {}; + const query = {}; + + // Build "query" for batch DELETE + if (endpoint === 'deleteElements') { + const elemIDs = [ + testData.elements[3].id, + testData.elements[4].id, + testData.elements[5].id + ]; + + query.ids = elemIDs.join(','); + } // Create the customized mocha function return function(done) { // Create request object - const req = testUtils.createRequest(adminUser, params, body, method); + const req = testUtils.createRequest(adminUser, params, body, method, query); req.query = { invalid: 'invalid option' }; // Create response object @@ -249,21 +273,39 @@ function conflictingIDs(endpoint) { */ function notFound(endpoint) { return function(done) { - // Get an unused element id - const id = testData.elements[3].id; + // Get unused element ids + const elemIDs = [ + testData.elements[3].id, + testData.elements[4].id, + testData.elements[5].id + ]; + + const id = elemIDs[0]; + + // For batch GET/DELETE + const ids = elemIDs.join(','); + // Parse the method const method = testUtils.parseMethod(endpoint); + + let body = { id: id }; + let query = {}; + // Body must be an array of ids for get and delete; key-value pair for anything else - const body = (endpoint === 'deleteElements' || endpoint === 'getElements') - ? [id] : { id: id }; + if (endpoint === 'deleteElements' || endpoint === 'getElements') { + body = []; + query = { ids: ids }; + } + const params = { orgid: org._id, projectid: projID, branchid: branchID }; + // Add in a params field for singular element endpoints if (!endpoint.includes('Elements') && endpoint.includes('Element')) { params.elementid = id; } // Create request object - const req = testUtils.createRequest(adminUser, params, body, method); + const req = testUtils.createRequest(adminUser, params, body, method, query); // Create response object const res = {}; diff --git a/test/6xx_api_tests/core_tests/601a-user-api-core-tests.js b/test/6xx_api_tests/core_tests/601a-user-api-core-tests.js index 31830548..ed5b0010 100644 --- a/test/6xx_api_tests/core_tests/601a-user-api-core-tests.js +++ b/test/6xx_api_tests/core_tests/601a-user-api-core-tests.js @@ -735,12 +735,15 @@ function deleteUsers(done) { testData.users[2], testData.users[3] ]; + + const userIDs = userData.map(u => u.username); + const ids = userIDs.join(','); + request({ - url: `${test.url}/api/users`, + url: `${test.url}/api/users?ids=${ids}`, headers: testUtils.getHeaders(), ca: testUtils.readCaFile(), - method: 'DELETE', - body: JSON.stringify(userData.map(u => u.username)) + method: 'DELETE' }, (err, response, body) => { // Expect no error diff --git a/test/6xx_api_tests/core_tests/602a-org-api-core-tests.js b/test/6xx_api_tests/core_tests/602a-org-api-core-tests.js index be8a0ed7..8a736a22 100644 --- a/test/6xx_api_tests/core_tests/602a-org-api-core-tests.js +++ b/test/6xx_api_tests/core_tests/602a-org-api-core-tests.js @@ -572,12 +572,15 @@ function deleteOrgs(done) { testData.orgs[2], testData.orgs[3] ]; + + const orgIDs = orgData.map(o => o.id); + const ids = orgIDs.join(','); + request({ - url: `${test.url}/api/orgs`, + url: `${test.url}/api/orgs?ids=${ids}`, headers: testUtils.getHeaders(), ca: testUtils.readCaFile(), - method: 'DELETE', - body: JSON.stringify(orgData) + method: 'DELETE' }, function(err, response, body) { // Expect no error @@ -588,7 +591,7 @@ function deleteOrgs(done) { const deletedIDs = JSON.parse(body); // Verify correct orgs deleted - chai.expect(deletedIDs).to.have.members(orgData.map(p => p.id)); + chai.expect(deletedIDs).to.have.members(orgIDs); done(); }); diff --git a/test/6xx_api_tests/core_tests/603a-project-api-core-tests.js b/test/6xx_api_tests/core_tests/603a-project-api-core-tests.js index 90fa11b8..ada91426 100644 --- a/test/6xx_api_tests/core_tests/603a-project-api-core-tests.js +++ b/test/6xx_api_tests/core_tests/603a-project-api-core-tests.js @@ -665,12 +665,15 @@ function deleteProjects(done) { testData.projects[3], testData.projects[4] ]; + + const projIDs = projData.map(p => p.id); + const ids = projIDs.join(','); + request({ - url: `${test.url}/api/orgs/${org._id}/projects`, + url: `${test.url}/api/orgs/${org._id}/projects?ids=${ids}`, headers: testUtils.getHeaders(), ca: testUtils.readCaFile(), - method: 'DELETE', - body: JSON.stringify(projData.map(p => p.id)) + method: 'DELETE' }, (err, response, body) => { // Expect no error @@ -681,7 +684,7 @@ function deleteProjects(done) { const deletedIDs = JSON.parse(body); // Verify correct project deleted - chai.expect(deletedIDs).to.have.members(projData.map(p => p.id)); + chai.expect(deletedIDs).to.have.members(projIDs); done(); }); diff --git a/test/6xx_api_tests/core_tests/604a-branch-api-core-tests.js b/test/6xx_api_tests/core_tests/604a-branch-api-core-tests.js index e9ee268b..2a1a90a8 100644 --- a/test/6xx_api_tests/core_tests/604a-branch-api-core-tests.js +++ b/test/6xx_api_tests/core_tests/604a-branch-api-core-tests.js @@ -446,12 +446,15 @@ function deleteBranches(done) { testData.branches[5], testData.branches[6] ]; + + const branchIDs = branchData.map(b => b.id); + const ids = branchIDs.join(','); + request({ - url: `${test.url}/api/orgs/${org._id}/projects/${projID}/branches`, + url: `${test.url}/api/orgs/${org._id}/projects/${projID}/branches?ids=${ids}`, headers: testUtils.getHeaders(), ca: testUtils.readCaFile(), - method: 'DELETE', - body: JSON.stringify(branchData.map(b => b.id)) + method: 'DELETE' }, (err, response, body) => { // Expect no error @@ -460,7 +463,7 @@ function deleteBranches(done) { chai.expect(response.statusCode).to.equal(200); // Verify response body const deletedBranchIDs = JSON.parse(body); - chai.expect(deletedBranchIDs).to.have.members(branchData.map(b => b.id)); + chai.expect(deletedBranchIDs).to.have.members(branchIDs); done(); }); } diff --git a/test/6xx_api_tests/core_tests/605a-element-api-core-tests.js b/test/6xx_api_tests/core_tests/605a-element-api-core-tests.js index 751a4223..26714ef1 100644 --- a/test/6xx_api_tests/core_tests/605a-element-api-core-tests.js +++ b/test/6xx_api_tests/core_tests/605a-element-api-core-tests.js @@ -756,12 +756,15 @@ function deleteElements(done) { testData.elements[5], testData.elements[6] ]; + + const elemIDs = elemData.map(e => e.id); + const ids = elemIDs.join(','); + request({ - url: `${test.url}/api/orgs/${org._id}/projects/${projID}/branches/master/elements`, + url: `${test.url}/api/orgs/${org._id}/projects/${projID}/branches/master/elements?ids=${ids}`, headers: testUtils.getHeaders(), ca: testUtils.readCaFile(), - method: 'DELETE', - body: JSON.stringify(elemData.map(e => e.id)) + method: 'DELETE' }, (err, response, body) => { // Expect no error @@ -771,7 +774,7 @@ function deleteElements(done) { // Verify response body const deletedElementIDs = JSON.parse(body); - chai.expect(deletedElementIDs).to.have.members(elemData.map(p => p.id)); + chai.expect(deletedElementIDs).to.have.members(elemIDs); done(); }); } diff --git a/test/6xx_api_tests/core_tests/606a-artifact-api-core-tests.js b/test/6xx_api_tests/core_tests/606a-artifact-api-core-tests.js index 9c94dc29..7b54feec 100644 --- a/test/6xx_api_tests/core_tests/606a-artifact-api-core-tests.js +++ b/test/6xx_api_tests/core_tests/606a-artifact-api-core-tests.js @@ -707,11 +707,12 @@ function deleteArtifacts(done) { testData.artifacts[2].id ]; + const ids = artIDs.join(','); + const options = { method: 'DELETE', - url: `${test.url}/api/orgs/${orgID}/projects/${projID}/branches/${branchID}/artifacts`, - headers: testUtils.getHeaders(), - body: JSON.stringify(artIDs) + url: `${test.url}/api/orgs/${orgID}/projects/${projID}/branches/${branchID}/artifacts?ids=${ids}`, + headers: testUtils.getHeaders() }; request(options, (err, response, body) => { diff --git a/test/6xx_api_tests/core_tests/607a-webhook-api-core-tests.js b/test/6xx_api_tests/core_tests/607a-webhook-api-core-tests.js index 56bf4da4..715e45cb 100644 --- a/test/6xx_api_tests/core_tests/607a-webhook-api-core-tests.js +++ b/test/6xx_api_tests/core_tests/607a-webhook-api-core-tests.js @@ -468,12 +468,13 @@ function deleteWebhook(done) { */ function deleteWebhooks(done) { const deleteIDs = testData.webhooks.slice(1, 3).map((w) => w.id); + const ids = deleteIDs.join(','); + request({ - url: `${test.url}/api/webhooks`, + url: `${test.url}/api/webhooks?ids=${ids}`, headers: testUtils.getHeaders(), ca: testUtils.readCaFile(), - method: 'DELETE', - body: JSON.stringify(deleteIDs) + method: 'DELETE' }, (err, response, body) => { // Expect no error