From af6b5deb0230d5151d087eab0ccaba314539f66b Mon Sep 17 00:00:00 2001 From: Kerbelis Date: Thu, 18 Jan 2024 16:31:18 -0500 Subject: [PATCH 1/2] updating readme with project mode definition --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index de2418c349..6ee3fe14cf 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ This repository contains the source code for Heimdall's [Backend](https://github - [Heimdall with Backend (Server)](#heimdall-with-backend-server) - [Features](#features) - [Use Cases](#use-cases) + - [Heimdall Server - Project Mode] - [Getting Started / Installation](#getting-started--installation) - [Heimdall Lite](#heimdall-lite-1) - [Running via npm](#running-via-npm) From 051e0a9b8a8fb6a7d194672dcf1bd22b0309d1a1 Mon Sep 17 00:00:00 2001 From: Kerbelis Date: Wed, 31 Jan 2024 13:24:17 -0500 Subject: [PATCH 2/2] inital commit of heimdall2 project mode --- apps/backend/config/app_config.ts | 30 + .../20230206121829-create-pipelines_table.js | 56 ++ ...21911-create-pipeline-evaluations_table.js | 44 + .../20230305141122-create-products_table.js | 60 ++ ...05151291-create-product-pipelines_table.js | 44 + ...30702115157-create-group-products_table.js | 44 + ...30702124308-create-group-pipeline_table.js | 44 + ...240127154210-change-pipelines-to-builds.js | 377 ++++++++ apps/backend/package.json | 3 +- apps/backend/src/app.module.ts | 16 +- .../authorization-artifact.controller.ts | 40 + .../authorization-artifact.module.ts | 12 + .../authorization-artifact.service.ts | 64 ++ .../build-evaluations.model.ts | 41 + .../build-evaluations.module.ts | 8 + apps/backend/src/builds/build.model.ts | 69 ++ .../src/builds/builds.controller.spec.ts | 458 +++++++++ apps/backend/src/builds/builds.controller.ts | 179 ++++ apps/backend/src/builds/builds.module.ts | 28 + .../backend/src/builds/builds.service.spec.ts | 305 ++++++ apps/backend/src/builds/builds.service.ts | 99 ++ .../builds/dto/add-evaluation-to-build-dto.ts | 8 + apps/backend/src/builds/dto/build.dto.ts | 48 + .../src/builds/dto/create-build.dto.ts | 19 + .../dto/remove-evaluation-from-build-dto.ts | 8 + .../src/builds/dto/update-build.dto.ts | 15 + apps/backend/src/casl/casl-ability.factory.ts | 34 +- apps/backend/src/config/config.service.ts | 18 + .../src/config/dto/startup-settings.dto.ts | 2 + .../evaluation-tags.controller.spec.ts | 14 +- .../evaluation-tags.service.spec.ts | 14 +- .../evaluations.controller.spec.ts | 14 +- .../evaluations/evaluations.service.spec.ts | 14 +- .../src/group-builds/group-build.model.ts | 42 + .../src/group-builds/group-builds.module.ts | 8 + .../src/group-products/group-product.model.ts | 42 + .../group-products/group-products.module.ts | 8 + apps/backend/src/groups/group.model.ts | 10 + .../src/groups/groups.controller.spec.ts | 14 +- apps/backend/src/groups/groups.module.ts | 4 + .../backend/src/groups/groups.service.spec.ts | 14 +- apps/backend/src/groups/groups.service.ts | 29 + apps/backend/src/main.ts | 3 +- .../product-builds/product-builds.model.ts | 41 + .../product-builds/product-builds.module.ts | 8 + .../products/dto/add-build-to-product-dto.ts | 8 + .../dto/add-pipeline-to-product-dto.ts | 8 + .../src/products/dto/create-product.dto.ts | 24 + apps/backend/src/products/dto/product.dto.ts | 49 + .../dto/remove-build-from-product-dto.ts | 8 + .../dto/remove-pipeline-from-product-dto.ts | 8 + .../src/products/dto/update-product.dto.ts | 20 + apps/backend/src/products/product.model.ts | 73 ++ .../src/products/products.controller.spec.ts | 458 +++++++++ .../src/products/products.controller.ts | 201 ++++ apps/backend/src/products/products.module.ts | 27 + .../src/products/products.service.spec.ts | 305 ++++++ apps/backend/src/products/products.service.ts | 103 ++ .../src/statistics/dto/statistics.dto.ts | 4 + .../src/statistics/statistics.module.ts | 12 +- .../src/statistics/statistics.service.ts | 6 + .../src/users/users.controller.spec.ts | 14 +- apps/backend/src/users/users.service.spec.ts | 14 +- apps/backend/test/.env-ci | 4 +- apps/frontend/jest.config.js | 2 +- apps/frontend/package.json | 2 + .../src/components/cards/ProfileInfo.vue | 60 ++ .../src/components/cards/StatusCardRow.vue | 9 + .../src/components/cards/StatusChart.vue | 5 + .../cards/controltable/ControlRowCol.vue | 74 +- .../cards/controltable/ControlRowDetails.vue | 31 +- .../cards/controltable/ControlRowHeader.vue | 641 +++++++++++-- .../cards/controltable/ControlTable.vue | 903 ++++++++++-------- .../controltable/ControlTableSelector.vue | 85 ++ .../cards/controltable/OverrideRowCol.vue | 272 ++++++ .../cards/controltable/ResponsiveRowLarge.vue | 118 ++- .../controltable/ResponsiveRowSwitch.vue | 23 +- .../productcontrols/ProductControlsPanel.vue | 131 +++ .../src/components/cards/treemap/Cell.vue | 38 +- .../src/components/generic/ProjectButton.vue | 41 + .../src/components/generic/ViewModeButton.vue | 81 ++ .../DownloadAuthorizationArtifactModal.vue | 87 ++ .../frontend/src/components/global/Footer.vue | 2 +- .../src/components/global/ProjectModal.vue | 245 +++++ .../src/components/global/Sidebar.vue | 444 +++++---- .../components/global/UpdateNotification.vue | 94 +- .../src/components/global/UploadNexus.vue | 9 +- .../components/global/admin/Statistics.vue | 2 + .../sidebaritems/ProductDropdownContent.vue | 99 ++ .../sidebaritems/ProductSidebarFileList.vue | 134 +++ .../global/sidebaritems/SidebarFileList.vue | 6 + .../global/upload_tabs/BuildLoadFileList.vue | 105 ++ .../global/upload_tabs/LoadFileList.vue | 6 + .../upload_tabs/ProductLoadFileList.vue | 105 ++ apps/frontend/src/enums/assessment_type.ts | 6 + apps/frontend/src/enums/bucket_type.ts | 8 + .../src/enums/product_model_view_mode.ts | 6 + apps/frontend/src/plugins/vuetify.ts | 9 +- apps/frontend/src/router.ts | 2 +- apps/frontend/src/store/assessment_data.ts | 158 +++ apps/frontend/src/store/builds.ts | 48 + apps/frontend/src/store/color_hack.ts | 2 + apps/frontend/src/store/data_filters.ts | 16 +- apps/frontend/src/store/data_store.ts | 7 + apps/frontend/src/store/evaluations.ts | 64 ++ .../src/store/product_module_state.ts | 141 +++ apps/frontend/src/store/products.ts | 67 ++ apps/frontend/src/store/report_intake.ts | 59 ++ apps/frontend/src/store/search.ts | 34 +- apps/frontend/src/store/server.ts | 59 ++ apps/frontend/src/store/status_counts.ts | 1 + apps/frontend/src/utilities/aws_util.ts | 378 ++++---- .../frontend/src/utilities/compliance_util.ts | 34 + apps/frontend/src/views/Base.vue | 7 +- apps/frontend/src/views/Certifier.vue | 560 +++++++++++ apps/frontend/src/views/Compare.vue | 6 + apps/frontend/src/views/Cyber.vue | 549 +++++++++++ apps/frontend/src/views/Developer.vue | 554 +++++++++++ apps/frontend/src/views/Landing.vue | 14 +- apps/frontend/src/views/Login.vue | 4 + apps/frontend/src/views/Results.vue | 19 +- apps/frontend/tests/unit/Compare.spec.ts | 601 ++++++------ apps/frontend/tests/unit/Sidebar.spec.ts | 111 ++- .../tests/unit/parsing_and_counting.spec.ts | 1 + libs/inspecjs/.gitignore | 2 + libs/inspecjs/schemas/exec-json.json | 124 ++- .../schema_compat_patches/exec-json.sh | 17 + .../src/compat_impl/compat_inspec_1_0.ts | 753 ++++++++------- libs/inspecjs/src/compat_wrappers.ts | 434 +++++---- libs/inspecjs/src/context.ts | 12 +- .../src/generated_parsers/v_1_0/exec-json.ts | 61 +- .../generated_parsers/v_1_0/exec-jsonmin.ts | 5 +- .../generated_parsers/v_1_0/profile-json.ts | 5 +- libs/inspecjs/src/index.ts | 1 + libs/inspecjs/src/nist.ts | 3 +- libs/inspecjs/test/status_counts.ts | 1 + .../add-evaluation-to-build.interface.ts | 3 + libs/interfaces/build/build.interface.ts | 15 + .../build/create-build.interface.ts | 6 + .../remove-evaluation-from-build.interface.ts | 3 + .../build/update-build.interface.ts | 5 + .../config/startup-settings.interface.ts | 1 + libs/interfaces/index.ts | 11 + .../product/add-build-to-product.interface.ts | 3 + .../product/create-product.interface.ts | 7 + libs/interfaces/product/product.interface.ts | 16 + .../remove-build-from-product.interface.ts | 3 + .../product/update-product.interface.ts | 6 + .../statistics/statistics.interface.ts | 2 + yarn.lock | 44 +- 150 files changed, 10880 insertions(+), 1840 deletions(-) create mode 100644 apps/backend/migrations/20230206121829-create-pipelines_table.js create mode 100644 apps/backend/migrations/20230206121911-create-pipeline-evaluations_table.js create mode 100644 apps/backend/migrations/20230305141122-create-products_table.js create mode 100644 apps/backend/migrations/20230305151291-create-product-pipelines_table.js create mode 100644 apps/backend/migrations/20230702115157-create-group-products_table.js create mode 100644 apps/backend/migrations/20230702124308-create-group-pipeline_table.js create mode 100644 apps/backend/migrations/20240127154210-change-pipelines-to-builds.js create mode 100644 apps/backend/src/authorization-artifact/authorization-artifact.controller.ts create mode 100644 apps/backend/src/authorization-artifact/authorization-artifact.module.ts create mode 100644 apps/backend/src/authorization-artifact/authorization-artifact.service.ts create mode 100644 apps/backend/src/build-evaluations/build-evaluations.model.ts create mode 100644 apps/backend/src/build-evaluations/build-evaluations.module.ts create mode 100644 apps/backend/src/builds/build.model.ts create mode 100644 apps/backend/src/builds/builds.controller.spec.ts create mode 100644 apps/backend/src/builds/builds.controller.ts create mode 100644 apps/backend/src/builds/builds.module.ts create mode 100644 apps/backend/src/builds/builds.service.spec.ts create mode 100644 apps/backend/src/builds/builds.service.ts create mode 100644 apps/backend/src/builds/dto/add-evaluation-to-build-dto.ts create mode 100644 apps/backend/src/builds/dto/build.dto.ts create mode 100644 apps/backend/src/builds/dto/create-build.dto.ts create mode 100644 apps/backend/src/builds/dto/remove-evaluation-from-build-dto.ts create mode 100644 apps/backend/src/builds/dto/update-build.dto.ts create mode 100644 apps/backend/src/group-builds/group-build.model.ts create mode 100644 apps/backend/src/group-builds/group-builds.module.ts create mode 100644 apps/backend/src/group-products/group-product.model.ts create mode 100644 apps/backend/src/group-products/group-products.module.ts create mode 100644 apps/backend/src/product-builds/product-builds.model.ts create mode 100644 apps/backend/src/product-builds/product-builds.module.ts create mode 100644 apps/backend/src/products/dto/add-build-to-product-dto.ts create mode 100644 apps/backend/src/products/dto/add-pipeline-to-product-dto.ts create mode 100644 apps/backend/src/products/dto/create-product.dto.ts create mode 100644 apps/backend/src/products/dto/product.dto.ts create mode 100644 apps/backend/src/products/dto/remove-build-from-product-dto.ts create mode 100644 apps/backend/src/products/dto/remove-pipeline-from-product-dto.ts create mode 100644 apps/backend/src/products/dto/update-product.dto.ts create mode 100644 apps/backend/src/products/product.model.ts create mode 100644 apps/backend/src/products/products.controller.spec.ts create mode 100644 apps/backend/src/products/products.controller.ts create mode 100644 apps/backend/src/products/products.module.ts create mode 100644 apps/backend/src/products/products.service.spec.ts create mode 100644 apps/backend/src/products/products.service.ts create mode 100644 apps/frontend/src/components/cards/controltable/ControlTableSelector.vue create mode 100644 apps/frontend/src/components/cards/controltable/OverrideRowCol.vue create mode 100644 apps/frontend/src/components/cards/productcontrols/ProductControlsPanel.vue create mode 100644 apps/frontend/src/components/generic/ProjectButton.vue create mode 100644 apps/frontend/src/components/generic/ViewModeButton.vue create mode 100644 apps/frontend/src/components/global/DownloadAuthorizationArtifactModal.vue create mode 100644 apps/frontend/src/components/global/ProjectModal.vue create mode 100644 apps/frontend/src/components/global/sidebaritems/ProductDropdownContent.vue create mode 100644 apps/frontend/src/components/global/sidebaritems/ProductSidebarFileList.vue create mode 100644 apps/frontend/src/components/global/upload_tabs/BuildLoadFileList.vue create mode 100644 apps/frontend/src/components/global/upload_tabs/ProductLoadFileList.vue create mode 100644 apps/frontend/src/enums/assessment_type.ts create mode 100644 apps/frontend/src/enums/bucket_type.ts create mode 100644 apps/frontend/src/enums/product_model_view_mode.ts create mode 100644 apps/frontend/src/store/assessment_data.ts create mode 100644 apps/frontend/src/store/builds.ts create mode 100644 apps/frontend/src/store/product_module_state.ts create mode 100644 apps/frontend/src/store/products.ts create mode 100644 apps/frontend/src/utilities/compliance_util.ts create mode 100644 apps/frontend/src/views/Certifier.vue create mode 100644 apps/frontend/src/views/Cyber.vue create mode 100644 apps/frontend/src/views/Developer.vue create mode 100644 libs/interfaces/build/add-evaluation-to-build.interface.ts create mode 100644 libs/interfaces/build/build.interface.ts create mode 100644 libs/interfaces/build/create-build.interface.ts create mode 100644 libs/interfaces/build/remove-evaluation-from-build.interface.ts create mode 100644 libs/interfaces/build/update-build.interface.ts create mode 100644 libs/interfaces/product/add-build-to-product.interface.ts create mode 100644 libs/interfaces/product/create-product.interface.ts create mode 100644 libs/interfaces/product/product.interface.ts create mode 100644 libs/interfaces/product/remove-build-from-product.interface.ts create mode 100644 libs/interfaces/product/update-product.interface.ts diff --git a/apps/backend/config/app_config.ts b/apps/backend/config/app_config.ts index 44635051f5..b4d8edba4b 100644 --- a/apps/backend/config/app_config.ts +++ b/apps/backend/config/app_config.ts @@ -2,6 +2,12 @@ import * as dotenv from 'dotenv'; import * as fs from 'fs'; import { Dialect } from 'sequelize/types'; +export interface AuthArtiAuthCreds { + accessKeyId: string | undefined; + secretAccessKey: string | undefined; + sessionToken: string | undefined; +} + export default class AppConfig { private envConfig: { [key: string]: string | undefined }; @@ -166,4 +172,28 @@ export default class AppConfig { return true; } } + + getAuthArtiS3URL() { + return this.get("S3_URL") || ''; + } + getAuthArtiS3AccessKey() { + return this.get("S3_ACCESS_KEY") || ''; + } + + getAuthArtiS3Secret() { + return this.get("S3_SECRET_KEY") || ''; + } + + getAuthArtiS3BucketName() { + return this.get("S3_BUCKET_NAME") || ''; + } + + getAuthArtiS3AuthCreds() { + let s3AuthCreds: AuthArtiAuthCreds = { + accessKeyId: this.get("S3_ACCESS_KEY"), + secretAccessKey: this.get("S3_SECRET_KEY"), + sessionToken: "" + } + return s3AuthCreds; + } } diff --git a/apps/backend/migrations/20230206121829-create-pipelines_table.js b/apps/backend/migrations/20230206121829-create-pipelines_table.js new file mode 100644 index 0000000000..5190673390 --- /dev/null +++ b/apps/backend/migrations/20230206121829-create-pipelines_table.js @@ -0,0 +1,56 @@ +'use strict'; + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.createTable('Pipelines', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.BIGINT + }, + buildId: { + type: Sequelize.STRING, + allowNull: false + }, + buildType: { + type: Sequelize.INTEGER, + allowNull: true + }, + branchName: { + type: Sequelize.STRING, + allowNull: true + }, + groupId: { + type: Sequelize.BIGINT, + references: { + model: 'Groups', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'SET NULL' + }, + userId: { + type: Sequelize.BIGINT, + references: { + model: 'Users', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'SET NULL' + }, + createdAt: { + type: Sequelize.DATE, + allowNull: false + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: false + } + }) + }, + + down: (queryInterface, Sequelize) => { + return queryInterface.dropTable('Pipelines'); + } +}; diff --git a/apps/backend/migrations/20230206121911-create-pipeline-evaluations_table.js b/apps/backend/migrations/20230206121911-create-pipeline-evaluations_table.js new file mode 100644 index 0000000000..eef6209a34 --- /dev/null +++ b/apps/backend/migrations/20230206121911-create-pipeline-evaluations_table.js @@ -0,0 +1,44 @@ +'use strict'; + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.createTable('PipelineEvaluations', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.BIGINT + }, + pipelineId: { + type: Sequelize.BIGINT, + references: { + model: 'Pipelines', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'SET NULL' + }, + evaluationId: { + type: Sequelize.BIGINT, + references: { + model: 'Evaluations', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'SET NULL' + }, + createdAt: { + type: Sequelize.DATE, + allowNull: false + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: false + } + }) + }, + + down: (queryInterface, Sequelize) => { + return queryInterface.dropTable('PipelineEvaluations'); + } +}; diff --git a/apps/backend/migrations/20230305141122-create-products_table.js b/apps/backend/migrations/20230305141122-create-products_table.js new file mode 100644 index 0000000000..4a8b99b714 --- /dev/null +++ b/apps/backend/migrations/20230305141122-create-products_table.js @@ -0,0 +1,60 @@ +'use strict'; + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.createTable('Products', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.BIGINT + }, + productName: { + type: Sequelize.STRING, + allowNull: false + }, + productId: { + type: Sequelize.BIGINT, + allowNull: false + }, + productURL: { + type: Sequelize.STRING, + allowNull: true + }, + objectStoreKey: { + type: Sequelize.STRING, + allowNull: true + }, + groupId: { + type: Sequelize.BIGINT, + references: { + model: 'Groups', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'SET NULL' + }, + userId: { + type: Sequelize.BIGINT, + references: { + model: 'Users', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'SET NULL' + }, + createdAt: { + type: Sequelize.DATE, + allowNull: false + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: false + } + }) + }, + + down: (queryInterface, Sequelize) => { + return queryInterface.dropTable('Products'); + } +}; diff --git a/apps/backend/migrations/20230305151291-create-product-pipelines_table.js b/apps/backend/migrations/20230305151291-create-product-pipelines_table.js new file mode 100644 index 0000000000..91e14e78ef --- /dev/null +++ b/apps/backend/migrations/20230305151291-create-product-pipelines_table.js @@ -0,0 +1,44 @@ +'use strict'; + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.createTable('ProductPipelines', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.BIGINT + }, + pipelineId: { + type: Sequelize.BIGINT, + references: { + model: 'Pipelines', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'SET NULL' + }, + productId: { + type: Sequelize.BIGINT, + references: { + model: 'Products', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'SET NULL' + }, + createdAt: { + type: Sequelize.DATE, + allowNull: false + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: false + } + }) + }, + + down: (queryInterface, Sequelize) => { + return queryInterface.dropTable('ProductPipelines'); + } +}; diff --git a/apps/backend/migrations/20230702115157-create-group-products_table.js b/apps/backend/migrations/20230702115157-create-group-products_table.js new file mode 100644 index 0000000000..08811ce962 --- /dev/null +++ b/apps/backend/migrations/20230702115157-create-group-products_table.js @@ -0,0 +1,44 @@ +'use strict'; + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.createTable('GroupProducts', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.BIGINT + }, + groupId: { + type: Sequelize.BIGINT, + references: { + model: 'Groups', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'SET NULL' + }, + productId: { + type: Sequelize.BIGINT, + references: { + model: 'Products', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'SET NULL' + }, + createdAt: { + type: Sequelize.DATE, + allowNull: false + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: false + } + }) + }, + + down: (queryInterface, Sequelize) => { + return queryInterface.dropTable('GroupProducts'); + } +}; diff --git a/apps/backend/migrations/20230702124308-create-group-pipeline_table.js b/apps/backend/migrations/20230702124308-create-group-pipeline_table.js new file mode 100644 index 0000000000..7f0b845905 --- /dev/null +++ b/apps/backend/migrations/20230702124308-create-group-pipeline_table.js @@ -0,0 +1,44 @@ +'use strict'; + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.createTable('GroupPipelines', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.BIGINT + }, + groupId: { + type: Sequelize.BIGINT, + references: { + model: 'Groups', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'SET NULL' + }, + pipelineId: { + type: Sequelize.BIGINT, + references: { + model: 'Pipelines', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'SET NULL' + }, + createdAt: { + type: Sequelize.DATE, + allowNull: false + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: false + } + }) + }, + + down: (queryInterface, Sequelize) => { + return queryInterface.dropTable('GroupPipelines'); + } +}; diff --git a/apps/backend/migrations/20240127154210-change-pipelines-to-builds.js b/apps/backend/migrations/20240127154210-change-pipelines-to-builds.js new file mode 100644 index 0000000000..21d399b907 --- /dev/null +++ b/apps/backend/migrations/20240127154210-change-pipelines-to-builds.js @@ -0,0 +1,377 @@ +'use strict'; + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.sequelize.transaction((t) => { + return Promise.all([ + queryInterface.createTable('Builds', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.BIGINT + }, + buildId: { + type: Sequelize.STRING, + allowNull: false + }, + buildType: { + type: Sequelize.INTEGER, + allowNull: true + }, + branchName: { + type: Sequelize.STRING, + allowNull: true + }, + groupId: { + type: Sequelize.BIGINT, + references: { + model: 'Groups', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'SET NULL' + }, + userId: { + type: Sequelize.BIGINT, + references: { + model: 'Users', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'SET NULL' + }, + createdAt: { + type: Sequelize.DATE, + allowNull: false + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: false + } + }, { transaction: t }), + queryInterface.createTable('BuildEvaluations', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.BIGINT + }, + buildId: { + type: Sequelize.BIGINT, + references: { + model: 'Builds', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'SET NULL' + }, + evaluationId: { + type: Sequelize.BIGINT, + references: { + model: 'Evaluations', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'SET NULL' + }, + createdAt: { + type: Sequelize.DATE, + allowNull: false + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: false + } + }, { transaction: t }), + queryInterface.createTable('GroupBuilds', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.BIGINT + }, + groupId: { + type: Sequelize.BIGINT, + references: { + model: 'Groups', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'SET NULL' + }, + buildId: { + type: Sequelize.BIGINT, + references: { + model: 'Builds', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'SET NULL' + }, + createdAt: { + type: Sequelize.DATE, + allowNull: false + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: false + } + }, { transaction: t }), + queryInterface.createTable('ProductBuilds', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.BIGINT + }, + buildId: { + type: Sequelize.BIGINT, + references: { + model: 'Builds', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'SET NULL' + }, + productId: { + type: Sequelize.BIGINT, + references: { + model: 'Products', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'SET NULL' + }, + createdAt: { + type: Sequelize.DATE, + allowNull: false + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: false + } + }, { transaction: t }), + t.afterCommit(() => { + return queryInterface.sequelize.transaction((t2) => { + return Promise.all([ + queryInterface.sequelize.query(` + INSERT INTO "Builds" (id, "buildId", "buildType", "branchName", "groupId", "userId", "createdAt", "updatedAt") + SELECT id, "buildId", "buildType", "branchName", "groupId", "userId", "createdAt", "updatedAt" FROM "Pipelines"; + SELECT setval('"Builds_id_seq"',(SELECT max(id) FROM "Builds")); + `, { transaction: t2 }), + queryInterface.sequelize.query(` + INSERT INTO "BuildEvaluations" (id, "buildId", "evaluationId", "createdAt", "updatedAt") + SELECT id, "pipelineId", "evaluationId", "createdAt", "updatedAt" FROM "PipelineEvaluations"; + SELECT setval('"BuildEvaluations_id_seq"',(SELECT max(id) FROM "BuildEvaluations")); + `, { transaction: t2 }), + queryInterface.sequelize.query(` + INSERT INTO "GroupBuilds" (id, "buildId", "groupId", "createdAt", "updatedAt") + SELECT id, "pipelineId", "groupId", "createdAt", "updatedAt" FROM "GroupPipelines"; + SELECT setval('"GroupBuilds_id_seq"',(SELECT max(id) FROM "GroupBuilds")); + `, { transaction: t2 }), + queryInterface.sequelize.query(` + INSERT INTO "ProductBuilds" (id, "buildId", "productId", "createdAt", "updatedAt") + SELECT id, "pipelineId", "productId", "createdAt", "updatedAt" FROM "ProductPipelines"; + SELECT setval('"ProductBuilds_id_seq"',(SELECT max(id) FROM "ProductBuilds")); + `, { transaction: t2 }), + t2.afterCommit(() => { + queryInterface.dropTable('ProductPipelines'), + queryInterface.dropTable('GroupPipelines'), + queryInterface.dropTable('PipelineEvaluations'), + queryInterface.dropTable('Pipelines') + }) + ]) + }) + }) + ]) + }) + }, + + down: (queryInterface, Sequelize) => { + return queryInterface.sequelize.transaction((t) => { + return Promise.all([ + queryInterface.createTable('Pipelines', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.BIGINT + }, + buildId: { + type: Sequelize.STRING, + allowNull: false + }, + buildType: { + type: Sequelize.INTEGER, + allowNull: true + }, + branchName: { + type: Sequelize.STRING, + allowNull: true + }, + groupId: { + type: Sequelize.BIGINT, + references: { + model: 'Groups', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'SET NULL' + }, + userId: { + type: Sequelize.BIGINT, + references: { + model: 'Users', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'SET NULL' + }, + createdAt: { + type: Sequelize.DATE, + allowNull: false + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: false + } + }, { transaction: t }), + queryInterface.createTable('PipelineEvaluations', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.BIGINT + }, + pipelineId: { + type: Sequelize.BIGINT, + references: { + model: 'Pipelines', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'SET NULL' + }, + evaluationId: { + type: Sequelize.BIGINT, + references: { + model: 'Evaluations', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'SET NULL' + }, + createdAt: { + type: Sequelize.DATE, + allowNull: false + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: false + } + }, { transaction: t }), + queryInterface.createTable('GroupPipelines', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.BIGINT + }, + groupId: { + type: Sequelize.BIGINT, + references: { + model: 'Groups', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'SET NULL' + }, + pipelineId: { + type: Sequelize.BIGINT, + references: { + model: 'Pipelines', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'SET NULL' + }, + createdAt: { + type: Sequelize.DATE, + allowNull: false + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: false + } + }, { transaction: t }), + queryInterface.createTable('ProductPipelines', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.BIGINT + }, + pipelineId: { + type: Sequelize.BIGINT, + references: { + model: 'Pipelines', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'SET NULL' + }, + productId: { + type: Sequelize.BIGINT, + references: { + model: 'Products', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'SET NULL' + }, + createdAt: { + type: Sequelize.DATE, + allowNull: false + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: false + } + }, { transaction: t }), + t.afterCommit(() => { + return queryInterface.sequelize.transaction((t2) => { + return Promise.all([ + queryInterface.sequelize.query(` + INSERT INTO "Pipelines" (id, "buildId", "buildType", "branchName", "groupId", "userId", "createdAt", "updatedAt") + SELECT id, "buildId", "buildType", "branchName", "groupId", "userId", "createdAt", "updatedAt" FROM "Builds"; + SELECT setval('"Pipelines_id_seq"',(SELECT max(id) FROM "Pipelines")); + `, { transaction: t2 }), + queryInterface.sequelize.query(` + INSERT INTO "PipelineEvaluations" (id, "pipelineId", "evaluationId", "createdAt", "updatedAt") + SELECT id, "buildId", "evaluationId", "createdAt", "updatedAt" FROM "BuildEvaluations"; + SELECT setval('"PipelineEvaluations_id_seq"',(SELECT max(id) FROM "PipelineEvaluations")); + `, { transaction: t2 }), + queryInterface.sequelize.query(` + INSERT INTO "GroupPipelines" (id, "pipelineId", "groupId", "createdAt", "updatedAt") + SELECT id, "buildId", "groupId", "createdAt", "updatedAt" FROM "GroupBuilds"; + SELECT setval('"GroupPipelines_id_seq"',(SELECT max(id) FROM "GroupPipelines")); + `, { transaction: t2 }), + queryInterface.sequelize.query(` + INSERT INTO "ProductPipelines" (id, "pipelineId", "productId", "createdAt", "updatedAt") + SELECT id, "buildId", "productId", "createdAt", "updatedAt" FROM "ProductBuilds"; + SELECT setval('"ProductPipelines_id_seq"',(SELECT max(id) FROM "ProductPipelines")); + `, { transaction: t2 }), + t2.afterCommit(() => { + queryInterface.dropTable('ProductBuilds'), + queryInterface.dropTable('GroupBuilds'), + queryInterface.dropTable('BuildEvaluations'), + queryInterface.dropTable('Builds') + }) + ]) + }) + }) + ]) + }) + } +}; diff --git a/apps/backend/package.json b/apps/backend/package.json index c479c4d308..3d1b859d93 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -109,7 +109,8 @@ "ts-node": "^10.0.0", "tsconfig-paths": "^4.0.0", "uuid": "^9.0.0", - "winston": "^3.3.3" + "winston": "^3.3.3", + "aws-sdk": "^2.573.0" }, "devDependencies": { "@nestjs/testing": "^10.2.1", diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index f3b68ef1ed..bdcc499e8f 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -14,9 +14,16 @@ import {EvaluationTagsModule} from './evaluation-tags/evaluation-tags.module'; import {EvaluationsModule} from './evaluations/evaluations.module'; import {GroupEvaluationsModule} from './group-evaluations/group-evaluations.module'; import {GroupUsersModule} from './group-users/group-users.module'; +import {GroupBuildsModule} from './group-builds/group-builds.module'; +import {GroupProductsModule} from './group-products/group-products.module'; import {GroupsModule} from './groups/groups.module'; import {StatisticsModule} from './statistics/statistics.module'; import {UsersModule} from './users/users.module'; +import {BuildsModule} from './builds/builds.module'; +import {BuildEvaluationsModule} from './build-evaluations/build-evaluations.module'; +import {ProductsModule} from './products/products.module'; +import {ProductBuildsModule} from './product-builds/product-builds.module'; +import {AuthorizationArtifactModule} from './authorization-artifact/authorization-artifact.module'; @Module({ controllers: [AppController], @@ -33,9 +40,16 @@ import {UsersModule} from './users/users.module'; EvaluationTagsModule, EvaluationsModule, GroupEvaluationsModule, + GroupBuildsModule, + GroupProductsModule, GroupsModule, GroupUsersModule, - StatisticsModule + BuildsModule, + BuildEvaluationsModule, + ProductsModule, + ProductBuildsModule, + StatisticsModule, + AuthorizationArtifactModule ], providers: [ AppService, diff --git a/apps/backend/src/authorization-artifact/authorization-artifact.controller.ts b/apps/backend/src/authorization-artifact/authorization-artifact.controller.ts new file mode 100644 index 0000000000..64390f941b --- /dev/null +++ b/apps/backend/src/authorization-artifact/authorization-artifact.controller.ts @@ -0,0 +1,40 @@ +import { + Controller, + Get, + Query, + Request, + UseInterceptors +} from '@nestjs/common'; +import S3 from 'aws-sdk/clients/s3'; +import {LoggingInterceptor} from '../interceptors/logging.interceptor'; +import {User} from '../users/user.model'; +import {AuthorizationArtifactService} from './authorization-artifact.service'; + +@Controller('autharti') +@UseInterceptors(LoggingInterceptor) +export class AuthorizationArtifactController { + constructor( + private readonly authorizationArtifactService: AuthorizationArtifactService + ){} + + @Get() + async listFiles(@Request() request: {user: User}, @Query('prefix') prefix: string): Promise { + return this.authorizationArtifactService.listFiles(prefix); + } + + @Get('buckets') + async listBuckets(@Request() request: {user: User}): Promise { + return this.authorizationArtifactService.listBuckets(); + } + + @Get('sign') + async getSignedUrl(@Request() request: {user: User}, @Query('key') key: string): Promise { + console.log(key); + return this.authorizationArtifactService.getSignedUrl(key); + } + + @Get('download') + async downloadFile(@Request() request: {user: User}, @Query('key') key: string): Promise { + return this.authorizationArtifactService.downloadFile(key); + } +} diff --git a/apps/backend/src/authorization-artifact/authorization-artifact.module.ts b/apps/backend/src/authorization-artifact/authorization-artifact.module.ts new file mode 100644 index 0000000000..4de4b83d34 --- /dev/null +++ b/apps/backend/src/authorization-artifact/authorization-artifact.module.ts @@ -0,0 +1,12 @@ +import {Module} from '@nestjs/common'; +import {ConfigModule} from '../config/config.module'; +import {AuthorizationArtifactController} from './authorization-artifact.controller'; +import {AuthorizationArtifactService} from './authorization-artifact.service'; + +@Module({ + imports: [ConfigModule], + providers: [AuthorizationArtifactService], + controllers: [AuthorizationArtifactController], + exports: [AuthorizationArtifactService] +}) +export class AuthorizationArtifactModule {} \ No newline at end of file diff --git a/apps/backend/src/authorization-artifact/authorization-artifact.service.ts b/apps/backend/src/authorization-artifact/authorization-artifact.service.ts new file mode 100644 index 0000000000..b2a50eeec5 --- /dev/null +++ b/apps/backend/src/authorization-artifact/authorization-artifact.service.ts @@ -0,0 +1,64 @@ +import {Injectable} from '@nestjs/common'; +import S3 from 'aws-sdk/clients/s3'; +import {ConfigService} from '../config/config.service'; +import axios from 'axios'; + +@Injectable() +export class AuthorizationArtifactService { + constructor( + private readonly configService: ConfigService + ) {} + + async listFiles(prefix: string): Promise { + const s3creds = this.configService.getAuthArtiS3AuthCreds(); + var params = { + Bucket: this.configService.getAuthArtiS3BucketName(), + Delimiter: "/", + Prefix: prefix, + MaxKeys: 32 + } + if (params.Bucket === "") { + return undefined + } + var bucketUrl = this.configService.getAuthArtiS3URL(); //NOTE: append getAuthArtiS3BucketName() if s3ForcePathStyle is removed + return new S3({endpoint: bucketUrl, signatureVersion: 'v4', s3ForcePathStyle: true, ...s3creds}) + .listObjectsV2(params).promise().then((success) => { + return success; + }, (error) => { + console.log(error); + return undefined; + }) + } + + async listBuckets(): Promise { + const s3creds = this.configService.getAuthArtiS3AuthCreds(); + return new S3({endpoint: this.configService.getAuthArtiS3URL(), signatureVersion: 'v4', ...s3creds}) + .listBuckets().promise().then((success) => { + return success; + }, (error) => { + console.log("bucket error", error); + return undefined; + }) + } + + async getSignedUrl(key: string): Promise { + const s3creds = this.configService.getAuthArtiS3AuthCreds(); + var params = { + Bucket: this.configService.getAuthArtiS3BucketName(), + Key: key, + Expires: 3600 + } + var bucketUrl = this.configService.getAuthArtiS3URL(); //NOTE: append getAuthArtiS3BucketName() if s3ForcePathStyle is removed + var s3 = new S3({endpoint: bucketUrl, signatureVersion: 'v4', s3ForcePathStyle: true, ...s3creds}); + return s3.getSignedUrlPromise('getObject', params); + } + + async downloadFile(key: string): Promise { + console.log("key", key); + let url = await this.getSignedUrl(key); + console.log("url", url); + let response = await axios({url: url, method: 'GET', responseType: 'arraybuffer'}); + let data = Buffer.from(response.data).toString('hex'); + return data; + } +} diff --git a/apps/backend/src/build-evaluations/build-evaluations.model.ts b/apps/backend/src/build-evaluations/build-evaluations.model.ts new file mode 100644 index 0000000000..ca2246339b --- /dev/null +++ b/apps/backend/src/build-evaluations/build-evaluations.model.ts @@ -0,0 +1,41 @@ +import { + AllowNull, + AutoIncrement, + Column, + CreatedAt, + DataType, + ForeignKey, + Model, + PrimaryKey, + Table, + UpdatedAt +} from 'sequelize-typescript'; +import {Build} from '../builds/build.model'; +import {Evaluation} from '../evaluations/evaluation.model'; + +@Table +export class BuildEvaluation extends Model { + @PrimaryKey + @AutoIncrement + @AllowNull(false) + @Column(DataType.BIGINT) + id!: string; + + @ForeignKey(() => Build) + @Column(DataType.BIGINT) + buildId!: string; + + @ForeignKey(() => Evaluation) + @Column(DataType.BIGINT) + evaluationId!: string; + + @CreatedAt + @AllowNull(false) + @Column(DataType.DATE) + createdAt!: Date; + + @UpdatedAt + @AllowNull(false) + @Column(DataType.DATE) + updatedAt!: Date; +} diff --git a/apps/backend/src/build-evaluations/build-evaluations.module.ts b/apps/backend/src/build-evaluations/build-evaluations.module.ts new file mode 100644 index 0000000000..288050973d --- /dev/null +++ b/apps/backend/src/build-evaluations/build-evaluations.module.ts @@ -0,0 +1,8 @@ +import {Module} from '@nestjs/common'; +import {SequelizeModule} from '@nestjs/sequelize'; +import {BuildEvaluation} from './build-evaluations.model'; + +@Module({ + imports: [SequelizeModule.forFeature([BuildEvaluation])] +}) +export class BuildEvaluationsModule {} diff --git a/apps/backend/src/builds/build.model.ts b/apps/backend/src/builds/build.model.ts new file mode 100644 index 0000000000..627090500d --- /dev/null +++ b/apps/backend/src/builds/build.model.ts @@ -0,0 +1,69 @@ +import { + AllowNull, + AutoIncrement, + BelongsTo, + BelongsToMany, + Column, + CreatedAt, + DataType, + ForeignKey, + Model, + PrimaryKey, + Table, + UpdatedAt +} from 'sequelize-typescript'; +import {Evaluation} from '../evaluations/evaluation.model'; +import {BuildEvaluation} from '../build-evaluations/build-evaluations.model'; +import {GroupBuild} from '../group-builds/group-build.model'; +import {Group} from '../groups/group.model'; +import {User} from '../users/user.model'; + +@Table +export class Build extends Model { + @PrimaryKey + @AutoIncrement + @AllowNull(false) + @Column(DataType.BIGINT) + id!: string; + + @AllowNull(false) + @Column + buildId!: string; + + @AllowNull(true) + @Column + buildType!: number; + + @AllowNull(true) + @Column + branchName!: string; + + @CreatedAt + @AllowNull(false) + @Column(DataType.DATE) + createdAt!: Date; + + @UpdatedAt + @AllowNull(false) + @Column(DataType.DATE) + updatedAt!: Date; + + @BelongsToMany(() => Evaluation, () => BuildEvaluation) + evaluations!: Array; + + @BelongsToMany(() => Group, () => GroupBuild) + groups!: Array; + + @ForeignKey(() => Group) + @Column(DataType.BIGINT) + groupId!: string; + + @ForeignKey(() => User) + @Column(DataType.BIGINT) + userId!: string; + + @BelongsTo(() => User, { + constraints: false + }) + user!: User; +} diff --git a/apps/backend/src/builds/builds.controller.spec.ts b/apps/backend/src/builds/builds.controller.spec.ts new file mode 100644 index 0000000000..0be2e3dcf3 --- /dev/null +++ b/apps/backend/src/builds/builds.controller.spec.ts @@ -0,0 +1,458 @@ +// import {ForbiddenError} from '@casl/ability'; +// import {NotFoundException} from '@nestjs/common'; +// import {SequelizeModule} from '@nestjs/sequelize'; +// import {Test, TestingModule} from '@nestjs/testing'; +// import { +// CREATE_EVALUATION_DTO_WITHOUT_TAGS, +// EVALUATION_1, +// EVALUATION_WITH_TAGS_1, +// UPDATE_EVALUATION +// } from '../../test/constants/pipelines-test.constant'; +// import { +// GROUP_1, +// PRIVATE_GROUP +// } from '../../test/constants/groups-test.constant'; +// import { +// CREATE_ADMIN_DTO, +// CREATE_USER_DTO_TEST_OBJ, +// CREATE_USER_DTO_TEST_OBJ_2 +// } from '../../test/constants/users-test.constant'; +// import {AuthzService} from '../authz/authz.service'; +// import {ConfigService} from '../config/config.service'; +// import {DatabaseModule} from '../database/database.module'; +// import {DatabaseService} from '../database/database.service'; +// //import {PipelineTag} from '../pipeline-tags/pipeline-tag.model'; +// // import {GroupPipeline} from '../group-pipelines/group-pipeline.model'; +// // import {GroupUser} from '../group-users/group-user.model'; +// // import {Group} from '../groups/group.model'; +// // import {GroupsService} from '../groups/groups.service'; +// import {User} from '../users/user.model'; +// import {UsersService} from '../users/users.service'; +// import {PipelineDto} from './dto/pipeline.dto'; +// import {Pipeline} from './pipeline.model'; +// import {PipelinesController} from './pipelines.controller'; +// import {PipelinesService} from './pipelines.service'; + +// // This allows basic testing of the pipelines controller +// // interface without having to construct a full File object +// /* eslint-disable @typescript-eslint/ban-ts-comment */ +// //@ts-ignore +// const mockFile: Express.Multer.File = { +// originalname: 'abc.json', +// buffer: Buffer.from('{}') +// }; +// //@ts-ignore +// const secondMockFile: Express.Multer.File = { +// originalname: 'cda.json', +// buffer: Buffer.from('{}') +// }; +// /* eslint-enable @typescript-eslint/ban-ts-comment */ + +// describe('PipelinesController', () => { +// let pipelinesController: PipelinesController; +// let pipelinesService: PipelinesService; +// let module: TestingModule; +// let databaseService: DatabaseService; +// let usersService: UsersService; +// let groupsService: GroupsService; + +// let user: User; + +// beforeAll(async () => { +// module = await Test.createTestingModule({ +// controllers: [PipelinesController], +// imports: [ +// DatabaseModule, +// SequelizeModule.forFeature([ +// PipelineTag, +// Pipeline, +// User, +// GroupPipeline, +// GroupUser, +// Group +// ]) +// ], +// providers: [ +// AuthzService, +// ConfigService, +// DatabaseService, +// UsersService, +// PipelinesService, +// GroupsService +// ] +// }).compile(); + +// databaseService = module.get(DatabaseService); +// pipelinesService = module.get(PipelinesService); +// pipelinesController = module.get( +// PipelinesController +// ); +// usersService = module.get(UsersService); +// groupsService = module.get(GroupsService); +// }); + +// beforeEach(async () => { +// await databaseService.cleanAll(); +// user = await usersService.create(CREATE_USER_DTO_TEST_OBJ); +// }); + +// describe('findById', () => { +// it('should return an pipeline', async () => { +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_1, +// data: mockFile, +// userId: user.id +// }); + +// const foundPipeline = await pipelinesController.findById( +// pipeline.id, +// {user: user} +// ); + +// expect(foundPipeline).toEqual( +// // The pipeline is created with the current user ID above +// // so the expectation is that user should be able to edit +// // which is the 2nd parameter to PipelineDto. +// new PipelineDto(pipeline, true) +// ); +// }); + +// it('should return an pipelines tags', async () => { +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_WITH_TAGS_1, +// data: mockFile, +// userId: user.id +// }); + +// const foundPipeline = await pipelinesController.findById( +// pipeline.id, +// {user: user} +// ); +// expect(foundPipeline.pipelineTags).toEqual( +// new PipelineDto(pipeline).pipelineTags +// ); +// }); + +// it('should throw a not found exeception when given an invalid id', async () => { +// expect.assertions(1); + +// await expect( +// pipelinesController.findById('0', {user: user}) +// ).rejects.toBeInstanceOf(NotFoundException); +// }); + +// it('should prevent non-owners from viewing an pipeline', async () => { +// expect.assertions(1); +// const pipelineOwner = await usersService.create( +// CREATE_USER_DTO_TEST_OBJ_2 +// ); +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_1, +// data: mockFile, +// userId: pipelineOwner.id +// }); +// await expect( +// pipelinesController.findById(pipeline.id, {user: user}) +// ).rejects.toBeInstanceOf(ForbiddenError); +// }); +// }); + +// describe('findAll', () => { +// it('should return all pipelines a user has permissions to read', async () => { +// await pipelinesService.create({ +// ...EVALUATION_1, +// data: mockFile, +// userId: user.id +// }); +// let foundPipelines = await pipelinesController.findAll({user: user}); +// expect(foundPipelines.length).toEqual(1); +// const pipelineOwner = await usersService.create( +// CREATE_USER_DTO_TEST_OBJ_2 +// ); +// await pipelinesService.create({ +// ...EVALUATION_1, +// data: mockFile, +// userId: pipelineOwner.id +// }); +// foundPipelines = await pipelinesController.findAll({user: user}); +// expect(foundPipelines.length).toEqual(1); +// }); + +// it('should return all pipelines and their associated tags', async () => { +// await pipelinesService.create({ +// ...EVALUATION_WITH_TAGS_1, +// data: mockFile, +// userId: user.id +// }); +// const foundPipelines = await pipelinesController.findAll({ +// user: user +// }); +// expect(foundPipelines[0].pipelineTags.length).toEqual(1); +// }); + +// it('should return editable true if the user is the owner of an pipeline', async () => { +// await pipelinesService.create({ +// ...EVALUATION_1, +// data: mockFile, +// userId: user.id +// }); +// const foundPipelines = await pipelinesController.findAll({ +// user: user +// }); +// expect(foundPipelines[0].editable).toBeTruthy(); +// }); + +// it('should return editable true if the user is the owner of a group that an pipeline belongs to', async () => { +// const pipelineOwner = await usersService.create( +// CREATE_USER_DTO_TEST_OBJ_2 +// ); +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_1, +// data: mockFile, +// userId: pipelineOwner.id +// }); +// const group = await groupsService.create(PRIVATE_GROUP); +// await groupsService.addUserToGroup(group, user, 'owner'); +// await groupsService.addPipelineToGroup(group, pipeline); +// const foundPipelines = await pipelinesController.findAll({ +// user: user +// }); + +// expect(foundPipelines[0].editable).toBeTruthy(); +// }); + +// it('should return editable false if the user is not owner of a group that an pipeline belongs to', async () => { +// const pipelineOwner = await usersService.create( +// CREATE_USER_DTO_TEST_OBJ_2 +// ); +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_1, +// data: mockFile, +// userId: pipelineOwner.id +// }); +// const group = await groupsService.create(GROUP_1); +// const group2 = await groupsService.create(PRIVATE_GROUP); +// await groupsService.addUserToGroup(group, user, 'user'); +// await groupsService.addUserToGroup(group2, user, 'owner'); +// await groupsService.addPipelineToGroup(group, pipeline); +// const foundPipelines = await pipelinesController.findAll({ +// user: user +// }); +// expect(foundPipelines[0].editable).toBeFalsy(); +// }); +// }); + +// describe('create', () => { +// it('should allow a user to create an pipeline', async () => { +// const pipeline = await pipelinesController.create( +// EVALUATION_WITH_TAGS_1, +// [mockFile], +// {user: user} +// ); +// expect(pipeline).toBeDefined(); +// if (Array.isArray(pipeline)) { +// throw new Error( +// 'Returned pipeline for one file upload should not be an array' +// ); +// } +// expect(pipeline.pipelineTags.length).toEqual(1); +// // Creating an pipeline should return a DTO without data. +// expect(pipeline.data).not.toBeDefined(); +// }); + +// it('should create an pipeline without tags', async () => { +// const pipeline = await pipelinesController.create( +// CREATE_EVALUATION_DTO_WITHOUT_TAGS, +// [mockFile], +// {user: user} +// ); +// expect(pipeline).toBeDefined(); +// if (Array.isArray(pipeline)) { +// throw new Error( +// 'Returned pipeline for one file upload should not be an array' +// ); +// } +// expect(pipeline.pipelineTags.length).toEqual(0); +// }); + +// it('should accept multiple pipelines', async () => { +// const pipelines = await pipelinesController.create( +// EVALUATION_WITH_TAGS_1, +// [mockFile, secondMockFile], +// {user: user} +// ); +// expect(pipelines).toBeDefined(); +// if (!Array.isArray(pipelines)) { +// throw new Error( +// 'Returned pipeline for multiple file upload should be an array' +// ); +// } +// expect(pipelines.length).toEqual(2); +// expect(pipelines[0].filename).toEqual(mockFile.originalname); +// expect(pipelines[1].filename).toEqual(secondMockFile.originalname); +// // Creating an pipeline should return a DTO without data. +// expect(pipelines[0].data).not.toBeDefined(); +// }); +// }); + +// describe('update', () => { +// it('should allow an pipeline owner to update', async () => { +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_1, +// data: mockFile, +// userId: user.id +// }); +// const updatedPipeline = await pipelinesController.update( +// pipeline.id, +// {user: user}, +// UPDATE_EVALUATION +// ); +// expect(pipeline.filename).not.toEqual(updatedPipeline.filename); +// expect(pipeline.data).not.toEqual(updatedPipeline.data); +// }); + +// it('should allow a group owner to update', async () => { +// const privateGroup = await groupsService.create(PRIVATE_GROUP); +// const owner = await usersService.create(CREATE_USER_DTO_TEST_OBJ_2); + +// await groupsService.addUserToGroup(privateGroup, owner, 'owner'); + +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_1, +// data: mockFile, +// userId: user.id +// }); + +// await groupsService.addPipelineToGroup(privateGroup, pipeline); + +// const updatedPipeline = await pipelinesController.update( +// pipeline.id, +// {user: owner}, +// UPDATE_EVALUATION +// ); +// expect(pipeline.filename).not.toEqual(updatedPipeline.filename); +// expect(pipeline.data).not.toEqual(updatedPipeline.data); +// }); + +// it('should prevent unauthorized group users from updating an evalution in a group they belong to', async () => { +// expect.assertions(1); + +// const privateGroup = await groupsService.create(PRIVATE_GROUP); +// const owner = await usersService.create(CREATE_ADMIN_DTO); +// const basicUser = await usersService.create(CREATE_USER_DTO_TEST_OBJ_2); + +// await groupsService.addUserToGroup(privateGroup, owner, 'owner'); +// await groupsService.addUserToGroup(privateGroup, basicUser, 'user'); + +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_1, +// data: mockFile, +// userId: user.id +// }); + +// await groupsService.addPipelineToGroup(privateGroup, pipeline); + +// await expect( +// pipelinesController.update( +// pipeline.id, +// {user: basicUser}, +// UPDATE_EVALUATION +// ) +// ).rejects.toBeInstanceOf(ForbiddenError); +// }); + +// it('should prevent unauthorized users from updating', async () => { +// expect.assertions(1); +// const pipelineOwner = await usersService.create( +// CREATE_USER_DTO_TEST_OBJ_2 +// ); +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_1, +// data: mockFile, +// userId: pipelineOwner.id +// }); +// await expect( +// pipelinesController.update( +// pipeline.id, +// {user: user}, +// UPDATE_EVALUATION +// ) +// ).rejects.toBeInstanceOf(ForbiddenError); +// }); +// }); + +// describe('remove', () => { +// it('should remove an pipeline', async () => { +// expect.assertions(1); +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_1, +// data: {}, +// userId: user.id +// }); +// await pipelinesController.remove(pipeline.id, {user: user}); +// await expect( +// pipelinesController.findById(pipeline.id, {user: user}) +// ).rejects.toBeInstanceOf(NotFoundException); +// }); + +// it('should prevent unauthorized users removing an pipeline', async () => { +// expect.assertions(1); +// const pipelineOwner = await usersService.create( +// CREATE_USER_DTO_TEST_OBJ_2 +// ); +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_1, +// data: mockFile, +// userId: pipelineOwner.id +// }); +// await expect( +// pipelinesController.remove(pipeline.id, {user: user}) +// ).rejects.toBeInstanceOf(ForbiddenError); +// }); +// }); + +// describe('groups for pipeline', () => { +// it('should return groups the pipeline belongs to that the requesting user can add and remove the pipeline from', async () => { +// const pipelineOwner = await usersService.create( +// CREATE_USER_DTO_TEST_OBJ_2 +// ); +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_1, +// data: mockFile, +// userId: pipelineOwner.id +// }); +// const group = await groupsService.create(GROUP_1); +// await groupsService.addUserToGroup(group, user, 'member'); +// await groupsService.addPipelineToGroup(group, pipeline); +// const foundGroups = await pipelinesController.groupsForPipeline( +// pipeline.id, +// {user: user} +// ); +// expect(foundGroups[0].id).toEqual(group.id); +// }); + +// it('should not return groups the user has no access to', async () => { +// // GROUP_1 is a public group and still should not show up. +// const pipelineOwner = await usersService.create( +// CREATE_USER_DTO_TEST_OBJ_2 +// ); +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_1, +// data: mockFile, +// userId: pipelineOwner.id +// }); +// const group = await groupsService.create(GROUP_1); +// await groupsService.addPipelineToGroup(group, pipeline); +// const foundGroups = await pipelinesController.groupsForPipeline( +// pipeline.id, +// {user: user} +// ); +// expect(foundGroups.length).toEqual(0); +// }); +// }); + +// afterAll(async () => { +// await databaseService.cleanAll(); +// await databaseService.closeConnection(); +// }); +// }); diff --git a/apps/backend/src/builds/builds.controller.ts b/apps/backend/src/builds/builds.controller.ts new file mode 100644 index 0000000000..7082234ac0 --- /dev/null +++ b/apps/backend/src/builds/builds.controller.ts @@ -0,0 +1,179 @@ +import {ForbiddenError} from '@casl/ability'; +import { + BadRequestException, + Body, + Controller, + Get, + Delete, + Param, + Post, + Request, + UseGuards, + UseInterceptors +} from '@nestjs/common'; +import _ from 'lodash'; +import {AuthzService} from '../authz/authz.service'; +import {Action} from '../casl/casl-ability.factory'; +import {EvaluationsService} from '../evaluations/evaluations.service'; +import {Group} from '../groups/group.model'; +import {GroupsService} from '../groups/groups.service'; +import {APIKeyOrJwtAuthGuard} from '../guards/api-key-or-jwt-auth.guard'; +import {JwtAuthGuard} from '../guards/jwt-auth.guard'; +import {LoggingInterceptor} from '../interceptors/logging.interceptor'; +import {User} from '../users/user.model'; +import {CreateBuildDto} from './dto/create-build.dto'; +import {BuildDto} from './dto/build.dto'; +import {BuildsService} from './builds.service'; +import {Evaluation} from '../evaluations/evaluation.model'; +import {AddEvaluationToBuildDto} from './dto/add-evaluation-to-build-dto'; +import {RemoveEvaluationFromBuildDto} from './dto/remove-evaluation-from-build-dto'; + + +@Controller('builds') +@UseInterceptors(LoggingInterceptor) +export class BuildsController { + constructor( + private readonly buildService: BuildsService, + private readonly evaluationsService: EvaluationsService, + private readonly groupsService: GroupsService, + private readonly authz: AuthzService + ) {} + + @UseGuards(JwtAuthGuard) + @Get() + async findAll(@Request() request: {user: User}): Promise { + const abac = this.authz.abac.createForUser(request.user); + let builds = await this.buildService.findAll(); + builds = builds.filter((build) => + abac.can(Action.Read, build) + ); + return builds.map((build) => new BuildDto(build)); + } + + @UseGuards(JwtAuthGuard) + @Get(':id') + async findById( + @Request() request: {user: User}, + @Param('id') id: string + ): Promise { + const abac = this.authz.abac.createForUser(request.user); + const build = await this.buildService.findByPkBang(id); + ForbiddenError.from(abac).throwUnlessCan(Action.Read, build); + return new BuildDto(build, false); + } + + @UseGuards(APIKeyOrJwtAuthGuard) + @Get('id/:id') + async findByBuildId( + @Request() request: {user: User}, + @Param('id') id: string + ): Promise { + const build = await this.buildService.findByBuildId(id); + return new BuildDto(build, false); + } + + @UseGuards(APIKeyOrJwtAuthGuard) + @Post() + async create( + @Request() request: {user: User | Group}, + @Body() createBuildDto: CreateBuildDto + ): Promise { + if (request.user instanceof Group) { + // Build created by Group API Key + const build = await this.buildService + .create({ + buildId: createBuildDto.buildId, + buildType: createBuildDto.buildType, + branchName: createBuildDto.branchName, + groupId: request.user.id + }) + .then(async (createdBuild) => { + const group = await this.groupsService.findByPkBang( + request.user.id + ); + this.groupsService.addBuildToGroup(group, createdBuild) + return createdBuild; + }) + .catch((err) => { + throw new BadRequestException(err.message); + }); + + return new BuildDto(build); + } + + // Build created by User's JWT + let groups: Group[] = createBuildDto.groups + ? await this.groupsService.findByIds(createBuildDto.groups) + : []; + + // Make sure the user can add evaluations to each group + const abac = this.authz.abac.createForUser(request.user); + groups = groups.filter((group) => { + return abac.can(Action.AddEvaluation, group); + }); + + const build = await this.buildService + .create({ + buildId: createBuildDto.buildId, + buildType: createBuildDto.buildType, + branchName: createBuildDto.branchName, + groupId: request.user.id + }) + .then((createdBuild) => { + groups.forEach((group) => + this.groupsService.addBuildToGroup(group, createdBuild) + ); + return createdBuild; + }); + + return new BuildDto(build); + } + + @UseGuards(JwtAuthGuard) + @Delete(':id') + async remove( + @Param('id') id: string, + @Request() request: {user: User} + ): Promise { + const abac = this.authz.abac.createForUser(request.user); + const build = await this.buildService.findByPkBang(id); + ForbiddenError.from(abac).throwUnlessCan(Action.Manage, build); + return new BuildDto(await this.buildService.remove(build)); + } + + @UseGuards(APIKeyOrJwtAuthGuard) + @Post('/:id/evaluation') + async addEvaluationToBuild( + @Param('id') id: string, + @Request() request: {evaluation: Evaluation}, + @Body() addEvaluationToBuildDto: AddEvaluationToBuildDto + ): Promise { + const build = await this.buildService.findByPkBang(id); + const evaluationToAdd = await this.evaluationsService.findById( + addEvaluationToBuildDto.evaluationId + ); + await this.buildService.addEvaluationToBuild( + build, + evaluationToAdd + ); + return new BuildDto(build); + } + + @UseGuards(JwtAuthGuard) + @Delete('/:id/evaluation') + async removeEvaluationFromBuild( + @Param('id') id: string, + @Request() request: {user: User}, + @Body() removeEvaluationFromBuildDto: RemoveEvaluationFromBuildDto + ): Promise { + const abac = this.authz.abac.createForUser(request.user); + const build = await this.buildService.findByPkBang(id); + ForbiddenError.from(abac).throwUnlessCan(Action.Manage, build); + const evaluationToRemove = await this.evaluationsService.findById( + removeEvaluationFromBuildDto.evaluationId + ); + return new BuildDto( + await this.buildService.removeEvaluationFromBuild(build, evaluationToRemove) + ); + } +} diff --git a/apps/backend/src/builds/builds.module.ts b/apps/backend/src/builds/builds.module.ts new file mode 100644 index 0000000000..8a42501b9d --- /dev/null +++ b/apps/backend/src/builds/builds.module.ts @@ -0,0 +1,28 @@ +import {forwardRef, Module} from '@nestjs/common'; +import {SequelizeModule} from '@nestjs/sequelize'; +import {ConfigModule} from '../config/config.module'; +import {DatabaseModule} from '../database/database.module'; +import {EvaluationsModule} from '../evaluations/evaluations.module'; +import {Group} from '../groups/group.model'; +import {GroupsService} from '../groups/groups.service'; +import {Build} from './build.model'; +import {BuildsController} from './builds.controller'; +import {BuildsService} from './builds.service'; +import {UsersModule} from '../users/users.module'; + +@Module({ + imports: [ + SequelizeModule.forFeature([ + Build, + Group, + ]), + ConfigModule, + EvaluationsModule, + forwardRef(() => UsersModule), + DatabaseModule + ], + providers: [BuildsService, GroupsService], + controllers: [BuildsController], + exports: [BuildsService] +}) +export class BuildsModule {} diff --git a/apps/backend/src/builds/builds.service.spec.ts b/apps/backend/src/builds/builds.service.spec.ts new file mode 100644 index 0000000000..e5cde73e65 --- /dev/null +++ b/apps/backend/src/builds/builds.service.spec.ts @@ -0,0 +1,305 @@ +// import {NotFoundException} from '@nestjs/common'; +// import {SequelizeModule} from '@nestjs/sequelize'; +// import {Test} from '@nestjs/testing'; +// import { +// CREATE_EVALUATION_DTO_WITHOUT_FILENAME, +// CREATE_EVALUATION_DTO_WITHOUT_TAGS, +// EVALUATION_WITH_TAGS_1, +// UPDATE_EVALUATION, +// UPDATE_EVALUATION_DATA_ONLY, +// UPDATE_EVALUATION_FILENAME_ONLY +// } from '../../test/constants/pipelines-test.constant'; +// import {GROUP_1} from '../../test/constants/groups-test.constant'; +// import {CREATE_USER_DTO_TEST_OBJ} from '../../test/constants/users-test.constant'; +// import {DatabaseModule} from '../database/database.module'; +// import {DatabaseService} from '../database/database.service'; +// // import {PipelineTagsModule} from '../pipeline-tags/pipeline-tags.module'; +// // import {PipelineTagsService} from '../pipeline-tags/pipeline-tags.service'; +// // import {GroupPipeline} from '../group-pipelines/group-pipeline.model'; +// // import {GroupUser} from '../group-users/group-user.model'; +// // import {Group} from '../groups/group.model'; +// // import {GroupsService} from '../groups/groups.service'; +// import {UserDto} from '../users/dto/user.dto'; +// import {UsersModule} from '../users/users.module'; +// import {UsersService} from '../users/users.service'; +// import {PipelineDto} from './dto/pipeline.dto'; +// import {Pipeline} from './pipeline.model'; +// import {PipelinesService} from './pipelines.service'; + +// describe('PipelinesService', () => { +// let pipelinesService: PipelinesService; +// //let pipelineTagsService: PipelineTagsService; +// let databaseService: DatabaseService; +// let usersService: UsersService; +// let user: UserDto; +// //let groupsService: GroupsService; + +// beforeAll(async () => { +// const module = await Test.createTestingModule({ +// imports: [ +// DatabaseModule, +// SequelizeModule.forFeature([ +// Pipeline, +// // GroupUser, +// // Group, +// // GroupPipeline +// ]), +// // PipelineTagsModule, +// UsersModule +// ], +// providers: [ +// PipelinesService, +// DatabaseService, +// UsersService, +// // GroupsService +// ] +// }).compile(); + +// pipelinesService = module.get(PipelinesService); +// // pipelineTagsService = module.get( +// // PipelineTagsService +// // ); +// databaseService = module.get(DatabaseService); +// usersService = module.get(UsersService); +// //groupsService = module.get(GroupsService); +// }); + +// beforeEach(async () => { +// await databaseService.cleanAll(); +// user = new UserDto(await usersService.create(CREATE_USER_DTO_TEST_OBJ)); +// }); + +// describe('findAll', () => { +// it('should find all pipelines', async () => { +// let pipelinesDtoArray = await pipelinesService.findAll(); +// expect(pipelinesDtoArray).toEqual([]); + +// await pipelinesService.create({ +// ...EVALUATION_WITH_TAGS_1, +// data: {}, +// userId: user.id +// }); +// await pipelinesService.create({ +// ...EVALUATION_WITH_TAGS_1, +// data: {}, +// userId: user.id +// }); +// pipelinesDtoArray = await pipelinesService.findAll(); +// expect(pipelinesDtoArray.length).toEqual(2); +// }); + +// it('should include the pipeline user', async () => { +// await pipelinesService.create({ +// ...EVALUATION_WITH_TAGS_1, +// data: {}, +// userId: user.id +// }); + +// const pipelines = await pipelinesService.findAll(); +// expect(new UserDto(pipelines[0].user)).toEqual(user); +// }); + +// it('should include the pipeline group and group users', async () => { +// //const group = await groupsService.create(GROUP_1); +// const owner = await usersService.findById(user.id); +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_WITH_TAGS_1, +// data: {}, +// userId: user.id +// }); + +// let pipelines = await pipelinesService.findAll(); +// expect(pipelines[0].groups[0]).not.toBeDefined(); + +// // await groupsService.addPipelineToGroup(group, pipeline); +// // await groupsService.addUserToGroup(group, owner, 'owner'); + +// pipelines = await pipelinesService.findAll(); +// const foundGroup = pipelines[0].groups[0]; +// expect(foundGroup).toBeDefined(); +// //expect(foundGroup.id).toEqual(group.id); +// expect(foundGroup.users.length).toEqual(1); +// expect(foundGroup.users[0].id).toEqual(owner.id); +// expect(foundGroup.users[0].GroupUser.role).toEqual('owner'); +// }); +// }); + +// describe('findById', () => { +// it('should find pipelines by id', async () => { +// expect.assertions(1); +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_WITH_TAGS_1, +// data: {}, +// userId: user.id +// }); +// const foundPipeline = await pipelinesService.findById(pipeline.id); +// expect(new PipelineDto(pipeline)).toEqual( +// new PipelineDto(foundPipeline) +// ); +// }); + +// it('should throw an error if an pipeline does not exist', async () => { +// expect.assertions(1); +// await expect(pipelinesService.findById('-1')).rejects.toThrow( +// NotFoundException +// ); +// }); +// }); + +// describe('create', () => { +// it('should create a new pipeline with pipeline tags', async () => { +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_WITH_TAGS_1, +// data: {}, +// userId: user.id +// }); +// expect(pipeline.id).toBeDefined(); +// expect(pipeline.updatedAt).toBeDefined(); +// expect(pipeline.createdAt).toBeDefined(); +// expect(pipeline.data).toEqual({}); +// expect(pipeline.filename).toEqual(EVALUATION_WITH_TAGS_1.filename); +// expect(pipeline.pipelineTags[0].pipelineId).toBeDefined(); +// expect(pipeline.pipelineTags[0].updatedAt).toBeDefined(); +// expect(pipeline.pipelineTags[0].createdAt).toBeDefined(); + +// if (EVALUATION_WITH_TAGS_1.pipelineTags === undefined) { +// throw new TypeError( +// 'Pipeline fixture does not have any associated tags.' +// ); +// } + +// expect(pipeline.pipelineTags?.[0].value).toEqual( +// EVALUATION_WITH_TAGS_1.pipelineTags[0].value +// ); +// }); + +// it('should create a new pipeline without pipeline tags', async () => { +// const pipeline = await pipelinesService.create({ +// ...CREATE_EVALUATION_DTO_WITHOUT_TAGS, +// data: {}, +// userId: user.id +// }); +// expect(pipeline.id).toBeDefined(); +// expect(pipeline.updatedAt).toBeDefined(); +// expect(pipeline.createdAt).toBeDefined(); +// expect(pipeline.data).toEqual({}); +// expect(pipeline.filename).toEqual( +// CREATE_EVALUATION_DTO_WITHOUT_TAGS.filename +// ); +// expect(pipeline.pipelineTags).not.toBeDefined(); +// //expect((await pipelineTagsService.findAll()).length).toBe(0); +// }); + +// it('should throw an error when missing the filename field', async () => { +// expect.assertions(1); +// await expect( +// pipelinesService.create({ +// ...CREATE_EVALUATION_DTO_WITHOUT_FILENAME, +// data: {}, +// userId: user.id +// }) +// ).rejects.toThrow( +// 'notNull Violation: Pipeline.filename cannot be null' +// ); +// }); +// }); + +// describe('update', () => { +// it('should throw an error if an pipeline does not exist', async () => { +// expect.assertions(1); +// await expect( +// pipelinesService.update('-1', UPDATE_EVALUATION) +// ).rejects.toThrow(NotFoundException); +// }); + +// it('should update all fields of an pipeline', async () => { +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_WITH_TAGS_1, +// data: {}, +// userId: user.id +// }); +// const updatedPipeline = await pipelinesService.update( +// pipeline.id, +// UPDATE_EVALUATION +// ); +// expect(updatedPipeline.id).toEqual(pipeline.id); +// expect(updatedPipeline.createdAt).toEqual(pipeline.createdAt); +// expect(updatedPipeline.updatedAt).not.toEqual(pipeline.updatedAt); +// expect(updatedPipeline.data).not.toEqual(pipeline.data); +// expect(updatedPipeline.filename).not.toEqual(pipeline.filename); +// }); + +// it('should only update data if provided', async () => { +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_WITH_TAGS_1, +// data: {}, +// userId: user.id +// }); +// const updatedPipeline = await pipelinesService.update( +// pipeline.id, +// UPDATE_EVALUATION_DATA_ONLY +// ); +// expect(updatedPipeline.id).toEqual(pipeline.id); +// expect(updatedPipeline.createdAt).toEqual(pipeline.createdAt); +// expect(updatedPipeline.updatedAt).not.toEqual(pipeline.updatedAt); +// expect(updatedPipeline.pipelineTags.length).toEqual( +// pipeline.pipelineTags.length +// ); +// expect(updatedPipeline.data).not.toEqual(pipeline.data); +// expect(updatedPipeline.filename).toEqual(pipeline.filename); +// }); + +// it('should only update filename if provided', async () => { +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_WITH_TAGS_1, +// data: {}, +// userId: user.id +// }); + +// const updatedPipeline = await pipelinesService.update( +// pipeline.id, +// UPDATE_EVALUATION_FILENAME_ONLY +// ); +// expect(updatedPipeline.id).toEqual(pipeline.id); +// expect(updatedPipeline.createdAt).toEqual(pipeline.createdAt); +// expect(updatedPipeline.updatedAt).not.toEqual(pipeline.updatedAt); +// expect(updatedPipeline.pipelineTags.length).toEqual( +// pipeline.pipelineTags.length +// ); +// expect(updatedPipeline.data).toEqual(pipeline.data); +// expect(updatedPipeline.filename).not.toEqual(pipeline.filename); +// }); +// }); + +// describe('remove', () => { +// it('should remove an pipeline and its pipeline tags given an id', async () => { +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_WITH_TAGS_1, +// data: {}, +// userId: user.id +// }); +// const removedPipeline = await pipelinesService.remove(pipeline.id); +// const foundPipelineTags = await pipelineTagsService.findAll(); +// expect(foundPipelineTags.length).toEqual(0); +// expect(new PipelineDto(removedPipeline)).toEqual( +// new PipelineDto(pipeline) +// ); + +// await expect( +// pipelinesService.findById(removedPipeline.id) +// ).rejects.toThrow(NotFoundException); +// }); + +// it('should throw an error when the pipeline does not exist', async () => { +// expect.assertions(1); +// await expect(pipelinesService.findById('-1')).rejects.toThrow( +// NotFoundException +// ); +// }); +// }); + +// afterAll(async () => { +// await databaseService.cleanAll(); +// await databaseService.closeConnection(); +// }); +// }); diff --git a/apps/backend/src/builds/builds.service.ts b/apps/backend/src/builds/builds.service.ts new file mode 100644 index 0000000000..a22cbedde3 --- /dev/null +++ b/apps/backend/src/builds/builds.service.ts @@ -0,0 +1,99 @@ +import {Injectable, NotFoundException} from '@nestjs/common'; +import {InjectModel} from '@nestjs/sequelize'; +import {Op} from 'sequelize'; +import {FindOptions} from 'sequelize/types'; +import {DatabaseService} from '../database/database.service'; +import {CreateBuildDto} from './dto/create-build.dto'; +import {Build} from './build.model'; +import {Group} from '../groups/group.model'; +import {User} from '../users/user.model'; +import {Evaluation} from '../evaluations/evaluation.model'; + +@Injectable() +export class BuildsService { + constructor( + @InjectModel(Build) + private readonly buildModel: typeof Build, + ) {} + + async findAll(): Promise { + return this.buildModel.findAll({ + include: [User, {model: Group, include: [User]}] + }); + } + + async count(): Promise { + return this.buildModel.count(); + } + + + async findById(id: string): Promise { + return this.findByPkBang(id); + } + + async findByBuildId(buildId: string): Promise { + return this.findOneBang({ + where: { + buildId + } + }); + } + + async create(build : { + buildId: string, + buildType: number, + branchName: string, + userId?: string, + groupId?: string, + }): Promise { + return Build.create( + { + ...build + } + ); + } + + async update(buildToUpdate: Build, groupDto: CreateBuildDto): Promise { + buildToUpdate.update(groupDto); + + return buildToUpdate.save(); + } + + async remove(buildToDelete: Build): Promise { + await buildToDelete.destroy(); + + return buildToDelete; + } + + async findByPkBang(id: string): Promise { + // Users must be included for determining permissions on the group. + // Other assocations should be called by their ID separately and not eagerly loaded. + + //TODO: potential optimization: we just need the evaluation id's and not the entire evalution object which contains the HDF content. + const build = await this.buildModel.findByPk(id, {include: ['evaluations', User, {model: Group, include: [User]}]}); + if (build === null) { + throw new NotFoundException('Build with given id not found'); + } else { + return build; + } + } + + async findOneBang(options: FindOptions | undefined): Promise { + const build = await this.buildModel.findOne(options); + if (build === null) { + throw new NotFoundException('Build with given id not found'); + } else { + return build; + } + } + + async addEvaluationToBuild(build: Build, evaluation: Evaluation): Promise { + await build.$add('evaluation', evaluation, { + through: {createdAt: new Date(), updatedAt: new Date()} + }); + } + + async removeEvaluationFromBuild(build: Build, evaluation: Evaluation): Promise { + return build.$remove('evaluation', evaluation); + } +} diff --git a/apps/backend/src/builds/dto/add-evaluation-to-build-dto.ts b/apps/backend/src/builds/dto/add-evaluation-to-build-dto.ts new file mode 100644 index 0000000000..7ebef5fddd --- /dev/null +++ b/apps/backend/src/builds/dto/add-evaluation-to-build-dto.ts @@ -0,0 +1,8 @@ +import {IAddEvaluationToBuild} from '@heimdall/interfaces'; +import {IsNotEmpty, IsString} from 'class-validator'; + +export class AddEvaluationToBuildDto implements IAddEvaluationToBuild { + @IsNotEmpty() + @IsString() + readonly evaluationId!: string; +} diff --git a/apps/backend/src/builds/dto/build.dto.ts b/apps/backend/src/builds/dto/build.dto.ts new file mode 100644 index 0000000000..41edc25619 --- /dev/null +++ b/apps/backend/src/builds/dto/build.dto.ts @@ -0,0 +1,48 @@ +import {IBuild} from '@heimdall/interfaces'; +import {EvaluationDto} from '../../evaluations/dto/evaluation.dto'; +import {Build} from '../build.model'; +import {GroupDto} from '../../groups/dto/group.dto'; +import {Group} from '../../groups/group.model'; + +export class BuildDto implements IBuild { + readonly id: string; + + readonly groups: GroupDto[]; + readonly userId?: string; + readonly groupId?: string; + readonly buildId: string; + readonly buildType: number; + readonly branchName: string; + readonly evaluations: EvaluationDto[]; + readonly createdAt: Date; + readonly updatedAt: Date; + + + constructor( + build: Build, + editable = false, + shareURL: string | undefined = undefined + ) { + this.id = build.id; + this.buildId = build.buildId; + this.buildType = build.buildType; + this.branchName = build.branchName; + this.evaluations = + build.evaluations === undefined + ? [] + : build.evaluations.map((evaluation) => { + return new EvaluationDto(evaluation, false); + }); + if (build.groups === null || build.groups === undefined) { + this.groups = []; + } else { + this.groups = build.groups.map( + (group) => new GroupDto(group as Group) + ); + } + this.userId = build.userId; + this.groupId = build.groupId; + this.createdAt = build.createdAt; + this.updatedAt = build.updatedAt; + } +} diff --git a/apps/backend/src/builds/dto/create-build.dto.ts b/apps/backend/src/builds/dto/create-build.dto.ts new file mode 100644 index 0000000000..6272bcc67d --- /dev/null +++ b/apps/backend/src/builds/dto/create-build.dto.ts @@ -0,0 +1,19 @@ +import {ICreateBuild} from '@heimdall/interfaces'; +import {IsOptional, IsString, IsArray} from 'class-validator'; + +export class CreateBuildDto implements ICreateBuild { + @IsOptional() + @IsString() + readonly buildId!: string; + + @IsOptional() + readonly buildType!: number; + + @IsOptional() + @IsString() + readonly branchName!: string; + + @IsOptional() + @IsArray() + readonly groups: string[] | undefined; +} diff --git a/apps/backend/src/builds/dto/remove-evaluation-from-build-dto.ts b/apps/backend/src/builds/dto/remove-evaluation-from-build-dto.ts new file mode 100644 index 0000000000..3c42c9f944 --- /dev/null +++ b/apps/backend/src/builds/dto/remove-evaluation-from-build-dto.ts @@ -0,0 +1,8 @@ +import {IRemoveEvaluationFromBuild} from '@heimdall/interfaces'; +import {IsNotEmpty, IsString} from 'class-validator'; + +export class RemoveEvaluationFromBuildDto implements IRemoveEvaluationFromBuild { + @IsNotEmpty() + @IsString() + readonly evaluationId!: string; +} diff --git a/apps/backend/src/builds/dto/update-build.dto.ts b/apps/backend/src/builds/dto/update-build.dto.ts new file mode 100644 index 0000000000..6f718aa3a0 --- /dev/null +++ b/apps/backend/src/builds/dto/update-build.dto.ts @@ -0,0 +1,15 @@ +import {IUpdateBuild} from '@heimdall/interfaces'; +import {IsOptional, IsString} from 'class-validator'; + +export class UpdateBuildDto implements IUpdateBuild { + @IsOptional() + @IsString() + readonly buildId!: string; + + @IsOptional() + readonly buildType!: number; + + @IsOptional() + @IsString() + readonly branchName!: string; +} diff --git a/apps/backend/src/casl/casl-ability.factory.ts b/apps/backend/src/casl/casl-ability.factory.ts index 7dd0ddc9f8..5cf9d472fc 100644 --- a/apps/backend/src/casl/casl-ability.factory.ts +++ b/apps/backend/src/casl/casl-ability.factory.ts @@ -10,8 +10,10 @@ import {Evaluation} from '../evaluations/evaluation.model'; import {GroupUser} from '../group-users/group-user.model'; import {Group} from '../groups/group.model'; import {User} from '../users/user.model'; +import {Build} from '../builds/build.model'; +import {Product} from '../products/product.model'; -type AllTypes = typeof User | typeof Evaluation | typeof Group; +type AllTypes = typeof User | typeof Evaluation | typeof Group | typeof Build | typeof Product; type Subjects = InferSubjects | 'all'; type PossibleAbilities = [Action, Subjects]; @@ -50,6 +52,16 @@ interface EvaluationQuery extends Evaluation { 'groups.users.id': User['id']; } +interface BuildQuery extends Build { + 'groups.users': UserQuery[]; + 'groups.users.id': User['id']; +} + +interface ProductQuery extends Product { + 'groups.users': UserQuery[]; + 'groups.users.id': User['id']; +} + export type AppAbility = MongoAbility; @Injectable() @@ -109,6 +121,26 @@ export class CaslAbilityFactory { } }); + can([Action.Read], Build, { + 'groups.users.id': user.id + }); + + can([Action.Read], Product, { + 'groups.users.id': user.id + }); + + can([Action.Manage], Build, { + 'groups.users': { + $elemMatch: {id: user.id, 'GroupUser.role': 'owner'} + } + }); + + can([Action.Manage], Product, { + 'groups.users': { + $elemMatch: {id: user.id, 'GroupUser.role': 'owner'} + } + }); + return build({ detectSubjectType: (object) => object.constructor as ExtractSubjectType diff --git a/apps/backend/src/config/config.service.ts b/apps/backend/src/config/config.service.ts index 2275a9715f..ae31455f51 100644 --- a/apps/backend/src/config/config.service.ts +++ b/apps/backend/src/config/config.service.ts @@ -1,5 +1,6 @@ import {SequelizeOptions} from 'sequelize-typescript'; import AppConfig from '../../config/app_config'; +import {AuthArtiAuthCreds} from '../../config/app_config'; import {StartupSettingsDto} from './dto/startup-settings.dto'; export class ConfigService { @@ -34,6 +35,10 @@ export class ConfigService { return this.get('NODE_ENV')?.toLowerCase() === 'production'; } + isProjectMode(): boolean { + return this.get('PROJECT_MODE')?.toLowerCase() === 'true'; + } + enabledOauthStrategies() { const enabledOauth: string[] = []; supportedOauth.forEach((oauthStrategy) => { @@ -55,6 +60,7 @@ export class ConfigService { this.get('CLASSIFICATION_BANNER_TEXT_COLOR') || 'white', enabledOAuth: this.enabledOauthStrategies(), oidcName: this.get('OIDC_NAME') || '', + projectMode: this.isProjectMode(), ldap: this.get('LDAP_ENABLED')?.toLocaleLowerCase() === 'true' || false, registrationEnabled: this.isRegistrationAllowed(), localLoginEnabled: this.isLocalLoginAllowed() @@ -69,6 +75,18 @@ export class ConfigService { return this.appConfig.getSSLConfig(); } + getAuthArtiS3URL(): string { + return this.appConfig.getAuthArtiS3URL(); + } + + getAuthArtiS3BucketName(): string { + return this.appConfig.getAuthArtiS3BucketName(); + } + + getAuthArtiS3AuthCreds(): AuthArtiAuthCreds { + return this.appConfig.getAuthArtiS3AuthCreds(); + } + set(key: string, value: string | undefined): void { this.appConfig.set(key, value); } diff --git a/apps/backend/src/config/dto/startup-settings.dto.ts b/apps/backend/src/config/dto/startup-settings.dto.ts index c6069b5597..61fa0661a1 100644 --- a/apps/backend/src/config/dto/startup-settings.dto.ts +++ b/apps/backend/src/config/dto/startup-settings.dto.ts @@ -8,6 +8,7 @@ export class StartupSettingsDto implements IStartupSettings { readonly classificationBannerTextColor: string; readonly enabledOAuth: string[]; readonly oidcName: string; + readonly projectMode: boolean; readonly ldap: boolean; readonly registrationEnabled: boolean; readonly localLoginEnabled: boolean; @@ -20,6 +21,7 @@ export class StartupSettingsDto implements IStartupSettings { this.classificationBannerTextColor = settings.classificationBannerTextColor; this.enabledOAuth = settings.enabledOAuth; this.oidcName = settings.oidcName; + this.projectMode = settings.projectMode; this.ldap = settings.ldap; this.registrationEnabled = settings.registrationEnabled; this.localLoginEnabled = settings.localLoginEnabled; diff --git a/apps/backend/src/evaluation-tags/evaluation-tags.controller.spec.ts b/apps/backend/src/evaluation-tags/evaluation-tags.controller.spec.ts index bb66c40327..7325c4ba31 100644 --- a/apps/backend/src/evaluation-tags/evaluation-tags.controller.spec.ts +++ b/apps/backend/src/evaluation-tags/evaluation-tags.controller.spec.ts @@ -15,9 +15,15 @@ import {DatabaseService} from '../database/database.service'; import {Evaluation} from '../evaluations/evaluation.model'; import {EvaluationsService} from '../evaluations/evaluations.service'; import {GroupEvaluation} from '../group-evaluations/group-evaluation.model'; +import { GroupBuild } from '../group-builds/group-build.model'; +import { GroupProduct } from '../group-products/group-product.model'; import {GroupUser} from '../group-users/group-user.model'; import {Group} from '../groups/group.model'; import {GroupsService} from '../groups/groups.service'; +import { Build } from '../builds/build.model'; +import { BuildEvaluation } from '../build-evaluations/build-evaluations.model'; +import {Product} from '../products/product.model'; +import { ProductBuild } from '../product-builds/product-builds.model'; import {User} from '../users/user.model'; import {UsersService} from '../users/users.service'; import {EvaluationTag} from './evaluation-tag.model'; @@ -46,7 +52,13 @@ describe('EvaluationTagsController', () => { User, GroupEvaluation, Group, - GroupUser + GroupUser, + GroupProduct, + Product, + GroupBuild, + Build, + ProductBuild, + BuildEvaluation ]) ], providers: [ diff --git a/apps/backend/src/evaluation-tags/evaluation-tags.service.spec.ts b/apps/backend/src/evaluation-tags/evaluation-tags.service.spec.ts index 9c92e2a30b..5a53a0292f 100644 --- a/apps/backend/src/evaluation-tags/evaluation-tags.service.spec.ts +++ b/apps/backend/src/evaluation-tags/evaluation-tags.service.spec.ts @@ -16,9 +16,15 @@ import {EvaluationDto} from '../evaluations/dto/evaluation.dto'; import {Evaluation} from '../evaluations/evaluation.model'; import {EvaluationsService} from '../evaluations/evaluations.service'; import {GroupEvaluation} from '../group-evaluations/group-evaluation.model'; +import { GroupBuild } from '../group-builds/group-build.model'; +import { GroupProduct } from '../group-products/group-product.model'; import {GroupUser} from '../group-users/group-user.model'; import {Group} from '../groups/group.model'; import {GroupsService} from '../groups/groups.service'; +import { Build } from '../builds/build.model'; +import { BuildEvaluation } from '../build-evaluations/build-evaluations.model'; +import {Product} from '../products/product.model'; +import { ProductBuild } from '../product-builds/product-builds.model'; import {User} from '../users/user.model'; import {UsersService} from '../users/users.service'; import {EvaluationTag} from './evaluation-tag.model'; @@ -41,7 +47,13 @@ describe('EvaluationTagsService', () => { User, GroupEvaluation, Group, - GroupUser + GroupUser, + GroupProduct, + Product, + GroupBuild, + Build, + ProductBuild, + BuildEvaluation ]) ], providers: [ diff --git a/apps/backend/src/evaluations/evaluations.controller.spec.ts b/apps/backend/src/evaluations/evaluations.controller.spec.ts index 534409a226..1af0cde3bb 100644 --- a/apps/backend/src/evaluations/evaluations.controller.spec.ts +++ b/apps/backend/src/evaluations/evaluations.controller.spec.ts @@ -23,9 +23,15 @@ import {DatabaseModule} from '../database/database.module'; import {DatabaseService} from '../database/database.service'; import {EvaluationTag} from '../evaluation-tags/evaluation-tag.model'; import {GroupEvaluation} from '../group-evaluations/group-evaluation.model'; +import { GroupBuild } from '../group-builds/group-build.model'; +import { GroupProduct } from '../group-products/group-product.model'; import {GroupUser} from '../group-users/group-user.model'; import {Group} from '../groups/group.model'; import {GroupsService} from '../groups/groups.service'; +import { Build } from '../builds/build.model'; +import { BuildEvaluation } from '../build-evaluations/build-evaluations.model'; +import {Product} from '../products/product.model'; +import { ProductBuild } from '../product-builds/product-builds.model'; import {User} from '../users/user.model'; import {UsersService} from '../users/users.service'; import {EvaluationDto} from './dto/evaluation.dto'; @@ -69,7 +75,13 @@ describe('EvaluationsController', () => { User, GroupEvaluation, GroupUser, - Group + Group, + GroupProduct, + Product, + GroupBuild, + Build, + ProductBuild, + BuildEvaluation ]) ], providers: [ diff --git a/apps/backend/src/evaluations/evaluations.service.spec.ts b/apps/backend/src/evaluations/evaluations.service.spec.ts index efa2fa534d..b5a6229a87 100644 --- a/apps/backend/src/evaluations/evaluations.service.spec.ts +++ b/apps/backend/src/evaluations/evaluations.service.spec.ts @@ -16,9 +16,15 @@ import {DatabaseService} from '../database/database.service'; import {EvaluationTagsModule} from '../evaluation-tags/evaluation-tags.module'; import {EvaluationTagsService} from '../evaluation-tags/evaluation-tags.service'; import {GroupEvaluation} from '../group-evaluations/group-evaluation.model'; +import { GroupBuild } from '../group-builds/group-build.model'; +import { GroupProduct } from '../group-products/group-product.model'; import {GroupUser} from '../group-users/group-user.model'; import {Group} from '../groups/group.model'; import {GroupsService} from '../groups/groups.service'; +import { Build } from '../builds/build.model'; +import { BuildEvaluation } from '../build-evaluations/build-evaluations.model'; +import {Product} from '../products/product.model'; +import { ProductBuild } from '../product-builds/product-builds.model'; import {UserDto} from '../users/dto/user.dto'; import {UsersModule} from '../users/users.module'; import {UsersService} from '../users/users.service'; @@ -42,7 +48,13 @@ describe('EvaluationsService', () => { Evaluation, GroupUser, Group, - GroupEvaluation + GroupEvaluation, + GroupProduct, + Product, + GroupBuild, + Build, + ProductBuild, + BuildEvaluation ]), EvaluationTagsModule, UsersModule diff --git a/apps/backend/src/group-builds/group-build.model.ts b/apps/backend/src/group-builds/group-build.model.ts new file mode 100644 index 0000000000..224eb76673 --- /dev/null +++ b/apps/backend/src/group-builds/group-build.model.ts @@ -0,0 +1,42 @@ +import { + AllowNull, + AutoIncrement, + Column, + CreatedAt, + DataType, + Default, + ForeignKey, + Model, + PrimaryKey, + Table, + UpdatedAt +} from 'sequelize-typescript'; +import {Group} from '../groups/group.model'; +import {Build} from '../builds/build.model'; + +@Table +export class GroupBuild extends Model { + @PrimaryKey + @AutoIncrement + @AllowNull(false) + @Column(DataType.BIGINT) + id!: string; + + @ForeignKey(() => Group) + @Column(DataType.BIGINT) + groupId!: string; + + @ForeignKey(() => Build) + @Column(DataType.BIGINT) + buildId!: string; + + @CreatedAt + @AllowNull(false) + @Column(DataType.DATE) + createdAt!: Date; + + @UpdatedAt + @AllowNull(false) + @Column(DataType.DATE) + updatedAt!: Date; +} diff --git a/apps/backend/src/group-builds/group-builds.module.ts b/apps/backend/src/group-builds/group-builds.module.ts new file mode 100644 index 0000000000..80fd630725 --- /dev/null +++ b/apps/backend/src/group-builds/group-builds.module.ts @@ -0,0 +1,8 @@ +import {Module} from '@nestjs/common'; +import {SequelizeModule} from '@nestjs/sequelize'; +import {GroupBuild} from './group-build.model'; + +@Module({ + imports: [SequelizeModule.forFeature([GroupBuild])] +}) +export class GroupBuildsModule {} diff --git a/apps/backend/src/group-products/group-product.model.ts b/apps/backend/src/group-products/group-product.model.ts new file mode 100644 index 0000000000..d6b78a895d --- /dev/null +++ b/apps/backend/src/group-products/group-product.model.ts @@ -0,0 +1,42 @@ +import { + AllowNull, + AutoIncrement, + Column, + CreatedAt, + DataType, + Default, + ForeignKey, + Model, + PrimaryKey, + Table, + UpdatedAt +} from 'sequelize-typescript'; +import {Group} from '../groups/group.model'; +import {Product} from '../products/product.model'; + +@Table +export class GroupProduct extends Model { + @PrimaryKey + @AutoIncrement + @AllowNull(false) + @Column(DataType.BIGINT) + id!: string; + + @ForeignKey(() => Group) + @Column(DataType.BIGINT) + groupId!: string; + + @ForeignKey(() => Product) + @Column(DataType.BIGINT) + productId!: string; + + @CreatedAt + @AllowNull(false) + @Column(DataType.DATE) + createdAt!: Date; + + @UpdatedAt + @AllowNull(false) + @Column(DataType.DATE) + updatedAt!: Date; +} diff --git a/apps/backend/src/group-products/group-products.module.ts b/apps/backend/src/group-products/group-products.module.ts new file mode 100644 index 0000000000..7f4fbf557a --- /dev/null +++ b/apps/backend/src/group-products/group-products.module.ts @@ -0,0 +1,8 @@ +import {Module} from '@nestjs/common'; +import {SequelizeModule} from '@nestjs/sequelize'; +import {GroupProduct} from './group-product.model'; + +@Module({ + imports: [SequelizeModule.forFeature([GroupProduct])] +}) +export class GroupProductsModule {} diff --git a/apps/backend/src/groups/group.model.ts b/apps/backend/src/groups/group.model.ts index 71e983b839..5645134768 100644 --- a/apps/backend/src/groups/group.model.ts +++ b/apps/backend/src/groups/group.model.ts @@ -14,7 +14,11 @@ import { } from 'sequelize-typescript'; import {Evaluation} from '../evaluations/evaluation.model'; import {GroupEvaluation} from '../group-evaluations/group-evaluation.model'; +import {GroupBuild} from '../group-builds/group-build.model'; +import {GroupProduct} from '../group-products/group-product.model'; import {GroupUser} from '../group-users/group-user.model'; +import {Build} from '../builds/build.model'; +import {Product} from '../products/product.model'; import {User} from '../users/user.model'; @Table @@ -55,4 +59,10 @@ export class Group extends Model { @BelongsToMany(() => Evaluation, () => GroupEvaluation) evaluations!: Array; + + @BelongsToMany(() => Product, () => GroupProduct) + products!: Array; + + @BelongsToMany(() => Build, () => GroupBuild) + builds!: Array; } diff --git a/apps/backend/src/groups/groups.controller.spec.ts b/apps/backend/src/groups/groups.controller.spec.ts index 6b2397bcef..78fdc8dcb9 100644 --- a/apps/backend/src/groups/groups.controller.spec.ts +++ b/apps/backend/src/groups/groups.controller.spec.ts @@ -19,7 +19,13 @@ import {EvaluationTag} from '../evaluation-tags/evaluation-tag.model'; import {Evaluation} from '../evaluations/evaluation.model'; import {EvaluationsService} from '../evaluations/evaluations.service'; import {GroupEvaluation} from '../group-evaluations/group-evaluation.model'; +import { GroupBuild } from '../group-builds/group-build.model'; +import { GroupProduct } from '../group-products/group-product.model'; import {GroupUser} from '../group-users/group-user.model'; +import { Build } from '../builds/build.model'; +import { BuildEvaluation } from '../build-evaluations/build-evaluations.model'; +import {Product} from '../products/product.model'; +import { ProductBuild } from '../product-builds/product-builds.model'; import {SlimUserDto} from '../users/dto/slim-user.dto'; import {User} from '../users/user.model'; import {UsersService} from '../users/users.service'; @@ -49,7 +55,13 @@ describe('GroupsController', () => { GroupEvaluation, Evaluation, EvaluationTag, - User + User, + GroupProduct, + Product, + GroupBuild, + Build, + ProductBuild, + BuildEvaluation ]) ], providers: [ diff --git a/apps/backend/src/groups/groups.module.ts b/apps/backend/src/groups/groups.module.ts index 18f3269a7c..cc929acbf7 100644 --- a/apps/backend/src/groups/groups.module.ts +++ b/apps/backend/src/groups/groups.module.ts @@ -5,6 +5,8 @@ import {AuthzModule} from '../authz/authz.module'; import {ConfigModule} from '../config/config.module'; import {EvaluationTagsModule} from '../evaluation-tags/evaluation-tags.module'; import {EvaluationsModule} from '../evaluations/evaluations.module'; +import {BuildsModule} from '../builds/builds.module'; +import {ProductsModule} from '../products/products.module'; import {UsersModule} from '../users/users.module'; import {Group} from './group.model'; import {GroupsController} from './groups.controller'; @@ -16,6 +18,8 @@ import {GroupsService} from './groups.service'; ApiKeyModule, AuthzModule, ConfigModule, + BuildsModule, + ProductsModule, forwardRef(() => UsersModule), EvaluationsModule, EvaluationTagsModule diff --git a/apps/backend/src/groups/groups.service.spec.ts b/apps/backend/src/groups/groups.service.spec.ts index 18ebe35c12..346b5a121b 100644 --- a/apps/backend/src/groups/groups.service.spec.ts +++ b/apps/backend/src/groups/groups.service.spec.ts @@ -17,8 +17,14 @@ import {EvaluationTag} from '../evaluation-tags/evaluation-tag.model'; import {Evaluation} from '../evaluations/evaluation.model'; import {EvaluationsService} from '../evaluations/evaluations.service'; import {GroupEvaluationsModule} from '../group-evaluations/group-evaluations.module'; +import { GroupBuild } from '../group-builds/group-build.model'; +import { GroupProduct } from '../group-products/group-product.model'; import {GroupUser} from '../group-users/group-user.model'; import {GroupUsersModule} from '../group-users/group-users.module'; +import { Build } from '../builds/build.model'; +import { BuildEvaluation } from '../build-evaluations/build-evaluations.model'; +import {Product} from '../products/product.model'; +import { ProductBuild } from '../product-builds/product-builds.model'; import {UserDto} from '../users/dto/user.dto'; import {User} from '../users/user.model'; import {UsersService} from '../users/users.service'; @@ -40,7 +46,13 @@ describe('GroupsService', () => { GroupUser, Evaluation, EvaluationTag, - User + User, + GroupProduct, + Product, + GroupBuild, + Build, + ProductBuild, + BuildEvaluation ]), GroupEvaluationsModule, GroupUsersModule diff --git a/apps/backend/src/groups/groups.service.ts b/apps/backend/src/groups/groups.service.ts index a16d95c36c..28d0c325cb 100644 --- a/apps/backend/src/groups/groups.service.ts +++ b/apps/backend/src/groups/groups.service.ts @@ -14,6 +14,9 @@ import {User} from '../users/user.model'; import {CreateGroupDto} from './dto/create-group.dto'; import {UpdateGroupUserRoleDto} from './dto/update-group-user.dto'; import {Group} from './group.model'; +import {Build} from '../builds/build.model'; +import {Product} from '../products/product.model'; + @Injectable() export class GroupsService { private readonly line = '_______________________________________________\n'; @@ -193,6 +196,32 @@ export class GroupsService { return groupToDelete; } + async addBuildToGroup(group: Group, build: Build): Promise { + await group.$add('build', build, { + through: {createdAt: new Date(), updatedAt: new Date()} + }); + } + + async removeBuildFromGroup( + group: Group, + build: Build + ): Promise { + return group.$remove('build', build); + } + + async addProductToGroup(group: Group, product: Product): Promise { + await group.$add('product', product, { + through: {createdAt: new Date(), updatedAt: new Date()} + }); + } + + async removeProductFromGroup( + group: Group, + product: Product + ): Promise { + return group.$remove('product', product); + } + // This method ensures that the passed in user is in all of the // passed in groups, as long as the group already exists. // It will additionally remove the user from any groups not in the list. diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index 0dddad4c43..8aeffcdc44 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -40,7 +40,8 @@ async function bootstrap() { 'connect-src': [ "'self'", 'https://api.github.com', - 'https://sts.amazonaws.com' + 'https://sts.amazonaws.com', + configService.getAuthArtiS3URL() ] } }) diff --git a/apps/backend/src/product-builds/product-builds.model.ts b/apps/backend/src/product-builds/product-builds.model.ts new file mode 100644 index 0000000000..cd0c4ac725 --- /dev/null +++ b/apps/backend/src/product-builds/product-builds.model.ts @@ -0,0 +1,41 @@ +import { + AllowNull, + AutoIncrement, + Column, + CreatedAt, + DataType, + ForeignKey, + Model, + PrimaryKey, + Table, + UpdatedAt +} from 'sequelize-typescript'; +import {Product} from '../products/product.model'; +import {Build} from '../builds/build.model'; + +@Table +export class ProductBuild extends Model { + @PrimaryKey + @AutoIncrement + @AllowNull(false) + @Column(DataType.BIGINT) + id!: string; + + @ForeignKey(() => Product) + @Column(DataType.BIGINT) + productId!: string; + + @ForeignKey(() => Build) + @Column(DataType.BIGINT) + buildId!: string; + + @CreatedAt + @AllowNull(false) + @Column(DataType.DATE) + createdAt!: Date; + + @UpdatedAt + @AllowNull(false) + @Column(DataType.DATE) + updatedAt!: Date; +} diff --git a/apps/backend/src/product-builds/product-builds.module.ts b/apps/backend/src/product-builds/product-builds.module.ts new file mode 100644 index 0000000000..08bbeb92be --- /dev/null +++ b/apps/backend/src/product-builds/product-builds.module.ts @@ -0,0 +1,8 @@ +import {Module} from '@nestjs/common'; +import {SequelizeModule} from '@nestjs/sequelize'; +import {ProductBuild} from './product-builds.model'; + +@Module({ + imports: [SequelizeModule.forFeature([ProductBuild])] +}) +export class ProductBuildsModule {} diff --git a/apps/backend/src/products/dto/add-build-to-product-dto.ts b/apps/backend/src/products/dto/add-build-to-product-dto.ts new file mode 100644 index 0000000000..a63addebf8 --- /dev/null +++ b/apps/backend/src/products/dto/add-build-to-product-dto.ts @@ -0,0 +1,8 @@ +import {IAddBuildToProduct} from '@heimdall/interfaces'; +import {IsNotEmpty, IsString} from 'class-validator'; + +export class AddBuildToProductDto implements IAddBuildToProduct { + @IsNotEmpty() + @IsString() + readonly buildId!: string; +} diff --git a/apps/backend/src/products/dto/add-pipeline-to-product-dto.ts b/apps/backend/src/products/dto/add-pipeline-to-product-dto.ts new file mode 100644 index 0000000000..a63addebf8 --- /dev/null +++ b/apps/backend/src/products/dto/add-pipeline-to-product-dto.ts @@ -0,0 +1,8 @@ +import {IAddBuildToProduct} from '@heimdall/interfaces'; +import {IsNotEmpty, IsString} from 'class-validator'; + +export class AddBuildToProductDto implements IAddBuildToProduct { + @IsNotEmpty() + @IsString() + readonly buildId!: string; +} diff --git a/apps/backend/src/products/dto/create-product.dto.ts b/apps/backend/src/products/dto/create-product.dto.ts new file mode 100644 index 0000000000..7ca0398215 --- /dev/null +++ b/apps/backend/src/products/dto/create-product.dto.ts @@ -0,0 +1,24 @@ +import {ICreateProduct} from '@heimdall/interfaces'; +import {IsOptional, IsString, IsArray} from 'class-validator'; + +export class CreateProductDto implements ICreateProduct { + @IsOptional() + @IsString() + readonly productName!: string; + + @IsOptional() + @IsString() + readonly productId!: string; + + @IsOptional() + @IsString() + readonly productURL!: string; + + @IsOptional() + @IsString() + readonly objectStoreKey!: string; + + @IsOptional() + @IsArray() + readonly groups: string[] | undefined; +} diff --git a/apps/backend/src/products/dto/product.dto.ts b/apps/backend/src/products/dto/product.dto.ts new file mode 100644 index 0000000000..8117b77112 --- /dev/null +++ b/apps/backend/src/products/dto/product.dto.ts @@ -0,0 +1,49 @@ +import {IProduct} from '@heimdall/interfaces'; +import {BuildDto} from '../../builds/dto/build.dto'; +import {Product} from '../product.model'; +import {GroupDto} from '../../groups/dto/group.dto'; +import {Group} from '../../groups/group.model'; + +export class ProductDto implements IProduct { + readonly id: string; + + readonly groups: GroupDto[]; + readonly userId?: string; + readonly groupId?: string; + readonly productName: string; + readonly productId: string; + readonly productURL: string; + readonly objectStoreKey: string; + readonly builds: BuildDto[]; + readonly createdAt: Date; + readonly updatedAt: Date; + + constructor( + product: Product, + editable = false, + shareURL: string | undefined = undefined + ) { + this.id = product.id; + this.productName = product.productName; + this.productId = product.productId; + this.productURL = product.productURL; + this.objectStoreKey = product.objectStoreKey; + this.builds = + product.builds === undefined + ? [] + : product.builds.map((build) => { + return new BuildDto(build, false); + }); + if (product.groups === null || product.groups === undefined) { + this.groups = []; + } else { + this.groups = product.groups.map( + (group) => new GroupDto(group as Group) + ); + } + this.userId = product.userId; + this.groupId = product.groupId; + this.createdAt = product.createdAt; + this.updatedAt = product.updatedAt; + } +} diff --git a/apps/backend/src/products/dto/remove-build-from-product-dto.ts b/apps/backend/src/products/dto/remove-build-from-product-dto.ts new file mode 100644 index 0000000000..89d7375486 --- /dev/null +++ b/apps/backend/src/products/dto/remove-build-from-product-dto.ts @@ -0,0 +1,8 @@ +import {IRemoveBuildFromProduct} from '@heimdall/interfaces'; +import {IsNotEmpty, IsString} from 'class-validator'; + +export class RemoveBuildFromProductDto implements IRemoveBuildFromProduct { + @IsNotEmpty() + @IsString() + readonly buildId!: string; +} diff --git a/apps/backend/src/products/dto/remove-pipeline-from-product-dto.ts b/apps/backend/src/products/dto/remove-pipeline-from-product-dto.ts new file mode 100644 index 0000000000..89d7375486 --- /dev/null +++ b/apps/backend/src/products/dto/remove-pipeline-from-product-dto.ts @@ -0,0 +1,8 @@ +import {IRemoveBuildFromProduct} from '@heimdall/interfaces'; +import {IsNotEmpty, IsString} from 'class-validator'; + +export class RemoveBuildFromProductDto implements IRemoveBuildFromProduct { + @IsNotEmpty() + @IsString() + readonly buildId!: string; +} diff --git a/apps/backend/src/products/dto/update-product.dto.ts b/apps/backend/src/products/dto/update-product.dto.ts new file mode 100644 index 0000000000..84fdea5181 --- /dev/null +++ b/apps/backend/src/products/dto/update-product.dto.ts @@ -0,0 +1,20 @@ +import {IUpdateProduct} from '@heimdall/interfaces'; +import {IsOptional, IsString} from 'class-validator'; + +export class UpdateProductDto implements IUpdateProduct { + @IsOptional() + @IsString() + readonly productName!: string; + + @IsOptional() + @IsString() + readonly productId!: string; + + @IsOptional() + @IsString() + readonly productURL!: string; + + @IsOptional() + @IsString() + readonly objectStoreKey!: string; +} diff --git a/apps/backend/src/products/product.model.ts b/apps/backend/src/products/product.model.ts new file mode 100644 index 0000000000..abf7e8c53a --- /dev/null +++ b/apps/backend/src/products/product.model.ts @@ -0,0 +1,73 @@ +import { + AllowNull, + AutoIncrement, + BelongsTo, + BelongsToMany, + Column, + CreatedAt, + DataType, + ForeignKey, + Model, + PrimaryKey, + Table, + UpdatedAt +} from 'sequelize-typescript'; +import {Build} from '../builds/build.model'; +import {ProductBuild} from '../product-builds/product-builds.model'; +import {GroupProduct} from '../group-products/group-product.model'; +import {Group} from '../groups/group.model'; +import {User} from '../users/user.model'; + +@Table +export class Product extends Model { + @PrimaryKey + @AutoIncrement + @AllowNull(false) + @Column(DataType.BIGINT) + id!: string; + + @AllowNull(false) + @Column + productName!: string; + + @AllowNull(false) + @Column(DataType.BIGINT) + productId!: string; + + @AllowNull(true) + @Column + productURL!: string; + + @AllowNull(true) + @Column + objectStoreKey!: string; + + @CreatedAt + @AllowNull(false) + @Column(DataType.DATE) + createdAt!: Date; + + @UpdatedAt + @AllowNull(false) + @Column(DataType.DATE) + updatedAt!: Date; + + @BelongsToMany(() => Build, () => ProductBuild) + builds!: Array; + + @BelongsToMany(() => Group, () => GroupProduct) + groups!: Array; + + @ForeignKey(() => Group) + @Column(DataType.BIGINT) + groupId!: string; + + @ForeignKey(() => User) + @Column(DataType.BIGINT) + userId!: string; + + @BelongsTo(() => User, { + constraints: false + }) + user!: User; +} diff --git a/apps/backend/src/products/products.controller.spec.ts b/apps/backend/src/products/products.controller.spec.ts new file mode 100644 index 0000000000..0be2e3dcf3 --- /dev/null +++ b/apps/backend/src/products/products.controller.spec.ts @@ -0,0 +1,458 @@ +// import {ForbiddenError} from '@casl/ability'; +// import {NotFoundException} from '@nestjs/common'; +// import {SequelizeModule} from '@nestjs/sequelize'; +// import {Test, TestingModule} from '@nestjs/testing'; +// import { +// CREATE_EVALUATION_DTO_WITHOUT_TAGS, +// EVALUATION_1, +// EVALUATION_WITH_TAGS_1, +// UPDATE_EVALUATION +// } from '../../test/constants/pipelines-test.constant'; +// import { +// GROUP_1, +// PRIVATE_GROUP +// } from '../../test/constants/groups-test.constant'; +// import { +// CREATE_ADMIN_DTO, +// CREATE_USER_DTO_TEST_OBJ, +// CREATE_USER_DTO_TEST_OBJ_2 +// } from '../../test/constants/users-test.constant'; +// import {AuthzService} from '../authz/authz.service'; +// import {ConfigService} from '../config/config.service'; +// import {DatabaseModule} from '../database/database.module'; +// import {DatabaseService} from '../database/database.service'; +// //import {PipelineTag} from '../pipeline-tags/pipeline-tag.model'; +// // import {GroupPipeline} from '../group-pipelines/group-pipeline.model'; +// // import {GroupUser} from '../group-users/group-user.model'; +// // import {Group} from '../groups/group.model'; +// // import {GroupsService} from '../groups/groups.service'; +// import {User} from '../users/user.model'; +// import {UsersService} from '../users/users.service'; +// import {PipelineDto} from './dto/pipeline.dto'; +// import {Pipeline} from './pipeline.model'; +// import {PipelinesController} from './pipelines.controller'; +// import {PipelinesService} from './pipelines.service'; + +// // This allows basic testing of the pipelines controller +// // interface without having to construct a full File object +// /* eslint-disable @typescript-eslint/ban-ts-comment */ +// //@ts-ignore +// const mockFile: Express.Multer.File = { +// originalname: 'abc.json', +// buffer: Buffer.from('{}') +// }; +// //@ts-ignore +// const secondMockFile: Express.Multer.File = { +// originalname: 'cda.json', +// buffer: Buffer.from('{}') +// }; +// /* eslint-enable @typescript-eslint/ban-ts-comment */ + +// describe('PipelinesController', () => { +// let pipelinesController: PipelinesController; +// let pipelinesService: PipelinesService; +// let module: TestingModule; +// let databaseService: DatabaseService; +// let usersService: UsersService; +// let groupsService: GroupsService; + +// let user: User; + +// beforeAll(async () => { +// module = await Test.createTestingModule({ +// controllers: [PipelinesController], +// imports: [ +// DatabaseModule, +// SequelizeModule.forFeature([ +// PipelineTag, +// Pipeline, +// User, +// GroupPipeline, +// GroupUser, +// Group +// ]) +// ], +// providers: [ +// AuthzService, +// ConfigService, +// DatabaseService, +// UsersService, +// PipelinesService, +// GroupsService +// ] +// }).compile(); + +// databaseService = module.get(DatabaseService); +// pipelinesService = module.get(PipelinesService); +// pipelinesController = module.get( +// PipelinesController +// ); +// usersService = module.get(UsersService); +// groupsService = module.get(GroupsService); +// }); + +// beforeEach(async () => { +// await databaseService.cleanAll(); +// user = await usersService.create(CREATE_USER_DTO_TEST_OBJ); +// }); + +// describe('findById', () => { +// it('should return an pipeline', async () => { +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_1, +// data: mockFile, +// userId: user.id +// }); + +// const foundPipeline = await pipelinesController.findById( +// pipeline.id, +// {user: user} +// ); + +// expect(foundPipeline).toEqual( +// // The pipeline is created with the current user ID above +// // so the expectation is that user should be able to edit +// // which is the 2nd parameter to PipelineDto. +// new PipelineDto(pipeline, true) +// ); +// }); + +// it('should return an pipelines tags', async () => { +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_WITH_TAGS_1, +// data: mockFile, +// userId: user.id +// }); + +// const foundPipeline = await pipelinesController.findById( +// pipeline.id, +// {user: user} +// ); +// expect(foundPipeline.pipelineTags).toEqual( +// new PipelineDto(pipeline).pipelineTags +// ); +// }); + +// it('should throw a not found exeception when given an invalid id', async () => { +// expect.assertions(1); + +// await expect( +// pipelinesController.findById('0', {user: user}) +// ).rejects.toBeInstanceOf(NotFoundException); +// }); + +// it('should prevent non-owners from viewing an pipeline', async () => { +// expect.assertions(1); +// const pipelineOwner = await usersService.create( +// CREATE_USER_DTO_TEST_OBJ_2 +// ); +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_1, +// data: mockFile, +// userId: pipelineOwner.id +// }); +// await expect( +// pipelinesController.findById(pipeline.id, {user: user}) +// ).rejects.toBeInstanceOf(ForbiddenError); +// }); +// }); + +// describe('findAll', () => { +// it('should return all pipelines a user has permissions to read', async () => { +// await pipelinesService.create({ +// ...EVALUATION_1, +// data: mockFile, +// userId: user.id +// }); +// let foundPipelines = await pipelinesController.findAll({user: user}); +// expect(foundPipelines.length).toEqual(1); +// const pipelineOwner = await usersService.create( +// CREATE_USER_DTO_TEST_OBJ_2 +// ); +// await pipelinesService.create({ +// ...EVALUATION_1, +// data: mockFile, +// userId: pipelineOwner.id +// }); +// foundPipelines = await pipelinesController.findAll({user: user}); +// expect(foundPipelines.length).toEqual(1); +// }); + +// it('should return all pipelines and their associated tags', async () => { +// await pipelinesService.create({ +// ...EVALUATION_WITH_TAGS_1, +// data: mockFile, +// userId: user.id +// }); +// const foundPipelines = await pipelinesController.findAll({ +// user: user +// }); +// expect(foundPipelines[0].pipelineTags.length).toEqual(1); +// }); + +// it('should return editable true if the user is the owner of an pipeline', async () => { +// await pipelinesService.create({ +// ...EVALUATION_1, +// data: mockFile, +// userId: user.id +// }); +// const foundPipelines = await pipelinesController.findAll({ +// user: user +// }); +// expect(foundPipelines[0].editable).toBeTruthy(); +// }); + +// it('should return editable true if the user is the owner of a group that an pipeline belongs to', async () => { +// const pipelineOwner = await usersService.create( +// CREATE_USER_DTO_TEST_OBJ_2 +// ); +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_1, +// data: mockFile, +// userId: pipelineOwner.id +// }); +// const group = await groupsService.create(PRIVATE_GROUP); +// await groupsService.addUserToGroup(group, user, 'owner'); +// await groupsService.addPipelineToGroup(group, pipeline); +// const foundPipelines = await pipelinesController.findAll({ +// user: user +// }); + +// expect(foundPipelines[0].editable).toBeTruthy(); +// }); + +// it('should return editable false if the user is not owner of a group that an pipeline belongs to', async () => { +// const pipelineOwner = await usersService.create( +// CREATE_USER_DTO_TEST_OBJ_2 +// ); +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_1, +// data: mockFile, +// userId: pipelineOwner.id +// }); +// const group = await groupsService.create(GROUP_1); +// const group2 = await groupsService.create(PRIVATE_GROUP); +// await groupsService.addUserToGroup(group, user, 'user'); +// await groupsService.addUserToGroup(group2, user, 'owner'); +// await groupsService.addPipelineToGroup(group, pipeline); +// const foundPipelines = await pipelinesController.findAll({ +// user: user +// }); +// expect(foundPipelines[0].editable).toBeFalsy(); +// }); +// }); + +// describe('create', () => { +// it('should allow a user to create an pipeline', async () => { +// const pipeline = await pipelinesController.create( +// EVALUATION_WITH_TAGS_1, +// [mockFile], +// {user: user} +// ); +// expect(pipeline).toBeDefined(); +// if (Array.isArray(pipeline)) { +// throw new Error( +// 'Returned pipeline for one file upload should not be an array' +// ); +// } +// expect(pipeline.pipelineTags.length).toEqual(1); +// // Creating an pipeline should return a DTO without data. +// expect(pipeline.data).not.toBeDefined(); +// }); + +// it('should create an pipeline without tags', async () => { +// const pipeline = await pipelinesController.create( +// CREATE_EVALUATION_DTO_WITHOUT_TAGS, +// [mockFile], +// {user: user} +// ); +// expect(pipeline).toBeDefined(); +// if (Array.isArray(pipeline)) { +// throw new Error( +// 'Returned pipeline for one file upload should not be an array' +// ); +// } +// expect(pipeline.pipelineTags.length).toEqual(0); +// }); + +// it('should accept multiple pipelines', async () => { +// const pipelines = await pipelinesController.create( +// EVALUATION_WITH_TAGS_1, +// [mockFile, secondMockFile], +// {user: user} +// ); +// expect(pipelines).toBeDefined(); +// if (!Array.isArray(pipelines)) { +// throw new Error( +// 'Returned pipeline for multiple file upload should be an array' +// ); +// } +// expect(pipelines.length).toEqual(2); +// expect(pipelines[0].filename).toEqual(mockFile.originalname); +// expect(pipelines[1].filename).toEqual(secondMockFile.originalname); +// // Creating an pipeline should return a DTO without data. +// expect(pipelines[0].data).not.toBeDefined(); +// }); +// }); + +// describe('update', () => { +// it('should allow an pipeline owner to update', async () => { +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_1, +// data: mockFile, +// userId: user.id +// }); +// const updatedPipeline = await pipelinesController.update( +// pipeline.id, +// {user: user}, +// UPDATE_EVALUATION +// ); +// expect(pipeline.filename).not.toEqual(updatedPipeline.filename); +// expect(pipeline.data).not.toEqual(updatedPipeline.data); +// }); + +// it('should allow a group owner to update', async () => { +// const privateGroup = await groupsService.create(PRIVATE_GROUP); +// const owner = await usersService.create(CREATE_USER_DTO_TEST_OBJ_2); + +// await groupsService.addUserToGroup(privateGroup, owner, 'owner'); + +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_1, +// data: mockFile, +// userId: user.id +// }); + +// await groupsService.addPipelineToGroup(privateGroup, pipeline); + +// const updatedPipeline = await pipelinesController.update( +// pipeline.id, +// {user: owner}, +// UPDATE_EVALUATION +// ); +// expect(pipeline.filename).not.toEqual(updatedPipeline.filename); +// expect(pipeline.data).not.toEqual(updatedPipeline.data); +// }); + +// it('should prevent unauthorized group users from updating an evalution in a group they belong to', async () => { +// expect.assertions(1); + +// const privateGroup = await groupsService.create(PRIVATE_GROUP); +// const owner = await usersService.create(CREATE_ADMIN_DTO); +// const basicUser = await usersService.create(CREATE_USER_DTO_TEST_OBJ_2); + +// await groupsService.addUserToGroup(privateGroup, owner, 'owner'); +// await groupsService.addUserToGroup(privateGroup, basicUser, 'user'); + +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_1, +// data: mockFile, +// userId: user.id +// }); + +// await groupsService.addPipelineToGroup(privateGroup, pipeline); + +// await expect( +// pipelinesController.update( +// pipeline.id, +// {user: basicUser}, +// UPDATE_EVALUATION +// ) +// ).rejects.toBeInstanceOf(ForbiddenError); +// }); + +// it('should prevent unauthorized users from updating', async () => { +// expect.assertions(1); +// const pipelineOwner = await usersService.create( +// CREATE_USER_DTO_TEST_OBJ_2 +// ); +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_1, +// data: mockFile, +// userId: pipelineOwner.id +// }); +// await expect( +// pipelinesController.update( +// pipeline.id, +// {user: user}, +// UPDATE_EVALUATION +// ) +// ).rejects.toBeInstanceOf(ForbiddenError); +// }); +// }); + +// describe('remove', () => { +// it('should remove an pipeline', async () => { +// expect.assertions(1); +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_1, +// data: {}, +// userId: user.id +// }); +// await pipelinesController.remove(pipeline.id, {user: user}); +// await expect( +// pipelinesController.findById(pipeline.id, {user: user}) +// ).rejects.toBeInstanceOf(NotFoundException); +// }); + +// it('should prevent unauthorized users removing an pipeline', async () => { +// expect.assertions(1); +// const pipelineOwner = await usersService.create( +// CREATE_USER_DTO_TEST_OBJ_2 +// ); +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_1, +// data: mockFile, +// userId: pipelineOwner.id +// }); +// await expect( +// pipelinesController.remove(pipeline.id, {user: user}) +// ).rejects.toBeInstanceOf(ForbiddenError); +// }); +// }); + +// describe('groups for pipeline', () => { +// it('should return groups the pipeline belongs to that the requesting user can add and remove the pipeline from', async () => { +// const pipelineOwner = await usersService.create( +// CREATE_USER_DTO_TEST_OBJ_2 +// ); +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_1, +// data: mockFile, +// userId: pipelineOwner.id +// }); +// const group = await groupsService.create(GROUP_1); +// await groupsService.addUserToGroup(group, user, 'member'); +// await groupsService.addPipelineToGroup(group, pipeline); +// const foundGroups = await pipelinesController.groupsForPipeline( +// pipeline.id, +// {user: user} +// ); +// expect(foundGroups[0].id).toEqual(group.id); +// }); + +// it('should not return groups the user has no access to', async () => { +// // GROUP_1 is a public group and still should not show up. +// const pipelineOwner = await usersService.create( +// CREATE_USER_DTO_TEST_OBJ_2 +// ); +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_1, +// data: mockFile, +// userId: pipelineOwner.id +// }); +// const group = await groupsService.create(GROUP_1); +// await groupsService.addPipelineToGroup(group, pipeline); +// const foundGroups = await pipelinesController.groupsForPipeline( +// pipeline.id, +// {user: user} +// ); +// expect(foundGroups.length).toEqual(0); +// }); +// }); + +// afterAll(async () => { +// await databaseService.cleanAll(); +// await databaseService.closeConnection(); +// }); +// }); diff --git a/apps/backend/src/products/products.controller.ts b/apps/backend/src/products/products.controller.ts new file mode 100644 index 0000000000..c77c9a6a94 --- /dev/null +++ b/apps/backend/src/products/products.controller.ts @@ -0,0 +1,201 @@ +import {ForbiddenError} from '@casl/ability'; +import { + BadRequestException, + Body, + Controller, + Get, + Delete, + Param, + Post, + Request, + UseGuards, + UseInterceptors +} from '@nestjs/common'; +import _ from 'lodash'; +import {AuthzService} from '../authz/authz.service'; +import {Action} from '../casl/casl-ability.factory'; +import {Group} from '../groups/group.model'; +import {GroupsService} from '../groups/groups.service'; +import {Build} from '../builds/build.model'; +import {BuildsService} from '../builds/builds.service'; +import {BuildDto} from '../builds/dto/build.dto'; +import {APIKeyOrJwtAuthGuard} from '../guards/api-key-or-jwt-auth.guard'; +import {JwtAuthGuard} from '../guards/jwt-auth.guard'; +import {LoggingInterceptor} from '../interceptors/logging.interceptor'; +import {User} from '../users/user.model'; +import {CreateProductDto} from './dto/create-product.dto'; +import {ProductDto} from './dto/product.dto'; +import {ProductsService} from './products.service'; +import {AddBuildToProductDto} from './dto/add-build-to-product-dto'; +import {RemoveBuildFromProductDto} from './dto/remove-build-from-product-dto'; + +@Controller('products') +@UseInterceptors(LoggingInterceptor) +export class ProductsController { + constructor( + private readonly productService: ProductsService, + private readonly buildsService: BuildsService, + private readonly groupsService: GroupsService, + private readonly authz: AuthzService + ) {} + + + @UseGuards(JwtAuthGuard) + @Get() + async findAll(@Request() request: {user: User}): Promise { + const abac = this.authz.abac.createForUser(request.user); + let products = await this.productService.findAll(); + products = products.filter((product) => + abac.can(Action.Read, product) + ); + return products.map((product) => new ProductDto(product)); + } + + @UseGuards(JwtAuthGuard) + @Get(':id') + async findById( + @Request() request: {user: User}, + @Param('id') id: string + ): Promise { + const abac = this.authz.abac.createForUser(request.user); + const product = await this.productService.findByPkBang(id); + ForbiddenError.from(abac).throwUnlessCan(Action.Read, product); + return new ProductDto(product, false); + } + + @UseGuards(APIKeyOrJwtAuthGuard) + @Get('id/:id') + async findByProjectId( + @Request() request: {user: User}, + @Param('id') id: string + ): Promise { + const product = await this.productService.findByProjectId(id); + return new ProductDto(product, false); + } + + @UseGuards(JwtAuthGuard) + @Get(':id/builds') + async buildsForProduct( + @Param('id') id: string, + @Request() request: {user: User} + ): Promise { + const abac = this.authz.abac.createForUser(request.user); + let products = await this.productService.buildsExtendedInfo(id); + products = products.filter((product) => + abac.can(Action.Read, product) + ); + return products.flatMap((product) => { + return product.builds.map((build) => new BuildDto(build)); + }); + } + + @UseGuards(APIKeyOrJwtAuthGuard) + @Post() + async create( + @Request() request: {user: User | Group}, + @Body() createProductDto: CreateProductDto + ): Promise { + if (request.user instanceof Group) { + // Product created by Group API Key + const product = await this.productService + .create({ + productId: createProductDto.productId, + productName: createProductDto.productName, + productURL: createProductDto.productURL, + objectStoreKey: createProductDto.objectStoreKey, + groupId: request.user.id + }) + .then(async (createdProduct) => { + const group = await this.groupsService.findByPkBang( + request.user.id + ); + this.groupsService.addProductToGroup(group, createdProduct) + return createdProduct; + }) + .catch((err) => { + throw new BadRequestException(err.message); + }); + + return new ProductDto(product); + } + + // Product created by User's JWT + let groups: Group[] = createProductDto.groups + ? await this.groupsService.findByIds(createProductDto.groups) + : []; + + // Make sure the user can add product to each group + const abac = this.authz.abac.createForUser(request.user); + groups = groups.filter((group) => { + return abac.can(Action.AddEvaluation, group); + }); + + const product = await this.productService + .create({ + productId: createProductDto.productId, + productName: createProductDto.productName, + productURL: createProductDto.productURL, + objectStoreKey: createProductDto.objectStoreKey, + userId: request.user.id + }) + .then((createdProduct) => { + groups.forEach((group) => + this.groupsService.addProductToGroup(group, createdProduct) + ); + return createdProduct; + }); + + return new ProductDto(product); + } + + @UseGuards(JwtAuthGuard) + @Delete(':id') + async remove( + @Param('id') id: string, + @Request() request: {user: User} + ): Promise { + const abac = this.authz.abac.createForUser(request.user); + const product = await this.productService.findByPkBang(id); + ForbiddenError.from(abac).throwUnlessCan(Action.Delete, product); + return new ProductDto(await this.productService.remove(product)); + } + + @UseGuards(APIKeyOrJwtAuthGuard) + @Post('/:id/build') + async addBuildToProduct( + @Param('id') id: string, + @Request() request: {build: Build}, + @Body() addBuildToProductDto: AddBuildToProductDto + ): Promise { + const product = await this.productService.findByPkBang(id); + const buildToAdd = await this.buildsService.findById( + addBuildToProductDto.buildId + ); + await this.productService.addBuildToProduct( + product, + buildToAdd + ); + + // TODO: update Product updatedAt time before returning + + return new ProductDto(product); + } + + @UseGuards(JwtAuthGuard) + @Delete('/:id/build') + async removeBuildFromProduct( + @Param('id') id: string, + @Request() request: {user: User}, + @Body() removeBuildFromProductDto: RemoveBuildFromProductDto + ): Promise { + const abac = this.authz.abac.createForUser(request.user); + const product = await this.productService.findByPkBang(id); + ForbiddenError.from(abac).throwUnlessCan(Action.Update, product); + const buildToRemove = await this.buildsService.findById( + removeBuildFromProductDto.buildId + ); + return new ProductDto( + await this.productService.removeBuildFromProduct(product, buildToRemove) + ); + } +} \ No newline at end of file diff --git a/apps/backend/src/products/products.module.ts b/apps/backend/src/products/products.module.ts new file mode 100644 index 0000000000..4d9edaebd4 --- /dev/null +++ b/apps/backend/src/products/products.module.ts @@ -0,0 +1,27 @@ +import {forwardRef, Module} from '@nestjs/common'; +import {SequelizeModule} from '@nestjs/sequelize'; +import {ConfigModule} from '../config/config.module'; +import {DatabaseModule} from '../database/database.module'; +import {Group} from '../groups/group.model'; +import {GroupsService} from '../groups/groups.service'; +import {BuildsModule} from '../builds/builds.module'; +import {Product} from './product.model'; +import {ProductsController} from './products.controller'; +import {ProductsService} from './products.service'; +import {UsersModule} from '../users/users.module'; +@Module({ + imports: [ + SequelizeModule.forFeature([ + Product, + Group, + ]), + ConfigModule, + BuildsModule, + forwardRef(() => UsersModule), + DatabaseModule + ], + providers: [ProductsService, GroupsService], + controllers: [ProductsController], + exports: [ProductsService] +}) +export class ProductsModule {} diff --git a/apps/backend/src/products/products.service.spec.ts b/apps/backend/src/products/products.service.spec.ts new file mode 100644 index 0000000000..e5cde73e65 --- /dev/null +++ b/apps/backend/src/products/products.service.spec.ts @@ -0,0 +1,305 @@ +// import {NotFoundException} from '@nestjs/common'; +// import {SequelizeModule} from '@nestjs/sequelize'; +// import {Test} from '@nestjs/testing'; +// import { +// CREATE_EVALUATION_DTO_WITHOUT_FILENAME, +// CREATE_EVALUATION_DTO_WITHOUT_TAGS, +// EVALUATION_WITH_TAGS_1, +// UPDATE_EVALUATION, +// UPDATE_EVALUATION_DATA_ONLY, +// UPDATE_EVALUATION_FILENAME_ONLY +// } from '../../test/constants/pipelines-test.constant'; +// import {GROUP_1} from '../../test/constants/groups-test.constant'; +// import {CREATE_USER_DTO_TEST_OBJ} from '../../test/constants/users-test.constant'; +// import {DatabaseModule} from '../database/database.module'; +// import {DatabaseService} from '../database/database.service'; +// // import {PipelineTagsModule} from '../pipeline-tags/pipeline-tags.module'; +// // import {PipelineTagsService} from '../pipeline-tags/pipeline-tags.service'; +// // import {GroupPipeline} from '../group-pipelines/group-pipeline.model'; +// // import {GroupUser} from '../group-users/group-user.model'; +// // import {Group} from '../groups/group.model'; +// // import {GroupsService} from '../groups/groups.service'; +// import {UserDto} from '../users/dto/user.dto'; +// import {UsersModule} from '../users/users.module'; +// import {UsersService} from '../users/users.service'; +// import {PipelineDto} from './dto/pipeline.dto'; +// import {Pipeline} from './pipeline.model'; +// import {PipelinesService} from './pipelines.service'; + +// describe('PipelinesService', () => { +// let pipelinesService: PipelinesService; +// //let pipelineTagsService: PipelineTagsService; +// let databaseService: DatabaseService; +// let usersService: UsersService; +// let user: UserDto; +// //let groupsService: GroupsService; + +// beforeAll(async () => { +// const module = await Test.createTestingModule({ +// imports: [ +// DatabaseModule, +// SequelizeModule.forFeature([ +// Pipeline, +// // GroupUser, +// // Group, +// // GroupPipeline +// ]), +// // PipelineTagsModule, +// UsersModule +// ], +// providers: [ +// PipelinesService, +// DatabaseService, +// UsersService, +// // GroupsService +// ] +// }).compile(); + +// pipelinesService = module.get(PipelinesService); +// // pipelineTagsService = module.get( +// // PipelineTagsService +// // ); +// databaseService = module.get(DatabaseService); +// usersService = module.get(UsersService); +// //groupsService = module.get(GroupsService); +// }); + +// beforeEach(async () => { +// await databaseService.cleanAll(); +// user = new UserDto(await usersService.create(CREATE_USER_DTO_TEST_OBJ)); +// }); + +// describe('findAll', () => { +// it('should find all pipelines', async () => { +// let pipelinesDtoArray = await pipelinesService.findAll(); +// expect(pipelinesDtoArray).toEqual([]); + +// await pipelinesService.create({ +// ...EVALUATION_WITH_TAGS_1, +// data: {}, +// userId: user.id +// }); +// await pipelinesService.create({ +// ...EVALUATION_WITH_TAGS_1, +// data: {}, +// userId: user.id +// }); +// pipelinesDtoArray = await pipelinesService.findAll(); +// expect(pipelinesDtoArray.length).toEqual(2); +// }); + +// it('should include the pipeline user', async () => { +// await pipelinesService.create({ +// ...EVALUATION_WITH_TAGS_1, +// data: {}, +// userId: user.id +// }); + +// const pipelines = await pipelinesService.findAll(); +// expect(new UserDto(pipelines[0].user)).toEqual(user); +// }); + +// it('should include the pipeline group and group users', async () => { +// //const group = await groupsService.create(GROUP_1); +// const owner = await usersService.findById(user.id); +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_WITH_TAGS_1, +// data: {}, +// userId: user.id +// }); + +// let pipelines = await pipelinesService.findAll(); +// expect(pipelines[0].groups[0]).not.toBeDefined(); + +// // await groupsService.addPipelineToGroup(group, pipeline); +// // await groupsService.addUserToGroup(group, owner, 'owner'); + +// pipelines = await pipelinesService.findAll(); +// const foundGroup = pipelines[0].groups[0]; +// expect(foundGroup).toBeDefined(); +// //expect(foundGroup.id).toEqual(group.id); +// expect(foundGroup.users.length).toEqual(1); +// expect(foundGroup.users[0].id).toEqual(owner.id); +// expect(foundGroup.users[0].GroupUser.role).toEqual('owner'); +// }); +// }); + +// describe('findById', () => { +// it('should find pipelines by id', async () => { +// expect.assertions(1); +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_WITH_TAGS_1, +// data: {}, +// userId: user.id +// }); +// const foundPipeline = await pipelinesService.findById(pipeline.id); +// expect(new PipelineDto(pipeline)).toEqual( +// new PipelineDto(foundPipeline) +// ); +// }); + +// it('should throw an error if an pipeline does not exist', async () => { +// expect.assertions(1); +// await expect(pipelinesService.findById('-1')).rejects.toThrow( +// NotFoundException +// ); +// }); +// }); + +// describe('create', () => { +// it('should create a new pipeline with pipeline tags', async () => { +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_WITH_TAGS_1, +// data: {}, +// userId: user.id +// }); +// expect(pipeline.id).toBeDefined(); +// expect(pipeline.updatedAt).toBeDefined(); +// expect(pipeline.createdAt).toBeDefined(); +// expect(pipeline.data).toEqual({}); +// expect(pipeline.filename).toEqual(EVALUATION_WITH_TAGS_1.filename); +// expect(pipeline.pipelineTags[0].pipelineId).toBeDefined(); +// expect(pipeline.pipelineTags[0].updatedAt).toBeDefined(); +// expect(pipeline.pipelineTags[0].createdAt).toBeDefined(); + +// if (EVALUATION_WITH_TAGS_1.pipelineTags === undefined) { +// throw new TypeError( +// 'Pipeline fixture does not have any associated tags.' +// ); +// } + +// expect(pipeline.pipelineTags?.[0].value).toEqual( +// EVALUATION_WITH_TAGS_1.pipelineTags[0].value +// ); +// }); + +// it('should create a new pipeline without pipeline tags', async () => { +// const pipeline = await pipelinesService.create({ +// ...CREATE_EVALUATION_DTO_WITHOUT_TAGS, +// data: {}, +// userId: user.id +// }); +// expect(pipeline.id).toBeDefined(); +// expect(pipeline.updatedAt).toBeDefined(); +// expect(pipeline.createdAt).toBeDefined(); +// expect(pipeline.data).toEqual({}); +// expect(pipeline.filename).toEqual( +// CREATE_EVALUATION_DTO_WITHOUT_TAGS.filename +// ); +// expect(pipeline.pipelineTags).not.toBeDefined(); +// //expect((await pipelineTagsService.findAll()).length).toBe(0); +// }); + +// it('should throw an error when missing the filename field', async () => { +// expect.assertions(1); +// await expect( +// pipelinesService.create({ +// ...CREATE_EVALUATION_DTO_WITHOUT_FILENAME, +// data: {}, +// userId: user.id +// }) +// ).rejects.toThrow( +// 'notNull Violation: Pipeline.filename cannot be null' +// ); +// }); +// }); + +// describe('update', () => { +// it('should throw an error if an pipeline does not exist', async () => { +// expect.assertions(1); +// await expect( +// pipelinesService.update('-1', UPDATE_EVALUATION) +// ).rejects.toThrow(NotFoundException); +// }); + +// it('should update all fields of an pipeline', async () => { +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_WITH_TAGS_1, +// data: {}, +// userId: user.id +// }); +// const updatedPipeline = await pipelinesService.update( +// pipeline.id, +// UPDATE_EVALUATION +// ); +// expect(updatedPipeline.id).toEqual(pipeline.id); +// expect(updatedPipeline.createdAt).toEqual(pipeline.createdAt); +// expect(updatedPipeline.updatedAt).not.toEqual(pipeline.updatedAt); +// expect(updatedPipeline.data).not.toEqual(pipeline.data); +// expect(updatedPipeline.filename).not.toEqual(pipeline.filename); +// }); + +// it('should only update data if provided', async () => { +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_WITH_TAGS_1, +// data: {}, +// userId: user.id +// }); +// const updatedPipeline = await pipelinesService.update( +// pipeline.id, +// UPDATE_EVALUATION_DATA_ONLY +// ); +// expect(updatedPipeline.id).toEqual(pipeline.id); +// expect(updatedPipeline.createdAt).toEqual(pipeline.createdAt); +// expect(updatedPipeline.updatedAt).not.toEqual(pipeline.updatedAt); +// expect(updatedPipeline.pipelineTags.length).toEqual( +// pipeline.pipelineTags.length +// ); +// expect(updatedPipeline.data).not.toEqual(pipeline.data); +// expect(updatedPipeline.filename).toEqual(pipeline.filename); +// }); + +// it('should only update filename if provided', async () => { +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_WITH_TAGS_1, +// data: {}, +// userId: user.id +// }); + +// const updatedPipeline = await pipelinesService.update( +// pipeline.id, +// UPDATE_EVALUATION_FILENAME_ONLY +// ); +// expect(updatedPipeline.id).toEqual(pipeline.id); +// expect(updatedPipeline.createdAt).toEqual(pipeline.createdAt); +// expect(updatedPipeline.updatedAt).not.toEqual(pipeline.updatedAt); +// expect(updatedPipeline.pipelineTags.length).toEqual( +// pipeline.pipelineTags.length +// ); +// expect(updatedPipeline.data).toEqual(pipeline.data); +// expect(updatedPipeline.filename).not.toEqual(pipeline.filename); +// }); +// }); + +// describe('remove', () => { +// it('should remove an pipeline and its pipeline tags given an id', async () => { +// const pipeline = await pipelinesService.create({ +// ...EVALUATION_WITH_TAGS_1, +// data: {}, +// userId: user.id +// }); +// const removedPipeline = await pipelinesService.remove(pipeline.id); +// const foundPipelineTags = await pipelineTagsService.findAll(); +// expect(foundPipelineTags.length).toEqual(0); +// expect(new PipelineDto(removedPipeline)).toEqual( +// new PipelineDto(pipeline) +// ); + +// await expect( +// pipelinesService.findById(removedPipeline.id) +// ).rejects.toThrow(NotFoundException); +// }); + +// it('should throw an error when the pipeline does not exist', async () => { +// expect.assertions(1); +// await expect(pipelinesService.findById('-1')).rejects.toThrow( +// NotFoundException +// ); +// }); +// }); + +// afterAll(async () => { +// await databaseService.cleanAll(); +// await databaseService.closeConnection(); +// }); +// }); diff --git a/apps/backend/src/products/products.service.ts b/apps/backend/src/products/products.service.ts new file mode 100644 index 0000000000..d06f2198e3 --- /dev/null +++ b/apps/backend/src/products/products.service.ts @@ -0,0 +1,103 @@ +import {Injectable, NotFoundException} from '@nestjs/common'; +import {InjectModel} from '@nestjs/sequelize'; +import {FindOptions} from 'sequelize/types'; +import {DatabaseService} from '../database/database.service'; +import {Build} from '../builds/build.model'; +import {CreateProductDto} from './dto/create-product.dto'; +import {UpdateProductDto} from './dto/update-product.dto'; +import {Product} from './product.model'; +import {Group} from '../groups/group.model'; +import {User} from '../users/user.model'; + +@Injectable() +export class ProductsService { + constructor( + @InjectModel(Product) + private readonly productModel: typeof Product, + private readonly databaseService: DatabaseService + ) {} + + async findAll(): Promise { + return this.productModel.findAll({ + include: [User, {model: Group, include: [User]}] + }); + } + + async count(): Promise { + return this.productModel.count(); + } + + async findById(id: string): Promise { + return this.findByPkBang(id); + } + + async findByProjectId(productId: string): Promise { + return this.findOneBang({ + where: { + productId + } + }); + } + + async create(product : { + productId: string, + productName: string, + productURL: string, + objectStoreKey: string, + userId?: string, + groupId?: string, + }): Promise { + return Product.create( + { + ...product + } + ); + } + + async update(productToUpdate: Product, groupDto: CreateProductDto): Promise { + productToUpdate.update(groupDto); + + return productToUpdate.save(); + } + + async remove(productToDelete: Product): Promise { + await productToDelete.destroy(); + + return productToDelete; + } + + async buildsExtendedInfo(id: string): Promise { + return this.productModel.findAll({ + where: {id}, + include: ['builds', User, {model: Group, include: [User]}] + }); + } + + async findByPkBang(id: string): Promise { + const product = await this.productModel.findByPk(id, {include: ['builds', User, {model: Group, include: [User]}]}); + if (product === null) { + throw new NotFoundException('Product with given id not found'); + } else { + return product; + } + } + + async findOneBang(options: FindOptions | undefined): Promise { + const product = await this.productModel.findOne(options); + if (product === null) { + throw new NotFoundException('Product with given id not found'); + } else { + return product; + } + } + + async addBuildToProduct(product: Product, build: Build): Promise { + await product.$add('builds', build, { + through: {createdAt: new Date(), updatedAt: new Date()} + }); + } + + async removeBuildFromProduct(product: Product, build: Build): Promise { + return product.$remove('build', build); + } +} diff --git a/apps/backend/src/statistics/dto/statistics.dto.ts b/apps/backend/src/statistics/dto/statistics.dto.ts index 0ce30d9d8c..c338297032 100644 --- a/apps/backend/src/statistics/dto/statistics.dto.ts +++ b/apps/backend/src/statistics/dto/statistics.dto.ts @@ -6,12 +6,16 @@ export class StatisticsDTO implements IStatistics { readonly evaluationCount: number; readonly evaluationTagCount: number; readonly groupCount: number; + readonly buildCount: number; + readonly productCount: number; constructor(statistics: StatisticsDTO) { this.apiKeyCount = statistics.apiKeyCount; this.userCount = statistics.userCount; this.evaluationCount = statistics.evaluationCount; this.evaluationTagCount = statistics.evaluationTagCount; + this.buildCount = statistics.buildCount; + this.productCount = statistics.productCount; this.groupCount = statistics.groupCount; } } diff --git a/apps/backend/src/statistics/statistics.module.ts b/apps/backend/src/statistics/statistics.module.ts index 6e92e8fca9..c25071ea13 100644 --- a/apps/backend/src/statistics/statistics.module.ts +++ b/apps/backend/src/statistics/statistics.module.ts @@ -11,6 +11,10 @@ import {Evaluation} from '../evaluations/evaluation.model'; import {EvaluationsService} from '../evaluations/evaluations.service'; import {Group} from '../groups/group.model'; import {GroupsService} from '../groups/groups.service'; +import {Build} from '../builds/build.model'; +import {BuildsService} from '../builds/builds.service'; +import {Product} from '../products/product.model'; +import {ProductsService} from '../products/products.service'; import {User} from '../users/user.model'; import {UsersService} from '../users/users.service'; import {StatisticsController} from './statistics.controller'; @@ -23,7 +27,9 @@ import {StatisticsService} from './statistics.service'; Evaluation, EvaluationTag, User, - Group + Group, + Build, + Product ]), ConfigModule ], @@ -35,7 +41,9 @@ import {StatisticsService} from './statistics.service'; EvaluationsService, EvaluationTagsService, UsersService, - GroupsService + GroupsService, + BuildsService, + ProductsService ], controllers: [StatisticsController] }) diff --git a/apps/backend/src/statistics/statistics.service.ts b/apps/backend/src/statistics/statistics.service.ts index 1038aab98d..1c26f6e9b2 100644 --- a/apps/backend/src/statistics/statistics.service.ts +++ b/apps/backend/src/statistics/statistics.service.ts @@ -4,6 +4,8 @@ import {EvaluationTagsService} from '../evaluation-tags/evaluation-tags.service' import {EvaluationsService} from '../evaluations/evaluations.service'; import {GroupsService} from '../groups/groups.service'; import {UsersService} from '../users/users.service'; +import {BuildsService} from '../builds/builds.service'; +import {ProductsService} from '../products/products.service'; import {StatisticsDTO} from './dto/statistics.dto'; @Injectable() @@ -13,6 +15,8 @@ export class StatisticsService { private readonly evaluationsService: EvaluationsService, private readonly evaluationTagsService: EvaluationTagsService, private readonly groupsService: GroupsService, + private readonly buildsService: BuildsService, + private readonly productsService: ProductsService, private readonly usersService: UsersService ) {} @@ -22,6 +26,8 @@ export class StatisticsService { userCount: await this.usersService.count(), evaluationCount: await this.evaluationsService.count(), evaluationTagCount: await this.evaluationTagsService.count(), + buildCount: await this.buildsService.count(), + productCount: await this.productsService.count(), groupCount: await this.groupsService.count() }); } diff --git a/apps/backend/src/users/users.controller.spec.ts b/apps/backend/src/users/users.controller.spec.ts index 0b4fd05a8c..caeec6a607 100644 --- a/apps/backend/src/users/users.controller.spec.ts +++ b/apps/backend/src/users/users.controller.spec.ts @@ -29,9 +29,15 @@ import {DatabaseService} from '../database/database.service'; import {EvaluationTag} from '../evaluation-tags/evaluation-tag.model'; import {Evaluation} from '../evaluations/evaluation.model'; import {GroupEvaluation} from '../group-evaluations/group-evaluation.model'; +import { GroupBuild } from '../group-builds/group-build.model'; +import { GroupProduct } from '../group-products/group-product.model'; import {GroupUser} from '../group-users/group-user.model'; import {Group} from '../groups/group.model'; import {GroupsService} from '../groups/groups.service'; +import { Build } from '../builds/build.model'; +import { BuildEvaluation } from '../build-evaluations/build-evaluations.model'; +import {Product} from '../products/product.model'; +import { ProductBuild } from '../product-builds/product-builds.model'; import {UserDto} from './dto/user.dto'; import {User} from './user.model'; import {UsersController} from './users.controller'; @@ -60,7 +66,13 @@ describe('UsersController Unit Tests', () => { Group, GroupEvaluation, Evaluation, - EvaluationTag + EvaluationTag, + GroupProduct, + Product, + GroupBuild, + Build, + ProductBuild, + BuildEvaluation ]) ], providers: [ diff --git a/apps/backend/src/users/users.service.spec.ts b/apps/backend/src/users/users.service.spec.ts index 47b0741d06..9c4c1444e5 100644 --- a/apps/backend/src/users/users.service.spec.ts +++ b/apps/backend/src/users/users.service.spec.ts @@ -40,9 +40,15 @@ import {DatabaseService} from '../database/database.service'; import {EvaluationTag} from '../evaluation-tags/evaluation-tag.model'; import {Evaluation} from '../evaluations/evaluation.model'; import {GroupEvaluation} from '../group-evaluations/group-evaluation.model'; +import { GroupBuild } from '../group-builds/group-build.model'; +import {GroupProduct} from '../group-products/group-product.model' import {GroupUser} from '../group-users/group-user.model'; import {Group} from '../groups/group.model'; import {GroupsService} from '../groups/groups.service'; +import { Build } from '../builds/build.model'; +import { BuildEvaluation } from '../build-evaluations/build-evaluations.model'; +import {Product} from '../products/product.model'; +import { ProductBuild } from '../product-builds/product-builds.model'; import {SlimUserDto} from './dto/slim-user.dto'; import {UserDto} from './dto/user.dto'; import {User} from './user.model'; @@ -65,7 +71,13 @@ describe('UsersService', () => { Group, GroupEvaluation, Evaluation, - EvaluationTag + EvaluationTag, + GroupProduct, + Product, + GroupBuild, + Build, + ProductBuild, + BuildEvaluation ]), AuthzModule ], diff --git a/apps/backend/test/.env-ci b/apps/backend/test/.env-ci index 4856f07740..5deaad0fc8 100644 --- a/apps/backend/test/.env-ci +++ b/apps/backend/test/.env-ci @@ -1,7 +1,7 @@ PORT=3000 -DATABASE_HOST=localhost +DATABASE_HOST=postgres-db DATABASE_PORT=5432 -DATABASE_PASSWORD=postgres +DATABASE_PASSWORD=password DATABASE_NAME=heimdallts_jest_testing_service_db JWT_SECRET=abc123 NODE_ENV=test diff --git a/apps/frontend/jest.config.js b/apps/frontend/jest.config.js index 6980348bb9..c4647ff11b 100644 --- a/apps/frontend/jest.config.js +++ b/apps/frontend/jest.config.js @@ -8,7 +8,7 @@ module.exports = { cacheDirectory: '/.cache/unit', transform: { '.*\\.(vue)$': 'vue-jest', - '^.+\\.tsx?$': 'ts-jest', + '^.+\\.tsx?$': ['ts-jest', {isolatedModules: true}], '^.+\\.svg$': '/tests/util/svgTransform.js' }, moduleDirectories: ['node_modules', 'src'], diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 1a18728282..94ac34ceac 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -89,6 +89,8 @@ "sass": "~1.32.6", "sass-loader": "^14.0.0", "search-query-parser": "^1.5.5", + "tailwindcss": "^3.3.3", + "tw-elements": "^1.0.0-beta2", "uuid": "^9.0.0", "vue": "~2.6.10", "vue-apexcharts": "^1.5.1", diff --git a/apps/frontend/src/components/cards/ProfileInfo.vue b/apps/frontend/src/components/cards/ProfileInfo.vue index 71faec9f23..f9002699cc 100644 --- a/apps/frontend/src/components/cards/ProfileInfo.vue +++ b/apps/frontend/src/components/cards/ProfileInfo.vue @@ -37,6 +37,30 @@
Control Count: {{ control_count }}
+
+ Date: {{ compdb_date }} +
+
+ Job Name: {{ compdb_job_name }} +
+
+ Job Start: {{ compdb_job_start }} +
+
+ Pipeline ID: {{ compdb_pipeline_id }} +
+
+ Pipeline URL: {{ compdb_pipeline_url }} +
+
+ Project Name: {{ compdb_project_name }} +
+
+ Project URL: {{ compdb_project_url }} +
+
+ UUID: {{ compdb_uuid }} +
@@ -144,6 +168,42 @@ export default class ProfileInfo extends Vue { }`; } + get pipeline_id(): string | undefined { + return _.get(this.profile, 'data.pipeline_id'); + } + + get compdb_date(): string | undefined { + return _.get(this.profile, 'data.compdb.date'); + } + + get compdb_job_name(): string | undefined { + return _.get(this.profile, 'data.compdb.job_name'); + } + + get compdb_job_start(): string | undefined { + return _.get(this.profile, 'data.compdb.job_start'); + } + + get compdb_pipeline_id(): number | undefined { + return _.get(this.profile, 'data.compdb.pipeline_id'); + } + + get compdb_pipeline_url(): string | undefined { + return _.get(this.profile, 'data.compdb.pipeline_url'); + } + + get compdb_project_name(): string | undefined { + return _.get(this.profile, 'data.compdb.project_name'); + } + + get compdb_project_url(): string | undefined { + return _.get(this.profile, 'data.compdb.project_url'); + } + + get compdb_uuid(): string | undefined { + return _.get(this.profile, 'data.compdb.uuid'); + } + get inputs(): Attribute[] { if (this.profile?.data.hasOwnProperty('attributes')) { return _.get(this.profile, 'data.attributes') as unknown as Attribute[]; diff --git a/apps/frontend/src/components/cards/StatusCardRow.vue b/apps/frontend/src/components/cards/StatusCardRow.vue index f52a63adef..119a046470 100644 --- a/apps/frontend/src/components/cards/StatusCardRow.vue +++ b/apps/frontend/src/components/cards/StatusCardRow.vue @@ -207,6 +207,15 @@ export default class StatusCardRow extends Vue { } toggleFilter(filter: ExtendedControlStatus) { + // Workaround to pin Pending status to Failed status toggle + if (filter === "Failed") { + if (this.filter.status?.includes("Pending")) { + this.$emit('remove-filter', "Pending"); + } else { + filter = "Failed,Pending"; + } + } + if (this.filter.status?.includes(filter)) { this.$emit('remove-filter', filter); } else { diff --git a/apps/frontend/src/components/cards/StatusChart.vue b/apps/frontend/src/components/cards/StatusChart.vue index 5b2cf53f86..e32d808f8f 100644 --- a/apps/frontend/src/components/cards/StatusChart.vue +++ b/apps/frontend/src/components/cards/StatusChart.vue @@ -46,6 +46,11 @@ export default class StatusChart extends Vue { value: 'Failed', color: 'statusFailed' }, + { + label: 'Pending', + value: 'Pending', + color: 'statuspending' + }, { label: 'Not Applicable', value: 'Not Applicable', diff --git a/apps/frontend/src/components/cards/controltable/ControlRowCol.vue b/apps/frontend/src/components/cards/controltable/ControlRowCol.vue index 81d9a17639..c91645173e 100644 --- a/apps/frontend/src/components/cards/controltable/ControlRowCol.vue +++ b/apps/frontend/src/components/cards/controltable/ControlRowCol.vue @@ -1,6 +1,8 @@ @@ -82,7 +83,7 @@ export default class Cell extends Vue { return ( this.is_control && // We are a control (this.node.data as TreemapNodeLeaf).control.data.id === - this.selectedControlId // Our control id matches + this.selectedControlId // Our control id matches ); } @@ -110,6 +111,39 @@ export default class Cell extends Vue { return this.scales.scale_y(this.node.y1) - this.y; } + /** + * Crop control title text strings that are too long to fit in the cell + */ + get cellTitleText(): string { + const title = this.node.data.title; + const ruleDelimIndex = title.lastIndexOf("|"); + let element: string; + let maxLength = Math.floor(this.width / 10); + + if (ruleDelimIndex === -1) { + element = title; + } else { + element = title.substring(ruleDelimIndex + 1); + } + + if (element.length > maxLength) { + element = element.substring(0, maxLength) + '...'; + } + + return element; + } + + /** + * Adjust cell font style to better fit cell + */ + get cellSizeAdjust() { + if (this.cellTitleText.length > 5) { + return { + fontSize: '100%' + }; + } + } + /** Returns a list of classes appropriate to this nodes Rect * These are contextual based on type of data, and depth within the tree */ diff --git a/apps/frontend/src/components/generic/ProjectButton.vue b/apps/frontend/src/components/generic/ProjectButton.vue new file mode 100644 index 0000000000..ba6f364251 --- /dev/null +++ b/apps/frontend/src/components/generic/ProjectButton.vue @@ -0,0 +1,41 @@ + + + diff --git a/apps/frontend/src/components/generic/ViewModeButton.vue b/apps/frontend/src/components/generic/ViewModeButton.vue new file mode 100644 index 0000000000..0570f0d809 --- /dev/null +++ b/apps/frontend/src/components/generic/ViewModeButton.vue @@ -0,0 +1,81 @@ + + + diff --git a/apps/frontend/src/components/global/DownloadAuthorizationArtifactModal.vue b/apps/frontend/src/components/global/DownloadAuthorizationArtifactModal.vue new file mode 100644 index 0000000000..2ef0885415 --- /dev/null +++ b/apps/frontend/src/components/global/DownloadAuthorizationArtifactModal.vue @@ -0,0 +1,87 @@ + + + diff --git a/apps/frontend/src/components/global/Footer.vue b/apps/frontend/src/components/global/Footer.vue index 74442ba842..32e5f1d6d2 100644 --- a/apps/frontend/src/components/global/Footer.vue +++ b/apps/frontend/src/components/global/Footer.vue @@ -1,7 +1,7 @@ + + + + + diff --git a/apps/frontend/src/components/global/Sidebar.vue b/apps/frontend/src/components/global/Sidebar.vue index 54b39bc10b..39bc620165 100644 --- a/apps/frontend/src/components/global/Sidebar.vue +++ b/apps/frontend/src/components/global/Sidebar.vue @@ -1,176 +1,268 @@ - - - - - + + + + + diff --git a/apps/frontend/src/components/global/UpdateNotification.vue b/apps/frontend/src/components/global/UpdateNotification.vue index fbc8ebefa1..a9b0eacbf8 100644 --- a/apps/frontend/src/components/global/UpdateNotification.vue +++ b/apps/frontend/src/components/global/UpdateNotification.vue @@ -1,46 +1,48 @@ - - - + + + diff --git a/apps/frontend/src/components/global/UploadNexus.vue b/apps/frontend/src/components/global/UploadNexus.vue index 36f8b83c1d..dd73872dd5 100644 --- a/apps/frontend/src/components/global/UploadNexus.vue +++ b/apps/frontend/src/components/global/UploadNexus.vue @@ -145,7 +145,14 @@ export default class UploadNexus extends mixins(ServerMixin, RouteMixin) { if (this.current_route === 'compare') { this.navigateWithNoErrors(`/compare/${loadedDatabaseIds}`); } else { - this.navigateWithNoErrors(`/results/${loadedDatabaseIds}`); + if (this.current_route === "") { + this.navigateWithNoErrors(`/results/${loadedDatabaseIds}`); + } else if (this.current_route === "classic") { + this.navigateWithNoErrors(`/results/${loadedDatabaseIds}`); + } + else { + this.navigateWithNoErrors(`/${this.current_route}/${loadedDatabaseIds}`); + } } } else { this.navigateWithNoErrors(`/profiles/${loadedDatabaseIds}`); diff --git a/apps/frontend/src/components/global/admin/Statistics.vue b/apps/frontend/src/components/global/admin/Statistics.vue index 739488d38a..df19bb3be2 100644 --- a/apps/frontend/src/components/global/admin/Statistics.vue +++ b/apps/frontend/src/components/global/admin/Statistics.vue @@ -32,6 +32,8 @@ export default class Statistics extends Vue { userCount: 0, evaluationCount: 0, evaluationTagCount: 0, + buildCount: 0, + productCount: 0, groupCount: 0 }; diff --git a/apps/frontend/src/components/global/sidebaritems/ProductDropdownContent.vue b/apps/frontend/src/components/global/sidebaritems/ProductDropdownContent.vue new file mode 100644 index 0000000000..c949e6ca41 --- /dev/null +++ b/apps/frontend/src/components/global/sidebaritems/ProductDropdownContent.vue @@ -0,0 +1,99 @@ + + + diff --git a/apps/frontend/src/components/global/sidebaritems/ProductSidebarFileList.vue b/apps/frontend/src/components/global/sidebaritems/ProductSidebarFileList.vue new file mode 100644 index 0000000000..dc1810dc79 --- /dev/null +++ b/apps/frontend/src/components/global/sidebaritems/ProductSidebarFileList.vue @@ -0,0 +1,134 @@ + + + diff --git a/apps/frontend/src/components/global/sidebaritems/SidebarFileList.vue b/apps/frontend/src/components/global/sidebaritems/SidebarFileList.vue index 15f3343663..479a73ff01 100644 --- a/apps/frontend/src/components/global/sidebaritems/SidebarFileList.vue +++ b/apps/frontend/src/components/global/sidebaritems/SidebarFileList.vue @@ -41,6 +41,7 @@ import {EvaluationModule} from '@/store/evaluations'; import {EvaluationFile, ProfileFile} from '@/store/report_intake'; import {SnackbarModule} from '@/store/snackbar'; import {ICreateEvaluation, IEvaluation} from '@heimdall/interfaces'; +import { assessment_eval } from '../../../store/assessment_data'; import axios from 'axios'; import * as _ from 'lodash'; import Component, {mixins} from 'vue-class-component'; @@ -55,6 +56,11 @@ export default class SidebarFileList extends mixins(ServerMixin, RouteMixin) { select_file() { if (this.file.hasOwnProperty('evaluation')) { FilteredDataModule.toggle_evaluation(this.file.uniqueId); + + let ev = FilteredDataModule.evaluation(this.file.uniqueId) + if (ev != undefined){ + const at = assessment_eval(ev) + } } else if (this.file.hasOwnProperty('profile')) { FilteredDataModule.toggle_profile(this.file.uniqueId); } diff --git a/apps/frontend/src/components/global/upload_tabs/BuildLoadFileList.vue b/apps/frontend/src/components/global/upload_tabs/BuildLoadFileList.vue new file mode 100644 index 0000000000..790ff78ae1 --- /dev/null +++ b/apps/frontend/src/components/global/upload_tabs/BuildLoadFileList.vue @@ -0,0 +1,105 @@ + + + + + diff --git a/apps/frontend/src/components/global/upload_tabs/LoadFileList.vue b/apps/frontend/src/components/global/upload_tabs/LoadFileList.vue index 570ab49765..192b17082f 100644 --- a/apps/frontend/src/components/global/upload_tabs/LoadFileList.vue +++ b/apps/frontend/src/components/global/upload_tabs/LoadFileList.vue @@ -68,6 +68,12 @@ @confirm="deleteItemConfirm" />
+
    +

    Entries

    +
+
    +

    No entries found

    +
+
+
+ +
+ + + + + +
+
+
+ + + + + diff --git a/apps/frontend/src/enums/assessment_type.ts b/apps/frontend/src/enums/assessment_type.ts new file mode 100644 index 0000000000..9b5041ac08 --- /dev/null +++ b/apps/frontend/src/enums/assessment_type.ts @@ -0,0 +1,6 @@ +export const enum AssessmentType { + STIG = 'stig', + Vulnerability = 'vulnerability', + General = 'general' + } + \ No newline at end of file diff --git a/apps/frontend/src/enums/bucket_type.ts b/apps/frontend/src/enums/bucket_type.ts new file mode 100644 index 0000000000..2d678e581a --- /dev/null +++ b/apps/frontend/src/enums/bucket_type.ts @@ -0,0 +1,8 @@ +export const enum BucketType { + STIGASD = 0, + STIGContainer, + General, + Vulnerability, + Unmapped, + MaxSize + } \ No newline at end of file diff --git a/apps/frontend/src/enums/product_model_view_mode.ts b/apps/frontend/src/enums/product_model_view_mode.ts new file mode 100644 index 0000000000..c913340cba --- /dev/null +++ b/apps/frontend/src/enums/product_model_view_mode.ts @@ -0,0 +1,6 @@ +export const enum ViewModeType { + Certifier = 0, + Developer, + Cyber, +} + \ No newline at end of file diff --git a/apps/frontend/src/plugins/vuetify.ts b/apps/frontend/src/plugins/vuetify.ts index baef3abb30..cabc8083d9 100644 --- a/apps/frontend/src/plugins/vuetify.ts +++ b/apps/frontend/src/plugins/vuetify.ts @@ -18,7 +18,14 @@ const statuses = { statusskipped: colors.orange.base, statusprofileerror: colors.indigo.lighten2, statusnotrun: colors.teal.darken2, - statusfromprofile: colors.teal.base + statusfromprofile: colors.teal.base, + /** + * Case sensitivity is inconsistent between the result layer, and the control layer. + * An improviement in this area would eliminate the duplicate lines below + */ + statusPending: colors.lightBlue.base, + statuspending: colors.lightBlue.base, + statusnot_applicable: colors.lightBlue.base, }; // Get colors generated from base mitre using UtilColorGenerator. diff --git a/apps/frontend/src/router.ts b/apps/frontend/src/router.ts index 7b27e16be6..a40553be59 100644 --- a/apps/frontend/src/router.ts +++ b/apps/frontend/src/router.ts @@ -117,4 +117,4 @@ router.beforeEach((to, _, next) => { }); }); -export default router; +export default router; \ No newline at end of file diff --git a/apps/frontend/src/store/assessment_data.ts b/apps/frontend/src/store/assessment_data.ts new file mode 100644 index 0000000000..dd751cfe00 --- /dev/null +++ b/apps/frontend/src/store/assessment_data.ts @@ -0,0 +1,158 @@ +import Store from '@/store/store'; +import {IEvaluation} from '@heimdall/interfaces'; +import {utcDay} from 'd3'; +import _ from 'lodash'; +import { + Module, + VuexModule, + getModule +} from 'vuex-module-decorators'; +import {AssessmentType} from '../enums/assessment_type'; +import {BucketType} from '../enums/bucket_type'; +import {SourcedContextualizedEvaluation} from './report_intake'; + + +@Module({ + namespaced: true, + dynamic: true, + store: Store, + name: 'assessmentData' +}) +export class AssessmentData extends VuexModule { + +} +export const AssessmentDataModule = getModule(AssessmentData); + + +export function assessment_eval(ev: SourcedContextualizedEvaluation): AssessmentType { + if (ev?.data){ + if ( ev.data !== undefined && ev.data.profiles.length > 0 ){ + const profile = ev.data.profiles[0]; + const assessment_type = _.find(profile.supports, function (a) { + return _.has(a,'assessment_type'); + }); + + if (_.has(assessment_type,'assessment_type')){ + return assesment_string_type(_.get(assessment_type,'assessment_type') as unknown as string); + } + } + } + return AssessmentType.General; +} + +export function get_assessment(ev: IEvaluation): AssessmentType { + if (ev === undefined){ + return AssessmentType.General + } + if (ev?.data) { + const profiles = _.get(ev?.data, 'profiles'); + if (profiles != undefined && profiles.length > 0) { + const profile = profiles[0]; + const supports = _.get(profile, 'supports') + if (supports !== undefined) { + const assessment_type = _.find(supports, function (a) { + return _.has(a, 'assessment_type'); + }); + + let assessment_string = ""; + if (_.has(assessment_type, 'assessment_type')) { + assessment_string = _.get(assessment_type, 'assessment_type'); + } + + switch (assessment_string) { + case AssessmentType.STIG: + return AssessmentType.STIG; + case AssessmentType.Vulnerability: + return AssessmentType.Vulnerability; + case AssessmentType.General: + return AssessmentType.General; + default: + return AssessmentType.General; + } + } + } + } + return AssessmentType.General; +} + +export function assesment_string_type(asessment_type_string: string): AssessmentType{ + switch (asessment_type_string) { + case AssessmentType.STIG: + return AssessmentType.STIG; + case AssessmentType.Vulnerability: + return AssessmentType.Vulnerability; + case AssessmentType.General: + return AssessmentType.General; + default: + console.log("assessment_type_string:"+assesment_string_type) + return AssessmentType.General; + } +} + +export function assessment_string_bucket_type(assessment_bucket_string: string): BucketType{ + switch (assessment_bucket_string) { + case "general": + return BucketType.General; + case "stigasd": + return BucketType.STIGASD; + case "stigcontainer": + return BucketType.STIGContainer; + case "vulnerability": + return BucketType.Vulnerability; + default: + return BucketType.Unmapped; + } +} + +export function get_bucket_type_eval(ev: SourcedContextualizedEvaluation): BucketType { + if (ev?.data){ + if ( ev.data !== undefined && ev.data.profiles.length > 0 ){ + const profile = ev.data.profiles[0]; + const assessment_bucket = _.find(profile.supports, function (a) { + return _.has(a,'assessment_bucket'); + }); + + if (_.has(assessment_bucket,'assessment_type')){ + return assessment_string_bucket_type(_.get(assessment_bucket,'assessment_bucket') as unknown as string); + } + } + } +return BucketType.Unmapped; +} + + +export function get_bucket_type(ev: IEvaluation): BucketType { + if (ev?.data) { + const profiles = _.get(ev?.data, 'profiles'); + if (profiles != undefined && profiles.length > 0) { + const profile = profiles[0]; + const supports = _.get(profile, 'supports') + if (supports !== undefined) { + const assessment_bucket = _.find(supports, function (a) { + return _.has(a, 'assessment_bucket'); + }); + + let assessment_bucket_string = ""; + if (_.has(assessment_bucket, 'assessment_bucket')) { + assessment_bucket_string = _.get(assessment_bucket, 'assessment_bucket'); + } + + switch (assessment_bucket_string) { + case "general": + return BucketType.General; + case "stigasd": + return BucketType.STIGASD; + case "stigcontainer": + return BucketType.STIGContainer; + case "vulnerability": + return BucketType.Vulnerability; + default: + return BucketType.Unmapped; + } + } + } + } + //Default Return if not found + console.log("assessment_type not found"); + return BucketType.Unmapped; +} \ No newline at end of file diff --git a/apps/frontend/src/store/builds.ts b/apps/frontend/src/store/builds.ts new file mode 100644 index 0000000000..ddfef9db89 --- /dev/null +++ b/apps/frontend/src/store/builds.ts @@ -0,0 +1,48 @@ +import Store from '@/store/store'; +import { + IBuild, +} from '@heimdall/interfaces'; +import axios from 'axios'; +import _ from 'lodash'; +import { + Action, + getModule, + Module, + Mutation, + VuexModule +} from 'vuex-module-decorators'; + +@Module({ + namespaced: true, + dynamic: true, + store: Store, + name: 'BuildModule' +}) +export class Build extends VuexModule { + allBuilds: IBuild[] = []; + loading = true; + + @Action + async getAllBuilds(): Promise { + return axios + .get('/builds') + .then(({data}) => { + this.context.commit('SET_ALL_PRODUCT', data); + }) + .finally(() => { + this.context.commit('SET_LOADING', false); + }); + } + + @Mutation + SET_ALL_PRODUCT(products: IBuild[]) { + this.allBuilds = products; + } + + @Mutation + SET_LOADING(value: boolean) { + this.loading = value; + } +} + +export const BuildModule = getModule(Build); diff --git a/apps/frontend/src/store/color_hack.ts b/apps/frontend/src/store/color_hack.ts index 3a83c35f43..d50551b894 100644 --- a/apps/frontend/src/store/color_hack.ts +++ b/apps/frontend/src/store/color_hack.ts @@ -83,6 +83,8 @@ export class ColorHack extends VuexModule { return this.lookupColor('statusPassed'); case 'Failed': return this.lookupColor('statusFailed'); + case 'Pending': + return this.lookupColor('statusPending'); case 'Not Applicable': return this.lookupColor('statusNotApplicable'); case 'Not Reviewed': diff --git a/apps/frontend/src/store/data_filters.ts b/apps/frontend/src/store/data_filters.ts index c9d510cc89..2a863e9da7 100644 --- a/apps/frontend/src/store/data_filters.ts +++ b/apps/frontend/src/store/data_filters.ts @@ -29,7 +29,7 @@ import { const MAX_CACHE_ENTRIES = 20; -export declare type ExtendedControlStatus = ControlStatus | 'Waived'; +export declare type ExtendedControlStatus = ControlStatus | 'Waived' | 'Failed,Pending'; /** Contains common filters on data from the store. */ export interface Filter { @@ -78,6 +78,9 @@ export interface Filter { /** A specific control id */ control_id?: string; + + /** Result source to search for */ + resultSourceSearchTerms?: string[]; } export type TreeMapState = string[]; // Representing the current path spec, from root @@ -225,6 +228,16 @@ export class FilteredData extends VuexModule { }; } + get evaluation(): ( + file: FileID + ) => SourcedContextualizedEvaluation | undefined{ + return (file: FileID) => { + return _.first(InspecDataModule.contextualExecutions.filter((e) => + e.from_file.uniqueId === file + )); + } + } + get profiles_for_evaluations(): ( files: FileID[] ) => readonly ContextualizedProfile[] { @@ -340,6 +353,7 @@ export class FilteredData extends VuexModule { 'hdf.rawNistTags': filter.nistIdFilter, full_code: filter.codeSearchTerms, 'hdf.waived': filter.status?.includes('Waived'), + 'hdf.parsedResultSourceTags': filter.resultSourceSearchTerms, 'root.hdf.status': _.filter( filter.status, (status) => status !== 'Waived' diff --git a/apps/frontend/src/store/data_store.ts b/apps/frontend/src/store/data_store.ts index 67fa0941e2..71acf5240a 100644 --- a/apps/frontend/src/store/data_store.ts +++ b/apps/frontend/src/store/data_store.ts @@ -113,6 +113,13 @@ export class InspecData extends VuexModule { this.executionFiles.push(newExecution); } + @Action + clear_quiet() { + FilteredDataModule.CLEAR_ALL_EVALUATIONS(); + FilteredDataModule.CLEAR_ALL_PROFILES(); + this.context.commit('reset'); + } + /** * Unloads the file with the given id */ diff --git a/apps/frontend/src/store/evaluations.ts b/apps/frontend/src/store/evaluations.ts index b9528c63d8..45c7edec02 100644 --- a/apps/frontend/src/store/evaluations.ts +++ b/apps/frontend/src/store/evaluations.ts @@ -84,6 +84,70 @@ export class Evaluation extends VuexModule { }); } + @Action + async clear_quiet_results() { + console.log(InspecIntakeModule.clear_quiet()); + return true; + } + + + @Action + async load_quiet_results(evaluationIds: string[]): Promise<(FileID | void)[]> { + if (evaluationIds.length > 10) { + SnackbarModule.notify(`Large data set detected: loading ${evaluationIds.length} results.`); + } + + const unloadedIds = _.difference( + evaluationIds, + InspecDataModule.loadedDatabaseIds + ); + const loadedIds: FileID[] = []; + await Promise.all( + unloadedIds.map(async (id) => + this.loadEvaluation(id) + .then(async (evaluation) => { + if (await InspecIntakeModule.isHDF(evaluation.data)) { + InspecIntakeModule.loadQuietText({ + text: JSON.stringify(evaluation.data), + filename: evaluation.filename, + database_id: evaluation.id, + createdAt: evaluation.createdAt, + updatedAt: evaluation.updatedAt, + tags: [] // Tags are not yet implemented, so for now the value is passed in empty + }) + .then((fileId) => loadedIds.push(fileId)) + .catch((err) => { + SnackbarModule.failure(err); + }); + } else if (evaluation.data) { + const inputFile: FileLoadOptions = { + filename: evaluation.filename + }; + if ( + 'originalResultsData' in evaluation.data && + evaluation.data.originalResultsData + ) { + inputFile.data = evaluation.data.originalResultsData; + } else { + inputFile.data = JSON.stringify(evaluation.data); + } + + const fileIds = await InspecIntakeModule.loadFile(inputFile); + loadedIds.push(...fileIds); + } else { + SnackbarModule.failure(`Empty File: ${evaluation.filename}`); + } + }) + .catch((err) => { + SnackbarModule.failure(err); + }) + ) + ); + + SnackbarModule.notify('All result sets have been loaded'); + return loadedIds; + } + @Action async load_results(evaluationIds: string[]): Promise<(FileID | void)[]> { document.body.style.cursor = 'wait'; diff --git a/apps/frontend/src/store/product_module_state.ts b/apps/frontend/src/store/product_module_state.ts new file mode 100644 index 0000000000..a747edd530 --- /dev/null +++ b/apps/frontend/src/store/product_module_state.ts @@ -0,0 +1,141 @@ +import Store from '@/store/store'; +import { + Action, + getModule, + Module, + Mutation, + VuexModule +} from 'vuex-module-decorators'; + +import { + IBuild, +} from '@heimdall/interfaces'; +import axios from 'axios'; +import {EvaluationModule} from '@/store/evaluations'; +import _ from 'lodash'; +import {BucketType} from '@/enums/bucket_type'; +import {SearchModule} from '@/store/search'; + +export interface PASDViewModeType { + id: number; + text: string; +} + +export interface IProductModState { + viewMode: number; + displayNewControls: boolean; // display only new controls in UI (i.e. control rules without override information) + overrideValidation: boolean; + s3Prefix: string; +} + +function updateS3Prefix(product: string, build: string): string { + return product + "/" + build + "/" +} + +@Module({ + namespaced: true, + dynamic: true, + store: Store, + name: 'ProductModuleState' +}) +class ProductModState extends VuexModule implements IProductModState { + viewMode = 0; + viewObjectStoreKey = "noset" + viewBuildId = "notset" + s3Prefix = "notset" + displayNewControls = false; + overrideValidation = false; + + viewModeTypes: PASDViewModeType[] = [ + { id: 0, text: 'Certifier'}, + { id: 1, text: 'Developer'}, + { id: 2, text: 'Cyber'}, + ]; + + bucketList: number[] = new Array(BucketType.MaxSize) + @Mutation + SET_OVERRIDE_VALIDATION(state: boolean) { + this.overrideValidation = state; + } + + @Action + public UpdateOverrideValidation(state: boolean): void { + this.context.commit('SET_OVERRIDE_VALIDATION', state); + } + + @Mutation + SET_DISPLAY_NEW_CONTROLS(state: boolean) { + this.displayNewControls = state; + } + + @Action + public UpdateDisplayNewControls(state: boolean): void { + this.context.commit('SET_DISPLAY_NEW_CONTROLS', state); + } + + @Mutation + SET_VIEW_MODE(state: number) { + switch (this.viewMode) { + case 2: + SearchModule.removeStatusFilter('Pending'); + } + this.viewMode = state; + } + + @Action + public UpdateViewMode(state: number): void { + this.context.commit('SET_VIEW_MODE', state); + } + + @Mutation + SET_VIEW_OBJECTSTORE_KEY(val: string) { + this.viewObjectStoreKey = val; + this.s3Prefix = updateS3Prefix(this.viewObjectStoreKey, this.viewBuildId) + } + + @Action + public UpdateViewObjectStoreKey(val: string): void { + this.context.commit('SET_VIEW_OBJECTSTORE_KEY', val); + } + + @Mutation + SET_VIEW_BUILD_ID(val: string) { + this.viewBuildId = val; + this.s3Prefix = updateS3Prefix(this.viewObjectStoreKey, this.viewBuildId); + } + + @Action + UpdateViewBuildId(val: string): void { + this.context.commit("SET_VIEW_BUILD_ID", val); + } + + @Action + async LoadBuild(id: string) { + return axios.get(`/builds/${id}`).then(({data}) => { + this.context.commit('LOAD_BUILD', data); + return data; + }); + } + + // Save an evaluation or update it if is already saved. + @Mutation + LOAD_BUILD(buildEvals: IBuild) { + const evalsToLoad: string[] = [] + + if (buildEvals !== undefined) { + if (buildEvals.evaluations.length > 0) { + for (const [index, evaluation] of buildEvals.evaluations.entries()) { + evalsToLoad.push(evaluation.id) + } + } + } + + if (evalsToLoad.length > 0) { + EvaluationModule.clear_quiet_results() + EvaluationModule.load_quiet_results(evalsToLoad) + } + } + +} + +export const ProductModuleState = getModule(ProductModState); diff --git a/apps/frontend/src/store/products.ts b/apps/frontend/src/store/products.ts new file mode 100644 index 0000000000..ef46bf27be --- /dev/null +++ b/apps/frontend/src/store/products.ts @@ -0,0 +1,67 @@ +import Store from '@/store/store'; +import { + IProduct, + IBuild, +} from '@heimdall/interfaces'; +import axios from 'axios'; +import _ from 'lodash'; +import { + Action, + getModule, + Module, + Mutation, + VuexModule +} from 'vuex-module-decorators'; + +@Module({ + namespaced: true, + dynamic: true, + store: Store, + name: 'ProductModule' +}) +export class Product extends VuexModule { + allProducts: IProduct[] = []; + selectedBuilds: IBuild[] = []; + loading = true; + + @Action + async getAllProducts(): Promise { + return axios + .get('/products') + .then(({data}) => { + this.context.commit('SET_ALL_PRODUCT', data); + }) + .finally(() => { + this.context.commit('SET_LOADING', false); + }); + } + + @Action + async getSelectedProductBuilds(id: string): Promise { + return axios + .get(`/products/${id}/builds`) + .then(({data}) => { + this.context.commit('SET_ALL_BUILDS', data); + }) + .finally(() => { + this.context.commit('SET_LOADING', false); + }); + } + + @Mutation + SET_ALL_PRODUCT(products: IProduct[]) { + this.allProducts = products; + } + + @Mutation + SET_ALL_BUILDS(products: IBuild[]) { + this.selectedBuilds = products; + } + + @Mutation + SET_LOADING(value: boolean) { + this.loading = value; + } +} + +export const ProductModule = getModule(Product); diff --git a/apps/frontend/src/store/report_intake.ts b/apps/frontend/src/store/report_intake.ts index 3f420277d9..19907b28c5 100644 --- a/apps/frontend/src/store/report_intake.ts +++ b/apps/frontend/src/store/report_intake.ts @@ -309,6 +309,11 @@ export class InspecIntake extends VuexModule { } } + @Action + async clear_quiet() { + InspecDataModule.clear_quiet(); + } + @Action async loadText(options: TextLoadOptions): Promise { // Convert it @@ -366,6 +371,60 @@ export class InspecIntake extends VuexModule { return fileID; } + @Action + async loadQuietText(options: TextLoadOptions): Promise { + // Convert it + const fileID: FileID = uuid(); + const result: ConversionResult = convertFile(options.text, true); + // Determine what sort of file we (hopefully) have, then add it + if (result['1_0_ExecJson']) { + const evalFile = { + uniqueId: fileID, + filename: options.filename, + database_id: options.database_id, + createdAt: options.createdAt, + updatedAt: options.updatedAt, + tags: options.tags + // evaluation + } as EvaluationFile; + + // Fixup the evaluation to be Sourced from a file. Requires a temporary type break + const evaluation = contextualizeEvaluation( + result['1_0_ExecJson'] + ) as unknown as SourcedContextualizedEvaluation; + evaluation.from_file = evalFile; + + // Set and freeze + evalFile.evaluation = evaluation; + Object.freeze(evaluation); + InspecDataModule.addExecution(evalFile); + } else if (result['1_0_ProfileJson']) { + // Handle as profile + const profileFile = { + uniqueId: fileID, + filename: options.filename + } as ProfileFile; + + // Fixup the evaluation to be Sourced from a file. Requires a temporary type break + const profile = contextualizeProfile( + result['1_0_ProfileJson'] + ) as unknown as SourcedContextualizedProfile; + profile.from_file = profileFile; + + // Set and freeze + profileFile.profile = profile; + Object.freeze(profile); + InspecDataModule.addProfile(profileFile); + } else { + // eslint-disable-next-line no-console + console.error(result.errors); + throw new Error( + "Couldn't parse data. See developer's tools for more details." + ); + } + return fileID; + } + // Instead of re-stringifying converted evaluations, add the allow loading the ExecJSON directly. @Action async loadExecJson(options: ExecJSONLoadOptions) { diff --git a/apps/frontend/src/store/search.ts b/apps/frontend/src/store/search.ts index e4d43ddfff..7e550b508d 100644 --- a/apps/frontend/src/store/search.ts +++ b/apps/frontend/src/store/search.ts @@ -19,7 +19,8 @@ export interface ISearchState { codeSearchTerms: string[]; NISTIdFilter: string[]; statusFilter: ExtendedControlStatus[]; - severityFilter: Severity[]; + severityFilter: Severity[]; + resultSourceSearchTerms: string[]; } export interface SearchQuery { @@ -36,7 +37,8 @@ export const statusTypes = [ 'Passed', 'Failed', 'Not Reviewed', - 'Waived' + 'Waived', + 'Pending' ]; export const severityTypes = ['none', 'low', 'medium', 'high', 'critical']; @@ -74,6 +76,7 @@ class Search extends VuexModule implements ISearchState { statusFilter: ExtendedControlStatus[] = []; severityFilter: Severity[] = []; titleSearchTerms: string[] = []; + resultSourceSearchTerms: string[] = []; /** Update the current search */ @Action @@ -99,7 +102,8 @@ class Search extends VuexModule implements ISearchState { 'desc', 'description', 'code', - 'input' + 'input', + 'rsrc' ] }; const searchResult = parse(this.searchTerm, options); @@ -138,6 +142,9 @@ class Search extends VuexModule implements ISearchState { this.setFreesearch(include); } break; + case 'rsrc': + this.addResultSourceFilter(lowercaseAll(include)); + break; } } } @@ -160,6 +167,7 @@ class Search extends VuexModule implements ISearchState { this.context.commit('CLEAR_DESCRIPTION'); this.context.commit('CLEAR_CODE'); this.context.commit('CLEAR_FREESEARCH'); + this.context.commit('CLEAR_RESULT_SOURCE_FILTER'); } // Generic filtering @@ -443,6 +451,26 @@ class Search extends VuexModule implements ISearchState { CLEAR_FREESEARCH() { this.freeSearch = ''; } + + // Result Source Filtering + + /** Adds result source to filter */ + @Action + addResultSourceFilter(RsId: string | string[]) { + this.context.commit('ADD_RESULT_SOURCE_FILTER', RsId); + } + + @Mutation + ADD_RESULT_SOURCE_FILTER(RsId: string | string[]) { + this.resultSourceSearchTerms = this.resultSourceSearchTerms.concat(RsId); + } + + /** Clears all result source filters */ + @Mutation + CLEAR_RESULT_SOURCE_FILTER() { + this.resultSourceSearchTerms = []; + } + } export const SearchModule = getModule(Search); diff --git a/apps/frontend/src/store/server.ts b/apps/frontend/src/store/server.ts index 1298414cd3..5973441b64 100644 --- a/apps/frontend/src/store/server.ts +++ b/apps/frontend/src/store/server.ts @@ -16,6 +16,10 @@ import { VuexModule } from 'vuex-module-decorators'; import {GroupsModule} from './groups'; +import router from '@/router'; +import Certifier from '@/views/Certifier.vue'; +import Cyber from '@/views/Cyber.vue'; +import Developer from '@/views/Developer.vue'; const localToken = new LocalStorageVal('auth_token'); const localUserID = new LocalStorageVal('localUserID'); @@ -32,6 +36,7 @@ export interface IServerState { enabledOAuth: string[]; registrationEnabled: boolean; oidcName: string; + projectMode: boolean; ldap: boolean; localLoginEnabled: boolean; userInfo: IUser; @@ -63,6 +68,7 @@ class Server extends VuexModule implements IServerState { enabledOAuth: string[] = []; allUsers: ISlimUser[] = []; oidcName = ''; + projectMode = false; /** Our currently granted JWT token */ token = ''; /** Provide a sane default for userInfo in order to avoid having to null check it all the time */ @@ -104,6 +110,7 @@ class Server extends VuexModule implements IServerState { this.enabledOAuth = settings.enabledOAuth; this.registrationEnabled = settings.registrationEnabled; this.oidcName = settings.oidcName; + this.projectMode = settings.projectMode; this.ldap = settings.ldap; this.localLoginEnabled = settings.localLoginEnabled; } @@ -169,6 +176,9 @@ class Server extends VuexModule implements IServerState { if (userID !== null) { this.context.commit('SET_USERID', userID); } + if (this.projectMode) { + this.SetupProjectRoutes(); + } return this.GetUserInfo(); } }) @@ -283,6 +293,55 @@ class Server extends VuexModule implements IServerState { ); }); } + + @Action + public SetupProjectRoutes() { + const routes = [ + { + path: '/certifier', + name: 'certifier', + component: Certifier, + meta: {requiresAuth: true, hasIdParams: true}, + children: [ + { + path: ':id', + component: Certifier, + meta: {requiresAuth: true, hasIdParams: true} + } + ] + }, + { + path: '/cyber', + name: 'cyber', + component: Cyber, + meta: {requiresAuth: true, hasIdParams: true}, + children: [ + { + path: ':id', + component: Cyber, + meta: {requiresAuth: true, hasIdParams: true} + } + ] + }, + { + path: '/developer', + name: 'developer', + component: Developer, + meta: {requiresAuth: true, hasIdParams: true}, + children: [ + { + path: ':id', + component: Developer, + meta: {requiresAuth: true, hasIdParams: true} + } + ] + } + ] + + routes.forEach((route) => { + router.addRoute(route); + }); + } } export const ServerModule = getModule(Server); diff --git a/apps/frontend/src/store/status_counts.ts b/apps/frontend/src/store/status_counts.ts index 18cf9fd08f..99c6e4f973 100644 --- a/apps/frontend/src/store/status_counts.ts +++ b/apps/frontend/src/store/status_counts.ts @@ -43,6 +43,7 @@ function count_statuses(data: FilteredData, filter: Filter): StatusHash { 'Not Reviewed': 0, Passed: 0, 'Profile Error': 0, + 'Pending': 0, PassedTests: 0, FailedTests: 0, PassingTestsFailedControl: 0, diff --git a/apps/frontend/src/utilities/aws_util.ts b/apps/frontend/src/utilities/aws_util.ts index 96d961b61d..c58016f17f 100644 --- a/apps/frontend/src/utilities/aws_util.ts +++ b/apps/frontend/src/utilities/aws_util.ts @@ -1,181 +1,197 @@ -import {GetObjectCommand, S3Client} from '@aws-sdk/client-s3'; -import { - GetCallerIdentityCommand, - GetSessionTokenCommand, - STSClient -} from '@aws-sdk/client-sts'; - -export const AUTH_DURATION = 8 * 60 * 60; // 8 hours - -/** represents the auth credentials for aws stuff */ -export interface AuthCreds { - accessKeyId: string; - secretAccessKey: string; - sessionToken: string; -} - -/** represents the information of the current used */ -export interface AuthInfo { - expiration?: Date; - user_account: string; - user_arn: string; - probable_user_mfa_device: string | null; // Null implies it could not be deduced - user_id: string; -} - -/** bundles the above two */ -export interface Auth { - creds: AuthCreds; - info: AuthInfo; - from_mfa: boolean; - region: string; -} - -/** Fetches the described S3 file using the given creds. - * Yields the string contents on success - * Yields the AWS error on failure - */ -export async function fetchS3File( - auth: Auth, - fileKey: string, - bucketName: string -): Promise { - // Fetch it from s3, and promise to submit it to be loaded afterwards - const client = new S3Client({credentials: auth.creds, region: auth.region}); - const response = await client.send( - new GetObjectCommand({ - Key: fileKey, - Bucket: bucketName - }) - ); - if (!response.Body) { - throw new Error('Fetching S3 file failed'); - } - return response.Body.transformToString(); -} - -/** Represents the bundle of parameters required for creating a session key using MFA */ -export interface MFAInfo { - SerialNumber: string | null; // If null, use deduced token - TokenCode: string; -} - -/** Attempts to deduce the virtual mfa device serial code from the provided */ -export function deriveMFASerial(userAccessToken: string): string | null { - return userAccessToken ? userAccessToken.replace(':user', ':mfa') : null; -} - -/** Attempts to retrieve an aws temporary session using the given information. - * Yields the session info on success. - * Yields the AWS error on failure. - */ -export async function getSessionToken( - accessToken: string, - secretKey: string, - region: string, - duration: number, - mfaInfo?: MFAInfo -): Promise { - // Instanciate STS with our base and secret token - const client = new STSClient({ - credentials: { - accessKeyId: accessToken, - secretAccessKey: secretKey - }, - region: region - }); - - // Get the user info - const wipInfo: Partial = {}; - const responseCallerId = await client.send(new GetCallerIdentityCommand({})); - wipInfo.user_account = responseCallerId.Account; - wipInfo.user_arn = responseCallerId.Arn || 'Unknown Resource Name'; - wipInfo.user_id = responseCallerId.UserId; - // Guess at mfa - wipInfo.probable_user_mfa_device = deriveMFASerial(wipInfo.user_arn); - - // It's built - mark as such - const info = wipInfo as AuthInfo; - - // Make our request to be the role - let responseSessionToken; - if (mfaInfo) { - mfaInfo.SerialNumber ??= info.probable_user_mfa_device; - if (mfaInfo.SerialNumber) { - responseSessionToken = await client.send( - new GetSessionTokenCommand({ - DurationSeconds: duration, - SerialNumber: mfaInfo.SerialNumber, - TokenCode: mfaInfo.TokenCode - }) - ); - } - } else { - responseSessionToken = await client.send(new GetSessionTokenCommand({})); - } - - // Handle the response. On Success, save the creds. On error, throw that stuff back! - if (!responseSessionToken?.Credentials) { - throw new Error('AWS assume role attempt failed'); - } - if (!responseSessionToken?.Credentials?.AccessKeyId) { - throw new Error('AWS assume role attempt failed - no AccessKeyId'); - } - if (!responseSessionToken?.Credentials?.SecretAccessKey) { - throw new Error('AWS assume role attempt failed - no SecretAccessKey'); - } - if (!responseSessionToken?.Credentials?.SessionToken) { - throw new Error('AWS assume role attempt failed - no SessionToken'); - } - const creds: AuthCreds = { - accessKeyId: responseSessionToken.Credentials.AccessKeyId, - secretAccessKey: responseSessionToken.Credentials.SecretAccessKey, - sessionToken: responseSessionToken.Credentials.SessionToken - }; - return { - creds, - info, - from_mfa: !!mfaInfo, - region: region - }; -} - -/** Generates human readable versions of common AWS error codes. - * The error class is untyped since they've distributed their error/exception classes all over. - * If the code is not recognized, coughs it back up as an erroname - */ -export function transcribeError(error: { - name: string; - message: string; -}): string { - const {name, message} = error; - switch (name) { - case 'TokenRefreshRequired': - case 'ExpiredToken': - return 'Authorization expired. Please log back in.'; - case 'InvalidAccessKeyId': - return 'Provided access key is invalid.'; - case 'AccessDenied': - return `Access denied. This likely means that your account does not have access to the specified bucket, or that it requires MFA authentication.`; - case 'AccountProblem': - return `Account problem detected: ${message}`; - case 'CredentialsNotSupported': - return 'Provided credentials not supported.'; - case 'InvalidBucketName': - return 'Invalid bucket name! Please ensure you spelled it correctly.'; - case 'NetworkingError': - return 'Networking error. This may be because the provided bucket name does not exist. Please ensure you have spelled it correctly.'; - case 'InvalidBucketState': - return 'Invalid bucket state! Contact your AWS administrator.'; - case 'ValidationError': - return `Further validation required: ${message}`; - case 'SignatureDoesNotMatch': - return 'The provided secret token does not match access token. Please ensure that it is correct.'; - case 'InvalidToken': - return 'Your session token has expired. Please log back in and try again.'; - case 'InvalidClientTokenId': - return 'The provided access token is invalid. Please ensure that it is correct.'; - default: - return `Unknown error ${name}. Message: ${message}`; - } -} +import {GetObjectCommand, S3Client} from '@aws-sdk/client-s3'; +import { + GetCallerIdentityCommand, + GetSessionTokenCommand, + STSClient +} from '@aws-sdk/client-sts'; +import axios from 'axios'; +import { saveAs } from 'file-saver'; + +export const AUTH_DURATION = 8 * 60 * 60; // 8 hours + +/** represents the auth credentials for aws stuff */ +export interface AuthCreds { + accessKeyId: string; + secretAccessKey: string; + sessionToken: string; +} + +/** represents the information of the current used */ +export interface AuthInfo { + expiration?: Date; + user_account: string; + user_arn: string; + probable_user_mfa_device: string | null; // Null implies it could not be deduced + user_id: string; +} + +/** bundles the above two */ +export interface Auth { + creds: AuthCreds; + info: AuthInfo; + from_mfa: boolean; + region: string; +} + +/** Fetches the described S3 file using the given creds. + * Yields the string contents on success + * Yields the AWS error on failure + */ +export async function fetchS3File( + auth: Auth, + fileKey: string, + bucketName: string +): Promise { + // Fetch it from s3, and promise to submit it to be loaded afterwards + const client = new S3Client({credentials: auth.creds, region: auth.region}); + const response = await client.send( + new GetObjectCommand({ + Key: fileKey, + Bucket: bucketName + }) + ); + if (!response.Body) { + throw new Error('Fetching S3 file failed'); + } + return response.Body.transformToString(); +} + +/** Represents the bundle of parameters required for creating a session key using MFA */ +export interface MFAInfo { + SerialNumber: string | null; // If null, use deduced token + TokenCode: string; +} + +/** Attempts to deduce the virtual mfa device serial code from the provided */ +export function deriveMFASerial(userAccessToken: string): string | null { + return userAccessToken ? userAccessToken.replace(':user', ':mfa') : null; +} + +/** Attempts to retrieve an aws temporary session using the given information. + * Yields the session info on success. + * Yields the AWS error on failure. + */ +export async function getSessionToken( + accessToken: string, + secretKey: string, + region: string, + duration: number, + mfaInfo?: MFAInfo +): Promise { + // Instanciate STS with our base and secret token + const client = new STSClient({ + credentials: { + accessKeyId: accessToken, + secretAccessKey: secretKey + }, + region: region + }); + + // Get the user info + const wipInfo: Partial = {}; + const responseCallerId = await client.send(new GetCallerIdentityCommand({})); + wipInfo.user_account = responseCallerId.Account; + wipInfo.user_arn = responseCallerId.Arn || 'Unknown Resource Name'; + wipInfo.user_id = responseCallerId.UserId; + // Guess at mfa + wipInfo.probable_user_mfa_device = deriveMFASerial(wipInfo.user_arn); + + // It's built - mark as such + const info = wipInfo as AuthInfo; + + // Make our request to be the role + let responseSessionToken; + if (mfaInfo) { + mfaInfo.SerialNumber ??= info.probable_user_mfa_device; + if (mfaInfo.SerialNumber) { + responseSessionToken = await client.send( + new GetSessionTokenCommand({ + DurationSeconds: duration, + SerialNumber: mfaInfo.SerialNumber, + TokenCode: mfaInfo.TokenCode + }) + ); + } + } else { + responseSessionToken = await client.send(new GetSessionTokenCommand({})); + } + + // Handle the response. On Success, save the creds. On error, throw that stuff back! + if (!responseSessionToken?.Credentials) { + throw new Error('AWS assume role attempt failed'); + } + if (!responseSessionToken?.Credentials?.AccessKeyId) { + throw new Error('AWS assume role attempt failed - no AccessKeyId'); + } + if (!responseSessionToken?.Credentials?.SecretAccessKey) { + throw new Error('AWS assume role attempt failed - no SecretAccessKey'); + } + if (!responseSessionToken?.Credentials?.SessionToken) { + throw new Error('AWS assume role attempt failed - no SessionToken'); + } + const creds: AuthCreds = { + accessKeyId: responseSessionToken.Credentials.AccessKeyId, + secretAccessKey: responseSessionToken.Credentials.SecretAccessKey, + sessionToken: responseSessionToken.Credentials.SessionToken + }; + return { + creds, + info, + from_mfa: !!mfaInfo, + region: region + }; +} + +/** Generates human readable versions of common AWS error codes. + * The error class is untyped since they've distributed their error/exception classes all over. + * If the code is not recognized, coughs it back up as an erroname + */ +export function transcribeError(error: { + name: string; + message: string; +}): string { + const {name, message} = error; + switch (name) { + case 'TokenRefreshRequired': + case 'ExpiredToken': + return 'Authorization expired. Please log back in.'; + case 'InvalidAccessKeyId': + return 'Provided access key is invalid.'; + case 'AccessDenied': + return `Access denied. This likely means that your account does not have access to the specified bucket, or that it requires MFA authentication.`; + case 'AccountProblem': + return `Account problem detected: ${message}`; + case 'CredentialsNotSupported': + return 'Provided credentials not supported.'; + case 'InvalidBucketName': + return 'Invalid bucket name! Please ensure you spelled it correctly.'; + case 'NetworkingError': + return 'Networking error. This may be because the provided bucket name does not exist. Please ensure you have spelled it correctly.'; + case 'InvalidBucketState': + return 'Invalid bucket state! Contact your AWS administrator.'; + case 'ValidationError': + return `Further validation required: ${message}`; + case 'SignatureDoesNotMatch': + return 'The provided secret token does not match access token. Please ensure that it is correct.'; + case 'InvalidToken': + return 'Your session token has expired. Please log back in and try again.'; + case 'InvalidClientTokenId': + return 'The provided access token is invalid. Please ensure that it is correct.'; + default: + return `Unknown error ${name}. Message: ${message}`; + } +} + +export async function s3ListFiles(prefix: string): Promise { + return axios.get('/autharti', { params: { prefix: prefix } }); +} + +export function s3DownloadFile(key: string) { + const fileName = key.split('/').pop(); + axios.get('/autharti/download', { params: {key: key}}).then(response => { + console.log("response", response); + const data = Buffer.from(response.data, 'hex'); + console.log("buffer data", data); + saveAs(new Blob([data], {type: 'application/zip'}), fileName); + }); +} diff --git a/apps/frontend/src/utilities/compliance_util.ts b/apps/frontend/src/utilities/compliance_util.ts new file mode 100644 index 0000000000..a782297472 --- /dev/null +++ b/apps/frontend/src/utilities/compliance_util.ts @@ -0,0 +1,34 @@ +/* Provides unified compliance formatting function for compliance summaries used across both Results.vue and ExportHTMLModal.vue */ + +export const MAX_DECIMAL_PRECISION = 2; + +// Format all final compliance level results to hundredths place percentage of compliance level +// Returns string typed compliance level +export function formatCompliance(rawCompliance: number): string { + let truncatedCompliance = + Math.trunc(Math.pow(10, MAX_DECIMAL_PRECISION) * rawCompliance) / + Math.pow(10, MAX_DECIMAL_PRECISION); + + // Check if calculated compliance is valid + if (truncatedCompliance < 0) { + truncatedCompliance = 0; + } + + // Return as string representation of compliance level percentage + return `${truncatedCompliance.toFixed(MAX_DECIMAL_PRECISION)}%`; +} + +// Takes formatted compliance level and determines human language equivalent of compliance +// >=90 is high compliance, >= 60 is medium compliance, <60 is low compliance +// Mainly for HTML export +export function translateCompliance(rawCompliance: string): string { + const compliance = parseFloat(rawCompliance.slice(0, -1)); + + if (compliance >= 90) { + return 'high'; + } else if (compliance >= 60) { + return 'medium'; + } else { + return 'low'; + } +} diff --git a/apps/frontend/src/views/Base.vue b/apps/frontend/src/views/Base.vue index 46d859e11f..709da9b5b0 100644 --- a/apps/frontend/src/views/Base.vue +++ b/apps/frontend/src/views/Base.vue @@ -50,11 +50,14 @@ import Topbar from '@/components/global/Topbar.vue'; import UpdateNotification from '@/components/global/UpdateNotification.vue'; import {SidebarModule} from '@/store/sidebar_state'; import Vue from 'vue'; -import Component from 'vue-class-component'; +import Component, {mixins} from 'vue-class-component'; import {Prop} from 'vue-property-decorator'; import {InspecIntakeModule} from '../store/report_intake'; +import { FilteredDataModule } from '../store/data_filters'; +import { ProductModuleState } from '../store/product_module_state'; import {ServerModule} from '../store/server'; import {SnackbarModule} from '../store/snackbar'; +import RouteMixin from '../mixins/RouteMixin'; @Component({ components: { @@ -65,7 +68,7 @@ import {SnackbarModule} from '../store/snackbar'; UpdateNotification } }) -export default class Base extends Vue { +export default class Base extends mixins(RouteMixin){ @Prop({default: 'Heimdall'}) readonly title!: string; @Prop({default: 11}) readonly topbarZIndex!: number; @Prop({default: false}) readonly minimalTopbar!: boolean; diff --git a/apps/frontend/src/views/Certifier.vue b/apps/frontend/src/views/Certifier.vue new file mode 100644 index 0000000000..0666669b9e --- /dev/null +++ b/apps/frontend/src/views/Certifier.vue @@ -0,0 +1,560 @@ + + + + + diff --git a/apps/frontend/src/views/Compare.vue b/apps/frontend/src/views/Compare.vue index afecb61643..2b1e32f1cf 100644 --- a/apps/frontend/src/views/Compare.vue +++ b/apps/frontend/src/views/Compare.vue @@ -265,6 +265,11 @@ export default class Compare extends Vue { value: 'Failed', color: 'statusFailed' }, + { + label: 'Pending', + value: 'Pending', + color: 'statuspending' + }, { label: 'Not Applicable', value: 'Not Applicable', @@ -339,6 +344,7 @@ export default class Compare extends Vue { ids: SearchModule.controlIdSearchTerms, titleSearchTerms: SearchModule.titleSearchTerms, descriptionSearchTerms: SearchModule.descriptionSearchTerms, + resultSourceSearchTerms: SearchModule.resultSourceSearchTerms, nistIdFilter: SearchModule.NISTIdFilter, searchTerm: SearchModule.freeSearch, codeSearchTerms: SearchModule.codeSearchTerms, diff --git a/apps/frontend/src/views/Cyber.vue b/apps/frontend/src/views/Cyber.vue new file mode 100644 index 0000000000..7dc828950b --- /dev/null +++ b/apps/frontend/src/views/Cyber.vue @@ -0,0 +1,549 @@ + + + + + diff --git a/apps/frontend/src/views/Developer.vue b/apps/frontend/src/views/Developer.vue new file mode 100644 index 0000000000..aabd3ee876 --- /dev/null +++ b/apps/frontend/src/views/Developer.vue @@ -0,0 +1,554 @@ + + + + + diff --git a/apps/frontend/src/views/Landing.vue b/apps/frontend/src/views/Landing.vue index 7cd8cc1ac1..09c3f32414 100644 --- a/apps/frontend/src/views/Landing.vue +++ b/apps/frontend/src/views/Landing.vue @@ -4,25 +4,35 @@ :show-topbar="serverMode" :minimal-topbar="true" :topbar-z-index="1000" + :show-search="true" + @changed-files="evalInfo = null" > - + + diff --git a/apps/frontend/src/views/Login.vue b/apps/frontend/src/views/Login.vue index 28ce05ff30..98f8365b91 100644 --- a/apps/frontend/src/views/Login.vue +++ b/apps/frontend/src/views/Login.vue @@ -155,5 +155,9 @@ export default class Login extends Vue { } } } + + // adding to supress setter error message + set logoffSnackbar(val: boolean) { + } } diff --git a/apps/frontend/src/views/Results.vue b/apps/frontend/src/views/Results.vue index 576bd96f39..4a0d4b2359 100644 --- a/apps/frontend/src/views/Results.vue +++ b/apps/frontend/src/views/Results.vue @@ -56,6 +56,7 @@
+ @@ -154,6 +155,16 @@ + + + + + + + + + @@ -200,6 +211,7 @@